From b95cad32895f6277585e2b78c31b9affc889ac8f Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Fri, 10 Jul 2026 10:28:13 +0800 Subject: [PATCH 1/2] ci: aggregate approved issue updates --- .github/README.md | 4 +- .github/workflows/approved-issue-update.yml | 57 +++++++++----- apps/github-action/src/index.ts | 86 ++++++++++++++++++++- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/.github/README.md b/.github/README.md index f39cafa..a4af81e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -4,7 +4,7 @@ Workflows should orchestrate shared workspace scripts. Business logic belongs in Current workflows: - `workflows/validate.yml`: runs the local validation suite on pull requests, `main`/`master` pushes, and manual dispatch. This uses `pnpm check`, including typecheck, validation, generated artifact checks, and committed smoke checks. -- `workflows/approved-issue-update.yml`: when an issue receives the `approved` label, parses the Issue Form, writes a stable contribution record, applies approved contributions, regenerates derived artifacts, validates the result, and opens a pull request. +- `workflows/approved-issue-update.yml`: when an issue receives the `approved` label, serializes approved-issue processing, syncs all open approved Issue Forms into stable contribution records, applies approved contributions, regenerates derived artifacts, validates the result, and creates or updates one aggregate pull request. - `workflows/release.yml`: on `main`/`master` data/code pushes, tags, or manual dispatch, regenerates and validates data, builds `release/animeatlas.sqlite`, and publishes it as the GitHub Release asset. -The approved-issue workflow does not push directly to `main`. It creates an `automation/approved-issue-` branch and PR after running the same workspace commands used locally. The generated PR body contains `Closes #` so the issue closes only after the PR is merged. When that PR is merged, the push to `main`/`master` triggers the SQLite release workflow. +The approved-issue workflow does not push directly to `main`. It updates the single `automation/approved-issues` branch and PR after running the same workspace commands used locally. The workflow uses one global concurrency group so approved issue updates do not run against `db/` and `generated/` concurrently. Each run re-syncs all open approved issues, so a later run can fill the aggregate PR even when several approved labels arrive close together. The generated PR body contains `Closes #` lines so issues close only after the PR is merged. When that PR is merged, the push to `main`/`master` triggers the SQLite release workflow. diff --git a/.github/workflows/approved-issue-update.yml b/.github/workflows/approved-issue-update.yml index b38a611..de63c58 100644 --- a/.github/workflows/approved-issue-update.yml +++ b/.github/workflows/approved-issue-update.yml @@ -6,17 +6,18 @@ on: - labeled permissions: + actions: write contents: write issues: write pull-requests: write concurrency: - group: approved-issue-${{ github.event.issue.number }} + group: approved-issue-update cancel-in-progress: false jobs: update-from-approved-issue: - name: Update From Approved Issue + name: Update From Approved Issues if: github.event.label.name == 'approved' runs-on: ubuntu-latest steps: @@ -40,8 +41,13 @@ jobs: - name: Build workspace run: pnpm build - - name: Write approved contribution record - run: pnpm --filter @animeatlas/github-action start -- write-approved-contribution "${{ github.event_path }}" + - name: List approved issues + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh issue list --state open --label approved --limit 1000 --json number,url,body,author,updatedAt > approved-issues.json + + - name: Sync approved contribution records + run: pnpm --filter @animeatlas/github-action start -- sync-approved-issues approved-issues.json --approved-by github-actions[bot] - name: Apply approved contributions run: pnpm cli -- contributions apply-approved --write @@ -52,21 +58,34 @@ jobs: - name: Validate result run: pnpm check - - name: Create update pull request + - name: Write pull request body + run: | + { + echo "Applies all currently open AnimeAtlas issues with the approved label." + echo + echo "This PR is updated by the approved issue workflow after running:" + echo "- \`pnpm build\`" + echo "- \`pnpm --filter @animeatlas/github-action start -- sync-approved-issues\`" + echo "- \`pnpm cli -- contributions apply-approved --write\`" + echo "- \`pnpm generate\`" + echo "- \`pnpm check\`" + echo + echo "Included issues:" + jq -r '.[] | "- Closes #\(.number)"' approved-issues.json + } > approved-issues-pr-body.md + + - name: Create or update aggregate pull request + id: cpr uses: peter-evans/create-pull-request@v6 with: - branch: automation/approved-issue-${{ github.event.issue.number }} - commit-message: "Apply approved issue #${{ github.event.issue.number }}" - title: "Apply approved issue #${{ github.event.issue.number }}" - body: | - Applies approved AnimeAtlas data contribution from #${{ github.event.issue.number }}. - - This PR was generated from the approved Issue Form workflow after running: - - `pnpm build` - - `pnpm --filter @animeatlas/github-action start -- write-approved-contribution` - - `pnpm cli -- contributions apply-approved --write` - - `pnpm generate` - - `pnpm check` - - Closes #${{ github.event.issue.number }} + branch: automation/approved-issues + commit-message: "Apply approved AnimeAtlas issues" + title: "Apply approved AnimeAtlas issues" + body-path: approved-issues-pr-body.md delete-branch: true + + - name: Run pull request validation workflow + if: steps.cpr.outputs.pull-request-number != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run validate.yml --ref automation/approved-issues diff --git a/apps/github-action/src/index.ts b/apps/github-action/src/index.ts index 5522a8c..d56fcc0 100644 --- a/apps/github-action/src/index.ts +++ b/apps/github-action/src/index.ts @@ -17,6 +17,14 @@ type IssueEvent = { sender?: { login?: string }; }; +type IssueListItem = { + number?: number; + url?: string; + body?: string | null; + author?: { login?: string }; + updatedAt?: string; +}; + type OperationType = "create_media" | "add_alias" | "add_provider_ref" | "correct_metadata"; type ContributionRecord = { @@ -41,6 +49,7 @@ type WriteApprovedContributionOptions = { outDir?: string; force?: boolean; root?: string; + approvedBy?: string; }; type WriteApprovedContributionResult = { @@ -169,6 +178,48 @@ export function approvedContributionFileName(contribution: ContributionRecord): return `issue-${String(contribution.issue.number).padStart(6, "0")}.json`; } +export function contributionFromIssueListItem(issue: IssueListItem, approvedBy = "approved-label"): ParseResult { + return contributionFromIssueEvent({ + action: "labeled", + label: { name: "approved" }, + issue: { + number: issue.number, + html_url: issue.url, + body: issue.body, + user: issue.author, + updated_at: issue.updatedAt + }, + sender: { login: approvedBy } + }); +} + +export function syncApprovedIssueRecords( + issues: IssueListItem[], + options: WriteApprovedContributionOptions = {} +): Array { + const root = options.root ?? findRepoRoot(process.cwd()); + const outDir = resolveFromRoot(root, options.outDir ?? "source/contributions/approved"); + const results: Array = []; + + for (const issue of issues) { + const result = contributionFromIssueListItem(issue, options.approvedBy); + if (!result.ok) { + throw new Error(`Issue #${issue.number ?? ""} cannot be synced: ${result.errors.join("; ")}`); + } + + const file = join(outDir, approvedContributionFileName(result.contribution)); + if (existsSync(file) && !options.force) { + results.push({ file, written: false, contribution: result.contribution, skipped_existing: true }); + continue; + } + + const writeResult = writeApprovedContributionRecord(result.contribution, { ...options, root, outDir }); + results.push({ ...writeResult, skipped_existing: false }); + } + + return results; +} + function operationFromFields(fields: Record, errors: string[]): ContributionRecord["operation"] | undefined { const type = fields.change_type; if (!isOperationType(type)) { @@ -352,17 +403,37 @@ function usage(): string { return [ "Usage:", " github-action parse-issue-event ", - " github-action write-approved-contribution [--out-dir source/contributions/approved] [--force]" + " github-action write-approved-contribution [--out-dir source/contributions/approved] [--force]", + " github-action sync-approved-issues [--out-dir source/contributions/approved] [--force] [--approved-by login]" ].join("\n"); } async function main(argv: string[]): Promise { - const [command, eventPath, ...rest] = argv; - if (!command || !eventPath) { + const [command, inputPath, ...rest] = argv; + if (!command || !inputPath) { throw new CliUsageError(usage()); } - const event = JSON.parse(readFileSync(eventPath, "utf8")) as IssueEvent; + if (command === "sync-approved-issues") { + const options = parseWriteOptions(rest); + const issues = JSON.parse(readFileSync(inputPath, "utf8")) as IssueListItem[]; + if (!Array.isArray(issues)) { + throw new CliUsageError("sync-approved-issues expects a JSON array from gh issue list."); + } + const results = syncApprovedIssueRecords(issues, options); + process.stdout.write(stableStringify({ + schema: "approved-issue-sync-result/v1", + summary: { + issues: issues.length, + written: results.filter((result) => result.written).length, + skipped_existing: results.filter((result) => result.skipped_existing).length + }, + results + })); + return; + } + + const event = JSON.parse(readFileSync(inputPath, "utf8")) as IssueEvent; const result = contributionFromIssueEvent(event); if (!result.ok) { for (const error of result.errors) { @@ -403,6 +474,13 @@ function parseWriteOptions(args: string[]): WriteApprovedContributionOptions { index += 1; } else if (arg === "--force") { options.force = true; + } else if (arg === "--approved-by") { + const value = args[index + 1]; + if (!value) { + throw new CliUsageError("--approved-by requires a login value."); + } + options.approvedBy = value; + index += 1; } else { throw new CliUsageError(`Unknown option: ${arg}`); } From ae214af0dfd1806a9cee3fc468e0892b26796f6c Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Fri, 10 Jul 2026 10:47:03 +0800 Subject: [PATCH 2/2] Fix approved issue sync path --- .github/workflows/approved-issue-update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/approved-issue-update.yml b/.github/workflows/approved-issue-update.yml index de63c58..2a6f426 100644 --- a/.github/workflows/approved-issue-update.yml +++ b/.github/workflows/approved-issue-update.yml @@ -47,7 +47,7 @@ jobs: run: gh issue list --state open --label approved --limit 1000 --json number,url,body,author,updatedAt > approved-issues.json - name: Sync approved contribution records - run: pnpm --filter @animeatlas/github-action start -- sync-approved-issues approved-issues.json --approved-by github-actions[bot] + run: pnpm --filter @animeatlas/github-action start -- sync-approved-issues "$GITHUB_WORKSPACE/approved-issues.json" --approved-by github-actions[bot] - name: Apply approved contributions run: pnpm cli -- contributions apply-approved --write