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: 6 additions & 3 deletions .github/scripts/run-agent-task/runner-workspace-publisher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ export async function publishRunnerWorkspace({ request, changedFiles, publicatio
const allowed = Array.isArray(record(request.access).allowed_repos) ? request.access.allowed_repos.map((value) => string(value).toLowerCase()) : []
if (!token) throw new Error("No GitHub token is available for runner workspace publication.")
if (!targetRepo || targetRepo !== configuredRepo || !allowed.includes(targetRepo)) throw new Error("Runner workspace publication repository is not authorized.")
const base = string(config.base || config.base_branch || "main")
let base = string(config.base) || string(config.base_branch)
const hasConfiguredBase = Boolean(base)
const prefix = string(config.branch_prefix || "wp-codebox/agent-task/")
const runId = string(config.run_id || request.workload?.id || "agent-task").replace(/[^A-Za-z0-9._/-]+/g, "-")
const head = `${prefix}${runId}`
if (!/^[A-Za-z0-9._/-]+$/.test(prefix) || !head.startsWith(prefix) || head.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(base)) throw new Error("Runner workspace branch configuration is invalid.")

const api = async (method, path, body) => {
const response = await fetchImpl(`https://api.github.com/repos/${targetRepo}${path}`, { method, headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${token}`, "X-GitHub-Api-Version": "2022-11-28", ...(body ? { "Content-Type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) })
const payload = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(`GitHub API ${method} ${path} failed with ${response.status}.`)
return payload
}
if (!base) base = string((await api("GET", "")).default_branch)
if (!/^[A-Za-z0-9._/-]+$/.test(prefix) || !head.startsWith(prefix) || head.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(base)) {
throw new Error(hasConfiguredBase ? "Runner workspace branch configuration is invalid." : "Runner workspace branch configuration is invalid: repository metadata must provide a valid default branch when base is omitted.")
}
let existing = null
try { existing = await api("GET", `/git/ref/heads/${head.split("/").map(encodeURIComponent).join("/")}`) } catch (error) { if (!String(error.message).includes(" 404.")) throw error }
// Existing PR branches are append-only publication targets. Their current tree
Expand Down
34 changes: 30 additions & 4 deletions tests/runner-workspace-publisher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,51 @@ const request = { target_repo: "owner/repo", workload: { id: "run-1", label: "Up
const publicationFiles = [{ path: "README.md", mode: "100644", content: Buffer.from("changed\n").toString("base64"), deleted: false }]

function response(status, body) { return { ok: status >= 200 && status < 300, status, json: async () => body } }
function fetchMock(existing = false) {
function fetchMock(existing = false, base = "main", defaultBranch) {
const calls = []
return { calls, fetch: async (url, init) => {
calls.push([url, init.method, init.body ? JSON.parse(init.body) : undefined])
const parsed = new URL(url)
const path = parsed.pathname.replace("/repos/owner/repo", "")
if (path === "/git/ref/heads/main") return response(200, { object: { sha: "base" } })
if (path === "") return response(200, { default_branch: defaultBranch })
if (path === `/git/ref/heads/${base}`) return response(200, { object: { sha: "base" } })
if (path === "/git/commits/base" || path === "/git/commits/old") return response(200, { tree: { sha: path.endsWith("old") ? "prior-tree" : "tree" } })
if (path.includes("/git/ref/heads/wp-codebox/agent-task/run-1")) return existing ? response(200, { object: { sha: "old" } }) : response(404, {})
if (path === "/git/blobs") return response(201, { sha: "blob" })
if (path === "/git/trees") return response(201, { sha: "next-tree" })
if (path === "/git/commits") return response(201, { sha: "commit" })
if (path === "/git/refs") return response(201, {})
if (path.includes("/git/refs/heads/")) return response(200, {})
if (path === "/pulls" && parsed.search) return response(200, existing ? [{ number: 4, html_url: "https://github.com/owner/repo/pull/4", base: { repo: { full_name: "owner/repo" }, ref: "main" }, head: { ref: "wp-codebox/agent-task/run-1" } }] : [])
if (path === "/pulls") return response(201, { number: 5, html_url: "https://github.com/owner/repo/pull/5", base: { repo: { full_name: "owner/repo" }, ref: "main" }, head: { ref: "wp-codebox/agent-task/run-1" } })
if (path === "/pulls" && parsed.search) return response(200, existing ? [{ number: 4, html_url: "https://github.com/owner/repo/pull/4", base: { repo: { full_name: "owner/repo" }, ref: base }, head: { ref: "wp-codebox/agent-task/run-1" } }] : [])
if (path === "/pulls") return response(201, { number: 5, html_url: "https://github.com/owner/repo/pull/5", base: { repo: { full_name: "owner/repo" }, ref: base }, head: { ref: "wp-codebox/agent-task/run-1" } })
throw new Error(`unexpected ${path}`)
} }
}
{
const nonMainRequest = { ...request, runner_workspace: { ...request.runner_workspace, base: "release", base_branch: "legacy" } }
const mock = fetchMock(false, "release")
const result = await publishRunnerWorkspace({ request: nonMainRequest, changedFiles: ["README.md"], publicationFiles, token: "secret", fetchImpl: mock.fetch })
assert.equal(result.branch.base, "release", "explicit base must remain authoritative")
assert(!mock.calls.some(([url]) => new URL(url).pathname === "/repos/owner/repo"), "explicit base must not look up repository metadata")
}
{
const aliasRequest = { ...request, runner_workspace: { ...request.runner_workspace, base: undefined, base_branch: "stable" } }
const mock = fetchMock(false, "stable")
const result = await publishRunnerWorkspace({ request: aliasRequest, changedFiles: ["README.md"], publicationFiles, token: "secret", fetchImpl: mock.fetch })
assert.equal(result.branch.base, "stable", "base_branch remains a supported base alias")
assert(!mock.calls.some(([url]) => new URL(url).pathname === "/repos/owner/repo"), "base_branch must not look up repository metadata")
}
{
const metadataRequest = { ...request, runner_workspace: { ...request.runner_workspace, base: undefined, base_branch: undefined } }
const mock = fetchMock(false, "trunk", "trunk")
const result = await publishRunnerWorkspace({ request: metadataRequest, changedFiles: ["README.md"], publicationFiles, token: "secret", fetchImpl: mock.fetch })
assert.equal(result.branch.base, "trunk", "omitted base must use the repository default branch")
assert(mock.calls.some(([url]) => new URL(url).pathname === "/repos/owner/repo"), "omitted base must look up repository metadata")
}
for (const defaultBranch of [undefined, "invalid branch"]) {
const metadataRequest = { ...request, runner_workspace: { ...request.runner_workspace, base: undefined, base_branch: undefined } }
await assert.rejects(() => publishRunnerWorkspace({ request: metadataRequest, changedFiles: ["README.md"], publicationFiles, token: "secret", fetchImpl: fetchMock(false, "main", defaultBranch).fetch }), /metadata must provide a valid default branch/)
}

{
const mock = fetchMock(false)
Expand Down
Loading