Skip to content

Monthly Audit Orchestrator #1

Monthly Audit Orchestrator

Monthly Audit Orchestrator #1

name: Monthly Audit Orchestrator
on:
schedule:
- cron: "0 3 1 * *"
workflow_dispatch:
inputs:
month:
description: "Optional month override in YYYY-MM format"
required: false
default: ""
dry_run:
description: "Render the payload without creating or updating the issue"
required: false
type: boolean
default: false
permissions:
contents: read
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
publish-monthly-issue:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Validate configuration
env:
AUDIT_TARGET_REPOS: ${{ vars.AUDIT_TARGET_REPOS || 'QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines' }}
AUDIT_MONTHLY_LABEL: ${{ vars.AUDIT_MONTHLY_LABEL || 'monthly-review' }}
AUDIT_AUTO_MERGE_LABEL: ${{ vars.AUDIT_AUTO_MERGE_LABEL || 'auto-merge-ok' }}
AUDIT_REVIEW_TITLE_PREFIX: ${{ vars.AUDIT_REVIEW_TITLE_PREFIX || 'Monthly Audit Review' }}
run: |
set -euo pipefail
test -n "${AUDIT_TARGET_REPOS:-}"
test -n "${AUDIT_MONTHLY_LABEL:-}"
test -n "${AUDIT_AUTO_MERGE_LABEL:-}"
test -n "${AUDIT_REVIEW_TITLE_PREFIX:-}"
node <<'NODE'
const targets = (process.env.AUDIT_TARGET_REPOS || '')
.split(/[\n,]/)
.map(s => s.trim())
.filter(Boolean);
const invalid = targets.filter(target => !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]*SnapshotPipelines$/.test(target));
if (invalid.length > 0) {
throw new Error(`Monthly report audits are only for snapshot repositories; invalid targets: ${invalid.join(', ')}`);
}
NODE
- name: Resolve month
id: resolve-month
env:
MONTH_OVERRIDE: ${{ github.event.inputs.month }}
run: |
set -euo pipefail
if [ -n "${MONTH_OVERRIDE:-}" ]; then
month="${MONTH_OVERRIDE}"
else
month="$(date -u +%Y-%m)"
fi
if ! printf '%s' "${month}" | grep -Eq '^[0-9]{4}-[0-9]{2}$'; then
echo "Invalid month: ${month}" >&2
exit 1
fi
echo "month=${month}" >> "$GITHUB_OUTPUT"
- name: Publish monthly audit issue
id: publish-monthly-issue
uses: actions/github-script@v8
env:
AUDIT_TARGET_REPOS: ${{ vars.AUDIT_TARGET_REPOS || 'QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines' }}
AUDIT_MONTHLY_LABEL: ${{ vars.AUDIT_MONTHLY_LABEL || 'monthly-review' }}
AUDIT_AUTO_MERGE_LABEL: ${{ vars.AUDIT_AUTO_MERGE_LABEL || 'auto-merge-ok' }}
AUDIT_REVIEW_TITLE_PREFIX: ${{ vars.AUDIT_REVIEW_TITLE_PREFIX || 'Monthly Audit Review' }}
MONTH_OVERRIDE: ${{ steps.resolve-month.outputs.month }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const titlePrefix = process.env.AUDIT_REVIEW_TITLE_PREFIX || 'Monthly Audit Review';
const label = process.env.AUDIT_MONTHLY_LABEL || 'monthly-review';
const autoMergeLabel = process.env.AUDIT_AUTO_MERGE_LABEL || 'auto-merge-ok';
const targets = (process.env.AUDIT_TARGET_REPOS || '')
.split(/[\n,]/)
.map(s => s.trim())
.filter(Boolean);
if (targets.length === 0) {
throw new Error('AUDIT_TARGET_REPOS is empty');
}
const invalidTargets = targets.filter(target => !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]*SnapshotPipelines$/.test(target));
if (invalidTargets.length > 0) {
throw new Error(`Monthly report audits are only for snapshot repositories; invalid targets: ${invalidTargets.join(', ')}`);
}
const dryRun = String(process.env.DRY_RUN || '').toLowerCase() === 'true';
const monthOverride = (process.env.MONTH_OVERRIDE || '').trim();
const now = new Date();
const stamp = monthOverride || now.toISOString().slice(0, 7);
const title = `${titlePrefix}: ${stamp}`;
const payload = {
month: stamp,
title,
monthly_label: label,
auto_merge_label: autoMergeLabel,
targets,
};
core.setOutput('month', stamp);
core.setOutput('title', title);
const body = [
'This issue tracks the monthly audit orchestration run.',
'',
'Targets:',
...targets.map(t => `- ${t}`),
'',
'Only snapshot artifact repositories are in scope for monthly report audits.',
'Each source repository owns its monthly review issue and dispatches AIAuditBridge with source_repo and issue_number.',
'',
'Source of truth for policy and scheduling stays in GitHub Actions.',
'',
'Payload:',
'```json',
JSON.stringify(payload, null, 2),
'```',
].join('\n');
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `# Monthly audit orchestration\n\n- Month: ${stamp}\n- Targets: ${targets.length}\n- Dry run: ${dryRun}\n- Issue title: ${title}\n`);
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n## Payload\n\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n`);
if (dryRun) {
console.log(`Dry run requested for ${title}`);
console.log(body);
return;
}
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: label,
per_page: 100,
});
const hit = existing.data.find(item => item.title === title);
if (hit) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: hit.number,
body: `Monthly audit orchestrator already exists for ${stamp}.`
});
core.setOutput('issue_number', String(hit.number));
core.setOutput('issue_url', hit.html_url);
return;
}
const created = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: [label],
});
core.setOutput('issue_number', String(created.data.number));
core.setOutput('issue_url', created.data.html_url);
- name: Record source-owned audit dispatch
if: ${{ github.event.inputs.dry_run != 'true' }}
run: |
set -euo pipefail
{
echo
echo "## Source-owned audit dispatch"
echo
echo "- Month: ${{ steps.resolve-month.outputs.month }}"
echo "- Tracking issue: ${{ steps.publish-monthly-issue.outputs.issue_url }}"
echo
echo "AIAuditBridge execution requires a source repository issue number."
echo "The source repositories create those issues in their own monthly workflows, then dispatch codex_audit.yml with source_repo and issue_number."
} >> "$GITHUB_STEP_SUMMARY"