Skip to content

Commit 028eacf

Browse files
authored
Allow Docs Agent runner contract cutover (#1752)
* Make runner recipe optional for Docs Agent cutover * Fail closed without runner recipe * Correct Docs Agent merge-order link
1 parent df634b9 commit 028eacf

4 files changed

Lines changed: 95 additions & 25 deletions

File tree

.github/scripts/run-agent-task/build-codebox-task-request.mjs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ function requiredString(name) {
2020
return value
2121
}
2222

23+
function optionalString(name) {
24+
const value = process.env[name]?.trim() ?? ""
25+
return value || undefined
26+
}
27+
2328
function booleanEnv(name) {
2429
return process.env[name] === "true"
2530
}
@@ -37,9 +42,17 @@ function output(name, value) {
3742
}
3843

3944
const artifactDeclarations = parseJson("ARTIFACT_DECLARATIONS", [], "array")
45+
const runnerRecipe = optionalString("RUNNER_RECIPE")
46+
const runAgent = booleanEnv("RUN_AGENT")
47+
const dryRun = booleanEnv("DRY_RUN")
48+
49+
if (!runnerRecipe && runAgent && !dryRun) {
50+
throw new Error("RUNNER_RECIPE may be omitted only when RUN_AGENT=false or DRY_RUN=true. The executable workflow in wp-codebox PR #1751 must land before a live agent run can omit it.")
51+
}
52+
4053
const request = {
4154
schema: "wp-codebox/agent-task-workflow-request/v1",
42-
runner_recipe: requiredString("RUNNER_RECIPE"),
55+
...(runnerRecipe ? { runner_recipe: runnerRecipe } : {}),
4356
agent_bundle: requiredString("AGENT_BUNDLE"),
4457
workload: {
4558
id: process.env.WORKLOAD_ID || "agent-task",
@@ -85,12 +98,12 @@ const request = {
8598
projections: parseJson("OUTPUT_PROJECTIONS", {}, "object"),
8699
},
87100
callback_data: parseJson("CALLBACK_DATA", {}, "object"),
88-
run_agent: booleanEnv("RUN_AGENT"),
89-
dry_run: booleanEnv("DRY_RUN"),
101+
run_agent: runAgent,
102+
dry_run: dryRun,
90103
}
91104

92105
const runId = `${request.workload.id}-${process.env.GITHUB_RUN_ID || "local"}`.replace(/[^A-Za-z0-9._-]+/g, "-")
93-
const status = request.run_agent && !request.dry_run ? "planned" : "skipped"
106+
const status = !request.run_agent ? "skipped" : request.dry_run ? "dry-run" : "planned"
94107
const result = {
95108
schema: "wp-codebox/agent-task-workflow-result/v1",
96109
run_id: runId,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ name: Run Agent Task (reusable)
44
workflow_call:
55
inputs:
66
runner_recipe:
7-
description: Codebox runner recipe descriptor, such as OWNER/REPO@ref:path.
7+
description: Optional temporary Codebox runner recipe descriptor, such as OWNER/REPO@ref:path.
88
type: string
9-
required: true
9+
required: false
1010
agent_bundle:
1111
description: Repository-relative path to the agent bundle selected by the caller.
1212
type: string

docs/agent-task-reusable-workflow.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ jobs:
77
run-agent-task:
88
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@main
99
with:
10-
runner_recipe: Automattic/example-runner@main:ci/runner-recipe.json
1110
agent_bundle: bundles/example-agent
1211
workload_id: example-maintenance
1312
workload_label: Run example maintenance
@@ -53,24 +52,26 @@ jobs:
5352
secrets: inherit
5453
```
5554
56-
Consumers provide product-level task inputs: the selected runner recipe, agent
57-
bundle, target repository, workspace publication request, verification commands,
58-
drift checks, artifact expectations, typed artifact declarations, and output
59-
projection. The workflow returns stable run outputs; implementation-specific
60-
runtime wiring, workspace adapters, plugins, and model setup stay behind the WP
61-
Codebox boundary.
55+
Consumers provide product-level task inputs: the agent bundle, target repository,
56+
workspace publication request, verification commands, drift checks, artifact
57+
expectations, typed artifact declarations, and output projection. The workflow
58+
returns stable run outputs; implementation-specific runtime wiring, workspace
59+
adapters, plugins, and model setup stay behind the WP Codebox boundary.
6260
6361
## Runner Recipe
6462
65-
`runner_recipe` is a descriptor for a committed runner recipe, such as
66-
`Automattic/example-runner@main:ci/runner-recipe.json`. The recipe stays owned by
67-
the product workflow. Consumers pass the descriptor and the selected
68-
`agent_bundle`; they do not pass worker filesystem paths, runtime substrate
69-
checkout rules, package internals, or private workflow names.
63+
`runner_recipe` is a temporary optional input while callers transition away from
64+
the runner-recipe contract. It may be omitted only for an explicit
65+
`run_agent: false` skipped result or `dry_run: true` dry-run result. A live
66+
`run_agent: true` request without a recipe fails closed until the executable
67+
[wp-codebox#1751](https://github.com/Automattic/wp-codebox/pull/1751) workflow
68+
lands. Merge the transition in this order: this bridge, Docs Agent caller cleanup
69+
([docs-agent#119](https://github.com/Automattic/docs-agent/pull/119)), then #1751,
70+
which deletes this input.
7071

7172
## Inputs
7273

73-
- `runner_recipe`: committed runner recipe descriptor.
74+
- `runner_recipe`: optional temporary runner recipe descriptor; removed by #1751 after Docs Agent caller cleanup.
7475
- `agent_bundle`: selected agent bundle path in the product repository.
7576
- `workload_id`, `workload_label`, and `component_id`: caller-owned run labels.
7677
- `target_repo`: `OWNER/REPO` target repository.
@@ -96,6 +97,6 @@ checkout rules, package internals, or private workflow names.
9697
- `declared_artifacts_json`: typed artifact declarations accepted for the run.
9798

9899
The workflow is intentionally product-input-first. Consumers should model new
99-
behavior as runner recipe fields or workflow inputs instead of depending on
100-
worker filesystem paths, runtime internals, package internals, or the private
101-
implementation that executes the task.
100+
behavior as workflow inputs instead of depending on worker filesystem paths,
101+
runtime internals, package internals, or the private implementation that
102+
executes the task.

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const publicWorkflowSurface = workflow.slice(0, workflow.indexOf("jobs:"))
1313
assert.match(workflow, /^name: Run Agent Task \(reusable\)$/m)
1414
assert.match(workflow, /workflow_call:/)
1515
assert.match(workflow, /runner_recipe:/)
16+
assert.match(workflow, /runner_recipe:\n\s+description:[^\n]*temporary[^\n]*\n\s+type: string\n\s+required: false/)
1617
assert.match(workflow, /agent_bundle:/)
1718
assert.match(workflow, /runner_workspace:/)
1819
assert.match(workflow, /artifact_declarations:/)
@@ -31,12 +32,14 @@ const docs = await readFile(new URL("../docs/agent-task-reusable-workflow.md", i
3132
assert.match(docs, /^# Agent Task Reusable Workflow/m)
3233
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@main/)
3334
assert.match(docs, /runner_recipe/)
35+
assert.match(docs, /temporary optional input/)
36+
assert.match(docs, /fails closed until the executable\s+\[wp-codebox#1751\]/)
3437
assert.match(docs, /agent_bundle/)
3538
assert.match(docs, /runner_workspace/)
3639
assert.match(docs, /access_token_repos/)
3740
assert.match(docs, /require_access_token/)
38-
assert.match(docs, /implementation-specific\s+runtime wiring, workspace adapters, plugins, and model setup stay behind the WP\s+Codebox boundary/)
39-
assert.doesNotMatch(docs, /docs-agent|wp-codebox\/docs-agent-runner-recipe\/v1|recipe_path|recipe_json|wp_codebox_ref|datamachine|data machine|data-machine|agents api|sandbox mounts|ability ids|provider internals|homeboy|require_app_token/i)
41+
assert.match(docs, /implementation-specific\s+runtime\s+wiring,\s+workspace\s+adapters,\s+plugins,\s+and\s+model\s+setup\s+stay\s+behind\s+the\s+WP\s+Codebox\s+boundary/)
42+
assert.doesNotMatch(docs, /wp-codebox\/docs-agent-runner-recipe\/v1|recipe_path|recipe_json|wp_codebox_ref|datamachine|data machine|data-machine|agents api|sandbox mounts|ability ids|provider internals|homeboy|require_app_token/i)
4043

4144
const tmp = await mkdtemp(join(tmpdir(), "wp-codebox-agent-task-workflow-"))
4245
const outputPath = join(tmp, "github-output.txt")
@@ -60,7 +63,7 @@ await execFileAsync("node", [new URL("../.github/scripts/run-agent-task/build-co
6063
WRITABLE_PATHS: "README.md,docs/**",
6164
PROVIDER: "openai",
6265
MODEL: "gpt-5.5",
63-
RUNNER_WORKSPACE: '{"enabled":true,"repo":"Automattic/example-target"}',
66+
RUNNER_WORKSPACE_CONFIG: '{"enabled":true,"repo":"Automattic/example-target"}',
6467
VALIDATION_DEPENDENCIES: "",
6568
CONTEXT_REPOSITORIES: "[]",
6669
VERIFICATION_COMMANDS: '[{"command":"npm test","description":"Run checks"}]',
@@ -105,4 +108,57 @@ assert.match(outputs, /credential_mode<<__WP_CODEBOX_OUTPUT__\napp-token\n__WP_C
105108
assert.match(outputs, /request_path<<__WP_CODEBOX_OUTPUT__\n\.codebox\/agent-task-request\.json\n__WP_CODEBOX_OUTPUT__/)
106109
assert.match(outputs, /result_path<<__WP_CODEBOX_OUTPUT__\n\.codebox\/agent-task-workflow-result\.json\n__WP_CODEBOX_OUTPUT__/)
107110

111+
async function runTaskRequest(runnerRecipe, runAgent, dryRun) {
112+
const cwd = await mkdtemp(join(tmpdir(), "wp-codebox-agent-task-workflow-no-recipe-"))
113+
const outputPath = join(cwd, "github-output.txt")
114+
await writeFile(outputPath, "")
115+
116+
const run = execFileAsync("node", [new URL("../.github/scripts/run-agent-task/build-codebox-task-request.mjs", import.meta.url).pathname], {
117+
cwd,
118+
env: {
119+
...process.env,
120+
GITHUB_OUTPUT: outputPath,
121+
RUNNER_RECIPE: runnerRecipe,
122+
AGENT_BUNDLE: "bundles/example-agent",
123+
TARGET_REPO: "Automattic/example-target",
124+
PROMPT: "Update the configured surface.",
125+
MAX_TURNS: "12",
126+
STEP_BUDGET: "16",
127+
TIME_BUDGET_MS: "600000",
128+
RUN_AGENT: String(runAgent),
129+
DRY_RUN: String(dryRun),
130+
},
131+
})
132+
133+
return { cwd, outputPath, run }
134+
}
135+
136+
for (const { runAgent, dryRun, status } of [
137+
{ runAgent: false, dryRun: false, status: "skipped" },
138+
{ runAgent: false, dryRun: true, status: "skipped" },
139+
{ runAgent: true, dryRun: true, status: "dry-run" },
140+
]) {
141+
const omittedRecipe = await runTaskRequest("", runAgent, dryRun)
142+
await omittedRecipe.run
143+
144+
const omittedRecipeRequest = JSON.parse(await readFile(join(omittedRecipe.cwd, ".codebox", "agent-task-request.json"), "utf8"))
145+
const omittedRecipeResult = JSON.parse(await readFile(join(omittedRecipe.cwd, ".codebox", "agent-task-workflow-result.json"), "utf8"))
146+
assert.equal(Object.hasOwn(omittedRecipeRequest, "runner_recipe"), false)
147+
assert.equal(omittedRecipeResult.status, status)
148+
assert.match(await readFile(omittedRecipe.outputPath, "utf8"), new RegExp(`job_status<<__WP_CODEBOX_OUTPUT__\\n${status}\\n__WP_CODEBOX_OUTPUT__`))
149+
}
150+
151+
const recipeBackedLiveRun = await runTaskRequest("Automattic/example-runner@abc123:ci/runner-recipe.json", true, false)
152+
await recipeBackedLiveRun.run
153+
const recipeBackedLiveResult = JSON.parse(await readFile(join(recipeBackedLiveRun.cwd, ".codebox", "agent-task-workflow-result.json"), "utf8"))
154+
assert.equal(recipeBackedLiveResult.status, "planned")
155+
156+
const omittedRecipeLiveRun = await runTaskRequest("", true, false)
157+
await assert.rejects(omittedRecipeLiveRun.run, (error: { stderr: string }) => {
158+
assert.match(error.stderr, /RUNNER_RECIPE may be omitted only when RUN_AGENT=false or DRY_RUN=true/)
159+
assert.match(error.stderr, /executable workflow in wp-codebox PR #1751 must land/)
160+
return true
161+
})
162+
assert.equal(await readFile(omittedRecipeLiveRun.outputPath, "utf8"), "")
163+
108164
console.log("agent task reusable workflow ok")

0 commit comments

Comments
 (0)