diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a60069..e93bdee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - name: Setup Node.js ${{ matrix.node-version }} @@ -89,6 +90,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - name: Setup Node.js diff --git a/README.md b/README.md index ce8ed0f..43d357b 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ it cannot force external automation to stop working. **Status:** public pre-release. The npm package remains unpublished (`private: true`, `0.1.0-dev`). The current Action release is [`v0.1.0-beta.5`](https://github.com/daichunghy/patchgate/releases/tag/v0.1.0-beta.5), -and consumers should pin the immutable commit shown on that release page for -**shadow** evaluation only. This is not production, not a `v0.1` claim, and -not evidence of external pilots or adoption. +and consumers should pin +`34d998bbd59fa09dd9081e24f22abe812f97fbab` for **shadow** evaluation only. +This is not production, not a `v0.1` claim, and not evidence of external +pilots or adoption. > If PatchGate saved you one wasted review, > [star it](https://github.com/daichunghy/patchgate/stargazers). That is the @@ -69,8 +70,8 @@ Longer walkthrough: [Getting started](docs/getting-started.md). The Action is bundled for the repository's local shadow workflow. The tagged pre-release [`v0.1.0-beta.5`](https://github.com/daichunghy/patchgate/releases/tag/v0.1.0-beta.5) -is the current release; pin its immutable commit SHA -for shadow evaluation. +is the current release; pin +`34d998bbd59fa09dd9081e24f22abe812f97fbab` for shadow evaluation. Production consumers must still wait for a stable public release. Do not use the placeholder `patchgate/patchgate@v0.1.0-dev` as an installable public reference. Consumer setup, permissions and the shadow workflow are documented diff --git a/docs/github-action-usage.md b/docs/github-action-usage.md index 0f63dbd..43b26e2 100644 --- a/docs/github-action-usage.md +++ b/docs/github-action-usage.md @@ -4,9 +4,10 @@ PatchGate contains a local/shadow GitHub Action candidate. It is not yet a released Marketplace action or a proven public `v0.1` distribution. For a real external shadow installation, use the [G4 shadow-installation -runbook](pilots/g4-shadow-installation-runbook.md). For beta publication and -rollback, use the [beta release runbook](releases/beta-release-and-rollback.md) -after the documented gates have been reviewed. +runbook](pilots/g4-shadow-installation-runbook.md). For release validation, +upgrade, downgrade and rollback, use the [beta release and rollback +guide](releases/beta-release-and-rollback.md) after the documented gates have +been reviewed. --- @@ -16,8 +17,8 @@ In **Shadow Mode**, PatchGate observes only (`fail-on: never`). It evaluates the PR, writes the `ContributionReceipt`, and can post a Check Run without blocking merge. Pin [`v0.1.0-beta.5`](https://github.com/daichunghy/patchgate/releases/tag/v0.1.0-beta.5) -for this pre-release and pin commit -the immutable commit SHA shown on that release page. This is not production and not a +for discovery, but install the immutable source commit +`34d998bbd59fa09dd9081e24f22abe812f97fbab`. This is not production and not a `v0.1` claim. The Action reads GitHub metadata through the API. Do **not** check out @@ -57,10 +58,10 @@ jobs: # github.token cannot read Administration, so native Rulesets / # branch-protection snapshots fail closed (correct). A PAT/App token # with administration:read is required for a complete native-control - # snapshot. beta.2 posts a Check Run for successful evaluations; - # snapshot-rejection Check Runs are included in beta.5. + # beta.5 posts a Check Run for successful evaluations and snapshot + # rejections when create-check-run is true. - name: Run PatchGate Shadow Gate - uses: daichunghy/patchgate@v0.1.0-beta.5 + uses: daichunghy/patchgate@34d998bbd59fa09dd9081e24f22abe812f97fbab # v0.1.0-beta.5 with: fail-on: never create-check-run: true @@ -76,7 +77,7 @@ workflow. It is still not production or a `v0.1` claim. ```yaml - name: Run PatchGate Enforcing Gate - uses: daichunghy/patchgate@v0.1.0-beta.5 + uses: daichunghy/patchgate@34d998bbd59fa09dd9081e24f22abe812f97fbab # v0.1.0-beta.5 with: fail-on: blocked create-check-run: true diff --git a/docs/releases/2026-08-23-beta.5.md b/docs/releases/2026-08-23-beta.5.md index 476a02f..0fd1ad5 100644 --- a/docs/releases/2026-08-23-beta.5.md +++ b/docs/releases/2026-08-23-beta.5.md @@ -3,10 +3,13 @@ **Release date:** 23 August 2026 **Tag:** [`v0.1.0-beta.5`](https://github.com/daichunghy/patchgate/releases/tag/v0.1.0-beta.5) +**Annotated tag object:** `c6e5fdcb95862d6db3909e9621fcbd25cbfb0e11` +**Action source commit:** `34d998bbd59fa09dd9081e24f22abe812f97fbab` + Beta.5 is the current documentation-synchronized public Action pre-release. It carries the beta.4 Action behavior and makes the public README, usage guide, support/security pages, and release record point to one current release tag. -Pin the immutable commit shown on the release page and keep `fail-on: never`. +Consumers must pin the full Action source commit above and keep `fail-on: never`. This remains shadow-only: it is not a production release, Marketplace listing, stable `v0.1` claim, or external adoption evidence. diff --git a/docs/releases/beta-release-and-rollback.md b/docs/releases/beta-release-and-rollback.md index 0125199..643e4a8 100644 --- a/docs/releases/beta-release-and-rollback.md +++ b/docs/releases/beta-release-and-rollback.md @@ -1,92 +1,272 @@ -# Beta release and rollback runbook +# Beta release, upgrade and rollback guide -**Status:** runbook for the current public beta; it does not authorize a stable -`v0.1` release or production enforcement. +**Status:** operational guidance for the current public beta. This document +does not authorize a stable `v0.1` release, Marketplace publication, +production enforcement or an external pilot claim. -This runbook closes the documentation path requested in [issue #5](https://github.com/daichunghy/patchgate/issues/5). -It does not override the constitution, branch protection, pilot consent or the -maintainer's final release decision. +This guide implements the documentation path requested in [issue #5](https://github.com/daichunghy/patchgate/issues/5). +It complements, and does not override, the [project constitution](../PROJECT_CONSTITUTION.md), +the [G6 roadmap gate](../implementation-roadmap.md#delivery-gates), branch +protection, pilot consent or the maintainer's final release decision. + +## Current release identity + +The current public Action pre-release was checked against the GitHub release +and tag metadata on 24 August 2026: + +| Field | Verified value | +| --- | --- | +| Release/tag | [`v0.1.0-beta.5`](https://github.com/daichunghy/patchgate/releases/tag/v0.1.0-beta.5) | +| Release date | 23 August 2026 | +| Annotated tag object | `c6e5fdcb95862d6db3909e9621fcbd25cbfb0e11` | +| Action source commit | `34d998bbd59fa09dd9081e24f22abe812f97fbab` | +| Previous beta release commit | `d8c67a848a95d456707e6c580a43e4e56e6071a0` (`v0.1.0-beta.4`) | +| Package state | `private: true`, version `0.1.0-dev`; npm is not an install channel | +| Evidence boundary | public pre-release and shadow-only; no Marketplace listing, stable `v0.1`, external pilot or adoption evidence | + +The **Action source commit** is the value to pin. The annotated tag object is +recorded so that a maintainer can distinguish the release tag from the commit +that contains `action.yml` and `dist/action/index.js`. + +The local consumer fixture and document validator test the reference shape and +the committed bundle. They do not constitute a native GitHub consumer run, +external pilot, Marketplace publication or proof of release readiness. The +current G6 exit evidence remains pending where the roadmap requires clean-room +consumer, compatibility, support/provenance and shadow-installation evidence. +The validator reads existing Git refs only; it never fetches, follows a branch, +or substitutes a metadata SHA when a release tag is unavailable. CI must retain +the tags before running `npm run verify`. If a tag lookup fails, record an audit +note that tag verification was unavailable and rerun after restoring the tag +fetch; do not infer release identity from the working tree. ## Release prerequisites -Before publishing a beta, the maintainer must have independently recorded: +Before publishing a new beta, the maintainer must record all of the following: + +1. A clean candidate commit containing the root `action.yml` and committed + `dist/action/index.js` bundle. +2. The full verification chain from a fresh checkout: + + ```bash + npm ci + npm run verify + npm run verify:dist + npm run check:release-candidate + ``` -- the root `action.yml` and committed bundle from a clean commit; -- a passing `npm run verify` and `npm run check:release-candidate`; -- public CI, CodeQL and Security Audit evidence for the release commit; -- an immutable Action commit reference and clean consumer install; -- a tested rollback to a known-good commit; -- two consented non-blocking shadow installations, or an explicit documented - no-go decision; -- support, security-reporting, compatibility and unsupported-behavior wording. +3. Public CI, CodeQL and Security Audit results for the candidate commit. +4. The candidate's full 40-character commit SHA, release tag, bundle check and + lockfile digest in the release evidence record. +5. A clean consumer installation using that full SHA, with `fail-on: never`. +6. A rollback to a known-good full SHA, tested in the same consumer shape. +7. Support, security-reporting, compatibility and unsupported-behavior wording. +8. Two consented non-blocking shadow installations, or an explicit documented + no-go decision. A local fixture cannot substitute for this evidence. The current repository has public pre-release `v0.1.0-beta.5` and a private development package. The two external shadow installations and production -gates remain open. - -## Release procedure - -1. Freeze the candidate commit and record its full SHA, Node matrix, package - lockfile digest and Action bundle verification output. -2. Run the complete verification chain from a fresh checkout. Keep the raw - command output with the release review record. -3. Confirm that the release notes describe the supported GitHub.com surface, - required permissions, merge-group limitation, human-review boundary and - known Rulesets limitations. -4. Create the beta tag and release only after maintainer approval. Do not use a - moving branch as the installation reference. -5. Verify that the release page, source commit, `action.yml`, bundle and CLI - artifacts all point to the same immutable candidate. -6. Run the clean consumer workflow with the full commit SHA. Keep the Action - non-blocking until the shadow evidence has been reviewed. - -The package is not automatically made public by this runbook. Registry +gates remain open. Passing local commands is a packaging and regression +precondition, not permission to publish or evidence of live GitHub behavior. + +## Validate a release commit + +Run these checks from a clean checkout of the candidate. Do not validate a +working tree with uncommitted changes or a bundle rebuilt from a different +revision. + +```bash +if ! git fetch --tags origin; then + echo "AUDIT NOTE: release-tag verification unavailable; do not claim tag identity" >&2 + exit 1 +fi +test -z "$(git status --porcelain)" +candidate_sha="$(git rev-parse HEAD)" +test "$(git cat-file -t "$candidate_sha")" = commit +test "$(git rev-parse "$candidate_sha^{commit}")" = "$candidate_sha" +test -n "$(git show "$candidate_sha:action.yml")" +test -n "$(git show "$candidate_sha:dist/action/index.js")" + +npm ci +npm run verify +npm run verify:dist +npm run check:release-candidate +npm run test:consumer-fixture +``` + +For an existing tagged release, resolve the tag to its commit before using it: + +```bash +release_tag="v0.1.0-beta.5" +release_sha="$(git rev-parse "$release_tag^{commit}")" +test "$release_sha" = "34d998bbd59fa09dd9081e24f22abe812f97fbab" +git show --no-patch --format=fuller "$release_sha" +``` + +This prevents an annotated tag object from being mistaken for the source +commit. Do not treat an abbreviated SHA, a branch name or an unresolved tag as +an immutable install reference. + +The package is not automatically made public by this guide. Registry ownership, package name and provenance must be confirmed separately before any publish command is run. -## Consumer reference +## Install in shadow mode + +Shadow mode observes the PR and may publish a Check Run, but it does not make +the Action step fail for a non-ready result. Install the current beta by full +SHA: ```yaml +permissions: + contents: read + pull-requests: read + actions: read + checks: write + +steps: - name: PatchGate beta shadow gate - uses: daichunghy/patchgate@ + uses: daichunghy/patchgate@34d998bbd59fa09dd9081e24f22abe812f97fbab # v0.1.0-beta.5 with: fail-on: never create-check-run: true github-token: ${{ github.token }} ``` -Pin `actions/checkout`, `actions/setup-node` and any artifact uploader in the -consumer workflow according to that repository's own supply-chain policy. Do -not imply that a convenient `v0` or `main` reference is immutable. +These are the intended shadow permissions: read-only `contents`, +`pull-requests` and `actions` metadata, plus `checks: write` only because this +example asks the Action to create or update a Check Run. Do not grant +`contents: write`, `pull-requests: write`, `actions: write`, deployment, secret +or administrative permissions. + +The workflow must not check out or execute pull-request code in this decision +lane. `GITHUB_TOKEN` cannot receive the Administration permission through the +workflow `permissions` block. Without a PAT or GitHub App token with +`administration: read`, the native branch-protection/Rulesets snapshot is +incomplete and PatchGate must fail closed. Do not work around that boundary by +granting broader write permissions. Pin `actions/checkout`, `actions/setup-node` +and any artifact uploader according to the consumer repository's own +supply-chain policy. + +## Immutable commit, release tag and moving major tag + +Use the references for different purposes: + +| Reference | Meaning | Consumer policy | +| --- | --- | --- | +| Full commit SHA, for example `34d998bbd59fa09dd9081e24f22abe812f97fbab` | One exact Action source and bundle | Recommended installation reference; retain it for rollback | +| Immutable release tag, if the repository's release/tag controls make it immutable | Human-readable release identity | Resolve it to a full commit and verify before use | +| Moving major/minor tag such as `v0`, `v0.1` or a future `v0.1.1` pointer | Convenience alias that may move to a later release | Do not use as the supply-chain pin for this beta | +| `main` or another branch | Development state | Never use as a release reference | + +The current beta.5 guide therefore shows the tag as a comment for readability +but installs the full commit SHA. A future moving major tag must be advanced +only as part of an approved release; advancing it is not a replacement for +retaining the old commit or documenting rollback. The beta.4 commit recorded +above is a release identity for the local transition check, not a claim that a +consumer's own previous runtime was externally validated. + +## Upgrade and downgrade + +Treat an Action reference change as a consumer workflow change: + +1. Record the current full SHA, release tag, workflow run and receipt/check + behavior. +2. Change only the `uses:` reference to the new full candidate SHA. Keep + `fail-on: never` during shadow evaluation. +3. Open or update the consumer workflow through its normal review path and run + one representative PR, including a fork or merge-group case when that + repository supports it. +4. Compare the target SHA, final status, receipt path, receipt digest, + Check-Run identity and permission-related diagnostics with the previous + version. +5. Retain the previous full SHA in the release record until the new version is + accepted or explicitly rejected. + +To downgrade, replace the current reference with the last known-good full SHA, +review the workflow change, and repeat the same shadow verification. Do not +delete the failed release or rewrite its tag to make the downgrade appear to +be an upgrade. ## Rollback procedure -1. Stop new installations and record the failing release SHA, workflow run, - check-run identity, receipt digest and user-visible symptom. -2. Change the consumer workflow back to the last known-good full Action SHA. -3. Rerun one representative PR in shadow mode and confirm the check target SHA, - receipt path, status and permissions are restored. -4. Leave the failed tag and release history intact for auditability; do not - rewrite or force-push release history. -5. Open a scoped issue with a redacted reproduction and decide whether the next +1. Stop new installations and record the failing SHA, release tag, workflow run + URL, Check-Run identity, receipt path/digest, target SHA and user-visible + symptom. Redact tokens, secrets and personal data. +2. Change the consumer workflow to the previous known-good full SHA. Keep the + workflow in shadow mode until the incident is understood. +3. Rerun one representative PR and confirm that the expected Action commit, + target SHA, receipt path, final status and permission diagnostics are + restored. +4. If a required check was already enabled, the authorized repository + maintainer must decide whether to suspend or change that native rule. The + Action does not perform that governance mutation. +5. Leave the failed release and tag history intact. Do not force-push, retag or + erase the evidence. +6. Open a scoped issue with a redacted reproduction and decide whether the next release is a patch, a new beta or a no-go. Rollback is a consumer workflow change, not an automatic branch-protection or ruleset mutation. If a required check has already been enabled, only the authorized repository maintainer may change that governance setting. +The local `check:release-guide` validator checks the documented full-SHA +transition and rollback controls. A rollback of a live external workflow is +still **pending evidence** until a consented consumer run records the result. + +## Marketplace publication prerequisites + +Marketplace publication is a separate maintainer decision and is not performed +by this guide. Before considering it, verify the current GitHub requirements: + +- the repository is public; +- one root `action.yml` or `action.yaml` is present, and its `name` is unique; +- the action code, committed runtime bundle and necessary documentation are in + the repository; +- the release has passed the repository's tests, security checks, release + candidate checks, compatibility review and rollback review; +- the owner has accepted the GitHub Marketplace Developer Agreement; +- the maintainer creates and publishes a versioned GitHub release, selects the + Marketplace publication option, chooses the required category metadata and + completes any required two-factor authentication. + +GitHub describes the current publication prerequisites in its [Marketplace +publication documentation](https://docs.github.com/en/actions/how-tos/create-and-publish-actions/publish-in-github-marketplace). +The current PatchGate repository is not making that claim: its npm package is +still private/unpublished and beta.5 is not a Marketplace listing. + +## Non-claims and unsupported evidence + +PatchGate's Action-only beta does not claim any of the following: + +- a distinct PatchGate GitHub App identity; its observable check source is + GitHub Actions, and a policy requiring a stronger App identity remains + unsupported or evidence-missing; +- functional correctness, security, license validity or merge-worthiness of + the pull-request code; +- a tamper-proof receipt, cryptographic signature, compliance certification or + universal provenance guarantee; +- that `human_review_required` proves a human has reviewed the code; +- that `fail-on: never` blocks a merge, or that a Check Run changes governance + without a maintainer-configured native GitHub rule; +- Marketplace publication, npm publication, production readiness, external + pilot, downstream adoption or program eligibility; +- live GitHub integration merely because local tests, a fixture, a release tag + or a successful command exists. + ## Release evidence record | Field | Value | | --- | --- | | Release/tag | | +| Annotated tag object | | | Source commit SHA | | | CLI/package artifact digest | | | Action bundle verification | | | Node/GitHub compatibility | | -| Clean consumer repository | | +| Clean consumer repository and consent | | | Previous known-good SHA | | -| Rollback run URL | | +| Rollback run URL and result | | +| Public CI, CodeQL and Security Audit | | | Maintainer approval and date | | | Known limitations accepted | | diff --git a/package.json b/package.json index 51381e6..0387f71 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "check:workflow-pins": "node scripts/check-workflow-pins.mjs", "check:workflow-events": "node scripts/check-workflow-events.mjs", "check:doc-links": "node scripts/check-doc-links.mjs", + "check:release-guide": "node scripts/check-release-guide.mjs", "check:community-schedule": "node scripts/check-community-schedule.mjs", "check:application-dossier": "node scripts/check-application-dossier.mjs", "test:consumer-fixture": "node scripts/test-consumer-fixture.mjs", @@ -66,7 +67,7 @@ "usability:session": "npm run build && node dist/scripts/run-usability-session.js", "lint": "node scripts/lint.mjs", "verify:dist": "node scripts/verify-dist.mjs", - "verify": "npm run lint && npm run typecheck && npm run check:fixture-budgets && npm run check:workflow-pins && npm run check:workflow-events && npm run check:doc-links && npm run check:community-schedule && npm run check:application-dossier && npm run audit && npm test && npm run test:security && npm run test:github && npm run build && npm run bundle:action && npm run test:consumer-fixture && npm run check:release-candidate && npm run test:cli && npm run verify:dist" + "verify": "npm run lint && npm run typecheck && npm run check:fixture-budgets && npm run check:workflow-pins && npm run check:workflow-events && npm run check:doc-links && npm run check:release-guide && npm run check:community-schedule && npm run check:application-dossier && npm run audit && npm test && npm run test:security && npm run test:github && npm run build && npm run bundle:action && npm run test:consumer-fixture && npm run check:release-candidate && npm run test:cli && npm run verify:dist" }, "dependencies": { "ajv": "^8.20.0", diff --git a/scripts/check-release-guide.mjs b/scripts/check-release-guide.mjs new file mode 100644 index 0000000..c4fb1bb --- /dev/null +++ b/scripts/check-release-guide.mjs @@ -0,0 +1,177 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const guidePath = path.join(root, "docs/releases/beta-release-and-rollback.md"); +const usagePath = path.join(root, "docs/github-action-usage.md"); +const metadataPath = path.join(root, "docs/releases/2026-08-23-beta.5.md"); +const packagePath = path.join(root, "package.json"); + +const guide = fs.readFileSync(guidePath, "utf8"); +const usage = fs.readFileSync(usagePath, "utf8"); +const metadata = fs.readFileSync(metadataPath, "utf8"); +const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); +const failures = []; +const beta5Tag = "v0.1.0-beta.5"; +const beta4Tag = "v0.1.0-beta.4"; +const previousReleaseSha = "d8c67a848a95d456707e6c580a43e4e56e6071a0"; + +function runGit(args) { + const result = spawnSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.error) return { ok: false, detail: result.error.message }; + if (result.status !== 0) { + return { ok: false, detail: result.stderr.trim() || `exit status ${result.status}` }; + } + return { ok: true, value: result.stdout.trim() }; +} + +function readGit(args, label) { + const result = runGit(args); + if (!result.ok) { + failures.push(`${label} failed: ${result.detail}`); + return null; + } + return result.value; +} + +function requireTag(tag) { + const ref = `refs/tags/${tag}`; + const result = runGit(["show-ref", "--verify", "--quiet", ref]); + if (!result.ok) { + const detail = result.detail === "exit status 1" ? "the ref is not present" : result.detail; + failures.push( + `${tag} is unavailable in this checkout; fetch tags before running check:release-guide ` + + `(CI checkout must retain tags): ${detail}`, + ); + return false; + } + return true; +} + +function requireGitType(spec, expected, label) { + const actual = readGit(["cat-file", "-t", spec], `${label} object lookup`); + if (actual && actual !== expected) { + failures.push(`${label} must be a ${expected}; found ${actual}`); + } + return actual === expected; +} + +function requireText(text, expected, label) { + if (!text.includes(expected)) failures.push(`${label} is missing '${expected}'`); +} + +const sourceSha = metadata.match(/\*\*Action source commit:\*\* `([0-9a-f]{40})`/i)?.[1]; +const tagObjectSha = metadata.match(/\*\*Annotated tag object:\*\* `([0-9a-f]{40})`/i)?.[1]; +if (!sourceSha) failures.push("beta.5 metadata must record a full Action source commit SHA"); +if (!tagObjectSha) failures.push("beta.5 metadata must record a full annotated tag object SHA"); + +if (requireTag(beta5Tag)) { + const beta5Ref = `refs/tags/${beta5Tag}`; + const actualTagType = readGit(["cat-file", "-t", beta5Ref], `${beta5Tag} tag type`); + const actualTagObjectSha = readGit(["rev-parse", "--verify", "--quiet", beta5Ref], `${beta5Tag} tag object`); + const actualSourceSha = readGit(["rev-parse", "--verify", "--quiet", `${beta5Ref}^{commit}`], `${beta5Tag} source commit`); + + if (actualTagType && actualTagType !== "tag") { + failures.push(`${beta5Tag} must be an annotated tag; found ${actualTagType}`); + } + if (tagObjectSha && actualTagObjectSha && actualTagObjectSha !== tagObjectSha) { + failures.push(`${beta5Tag} tag object does not match metadata: expected ${tagObjectSha}, found ${actualTagObjectSha}`); + } + if (sourceSha && actualSourceSha && actualSourceSha !== sourceSha) { + failures.push(`${beta5Tag} does not resolve to the metadata source commit: expected ${sourceSha}, found ${actualSourceSha}`); + } + if (actualSourceSha) { + requireGitType(actualSourceSha, "commit", `${beta5Tag} source commit`); + requireGitType(`${actualSourceSha}:action.yml`, "blob", `${beta5Tag} source action.yml`); + requireGitType(`${actualSourceSha}:dist/action/index.js`, "blob", `${beta5Tag} source dist/action/index.js`); + } +} + +if (!/^[0-9a-f]{40}$/i.test(previousReleaseSha)) { + failures.push("beta.4 reference must be a full 40-character commit SHA"); +} else { + requireGitType(previousReleaseSha, "commit", "beta.4 reference"); + if (requireTag(beta4Tag)) { + const beta4Ref = `refs/tags/${beta4Tag}`; + const actualBeta4Sha = readGit(["rev-parse", "--verify", "--quiet", `${beta4Ref}^{commit}`], `${beta4Tag} source commit`); + if (actualBeta4Sha && actualBeta4Sha !== previousReleaseSha) { + failures.push(`${beta4Tag} does not resolve to the documented commit: expected ${previousReleaseSha}, found ${actualBeta4Sha}`); + } + } +} + +if (sourceSha) { + requireText(guide, `daichunghy/patchgate@${sourceSha}`, "release guide current install reference"); + requireText(usage, `daichunghy/patchgate@${sourceSha}`, "usage guide current install reference"); +} +requireText(guide, previousReleaseSha, "release guide previous beta release reference"); + +if (sourceSha) { + const previousReference = `uses: daichunghy/patchgate@${previousReleaseSha}`; + const upgradedReference = previousReference.replace(previousReleaseSha, sourceSha); + const rolledBackReference = upgradedReference.replace(sourceSha, previousReleaseSha); + if (rolledBackReference !== previousReference) { + failures.push("release guide full-SHA upgrade and rollback transition did not round-trip"); + } +} + +for (const [text, label] of [[guide, "release guide"], [usage, "usage guide"]]) { + const references = [...text.matchAll(/^\s*uses:\s*daichunghy\/patchgate@([^\s#]+)/gmi)].map((match) => match[1]); + for (const reference of references) { + if (!/^[0-9a-f]{40}$/i.test(reference)) { + failures.push(`${label} contains a non-immutable PatchGate consumer reference '${reference}'`); + } + } +} + +for (const heading of [ + "## Current release identity", + "## Validate a release commit", + "## Install in shadow mode", + "## Immutable commit, release tag and moving major tag", + "## Upgrade and downgrade", + "## Rollback procedure", + "## Marketplace publication prerequisites", + "## Non-claims and unsupported evidence", +]) requireText(guide, heading, "release guide section"); + +for (const expected of [ + "release_tag=\"v0.1.0-beta.5\"", + "git rev-parse \"$release_tag^{commit}\"", + "npm run verify", + "npm run verify:dist", + "npm run test:consumer-fixture", + "fail-on: never", + "contents: read", + "pull-requests: read", + "actions: read", + "checks: write", + "not a Marketplace listing", + "tamper-proof receipt", + "external pilot", + "downstream adoption", + "pending evidence", +]) requireText(guide, expected, "release guide safety or evidence control"); + +for (const forbidden of ["contents", "pull-requests", "actions", "administration"]) { + if (new RegExp(`^\\s*${forbidden}:\\s*write\\s*$`, "m").test(guide)) { + failures.push(`release guide must not grant '${forbidden}: write'`); + } +} + +if (packageJson.private !== true || packageJson.version !== "0.1.0-dev") { + failures.push("release guide validator expects the package to remain private at 0.1.0-dev"); +} + +if (failures.length > 0) { + for (const failure of failures) console.error(`release guide check failed: ${failure}`); + process.exit(1); +} + +console.log("release guide checks passed: beta.5 metadata, full-SHA install, shadow permissions, release validation, upgrade/downgrade, rollback and non-claims are documented");