From c1add286a93e544fc348e3fa2f533c8a822e123d Mon Sep 17 00:00:00 2001 From: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:47:54 -0400 Subject: [PATCH 1/4] refactor: add shared open-automation-pr composite action Centralize the peter-evans/create-pull-request@v8.1.1 step used by four automation workflows (architecture-submission, refresh-community-people, refresh-metrics, refresh-radar-reports) into a single composite action that fixes delete-branch, labels and signoff, matching the pattern already used by .github/actions/setup-node-deps. Each workflow still needs updating to call ./.github/actions/open-automation-pr instead of peter-evans/create-pull-request directly; that edit touches .github/workflows/** and is out of reach for this lane's contributor-tier token, so it is left for a human or an ISSUES_PRS_MERGE agent per issue #1622. Signed-off-by: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- .github/actions/open-automation-pr/action.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/actions/open-automation-pr/action.yml diff --git a/.github/actions/open-automation-pr/action.yml b/.github/actions/open-automation-pr/action.yml new file mode 100644 index 00000000..a157d4c5 --- /dev/null +++ b/.github/actions/open-automation-pr/action.yml @@ -0,0 +1,32 @@ +name: Open an automation pull request +description: >- + Open a PR for an automated content/data refresh with this repo's required + labels, signoff and branch-deletion policy fixed, so each automation + workflow only has to supply its branch name and PR text. + +inputs: + branch: + description: Branch name for the automated change. + required: true + commit-message: + description: Commit message for the automated change. + required: true + title: + description: Pull request title. + required: true + body: + description: Pull request body. + required: true + +runs: + using: composite + steps: + - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: ${{ inputs.branch }} + delete-branch: true + commit-message: ${{ inputs.commit-message }} + title: ${{ inputs.title }} + body: ${{ inputs.body }} + labels: automated + signoff: true From c3eb0f4d4e0a2d5d453fa5340136bce53b0ec714 Mon Sep 17 00:00:00 2001 From: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:52:07 -0400 Subject: [PATCH 2/4] fix: reformat open-automation-pr action.yml per prettier Signed-off-by: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- .github/actions/open-automation-pr/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/open-automation-pr/action.yml b/.github/actions/open-automation-pr/action.yml index a157d4c5..c6ba9e7d 100644 --- a/.github/actions/open-automation-pr/action.yml +++ b/.github/actions/open-automation-pr/action.yml @@ -1,8 +1,8 @@ name: Open an automation pull request description: >- Open a PR for an automated content/data refresh with this repo's required - labels, signoff and branch-deletion policy fixed, so each automation - workflow only has to supply its branch name and PR text. + labels, signoff and branch-deletion policy fixed, so each automation workflow + only has to supply its branch name and PR text. inputs: branch: From a9365e86d4f188ba1391a867ad4e33d5ecff8a96 Mon Sep 17 00:00:00 2001 From: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:57:27 -0400 Subject: [PATCH 3/4] fix: cover open-automation-pr composite action in dependabot config Dependabot's github-actions ecosystem entry only discovers SHA-pinned actions under the repository root (.github/workflows) and explicitly listed composite action directories. The new .github/actions/open-automation-pr/action.yml composite action pins peter-evans/create-pull-request by SHA but was not added to that list, so its pin would never be refreshed. Add the directory entry so tests/dependabot-contract.test.mjs passes. Signed-off-by: architect <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a055f413..bcc84502 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,6 +28,7 @@ updates: directories: - '/' - '/.github/actions/setup-node-deps' + - '/.github/actions/open-automation-pr' schedule: interval: 'weekly' day: 'monday' From 78ff7d7278105759a38065ef174104996dc914da Mon Sep 17 00:00:00 2001 From: mrbobbytables Date: Mon, 19 Oct 2026 10:38:38 -0500 Subject: [PATCH 4/4] ci: open automation PRs through the shared composite action Replace the four direct peter-evans/create-pull-request steps in architecture-submission.yml, refresh-community-people.yml, refresh-metrics.yml and refresh-radar-reports.yml with ./.github/actions/open-automation-pr, keeping each workflow's branch, commit message, title and body unchanged. The fixed delete-branch, labels and signoff values now live in one place. Two test guards found their workflows by the direct `uses:` and would have gone blind on the swap: the generate/format/validate/test ordering contract (helpers-workflow-ordering.mjs) and the pull-request-branch race contract (ci-concurrency.test.mjs). Both now resolve a step through local composite actions with walkSteps(), so a workflow is detected whether it calls the action directly or via the wrapper, and the branch is still read from the step the workflow wrote. The fixture tests cover the wrapped, non-wrapping and unknown-action cases. Closes #1622 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: mrbobbytables --- .github/workflows/architecture-submission.yml | 5 +- .../workflows/refresh-community-people.yml | 5 +- .github/workflows/refresh-metrics.yml | 5 +- .github/workflows/refresh-radar-reports.yml | 5 +- tests/ci-concurrency.test.mjs | 55 ++++++++++++++++--- tests/helpers-workflow-ordering.mjs | 34 ++++++++++-- tests/workflow-ordering-helpers.test.mjs | 25 +++++++++ 7 files changed, 106 insertions(+), 28 deletions(-) diff --git a/.github/workflows/architecture-submission.yml b/.github/workflows/architecture-submission.yml index f9883bca..e9c2fbf4 100644 --- a/.github/workflows/architecture-submission.yml +++ b/.github/workflows/architecture-submission.yml @@ -47,10 +47,9 @@ jobs: - run: npm run validate:architectures - run: npm run validate:architecture-assets - run: npm run build - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + - uses: ./.github/actions/open-automation-pr with: branch: automation/architecture-issue-${{ github.event.issue.number }} - delete-branch: true commit-message: 'docs: add reference architecture from issue #${{ github.event.issue.number }}' @@ -64,8 +63,6 @@ jobs: The production build and architecture validation passed. Please still review the generated page for accuracy before merging. - labels: automated - signoff: true - name: Comment on failure if: failure() run: | diff --git a/.github/workflows/refresh-community-people.yml b/.github/workflows/refresh-community-people.yml index 7986ace5..e1a09813 100644 --- a/.github/workflows/refresh-community-people.yml +++ b/.github/workflows/refresh-community-people.yml @@ -33,13 +33,10 @@ jobs: - run: npm run validate:community-groups - run: npm run test:unit - run: npm run build - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + - uses: ./.github/actions/open-automation-pr with: branch: automation/refresh-community-people - delete-branch: true commit-message: 'chore: refresh community profiles and group status' title: 'chore: refresh community profiles and group status' body: | Automated refresh of the public GitHub profile data used by the End User Community lightboxes, and of the archived/reachable status recorded for each community group in data/community-groups.json. - labels: automated - signoff: true diff --git a/.github/workflows/refresh-metrics.yml b/.github/workflows/refresh-metrics.yml index be30af7d..db53b1f0 100644 --- a/.github/workflows/refresh-metrics.yml +++ b/.github/workflows/refresh-metrics.yml @@ -37,10 +37,9 @@ jobs: - run: npm run validate:members - run: npm run test:unit - run: npm run build - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + - uses: ./.github/actions/open-automation-pr with: branch: automation/refresh-metrics - delete-branch: true commit-message: 'chore: refresh metrics' title: 'chore: refresh metrics and member data' body: | @@ -48,5 +47,3 @@ jobs: list and the generated member directory. Validation, the unit tests and the production build passed. - labels: automated - signoff: true diff --git a/.github/workflows/refresh-radar-reports.yml b/.github/workflows/refresh-radar-reports.yml index 5e3b0dc6..b008f7a5 100644 --- a/.github/workflows/refresh-radar-reports.yml +++ b/.github/workflows/refresh-radar-reports.yml @@ -34,10 +34,9 @@ jobs: - run: npm run validate:radar-reports - run: npm run test:unit - run: npm run build - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + - uses: ./.github/actions/open-automation-pr with: branch: automation/refresh-radar-reports - delete-branch: true commit-message: 'chore: refresh radar reports' title: 'chore: refresh radar reports' body: | @@ -47,5 +46,3 @@ jobs: New reports get a placeholder summary (flagged as a warning by `npm run validate:radar-reports`); replace it with a real one-sentence description before merging. - labels: automated - signoff: true diff --git a/tests/ci-concurrency.test.mjs b/tests/ci-concurrency.test.mjs index cb7877cf..13f122d2 100644 --- a/tests/ci-concurrency.test.mjs +++ b/tests/ci-concurrency.test.mjs @@ -40,6 +40,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { loadWorkflows } from './helpers-workflows.mjs'; +import { + CREATE_PR_ACTION as PR_BRANCH_ACTION, + opensPullRequest, +} from './helpers-workflow-ordering.mjs'; const workflows = loadWorkflows(); @@ -156,18 +160,15 @@ function isPerEvent(expression) { // names itself. The action force-pushes `branch:` to `refs/heads/` and, // with `delete-branch: true`, deletes that same ref once it stops differing // from the base, so the branch name is the shared resource two runs contend -// for. -const PR_BRANCH_ACTION = 'peter-evans/create-pull-request'; - -function pullRequestBranches(doc) { +// for. The branch is read from the step the workflow wrote, whether it uses +// the action directly or through .github/actions/open-automation-pr, which +// forwards `branch:` unchanged. +function pullRequestBranches(doc, options) { const jobs = doc?.jobs; if (typeof jobs !== 'object' || jobs === null) return []; return Object.values(jobs) .flatMap((job) => (Array.isArray(job?.steps) ? job.steps : [])) - .filter( - (step) => - typeof step?.uses === 'string' && step.uses.includes(PR_BRANCH_ACTION), - ) + .filter((step) => opensPullRequest(step, options)) .map((step) => step?.with?.branch) .filter((branch) => typeof branch === 'string') .map((branch) => branch.replace(/\s+/g, ' ').trim()); @@ -856,6 +857,44 @@ test('the branch reader finds the action wherever it sits', () => { }), ['x', 'y'], ); + // The action reached through a local composite wrapper: the branch is the + // one the workflow passed to the wrapper, not the `${{ inputs.branch }}` + // the wrapper forwards. A local action that does not open a PR, or one + // the fixture does not know, contributes nothing. + const actions = new Map([ + [ + 'open-pr', + { + runs: { + using: 'composite', + steps: [ + { + uses: `${PR_BRANCH_ACTION}@v8`, + with: { branch: '${{ inputs.branch }}' }, + }, + ], + }, + }, + ], + ['setup', { runs: { using: 'composite', steps: [{ run: 'npm ci' }] } }], + ]); + assert.deepEqual( + pullRequestBranches( + { + jobs: { + a: { + steps: [ + { uses: './.github/actions/setup' }, + { uses: './.github/actions/open-pr', with: { branch: 'z' } }, + { uses: './.github/actions/unknown', with: { branch: 'w' } }, + ], + }, + }, + }, + { actions }, + ), + ['z'], + ); }); test('the expression reader compares what a key varies with, not its text', () => { diff --git a/tests/helpers-workflow-ordering.mjs b/tests/helpers-workflow-ordering.mjs index e606c840..c853359e 100644 --- a/tests/helpers-workflow-ordering.mjs +++ b/tests/helpers-workflow-ordering.mjs @@ -11,7 +11,12 @@ // // Nothing here reads the repository. The caller supplies `{ name, doc }` // records and a scripts map, which is what lets a fixture stand in for -// .github/workflows/ without writing anything to disk. +// .github/workflows/ without writing anything to disk. The one exception is +// opensPullRequest(), which descends into local composite actions through +// walkSteps() in ./helpers-workflows.mjs; a fixture passes its own `actions` +// map to keep that read out too. + +import { stepsOf, walkSteps } from './helpers-workflows.mjs'; // A script that writes data into the repository, as opposed to one that reads // it. Matched on the package.json namespaces the repository already uses for @@ -88,12 +93,33 @@ export function validatorFor(generator, scripts) { return validator in scripts ? validator : undefined; } +export const CREATE_PR_ACTION = 'peter-evans/create-pull-request'; + +// Whether a step opens a pull request: by using the action itself, or by +// using a local composite action whose steps do. +// .github/actions/open-automation-pr wraps the action so each automation +// workflow only supplies its branch and PR text, and a detector that matched +// only the direct `uses:` would stop seeing every workflow that calls the +// wrapper. `options` is handed to walkSteps(), so `{ actions }` can supply a +// fixture's composite actions in place of the repository's. +export function opensPullRequest(step, options) { + for (const { step: inner } of walkSteps({ steps: [step] }, options)) { + if ( + typeof inner?.uses === 'string' && + inner.uses.startsWith(CREATE_PR_ACTION) + ) { + return true; + } + } + return false; +} + // A workflow that opens a pull request out of what it just generated. Detected // by the action it uses rather than by filename, so a renamed or newly added // automation job is covered without editing this file. -export function commitsGeneratedOutput(doc) { - return actionsUsedBy(doc).some((uses) => - uses.startsWith('peter-evans/create-pull-request'), +export function commitsGeneratedOutput(doc, options) { + return jobsOf(doc).some(([, job]) => + stepsOf(job).some((step) => opensPullRequest(step, options)), ); } diff --git a/tests/workflow-ordering-helpers.test.mjs b/tests/workflow-ordering-helpers.test.mjs index a13d6377..2520482d 100644 --- a/tests/workflow-ordering-helpers.test.mjs +++ b/tests/workflow-ordering-helpers.test.mjs @@ -139,6 +139,31 @@ test('commitsGeneratedOutput and publishesSite match on the action, not the name commitsGeneratedOutput({ jobs: { a: { steps: [{ uses: CREATE_PR }] } } }), true, ); + // Through a local composite wrapper, resolved against the actions the + // caller supplies: known and wrapping the action, known and not, unknown. + const wrapper = { uses: './.github/actions/open-pr' }; + const actions = new Map([ + ['open-pr', { runs: { using: 'composite', steps: [{ uses: CREATE_PR }] } }], + ['setup', { runs: { using: 'composite', steps: [{ run: 'npm ci' }] } }], + ]); + assert.equal( + commitsGeneratedOutput({ jobs: { a: { steps: [wrapper] } } }, { actions }), + true, + ); + assert.equal( + commitsGeneratedOutput( + { jobs: { a: { steps: [{ uses: './.github/actions/setup' }] } } }, + { actions }, + ), + false, + ); + assert.equal( + commitsGeneratedOutput( + { jobs: { a: { steps: [wrapper] } } }, + { actions: new Map() }, + ), + false, + ); assert.equal(publishesSite({ jobs: { a: { steps: [] } } }), false); assert.equal( publishesSite({