ci: allow release verifier to read unpublished drafts
This commit is contained in:
@@ -197,21 +197,24 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
# GitHub hides drafts from tokens without push access. The commands below
|
||||
# remain GET-only; expose this token only to the two API-reading steps.
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
contents: write
|
||||
steps:
|
||||
# The tagged revision — verify exactly what was packaged, not a moving default.
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "26"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: List every release, drafts included
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
# --slurp wraps the paginated pages into one JSON document
|
||||
# ([[release,...],[release,...]]); the validator flattens it.
|
||||
run: gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases?per_page=100" > "$RUNNER_TEMP/releases.json"
|
||||
@@ -219,6 +222,8 @@ jobs:
|
||||
# Machine JSON on stdout (release id + asset ids); warnings on stderr.
|
||||
run: npx tsx scripts/verify-release.ts select "$RUNNER_TEMP/releases.json" --tag "$GITHUB_REF_NAME" > "$RUNNER_TEMP/draft.json"
|
||||
- name: Download the three update feeds
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/metadata"
|
||||
|
||||
@@ -53,7 +53,7 @@ The three jobs upload distinct updater feeds: `latest-mac.yml`, `latest.yml`, `l
|
||||
|
||||
### The verification job
|
||||
|
||||
`verify` runs only on tag pushes (`needs: package`, so after **all three** packaging legs; manual rehearsals upload nothing and skip it). With a read-only `contents: read` token it checks out the tagged revision and runs `scripts/verify-release.ts`, which:
|
||||
`verify` runs only on tag pushes (`needs: package`, so after **all three** packaging legs; manual rehearsals upload nothing and skip it). It checks out the tagged revision without persisting credentials and runs `scripts/verify-release.ts`. GitHub requires push access to list **draft** releases, so this job needs `contents: write` even though every API operation is GET-only. The token is exposed only to the release-list and feed-download steps, not dependency installation or the offline validator. The validator:
|
||||
|
||||
- finds the draft using the same tag matching as the draft job and electron-builder's publisher — `tag_name` equals `v<version>` **or** the bare `<version>`; zero matches, two matches, or an already-published match fail the run, and a draft matched only via the unprefixed form is flagged with a warning (a later run could create a second draft under the `v`-tag and split the assets — rename before publishing);
|
||||
- fails unless the release holds **exactly** these 11 assets — nothing missing, nothing extra (a stale name from an older run is an error), nothing zero-size, nothing still in a non-`uploaded` state:
|
||||
|
||||
@@ -312,17 +312,38 @@ describe("release gate — workflow wiring", () => {
|
||||
const workflow = yaml.load(fs.readFileSync(path.join(repoRoot, ".github", "workflows", "release.yml"), "utf8"));
|
||||
const verify = workflow.jobs.verify;
|
||||
|
||||
it("runs read-only after ALL packaging jobs, only on real tag pushes", () => {
|
||||
it("can see drafts after ALL packaging jobs, only on real tag pushes", () => {
|
||||
assert.ok(verify);
|
||||
assert.equal(verify.needs, "package");
|
||||
assert.equal(verify.if, "github.event_name == 'push'");
|
||||
assert.deepEqual(verify.permissions, { contents: "read" });
|
||||
assert.equal(verify.env.GH_TOKEN, "${{ github.token }}");
|
||||
// GitHub's release-list API hides drafts from read-only tokens.
|
||||
assert.deepEqual(verify.permissions, { contents: "write" });
|
||||
assert.equal(verify.env?.GH_TOKEN, undefined);
|
||||
});
|
||||
|
||||
it("exposes the elevated token only to the two API-reading steps", () => {
|
||||
const readers = ["List every release, drafts included", "Download the three update feeds"];
|
||||
for (const step of verify.steps) {
|
||||
if (readers.includes(step.name)) assert.equal(step.env?.GH_TOKEN, "${{ github.token }}");
|
||||
else assert.equal(step.env?.GH_TOKEN, undefined, String(step.name ?? step.uses));
|
||||
}
|
||||
assert.equal(verify.steps.filter((step: any) => step.env?.GH_TOKEN).length, 2);
|
||||
});
|
||||
|
||||
it("uses only implicit GET requests, never release-mutating commands", () => {
|
||||
const calls: string[] = verify.steps.flatMap((step: any) =>
|
||||
(step.run ?? "").split("\n").filter((line: string) => /^\s*gh\s/.test(line)),
|
||||
);
|
||||
assert.equal(calls.length, 2);
|
||||
for (const call of calls) {
|
||||
assert.match(call, /^\s*gh api /);
|
||||
assert.doesNotMatch(call, /(?:\s|^)(?:--(?:method|input|field|raw-field)(?:[=\s]|$)|-[XfF])/);
|
||||
}
|
||||
});
|
||||
|
||||
it("checks out the tagged revision and runs select + verify", () => {
|
||||
const checkout = verify.steps.find((step: any) => step.uses?.startsWith("actions/checkout"));
|
||||
assert.deepEqual(checkout.with, { ref: "${{ github.sha }}" });
|
||||
assert.deepEqual(checkout.with, { ref: "${{ github.sha }}", "persist-credentials": false });
|
||||
const select = verify.steps.find((step: any) => step.run?.includes("verify-release.ts select"));
|
||||
const final = verify.steps.find((step: any) => step.run?.includes("verify-release.ts verify"));
|
||||
assert.ok(select && final);
|
||||
|
||||
Reference in New Issue
Block a user