| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; |
| 4 | import { tmpdir } from "node:os"; |
| 5 | import path from "node:path"; |
| 6 | import { spawnSync } from "node:child_process"; |
| 7 | import { verifyIdentity } from "./verify-release-tag-identity.mjs"; |
| 8 | |
| 9 | const rule = { id: 1, target: "tag", enforcement: "active", current_user_can_bypass: "always", conditions: { ref_name: { include: ["refs/tags/v*", "refs/tags/npm-v*", "refs/tags/desktop-v*"], exclude: [] } }, rules: [{ type: "creation" }, { type: "update" }, { type: "deletion" }] }; |
| 10 | const fixture = () => ({ actor: { login: "publisher", id: 42, type: "User" }, repo: { full_name: "esengine/DeepSeek-Reasonix", permissions: { push: true } }, rulesets: [structuredClone(rule)], expectedActor: "publisher", version: "1.2.3" }); |
| 11 | |
| 12 | test("authorized identity, unrelated rules, and immutable-only restrictions", () => { |
| 13 | assert.equal(verifyIdentity(fixture()).actorID, "42"); |
| 14 | const input = fixture(); |
| 15 | input.rulesets.push({ ...rule, id: 2, current_user_can_bypass: "never", conditions: { ref_name: { include: ["refs/tags/unrelated-*"], exclude: [] } } }); |
| 16 | assert.deepEqual(verifyIdentity(input).rulesets, [1]); |
| 17 | input.rulesets[0].rules = [{ type: "update" }, { type: "deletion" }]; |
| 18 | input.rulesets[0].current_user_can_bypass = "never"; |
| 19 | verifyIdentity(input); |
| 20 | }); |
| 21 | |
| 22 | test("the GH013 mechanism fails before approval, including inherited and unknown rules", () => { |
| 23 | for (const bypass of ["never", "pull_request", undefined]) { |
| 24 | const input = fixture(); |
| 25 | input.rulesets[0].current_user_can_bypass = bypass; |
| 26 | assert.throws(() => verifyIdentity(input), /cannot bypass/); |
| 27 | } |
| 28 | for (const pattern of ["~ALL", "refs/tags/*", "refs/tags/v[0-9]*", "refs/**", "~UNKNOWN"]) { |
| 29 | const input = fixture(); |
| 30 | input.rulesets.push({ ...rule, id: 2, source_type: "Organization", current_user_can_bypass: "never", conditions: { ref_name: { include: [pattern], exclude: [] } } }); |
| 31 | assert.throws(() => verifyIdentity(input), /ruleset 2/); |
| 32 | } |
| 33 | }); |
| 34 | |
| 35 | test("missing credentials, mismatched actor, token rotation, and repository permissions fail closed", () => { |
| 36 | for (const change of [ |
| 37 | { expectedActor: "" }, { expectedActor: "someone-else" }, { expectedID: "99" }, |
| 38 | { actor: { login: "publisher", id: 42, type: "Bot" } }, |
| 39 | { repo: { full_name: "esengine/DeepSeek-Reasonix", permissions: { push: false } } }, |
| 40 | ]) assert.throws(() => verifyIdentity({ ...fixture(), ...change })); |
| 41 | }); |
| 42 | |
| 43 | test("CLI uses credential-scoped paginated observations and exports the bound actor ID", () => { |
| 44 | const root = mkdtempSync(path.join(tmpdir(), "tag-identity-")); |
| 45 | try { |
| 46 | const gh = path.join(root, "gh"); |
| 47 | const input = fixture(); |
| 48 | writeFileSync(gh, `#!/usr/bin/env node |
| 49 | const args = process.argv.slice(2); |
| 50 | const responses = ${JSON.stringify({ user: input.actor, "repos/esengine/DeepSeek-Reasonix": input.repo, "repos/esengine/DeepSeek-Reasonix/rulesets?includes_parents=true&per_page=100": [[rule]], "repos/esengine/DeepSeek-Reasonix/rulesets/1?includes_parents=true": rule })}; |
| 51 | if (process.env.GH_TOKEN !== 'test-credential') process.exit(2); |
| 52 | if (args.at(-1).includes('per_page') && (!args.includes('--paginate') || !args.includes('--slurp'))) process.exit(3); |
| 53 | if (!(args.at(-1) in responses)) process.exit(4); |
| 54 | console.log(JSON.stringify(responses[args.at(-1)])); |
| 55 | `, { mode: 0o755 }); |
| 56 | const env = { ...process.env, PATH: `${root}:${process.env.PATH}`, GH_TOKEN: "test-credential", RELEASE_TAG_ACTOR: "publisher", RELEASE_TAG_ACTOR_ID: "", GITHUB_ACTIONS: "true", GITHUB_REF: "refs/heads/main-v2", GITHUB_REF_PROTECTED: "true", GITHUB_REPOSITORY: "esengine/DeepSeek-Reasonix", GITHUB_OUTPUT: path.join(root, "output") }; |
| 57 | const run = extra => spawnSync(process.execPath, ["scripts/verify-release-tag-identity.mjs", "1.2.3"], { env: { ...env, ...extra }, encoding: "utf8" }); |
| 58 | const result = run({}); |
| 59 | assert.equal(result.status, 0, result.stderr); |
| 60 | assert.equal(readFileSync(env.GITHUB_OUTPUT, "utf8"), "actor_id=42\n"); |
| 61 | assert.notEqual(run({ GH_TOKEN: "" }).status, 0); |
| 62 | assert.notEqual(run({ GITHUB_REF: "refs/heads/untrusted" }).status, 0); |
| 63 | assert.notEqual(run({ GITHUB_REF_PROTECTED: "false" }).status, 0); |
| 64 | } finally { rmSync(root, { recursive: true, force: true }); } |
| 65 | }); |
| 66 | |
| 67 | test("workflow binds preflight and activation to the same credential without checkout fallback", () => { |
| 68 | const workflow = readFileSync(".github/workflows/release-promote.yml", "utf8"); |
| 69 | const preflight = workflow.split("\n authorize:")[0]; |
| 70 | const activate = workflow.split("\n activate:")[1].split("\n cli:")[0]; |
| 71 | assert.match(preflight, /id: identity/); |
| 72 | assert.match(preflight, /secrets.RELEASE_TAG_TOKEN/); |
| 73 | assert.match(activate, /secrets.RELEASE_TAG_TOKEN/); |
| 74 | assert.match(activate, /needs.preflight.outputs.tag_actor_id/); |
| 75 | assert.match(activate, /persist-credentials: false/); |
| 76 | assert.ok(activate.indexOf("verify-release-tag-identity.mjs") < activate.indexOf("release-candidate-tags.sh activate")); |
| 77 | assert.match(activate, /gh auth setup-git --hostname github.com/); |
| 78 | }); |
| 79 |