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
9 changes: 1 addition & 8 deletions .github/workflows/run-agent-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,12 @@ jobs:
credential_mode: ${{ steps.execute.outputs.credential_mode }}
declared_artifacts_json: ${{ steps.execute.outputs.declared_artifacts_json }}
steps:
- name: Validate coherent WP Codebox release tag
env:
WORKFLOW_REF: ${{ github.workflow_ref }}
- name: Validate WP Codebox release tag
run: |
if ! [[ "${WP_CODEBOX_RELEASE_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "wp_codebox_release_ref must be an immutable vX.Y.Z release tag" >&2
exit 1
fi
expected_workflow_ref="Automattic/wp-codebox/.github/workflows/run-agent-task.yml@${WP_CODEBOX_RELEASE_REF}"
if [[ "${WORKFLOW_REF}" != "${expected_workflow_ref}" ]]; then
echo "The reusable workflow tag and wp_codebox_release_ref must match exactly" >&2
exit 1
fi

- name: Checkout WP Codebox workflow helpers
uses: actions/checkout@v4
Expand Down
20 changes: 12 additions & 8 deletions docs/agent-task-reusable-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ with `wp_codebox_release_ref: v0.12.3`. The accepted format is exactly
`vX.Y.Z`; branches, commit SHAs, moving major tags, prereleases, and arbitrary
refs are rejected.

The workflow validates that its own `uses:` reference and
`wp_codebox_release_ref` match exactly before checkout. It always checks helpers
out from `Automattic/wp-codebox`, verifies the checked-out commit equals the
remote release tag commit, and verifies the checked-out `package.json` version
equals the requested tag without its `v` prefix. The caller cannot select a
different helper repository.
The workflow requires `wp_codebox_release_ref` to be an exact release tag. It
always checks helpers out from `Automattic/wp-codebox`, verifies the checked-out
commit equals the remote release tag commit, and verifies the checked-out
`package.json` version equals the requested tag without its `v` prefix. The
caller cannot select a different helper repository. GitHub nested workflows
expose the caller's `github.workflow_ref`, and the running workflow cannot
introspect its own `uses:` ref, so helper selection relies on the required input
and verified checkout rather than that caller context.

This release-coherence contract fixes [#1759](https://github.com/Automattic/wp-codebox/issues/1759).

## Inputs

- `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.
- `wp_codebox_release_ref`: required exact immutable WP Codebox release tag in `vX.Y.Z` form.
- `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.
- `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.
- `target_repo`: `OWNER/REPO` target repository.
Expand Down Expand Up @@ -173,7 +175,9 @@ remain caller-workflow responsibilities. This is an intentional exposed-workflow

`wp_codebox_release_ref` is a required v1 input. Existing callers must switch
their `uses:` reference from a branch or other ref to an exact release tag and
pass that identical tag through this input.
pass that exact release tag through this input. Consumer contract tests can
compare both values where the caller workflow is available; the reusable
workflow itself cannot inspect the caller's `uses:` declaration at runtime.

## Upload safety limits

Expand Down
15 changes: 15 additions & 0 deletions fixtures/agent-task-reusable-workflow-build-run-29295530010.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Regression fixture from Build run 29295530010. In a nested workflow GitHub
# exposed this caller context, not the reusable workflow's own uses ref.
# github.workflow_ref: Automattic/build-with-wordpress/.github/workflows/build.yml@trunk
name: Build consumer with foreign caller workflow ref

on: workflow_dispatch

jobs:
run-agent-task:
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.4
with:
wp_codebox_release_ref: v0.12.4
external_package_source: '{"repository":"Automattic/example-agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
target_repo: Automattic/example-target
prompt: Refresh the configured surface.
9 changes: 6 additions & 3 deletions tests/agent-task-reusable-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,15 @@ assert.match(workflow, /agent-task-upload/)
assert.match(workflow, /if: always\(\)/)
assert.match(workflow, /WP_CODEBOX_RELEASE_REF: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
assert.match(workflow, /\^v\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$/)
assert.match(workflow, /WORKFLOW_REF: \$\{\{ github\.workflow_ref \}\}/)
assert.match(workflow, /The reusable workflow tag and wp_codebox_release_ref must match exactly/)
assert.doesNotMatch(workflow, /github\.workflow_ref|WORKFLOW_REF|expected_workflow_ref/)
assert.match(workflow, /repository: Automattic\/wp-codebox/)
assert.match(workflow, /ref: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
assert.match(workflow, /Verify WP Codebox workflow helper release/)
assert.match(workflow, /git ls-remote --exit-code --refs origin "refs\/tags\/\$\{WP_CODEBOX_RELEASE_REF\}"/)
assert.match(workflow, /checked_out_commit.*remote_tag_commit/)
assert.match(workflow, /JSON\.parse\(readFileSync\("package\.json", "utf8"\)\)\.version/)
assert.doesNotMatch(workflow, /steps\.[^.]+\.outputs\.ref/)
assert.match(workflow, /Validate coherent WP Codebox release tag/)
assert.match(workflow, /Validate WP Codebox release tag/)

const parseConsumerReleaseTags = (consumer: string) => ({
workflow: consumer.match(/uses: Automattic\/wp-codebox\/\.github\/workflows\/run-agent-task\.yml@([^\s]+)/)?.[1],
Expand All @@ -64,8 +63,11 @@ const isCoherentConsumer = (consumer: string) => {
}
const coherentConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer.yml", import.meta.url), "utf8")
const mismatchedConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer-mismatched.yml", import.meta.url), "utf8")
const buildRunConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-build-run-29295530010.yml", import.meta.url), "utf8")
assert.equal(isCoherentConsumer(coherentConsumer), true, "An exact matching workflow and helper release tag must succeed")
assert.equal(isCoherentConsumer(mismatchedConsumer), false, "Mismatched workflow and helper release tags must fail")
assert.match(buildRunConsumer, /github\.workflow_ref: Automattic\/build-with-wordpress\/\.github\/workflows\/build\.yml@trunk/)
assert.equal(isCoherentConsumer(buildRunConsumer), true, "A foreign caller workflow_ref must not affect the paired release tags")
for (const invalidRef of ["main", "v0", "v0.12", "v0.12.3-rc.1", "0123456789abcdef0123456789abcdef01234567"]) {
assert.equal(isExactReleaseTag(invalidRef), false, `Non-release ref must fail: ${invalidRef}`)
}
Expand All @@ -79,6 +81,7 @@ assert.match(docs, /^# Agent Task Reusable Workflow/m)
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@v0\.12\.3/)
assert.match(docs, /wp_codebox_release_ref: v0\.12\.3/)
assert.match(docs, /branches, commit SHAs, moving major tags, prereleases, and arbitrary\nrefs are rejected/)
assert.match(docs, /GitHub nested workflows\s+expose the caller's `github\.workflow_ref`, and the running workflow cannot\s+introspect its own `uses:` ref/)
assert.match(docs, /external_package_source/)
assert.match(docs, /runner_workspace/)
assert.match(docs, /access_token_repos/)
Expand Down
Loading