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
8 changes: 7 additions & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ File: `all-green.yml`

Wraps the `all-green` composite action as a reusable workflow. Use it when a repository wants one final required check that confirms all other required PR checks have passed.

It accepts the action's `checks-include`, `checks-exclude` and `fail-on-missing-checks` inputs with the same defaults; see [All Green](../../all-green/README.md).

```yaml
jobs:
all-green:
Expand All @@ -22,7 +24,9 @@ jobs:

File: `build.yml`

Calculates a build version, optionally updates version files, runs configurable pre/post commands, builds Docker images with Buildx Bake, and can publish multi-platform images to a registry. Pull request image SHA tags use the PR head commit rather than GitHub's synthetic merge commit.
Calculates a build version, optionally updates version files, runs configurable pre/post commands, builds Docker images with Buildx Bake, and can publish multi-platform images to a registry.

`pr-checkout` selects the commit built for `pull_request` events: `merge` (default) builds GitHub's merge of the PR into its base branch, and `head` builds the PR head commit. Other events, including `pull_request_target`, keep the default checkout. The full-SHA tags, the `sha-` tag, the `org.opencontainers.image.revision` label and the `commit-sha` output name the commit that was built. Earlier releases tagged and labelled merge builds with the PR head SHA as well; callers that pull PR images by head SHA should set `pr-checkout: head` or use `BUILD_COMMIT_SHA`. In `head` mode, the SemVer bump counts only commits reachable from the PR head.

`version-strategy` accepts `semver` (default) or `calver`. CalVer uses UTC `YYYY.M.PATCH` from Git release tags and preserves this workflow's default `-build` output suffix. `version-semver-previous` and `version-semver-next` apply only to SemVer; `version-output-format` and `version-tag-prefix` apply to both. Strategy-specific inputs use `version-semver-` or `version-calver-` prefixes; there are currently no CalVer-only inputs. Keep the default full-history checkout so all release tags are available. See the [CalVer action](../../release/version/calver/README.md) for the calculation and supported template fields.

Expand Down Expand Up @@ -54,6 +58,8 @@ append their configured `*-name` as a matrix suffix, such as `Post Step (System
for `post-test-name: System Tests`. Empty phases keep the fixed name and appear
as skipped. Command steps and report sections retain the configured name.

Prepare, pre and post commands receive `BUILD_COMMIT_SHA`, the commit the images are built from (the `commit-sha` output). It can differ from `GITHUB_SHA`. Reference it as a shell variable inside the command, for example `post-test-command: ./scripts/test ghcr.io/example/app:$BUILD_COMMIT_SHA`.

The Build workflow enables Git LFS downloads during checkout by default. Set `lfs: false` when the caller does not need Git LFS files.

When `lfs` is enabled, the workflow prints the tracked LFS files and fails before image build if any checked-out file is still an unresolved LFS pointer. Image builds use the uploaded source artifact as a local path context, so the hydrated checkout is what gets baked into the image.
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/all-green.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ name: All Green

on:
workflow_call:
inputs:
checks-include:
description: 'Comma-separated regexes of the check names to evaluate. Empty or -1 evaluates every check; any other value makes checks-exclude ignored'
required: false
type: string
default: ''
checks-exclude:
description: 'Comma-separated regexes of the check names to ignore; -1 ignores none. The default skips the advisory Copilot review check'
required: false
type: string
default: '^copilot-pull-request-reviewer$'
fail-on-missing-checks:
description: 'Whether to fail when a checks-include pattern matches no check'
required: false
type: boolean
default: false

jobs:
all-green:
Expand All @@ -12,3 +28,7 @@ jobs:

- name: All Green
uses: $/all-green
with:
checks-include: ${{ inputs.checks-include }}
checks-exclude: ${{ inputs.checks-exclude }}
fail-on-missing-checks: ${{ inputs.fail-on-missing-checks }}
53 changes: 42 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
description: 'Detected or calculated version'
value: ${{ jobs.prepare.outputs.version }}
commit-sha:
description: 'The commit SHA being built'
description: 'The checked-out commit SHA the images are built from'
value: ${{ jobs.prepare.outputs.commit-sha }}
image-digests:
description: 'JSON with all merged image manifest digests'
Expand Down Expand Up @@ -928,6 +928,11 @@ on:
required: false
type: boolean
default: true
pr-checkout:
description: 'Commit to build for pull_request events: merge (the PR merged into its base branch) or head (the PR head commit)'
required: false
type: string
default: 'merge'

jobs:
prepare:
Expand Down Expand Up @@ -957,6 +962,30 @@ jobs:
*) echo '::error::version-strategy must be semver or calver' >&2; exit 1 ;;
esac

- name: Resolve pull request checkout
id: pr_checkout
shell: bash
env:
PR_CHECKOUT: ${{ inputs.pr-checkout }}
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
ref=''
case "${PR_CHECKOUT}" in
merge) ;;
head)
# Only pull_request: pull_request_target must not build PR code with base-repository secrets.
if [ "${EVENT_NAME}" = 'pull_request' ]; then
ref="${PR_HEAD_SHA}"
elif [ -n "${PR_HEAD_SHA}" ]; then
echo "::notice::pr-checkout: head applies only to pull_request events; using the default checkout for ${EVENT_NAME}"
fi
;;
*) echo '::error::pr-checkout must be merge or head' >&2; exit 1 ;;
esac
printf 'ref=%s\n' "${ref}" >> "${GITHUB_OUTPUT}"

- name: Prepare build inputs
id: prepare_report
uses: $/build/report/prepare
Expand All @@ -967,6 +996,7 @@ jobs:
uses: actions/checkout@v6
id: checkout
with:
ref: ${{ steps.pr_checkout.outputs.ref }}
fetch-depth: ${{ inputs.fetch-depth }}
fetch-tags: ${{ inputs.fetch-tags }}
lfs: ${{ inputs.lfs }}
Expand Down Expand Up @@ -999,13 +1029,9 @@ jobs:
id: determine_sha
shell: bash
run: |
if [ -n "${{ github.event.pull_request.head.sha }}" ]; then
SHA="${{ github.event.pull_request.head.sha }}"
echo "Using PR HEAD SHA: ${SHA}"
else
SHA=$(git rev-parse HEAD)
echo "Using current HEAD SHA: ${SHA}"
fi
set -euo pipefail
SHA=$(git rev-parse HEAD)
echo "Using checked-out commit: ${SHA}"
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"

- name: Next semantic version
Expand Down Expand Up @@ -1058,7 +1084,8 @@ jobs:
id: prepare_image
uses: $/buildx-bake/prepare
env:
DOCKER_METADATA_PR_HEAD_SHA: true
# Label and tag the PR head only when it is the commit checked out above.
DOCKER_METADATA_PR_HEAD_SHA: ${{ steps.pr_checkout.outputs.ref != '' }}
with:
bake-file: ${{ inputs.bake-file }}
bake-target: ${{ inputs.bake-target }}
Expand All @@ -1068,8 +1095,8 @@ jobs:
meta-labels:
${{ inputs.meta-labels }}
meta-tags: |
type=sha,prefix=,format=long,event=push
type=sha,event=push
type=sha,prefix=,format=long
type=sha
type=ref,event=branch
type=ref,event=pr
type=ref,event=tag
Expand All @@ -1086,6 +1113,8 @@ jobs:
if: inputs.prepare-command != ''
id: prepare_command
shell: bash
env:
BUILD_COMMIT_SHA: ${{ steps.determine_sha.outputs.sha }}
run: ${{ inputs.prepare-command }}

- name: Verify Git LFS checkout
Expand Down Expand Up @@ -1144,6 +1173,7 @@ jobs:
name: ${{ fromJson(needs.prepare.outputs.pre-step-names) }}
env:
BUILD_STEP_CONFIG: ${{ toJSON(fromJSON(needs.prepare.outputs.pre-step-matrix)[strategy.job-index]) }}
BUILD_COMMIT_SHA: ${{ needs.prepare.outputs.commit-sha }}
steps:
- name: Download updated source
uses: atos-actions/download-artifact@v2
Expand Down Expand Up @@ -1278,6 +1308,7 @@ jobs:
name: ${{ fromJson(needs.prepare.outputs.post-step-names) }}
env:
BUILD_STEP_CONFIG: ${{ toJSON(fromJSON(needs.prepare.outputs.post-step-matrix)[strategy.job-index]) }}
BUILD_COMMIT_SHA: ${{ needs.prepare.outputs.commit-sha }}
steps:
- name: Download updated source
uses: atos-actions/download-artifact@v2
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ test-report:
.PHONY: test-build-steps
test-build-steps:
python3 -B -m unittest discover -s tests -p 'test_build_steps.py' -v

.PHONY: test-build-checkout
test-build-checkout:
python3 -B -m unittest discover -s tests -p 'test_build_checkout.py' -v

.PHONY: test-all-green
test-all-green:
node --test tests/test_all_green.js
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ Run `make test-report` for offline report aggregation and publishing tests. Thes

Run `make test-build-steps` for pre/post matrix naming and configuration tests. These require Python 3.10 or newer and run offline.

Run `make test-build-checkout` for the Build workflow's pull request checkout and commit SHA tests. These require Python 3.10 or newer, Bash, and Git, and run offline.

Run `make test-all-green` for All Green input mapping tests. These require Node.js with `node:test` support and run offline.

For workflow lint, actionlint currently [does not recognize GitHub's `$/` references](https://github.com/rhysd/actionlint/issues/711). Lint a temporary copy with `uses: $/` rewritten to `uses: ./`, preserving the repository layout so local action paths and reusable-workflow inputs are checked. Keep the committed references as `$/`.

The `Build cache` PR check calls the Bake composites from the workflow’s commit with a small two-target fixture on native AMD64 and ARM64 runners. It checks separate target/platform scopes, caller overrides, two warm imports on fresh builders, and source/dependency invalidation after all cold exports complete. It publishes only Actions cache entries and temporary test artifacts. Per-run cache prefixes prevent previous PR runs from warming the cold comparison. Measurements appear in job summaries and the `cache-measurements-*` artifacts; these synthetic timings are not consumer build benchmarks.
Expand Down
17 changes: 16 additions & 1 deletion all-green/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@

Checks that all required pull-request checks have passed.

Use this action as a final PR gate after the repository has configured its required checks. It wraps `wechuli/allcheckspassed` with polling disabled, so it reports the current state instead of waiting.
Use this action as a final PR gate after the repository has configured its required checks. It wraps `wechuli/allcheckspassed` with polling disabled, so it reports the current state instead of waiting: a check that has not completed (queued, waiting or running) fails the gate, and a check that has not been created yet is not seen. By default it evaluates every check on the commit except the advisory `copilot-pull-request-reviewer` check, because a Copilot review still in progress would otherwise fail the gate.

Inputs take comma-separated JavaScript regexes, unanchored and case-sensitive; use `^...$` for exact names. Patterns cannot contain commas, and a value starting with `[` or `{` is read as allcheckspassed's JSON form, so start such a pattern with `^`:

- `checks-include`: evaluate only matching checks. Empty or `-1` evaluates every check; any other value, even whitespace, makes `checks-exclude` ignored.
- `checks-exclude`: ignore matching checks. Set `-1` to ignore none.
- `fail-on-missing-checks`: fail when a `checks-include` pattern matches no check. Enable it with `checks-include`, since a check that has not been created yet would otherwise pass with only a warning.

```yaml
steps:
- uses: actions/checkout@v6
- uses: getdevopspro/github-actions/all-green@v8.3.7
```

```yaml
steps:
- uses: actions/checkout@v6
- uses: getdevopspro/github-actions/all-green@v12.0.0
with:
checks-include: '^Lint$,^Unit Tests$'
fail-on-missing-checks: true
```
18 changes: 18 additions & 0 deletions all-green/action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
name: "All Green"
description: "Checks that all required checks have passed and the PR is green."

inputs:
checks-include:
description: 'Comma-separated regexes of the check names to evaluate. Empty or -1 evaluates every check; any other value makes checks-exclude ignored'
required: false
default: ''
checks-exclude:
description: 'Comma-separated regexes of the check names to ignore; -1 ignores none. The default skips the advisory Copilot review, which would otherwise fail the gate while still in progress'
required: false
default: '^copilot-pull-request-reviewer$'
fail-on-missing-checks:
description: 'Whether to fail when a checks-include pattern matches no check'
required: false
default: 'false'

runs:
using: "composite"
steps:
Expand All @@ -10,3 +24,7 @@ runs:
poll: false
delay: 0
retries: 0
# allcheckspassed rejects include and exclude together, so include wins.
checks_include: ${{ inputs.checks-include || '-1' }}
checks_exclude: ${{ ((inputs.checks-include == '' || inputs.checks-include == '-1') && inputs.checks-exclude) || '-1' }}
fail_on_missing_checks: ${{ inputs.fail-on-missing-checks }}
55 changes: 55 additions & 0 deletions tests/test_all_green.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');

const root = path.join(__dirname, '..');
const action = fs.readFileSync(path.join(root, 'all-green/action.yml'), 'utf8');
const workflow = fs.readFileSync(path.join(root, '.github/workflows/all-green.yml'), 'utf8');
const COPILOT = '^copilot-pull-request-reviewer$';

// Evaluates an allcheckspassed `with:` expression from the action for the given inputs.
function expression(name) {
const source = action.match(new RegExp(`^ ${name}: \\$\\{\\{ (.*) \\}\\}$`, 'm'))[1]
.replace(/\.([\w]+(?:-[\w]+)+)/g, "['$1']");
return new Function('inputs', `return (${source})`);
}

function inputBlock(source, name, indent) {
const lines = source.split('\n');
const start = lines.indexOf(`${' '.repeat(indent)}${name}:`);
assert.notEqual(start, -1, name);
const rest = lines.slice(start + 1);
const end = rest.findIndex(line => !line.startsWith(' '.repeat(indent + 2)));
return rest.slice(0, end).join('\n');
}

test('include wins over exclude, so allcheckspassed never receives both', () => {
const include = expression('checks_include');
const exclude = expression('checks_exclude');
for (const [inputs, expected] of [
[{'checks-include': '', 'checks-exclude': COPILOT}, ['-1', COPILOT]],
[{'checks-include': '', 'checks-exclude': ''}, ['-1', '-1']],
[{'checks-include': '', 'checks-exclude': '-1'}, ['-1', '-1']],
[{'checks-include': '-1', 'checks-exclude': COPILOT}, ['-1', COPILOT]],
[{'checks-include': '^Lint$', 'checks-exclude': COPILOT}, ['^Lint$', '-1']],
[{'checks-include': '^Lint$,^Unit Tests$', 'checks-exclude': '^Other$'}, ['^Lint$,^Unit Tests$', '-1']],
]) {
assert.deepEqual([include(inputs), exclude(inputs)], expected, JSON.stringify(inputs));
}
});

test('the reusable workflow repeats the action defaults and forwards every input', () => {
assert.match(inputBlock(action, 'checks-include', 2), /^ default: ''$/m);
assert.match(inputBlock(workflow, 'checks-include', 6), /^ default: ''$/m);
assert.match(inputBlock(action, 'checks-exclude', 2), /^ default: '\^copilot-pull-request-reviewer\$'$/m);
assert.match(inputBlock(workflow, 'checks-exclude', 6), /^ default: '\^copilot-pull-request-reviewer\$'$/m);
assert.match(inputBlock(action, 'fail-on-missing-checks', 2), /^ default: 'false'$/m);
assert.match(inputBlock(workflow, 'fail-on-missing-checks', 6), /^ default: false$/m);
for (const name of ['checks-include', 'checks-exclude', 'fail-on-missing-checks']) {
assert.ok(workflow.includes(` ${name}: \${{ inputs.${name} }}\n`), name);
}
assert.match(action, /^ fail_on_missing_checks: \$\{\{ inputs\.fail-on-missing-checks \}\}$/m);
});
Loading
Loading