Skip to content

Commit 66463b8

Browse files
authored
Load reusable workflow helpers from release tag (#1760)
1 parent c56406b commit 66463b8

7 files changed

Lines changed: 114 additions & 39 deletions

.github/workflows/agent-task-contracts.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- "contracts/agent-task-workflow-request.fixture.json"
1010
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
1111
- "docs/agent-task-reusable-workflow.md"
12+
- "fixtures/agent-task-reusable-workflow-consumer*.yml"
1213
- "package-lock.json"
1314
- "package.json"
1415
- "tests/agent-task-*.test.ts"
@@ -23,6 +24,7 @@ on:
2324
- "contracts/agent-task-workflow-request.fixture.json"
2425
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
2526
- "docs/agent-task-reusable-workflow.md"
27+
- "fixtures/agent-task-reusable-workflow-consumer*.yml"
2628
- "package-lock.json"
2729
- "package.json"
2830
- "tests/agent-task-*.test.ts"

.github/workflows/run-agent-task.yml

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ name: Run Agent Task (reusable)
33
'on':
44
workflow_call:
55
inputs:
6+
wp_codebox_release_ref:
7+
description: Exact immutable WP Codebox release tag used for this workflow invocation, in vX.Y.Z form.
8+
type: string
9+
required: true
610
external_package_source:
711
description: "JSON descriptor for exactly one standalone .agent.json file: repository, immutable revision, file path, and sha256-bytes-v1 digest."
812
type: string
@@ -146,8 +150,7 @@ jobs:
146150
run-agent-task:
147151
runs-on: ubuntu-latest
148152
env:
149-
# Advance this only in a subsequent workflow commit after helper changes land.
150-
WP_CODEBOX_HELPER_REVISION: d9e329e7c3bb852b857870a26d8f3853feef751d
153+
WP_CODEBOX_RELEASE_REF: ${{ inputs.wp_codebox_release_ref }}
151154
permissions:
152155
contents: write
153156
pull-requests: write
@@ -160,22 +163,48 @@ jobs:
160163
credential_mode: ${{ steps.execute.outputs.credential_mode }}
161164
declared_artifacts_json: ${{ steps.execute.outputs.declared_artifacts_json }}
162165
steps:
163-
- name: Validate pinned WP Codebox helper revision
166+
- name: Validate coherent WP Codebox release tag
167+
env:
168+
WORKFLOW_REF: ${{ github.workflow_ref }}
164169
run: |
165-
if ! [[ "${WP_CODEBOX_HELPER_REVISION}" =~ ^[0-9a-f]{40}$ ]]; then
166-
echo "WP_CODEBOX_HELPER_REVISION must be a full lowercase Git commit SHA" >&2
170+
if ! [[ "${WP_CODEBOX_RELEASE_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
171+
echo "wp_codebox_release_ref must be an immutable vX.Y.Z release tag" >&2
172+
exit 1
173+
fi
174+
expected_workflow_ref="Automattic/wp-codebox/.github/workflows/run-agent-task.yml@${WP_CODEBOX_RELEASE_REF}"
175+
if [[ "${WORKFLOW_REF}" != "${expected_workflow_ref}" ]]; then
176+
echo "The reusable workflow tag and wp_codebox_release_ref must match exactly" >&2
167177
exit 1
168178
fi
169179
170180
- name: Checkout WP Codebox workflow helpers
171181
uses: actions/checkout@v4
172182
with:
173183
repository: Automattic/wp-codebox
174-
# This checkout fetches the exact immutable helper source and verifies
175-
# its reachability independently of the caller's workflow revision.
176-
ref: ${{ env.WP_CODEBOX_HELPER_REVISION }}
184+
ref: ${{ inputs.wp_codebox_release_ref }}
177185
path: .wp-codebox-workflow
178186

187+
- name: Verify WP Codebox workflow helper release
188+
working-directory: .wp-codebox-workflow
189+
run: |
190+
git ls-remote --exit-code --refs origin "refs/tags/${WP_CODEBOX_RELEASE_REF}" >/dev/null
191+
remote_tag_object="$(git ls-remote --refs origin "refs/tags/${WP_CODEBOX_RELEASE_REF}" | cut -f1)"
192+
remote_tag_commit="$(git ls-remote origin "refs/tags/${WP_CODEBOX_RELEASE_REF}^{}" | cut -f1)"
193+
remote_tag_commit="${remote_tag_commit:-${remote_tag_object}}"
194+
checked_out_commit="$(git rev-parse HEAD)"
195+
if [[ "${checked_out_commit}" != "${remote_tag_commit}" ]]; then
196+
echo "Checked-out WP Codebox helpers do not match the remote release tag commit" >&2
197+
exit 1
198+
fi
199+
EXPECTED_VERSION="${WP_CODEBOX_RELEASE_REF#v}" node --input-type=module -e '
200+
import { readFileSync } from "node:fs";
201+
const expected = process.env.EXPECTED_VERSION;
202+
const actual = JSON.parse(readFileSync("package.json", "utf8")).version;
203+
if (actual !== expected) {
204+
throw new Error("WP Codebox helper package version " + actual + " does not match release tag v" + expected);
205+
}
206+
'
207+
179208
- name: Set up Node.js
180209
uses: actions/setup-node@v4
181210
with:

contracts/run-agent-task-reusable-workflow-interface.v1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"schema": "wp-codebox/reusable-workflow-interface/v1",
33
"workflow": ".github/workflows/run-agent-task.yml",
44
"inputs": {
5+
"wp_codebox_release_ref": { "required": true, "type": "string" },
56
"external_package_source": { "required": true, "type": "string" },
67
"workload_id": { "required": false, "type": "string", "default": "agent-task" },
78
"workload_label": { "required": false, "type": "string", "default": "Run Agent Task" },

docs/agent-task-reusable-workflow.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ WP Codebox publishes a reusable GitHub Actions workflow for generic agent tasks:
55
```yaml
66
jobs:
77
run-agent-task:
8-
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@main
8+
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
99
with:
10+
wp_codebox_release_ref: v0.12.3
1011
external_package_source: '{"repository":"OWNER/agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
1112
target_repo: Automattic/example-target
1213
prompt: Refresh the configured surface from source evidence.
@@ -29,24 +30,27 @@ public native package, invokes the package-declared agent through the native cha
2930
in a credential-free verification environment, and returns actual runtime and
3031
publication data.
3132
32-
## Helper Source Revision
33+
## Helper Release Tag
3334
34-
The workflow checks out its helper scripts from one immutable WP Codebox commit
35-
configured as `WP_CODEBOX_HELPER_REVISION`. This source revision is independent
36-
of the caller repository, caller commit, and future revisions of
37-
`run-agent-task.yml`; no GitHub workflow SHA context selects helper source.
35+
Callers must invoke the reusable workflow from an exact WP Codebox release tag
36+
and pass that same tag as `wp_codebox_release_ref`. For example, callers use
37+
`Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3` together
38+
with `wp_codebox_release_ref: v0.12.3`. The accepted format is exactly
39+
`vX.Y.Z`; branches, commit SHAs, moving major tags, prereleases, and arbitrary
40+
refs are rejected.
3841

39-
Helper changes land first. A subsequent commit that updates this reusable
40-
workflow advances the helper revision to the full SHA containing those merged
41-
files. The workflow validates the full-SHA shape and `actions/checkout` fetches
42-
that exact source before running helpers. The regression test reads the required
43-
helper files at the pinned revision and checks their SHA-256 digests.
42+
The workflow validates that its own `uses:` reference and
43+
`wp_codebox_release_ref` match exactly before checkout. It always checks helpers
44+
out from `Automattic/wp-codebox`, verifies the checked-out commit equals the
45+
remote release tag commit, and verifies the checked-out `package.json` version
46+
equals the requested tag without its `v` prefix. The caller cannot select a
47+
different helper repository.
4448

45-
This pin fixes [#1755](https://github.com/Automattic/wp-codebox/issues/1755) and the failed Build callers [29281470179](https://github.com/Automattic/build-with-wordpress/actions/runs/29281470179)
46-
and [29281470159](https://github.com/Automattic/build-with-wordpress/actions/runs/29281470159), where a foreign caller SHA was incorrectly used as a WP Codebox checkout ref.
49+
This release-coherence contract fixes [#1759](https://github.com/Automattic/wp-codebox/issues/1759).
4750

4851
## Inputs
4952

53+
- `wp_codebox_release_ref`: required exact immutable WP Codebox release tag. It must match the `@vX.Y.Z` tag in the caller's `uses:` declaration exactly.
5054
- `external_package_source`: immutable descriptor with `repository`, full commit `revision`, one package-relative `.agent.json` `path`, and `digest`. Packages are supported only from publicly accessible GitHub repositories, fetched from canonical `https://github.com/OWNER/REPOSITORY.git` without credentials. `digest` is exactly `sha256-bytes-v1:<lowercase-sha256>` over the raw file bytes; filenames and JSON content are UTF-8-safe and are not normalized before hashing.
5155
- `EXTERNAL_PACKAGE_SOURCE_POLICY`: required reusable-workflow secret, supplied by the caller's operator-controlled secret configuration. Its strict version 1 JSON shape is `{"version":1,"repositories":{"owner/repository":["agents/example.agent.json"]}}`. Every entry is an exact standalone `.agent.json` path. The policy is validated in runner memory, is never part of task input, and is not uploaded.
5256
- `target_repo`: `OWNER/REPO` target repository.
@@ -167,6 +171,10 @@ must migrate checks to executable `validation_dependencies`,
167171
`verification_commands`, or `drift_checks`; context and artifact preparation
168172
remain caller-workflow responsibilities. This is an intentional exposed-workflow breaking change.
169173

174+
`wp_codebox_release_ref` is a required v1 input. Existing callers must switch
175+
their `uses:` reference from a branch or other ref to an exact release tag and
176+
pass that identical tag through this input.
177+
170178
## Upload safety limits
171179

172180
The reusable workflow stages every uploaded file through a fail-closed policy. Only regular UTF-8 files of 4 MiB or less are uploaded, after configured secret values are redacted. Symlinks, special files, binary files, and files larger than 4 MiB are excluded from the upload staging directory. This applies to task artifacts and workflow request, input, and result files.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Mismatched WP Codebox release consumer
2+
3+
on: workflow_dispatch
4+
5+
jobs:
6+
run-agent-task:
7+
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
8+
with:
9+
wp_codebox_release_ref: v0.12.4
10+
external_package_source: '{"repository":"Automattic/example-agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
11+
target_repo: Automattic/example-target
12+
prompt: Refresh the configured surface.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Coherent WP Codebox release consumer
2+
3+
on: workflow_dispatch
4+
5+
jobs:
6+
run-agent-task:
7+
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
8+
with:
9+
wp_codebox_release_ref: v0.12.3
10+
external_package_source: '{"repository":"Automattic/example-agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
11+
target_repo: Automattic/example-target
12+
prompt: Refresh the configured surface.

tests/agent-task-reusable-workflow.test.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import assert from "node:assert/strict"
22
import { execFile } from "node:child_process"
3-
import { createHash } from "node:crypto"
43
import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from "node:fs/promises"
54
import { tmpdir } from "node:os"
65
import { join } from "node:path"
@@ -14,6 +13,7 @@ const publicWorkflowSurface = workflow.slice(0, workflow.indexOf("jobs:"))
1413

1514
assert.match(workflow, /^name: Run Agent Task \(reusable\)$/m)
1615
assert.match(workflow, /workflow_call:/)
16+
assert.match(workflow, /wp_codebox_release_ref:/)
1717
assert.match(workflow, /external_package_source:/)
1818
assert.match(workflow, /EXTERNAL_PACKAGE_SOURCE_POLICY:/)
1919
assert.doesNotMatch(publicWorkflowSurface, /external_package_allowed_repositories:|external_package_allowed_paths:/)
@@ -40,25 +40,34 @@ assert.match(workflow, /agent-task-artifacts/)
4040
assert.match(workflow, /prepare-agent-task-upload\.mjs/)
4141
assert.match(workflow, /agent-task-upload/)
4242
assert.match(workflow, /if: always\(\)/)
43-
const helperRevision = workflow.match(/WP_CODEBOX_HELPER_REVISION:\s*([0-9a-f]{40})/)?.[1]
44-
assert.ok(helperRevision, "The workflow must declare one full immutable WP Codebox helper revision")
45-
assert.match(workflow, /ref: \$\{\{ env\.WP_CODEBOX_HELPER_REVISION \}\}/)
46-
assert.doesNotMatch(workflow, /github\.(?:workflow_sha|workflow_ref)/)
43+
assert.match(workflow, /WP_CODEBOX_RELEASE_REF: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
44+
assert.match(workflow, /\^v\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$/)
45+
assert.match(workflow, /WORKFLOW_REF: \$\{\{ github\.workflow_ref \}\}/)
46+
assert.match(workflow, /The reusable workflow tag and wp_codebox_release_ref must match exactly/)
47+
assert.match(workflow, /repository: Automattic\/wp-codebox/)
48+
assert.match(workflow, /ref: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
49+
assert.match(workflow, /Verify WP Codebox workflow helper release/)
50+
assert.match(workflow, /git ls-remote --exit-code --refs origin "refs\/tags\/\$\{WP_CODEBOX_RELEASE_REF\}"/)
51+
assert.match(workflow, /checked_out_commit.*remote_tag_commit/)
52+
assert.match(workflow, /JSON\.parse\(readFileSync\("package\.json", "utf8"\)\)\.version/)
4753
assert.doesNotMatch(workflow, /steps\.[^.]+\.outputs\.ref/)
48-
const foreignCallerSha = "ba2df8c2215407b1a58edbff29e3ddcb5efa2249"
49-
assert.notEqual(helperRevision, foreignCallerSha, "A caller repository SHA must not select WP Codebox helpers")
50-
assert.doesNotMatch(workflow, new RegExp(foreignCallerSha))
51-
assert.match(workflow, /Validate pinned WP Codebox helper revision/)
54+
assert.match(workflow, /Validate coherent WP Codebox release tag/)
5255

53-
const requiredHelperFiles = {
54-
".github/scripts/run-agent-task/build-codebox-task-request.mjs": "ed1fbc144428bfbf222810a8a467390e8cd5b45daa0f3e64e03162949d6c84df",
55-
".github/scripts/run-agent-task/execute-native-agent-task.mjs": "324c6d12dd01880bc9e4cd880ea6c713cb1bfd630b4925c53d78af7283e4a459",
56-
".github/scripts/run-agent-task/prepare-agent-task-upload.mjs": "93511e4174e705f55ff014ba68ac52fdaca771ce3f19f3921cceb661e69569da",
56+
const parseConsumerReleaseTags = (consumer: string) => ({
57+
workflow: consumer.match(/uses: Automattic\/wp-codebox\/\.github\/workflows\/run-agent-task\.yml@([^\s]+)/)?.[1],
58+
helpers: consumer.match(/wp_codebox_release_ref: ([^\s]+)/)?.[1],
59+
})
60+
const isExactReleaseTag = (value: string | undefined) => /^v\d+\.\d+\.\d+$/.test(value ?? "")
61+
const isCoherentConsumer = (consumer: string) => {
62+
const { workflow: workflowTag, helpers: helperTag } = parseConsumerReleaseTags(consumer)
63+
return isExactReleaseTag(workflowTag) && workflowTag === helperTag
5764
}
58-
for (const [path, digest] of Object.entries(requiredHelperFiles)) {
59-
const { stdout } = await execFileAsync("git", ["show", `${helperRevision}:${path}`], { encoding: "buffer" })
60-
const actualDigest = createHash("sha256").update(stdout).digest("hex")
61-
assert.equal(actualDigest, digest, `Pinned helper source changed unexpectedly: ${path}`)
65+
const coherentConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer.yml", import.meta.url), "utf8")
66+
const mismatchedConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer-mismatched.yml", import.meta.url), "utf8")
67+
assert.equal(isCoherentConsumer(coherentConsumer), true, "An exact matching workflow and helper release tag must succeed")
68+
assert.equal(isCoherentConsumer(mismatchedConsumer), false, "Mismatched workflow and helper release tags must fail")
69+
for (const invalidRef of ["main", "v0", "v0.12", "v0.12.3-rc.1", "0123456789abcdef0123456789abcdef01234567"]) {
70+
assert.equal(isExactReleaseTag(invalidRef), false, `Non-release ref must fail: ${invalidRef}`)
6271
}
6372
assert.doesNotMatch(publicWorkflowSurface, /step_budget:|tool_results_key:/)
6473
assert.doesNotMatch(workflow, /steps\.plan\.outputs/)
@@ -67,7 +76,9 @@ assert.doesNotMatch(publicWorkflowSurface, /mount|component path|ability id|prov
6776

6877
const docs = await readFile(new URL("../docs/agent-task-reusable-workflow.md", import.meta.url), "utf8")
6978
assert.match(docs, /^# Agent Task Reusable Workflow/m)
70-
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@main/)
79+
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@v0\.12\.3/)
80+
assert.match(docs, /wp_codebox_release_ref: v0\.12\.3/)
81+
assert.match(docs, /branches, commit SHAs, moving major tags, prereleases, and arbitrary\nrefs are rejected/)
7182
assert.match(docs, /external_package_source/)
7283
assert.match(docs, /runner_workspace/)
7384
assert.match(docs, /access_token_repos/)

0 commit comments

Comments
 (0)