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
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ jobs:
contents: write
id-token: write
attestations: write
issues: write
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -735,3 +737,22 @@ jobs:
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6
with:
subject-path: dist/*.tar.gz
- name: Reconcile Release Please lineage
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pr_number=$(jq -er '.pullRequest | select(type == "number" and . > 0 and . == floor)' persisted-candidate.json)
labels_endpoint="repos/${GITHUB_REPOSITORY}/issues/${pr_number}/labels"

gh api --method POST "$labels_endpoint" -f 'labels[]=autorelease: tagged' >/dev/null
current_labels=$(gh api "$labels_endpoint" --jq '.[].name')
if grep -Fxq 'autorelease: pending' <<< "$current_labels"; then
gh api --method DELETE "${labels_endpoint}/autorelease%3A%20pending" >/dev/null
fi

current_labels=$(gh api "$labels_endpoint" --jq '.[].name')
if ! grep -Fxq 'autorelease: tagged' <<< "$current_labels" || grep -Fxq 'autorelease: pending' <<< "$current_labels"; then
echo "Release Please lineage did not converge for PR ${pr_number}." >&2
exit 1
fi
120 changes: 119 additions & 1 deletion scripts/release-validate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { cpSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"
import {
chmodSync,
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs"
import { tmpdir } from "node:os"
import { basename, join, resolve } from "node:path"

Expand Down Expand Up @@ -367,6 +375,116 @@ test("release workflow is pinned and publishes proven assets after validation",
expect(workflow).not.toContain("endswith($repository)")
})

test("successful publication closes the provenance-bound Release Please lineage", () => {
const workflowSource = readFileSync(join(root, ".github", "workflows", "release.yml"), "utf8")
const workflow = Bun.YAML.parse(workflowSource) as {
jobs: {
release: {
permissions: Record<string, string>
steps: Array<{ name?: string; run?: string }>
}
}
}
const releaseJob = workflow.jobs.release
const lineageStep = releaseJob.steps.find(
(step) => step.name === "Reconcile Release Please lineage",
)

expect(releaseJob.permissions.issues).toBe("write")
expect(releaseJob.permissions["pull-requests"]).toBe("read")
expect(lineageStep?.run).toContain("persisted-candidate.json")
expect(lineageStep?.run).toContain("autorelease: tagged")
expect(lineageStep?.run).toContain("autorelease%3A%20pending")
expect(lineageStep?.run).toContain("Release Please lineage did not converge")
expect(lineageStep?.run).toBeString()

const temporaryRoot = mkdtempSync(join(tmpdir(), "release-lineage-"))
const fakeBin = join(temporaryRoot, "bin")
const labelsPath = join(temporaryRoot, "labels.txt")
const logPath = join(temporaryRoot, "gh.log")
try {
mkdirSync(fakeBin)
writeFileSync(join(temporaryRoot, "persisted-candidate.json"), '{"pullRequest":18}\n')
writeFileSync(labelsPath, "autorelease: pending\n")
const fakeGh = join(fakeBin, "gh")
writeFileSync(
fakeGh,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$GH_FAKE_LOG"
[[ "$*" == *"repos/myagentdojo/agent-plugin-template/issues/18/labels"* ]]
if [[ "$*" == *"--method POST"* ]]; then
label=""
for argument in "$@"; do
case "$argument" in
'labels[]='*) label="\${argument#*=}" ;;
esac
done
if [[ -z "$label" ]]; then
echo 'missing labels[] POST field' >&2
exit 2
fi
grep -Fxq "$label" "$GH_FAKE_LABELS" || printf '%s\\n' "$label" >> "$GH_FAKE_LABELS"
elif [[ "$*" == *"--method DELETE"* ]]; then
if [[ "\${GH_FAKE_MODE:-}" == "delete_forbidden" ]]; then
echo 'gh: Forbidden (HTTP 403)' >&2
exit 1
fi
if [[ "\${GH_FAKE_MODE:-}" == "stale_labels" ]]; then
exit 0
fi
if ! grep -Fxq 'autorelease: pending' "$GH_FAKE_LABELS"; then
echo 'gh: Not Found (HTTP 404)' >&2
exit 1
fi
grep -Fxv 'autorelease: pending' "$GH_FAKE_LABELS" > "$GH_FAKE_LABELS.next" || true
mv "$GH_FAKE_LABELS.next" "$GH_FAKE_LABELS"
else
cat "$GH_FAKE_LABELS"
fi
`,
)
chmodSync(fakeGh, 0o755)

const runLineage = (mode = "") =>
Bun.spawnSync({
cmd: ["bash", "-c", lineageStep?.run ?? ""],
cwd: temporaryRoot,
env: {
...process.env,
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
GITHUB_REPOSITORY: "myagentdojo/agent-plugin-template",
GH_FAKE_LABELS: labelsPath,
GH_FAKE_LOG: logPath,
GH_FAKE_MODE: mode,
},
stdout: "pipe",
stderr: "pipe",
})

for (let attempt = 0; attempt < 2; attempt += 1) {
const result = runLineage()
expect(result.exitCode, result.stderr.toString()).toBe(0)
expect(readFileSync(labelsPath, "utf8")).toBe("autorelease: tagged\n")
}
expect(readFileSync(logPath, "utf8").match(/--method DELETE/g)).toHaveLength(1)

writeFileSync(labelsPath, "autorelease: pending\n")
const deleteFailure = runLineage("delete_forbidden")
expect(deleteFailure.exitCode).not.toBe(0)
expect(deleteFailure.stderr.toString()).toContain("Forbidden (HTTP 403)")

writeFileSync(labelsPath, "autorelease: pending\n")
const staleLabels = runLineage("stale_labels")
expect(staleLabels.exitCode).not.toBe(0)
expect(staleLabels.stderr.toString()).toContain(
"Release Please lineage did not converge for PR 18.",
)
} finally {
rmSync(temporaryRoot, { recursive: true, force: true })
}
})

test("immutable tag creation is retry-safe and rejects a rebound candidate", () => {
const workflowSource = readFileSync(join(root, ".github", "workflows", "release.yml"), "utf8")
const workflow = Bun.YAML.parse(workflowSource) as {
Expand Down