Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/actions/open-automation-pr/action.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ updates:
directories:
- '/'
- '/.github/actions/setup-node-deps'
- '/.github/actions/open-automation-pr'
schedule:
interval: 'weekly'
day: 'monday'
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/architecture-submission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand All @@ -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: |
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/refresh-community-people.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 1 addition & 4 deletions .github/workflows/refresh-metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,13 @@ 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: |
Automated refresh of `data/metrics.json`, the end user member
list and the generated member directory.

Validation, the unit tests and the production build passed.
labels: automated
signoff: true
5 changes: 1 addition & 4 deletions .github/workflows/refresh-radar-reports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
55 changes: 47 additions & 8 deletions tests/ci-concurrency.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -156,18 +160,15 @@ function isPerEvent(expression) {
// names itself. The action force-pushes `branch:` to `refs/heads/<branch>` 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());
Expand Down Expand Up @@ -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', () => {
Expand Down
34 changes: 30 additions & 4 deletions tests/helpers-workflow-ordering.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)),
);
}

Expand Down
25 changes: 25 additions & 0 deletions tests/workflow-ordering-helpers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading