Merge pull request #42 from pylon-code/feat/pylon-protected-publication #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Pylon preview publication | |
| on: | |
| push: | |
| branches: [pylon] | |
| permissions: {} | |
| concurrency: | |
| group: pylon-preview-${{ github.sha }} | |
| cancel-in-progress: false | |
| env: | |
| PYLON_RELEASE_NODE: 22.23.2 | |
| PYLON_RELEASE_NPM: 11.10.1 | |
| jobs: | |
| admission: | |
| name: Preview source admission | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Require the canonical protected push | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| if ( | |
| owner !== "pylon-code" || | |
| repo !== "prime-agent" || | |
| context.eventName !== "push" || | |
| context.ref !== "refs/heads/pylon" || | |
| !/^[0-9a-f]{40}$/.test(context.sha) | |
| ) { | |
| core.setFailed("Preview publication requires an exact canonical pylon push."); | |
| return; | |
| } | |
| const pylon = await github.rest.git.getRef({ owner, repo, ref: "heads/pylon" }); | |
| if (pylon.data.object.type !== "commit" || pylon.data.object.sha !== context.sha) { | |
| core.setFailed("Preview publication event is stale relative to protected pylon."); | |
| return; | |
| } | |
| const expectedPolicy = [ | |
| { context: "Check changelog fragment", appId: 15368, workflowPath: ".github/workflows/changelog-merged-proof.yml" }, | |
| { context: "build-check-test", appId: 15368, workflowPath: ".github/workflows/ci.yml" }, | |
| ]; | |
| const protection = await github.graphql( | |
| `query($owner:String!,$repo:String!,$ref:String!){repository(owner:$owner,name:$repo){ref(qualifiedName:$ref){branchProtectionRule{requiresStatusChecks requiredStatusChecks{context app{databaseId}}}}}}`, | |
| { ...context.repo, ref: "refs/heads/pylon" }, | |
| ); | |
| const rule = protection.repository?.ref?.branchProtectionRule; | |
| const actualPolicy = Array.isArray(rule?.requiredStatusChecks) ? rule.requiredStatusChecks.map((requirement) => ({ | |
| context: requirement.context, appId: requirement.app?.databaseId ?? null, | |
| })).sort((left, right) => left.context < right.context ? -1 : left.context > right.context ? 1 : 0) : null; | |
| if (!rule?.requiresStatusChecks || JSON.stringify(actualPolicy) !== JSON.stringify(expectedPolicy.map(({ context, appId }) => ({ context, appId })))) { | |
| throw new Error("Protected pylon must require exactly the two app-bound publication checks."); | |
| } | |
| // Exact-SHA workflow proof is checked by the final publisher after the push checks can complete. | |
| pack: | |
| name: Preview offline pack (${{ matrix.copy }}) | |
| needs: admission | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| copy: [a, b] | |
| steps: | |
| - name: Checkout exact pushed source | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.sha }} | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - name: Setup pinned Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ env.PYLON_RELEASE_NODE }} | |
| - name: Install pinned build inputs | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev | |
| npm install --global "npm@${PYLON_RELEASE_NPM}" | |
| test "$(node --version)" = "v${PYLON_RELEASE_NODE}" | |
| test "$(npm --version)" = "${PYLON_RELEASE_NPM}" | |
| npm ci | |
| - name: Test publication contract | |
| run: | | |
| npm run test:pylon-release | |
| npm run test:pylon-publication | |
| - name: Build and pack without network | |
| run: | | |
| sudo env \ | |
| "PATH=$PATH" \ | |
| "HOME=$HOME" \ | |
| "GIT_CONFIG_COUNT=1" \ | |
| "GIT_CONFIG_KEY_0=safe.directory" \ | |
| "GIT_CONFIG_VALUE_0=$GITHUB_WORKSPACE" \ | |
| unshare --net -- npm run release:pylon:pack | |
| - name: Verify and prepare six exact subjects | |
| run: | | |
| npm run release:pylon:verify | |
| npm run release:pylon:preview -- --publication-policy-revision 1 | |
| npm run release:pylon:verify-preview | |
| - name: Upload isolated preview subjects | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: pylon-preview-pack-${{ matrix.copy }} | |
| path: .npm/pylon-release/artifacts | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| retention-days: 3 | |
| reproducibility: | |
| name: Preview byte reproducibility | |
| needs: pack | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Verify first workflow artifact provenance | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const run = (await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ ...context.repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| context.repo.owner !== "pylon-code" || context.repo.repo !== "prime-agent" || | |
| run.id !== context.runId || run.run_number !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| String(run.run_attempt) !== process.env.GITHUB_RUN_ATTEMPT || run.repository?.id !== 1349002285 || | |
| run.repository?.full_name !== "pylon-code/prime-agent" || run.head_repository?.id !== 1349002285 || | |
| run.head_repository?.full_name !== "pylon-code/prime-agent" || run.event !== context.eventName || | |
| run.head_sha !== context.sha || run.head_branch !== "pylon" || context.ref !== "refs/heads/pylon" || | |
| workflow.path !== ".github/workflows/pylon-preview-release.yml" | |
| ) throw new Error("Artifact workflow provenance is not canonical."); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| ...context.repo, run_id: context.runId, per_page: 100, | |
| }); | |
| const matches = artifacts.filter((artifact) => artifact.name === "pylon-preview-pack-a"); | |
| if (matches.length !== 1 || matches[0].expired || !/^sha256:[0-9a-f]{64}$/.test(matches[0].digest ?? "")) { | |
| throw new Error("Artifact is ambiguous, expired, or lacks a SHA-256 transport digest."); | |
| } | |
| - name: Verify second workflow artifact provenance | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const run = (await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ ...context.repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| context.repo.owner !== "pylon-code" || context.repo.repo !== "prime-agent" || | |
| run.id !== context.runId || run.run_number !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| String(run.run_attempt) !== process.env.GITHUB_RUN_ATTEMPT || run.repository?.id !== 1349002285 || | |
| run.repository?.full_name !== "pylon-code/prime-agent" || run.head_repository?.id !== 1349002285 || | |
| run.head_repository?.full_name !== "pylon-code/prime-agent" || run.event !== context.eventName || | |
| run.head_sha !== context.sha || run.head_branch !== "pylon" || context.ref !== "refs/heads/pylon" || | |
| workflow.path !== ".github/workflows/pylon-preview-release.yml" | |
| ) throw new Error("Artifact workflow provenance is not canonical."); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| ...context.repo, run_id: context.runId, per_page: 100, | |
| }); | |
| const matches = artifacts.filter((artifact) => artifact.name === "pylon-preview-pack-b"); | |
| if (matches.length !== 1 || matches[0].expired || !/^sha256:[0-9a-f]{64}$/.test(matches[0].digest ?? "")) { | |
| throw new Error("Artifact is ambiguous, expired, or lacks a SHA-256 transport digest."); | |
| } | |
| - name: Download first offline pack | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: pack-a | |
| - name: Download second offline pack | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-b | |
| path: pack-b | |
| - name: Require byte-identical six-subject outputs | |
| run: diff --recursive --brief pack-a pack-b | |
| install: | |
| name: Preview installed artifact (${{ matrix.os }}) | |
| needs: [pack, reproducibility] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 15 | |
| permissions: | |
| actions: read | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-24.04, macos-15] | |
| steps: | |
| - name: Checkout exact pushed source | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.sha }} | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - name: Setup pinned Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ env.PYLON_RELEASE_NODE }} | |
| - name: Install pinned npm | |
| run: npm install --global "npm@${{ env.PYLON_RELEASE_NPM }}" | |
| - name: Verify workflow artifact provenance | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const run = (await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ ...context.repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| context.repo.owner !== "pylon-code" || context.repo.repo !== "prime-agent" || | |
| run.id !== context.runId || run.run_number !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| String(run.run_attempt) !== process.env.GITHUB_RUN_ATTEMPT || run.repository?.id !== 1349002285 || | |
| run.repository?.full_name !== "pylon-code/prime-agent" || run.head_repository?.id !== 1349002285 || | |
| run.head_repository?.full_name !== "pylon-code/prime-agent" || run.event !== context.eventName || | |
| run.head_sha !== context.sha || run.head_branch !== "pylon" || context.ref !== "refs/heads/pylon" || | |
| workflow.path !== ".github/workflows/pylon-preview-release.yml" | |
| ) throw new Error("Artifact workflow provenance is not canonical."); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| ...context.repo, run_id: context.runId, per_page: 100, | |
| }); | |
| const matches = artifacts.filter((artifact) => artifact.name === "pylon-preview-pack-a"); | |
| if (matches.length !== 1 || matches[0].expired || !/^sha256:[0-9a-f]{64}$/.test(matches[0].digest ?? "")) { | |
| throw new Error("Artifact is ambiguous, expired, or lacks a SHA-256 transport digest."); | |
| } | |
| - name: Download byte-identical preview subjects | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: .npm/pylon-release/artifacts | |
| - name: Verify and install the exact preview bytes | |
| run: | | |
| npm run release:pylon:verify-preview | |
| node -e "require('node:fs').unlinkSync('.npm/pylon-release/artifacts/pylon-preview-channel-v1.json')" | |
| npm run release:pylon:smoke | |
| stage-draft: | |
| name: Stage exact preview draft | |
| needs: [admission, pack, reproducibility, install, verify-attestation] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| environment: pylon-preview | |
| permissions: | |
| actions: read | |
| contents: write | |
| outputs: | |
| draft_id: ${{ steps.stage.outputs.result }} | |
| steps: | |
| - name: Download approved preview subjects | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: publication | |
| - name: Validate exact preview tag identity before protected mutation | |
| id: preview-tag | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ARTIFACT_DIR: publication | |
| with: | |
| script: | | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const crypto = require("node:crypto"); | |
| if (`${context.repo.owner}/${context.repo.repo}` !== "pylon-code/prime-agent" || context.eventName !== "push" || context.ref !== "refs/heads/pylon") { | |
| throw new Error("Preview tag planning requires the canonical pylon push."); | |
| } | |
| const releaseBytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-prime-agent-release-v1.json")); | |
| const previewBytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-preview-channel-v1.json")); | |
| const release = JSON.parse(releaseBytes); | |
| const preview = JSON.parse(previewBytes); | |
| const tag = `pylon-build-g${context.sha.slice(0, 12)}-r${release.build?.recipeRevision}`; | |
| if ( | |
| release.source?.commit !== context.sha || release.source?.tree !== preview.build?.source?.tree || | |
| release.build?.id !== tag || preview.build?.tag !== tag || | |
| preview.build?.releaseManifest?.sha256 !== crypto.createHash("sha256").update(releaseBytes).digest("hex") || | |
| preview.publicationPolicyRevision !== 1 || preview.sequenceEpoch !== 1 || | |
| preview.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || preview.workflowRunId !== process.env.GITHUB_RUN_ID | |
| ) throw new Error("Preview tag plan is not bound to the exact source and workflow sequence."); | |
| core.setOutput("tag", tag); | |
| - name: Mint repository-scoped ruleset auditor token | |
| id: ruleset-auditor | |
| uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 | |
| with: | |
| app-id: ${{ vars.PYLON_RULESET_AUDITOR_APP_ID }} | |
| private-key: ${{ secrets.PYLON_RULESET_AUDITOR_PRIVATE_KEY }} | |
| owner: pylon-code | |
| repositories: prime-agent | |
| permission-administration: read | |
| - name: Require live pylon immediately before the preview tag ruleset audit | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const pylon = await github.rest.git.getRef({ ...context.repo, ref: "heads/pylon" }); | |
| if (pylon.data.object.type !== "commit" || pylon.data.object.sha !== context.sha) { | |
| throw new Error("Preview tag transaction became stale before its authoritative ruleset audit."); | |
| } | |
| - name: Require authoritative publication tag ruleset before preview tag CAS | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| github-token: ${{ steps.ruleset-auditor.outputs.token }} | |
| script: | | |
| const response = await github.request("GET /repos/{owner}/{repo}/rulesets/{ruleset_id}", { | |
| owner: "pylon-code", repo: "prime-agent", ruleset_id: 21950766, includes_parents: false, | |
| headers: { accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" }, | |
| }); | |
| const restRuleset = response?.data; | |
| const exactKeys = (value, keys) => value !== null && typeof value === "object" && !Array.isArray(value) && | |
| JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort()); | |
| const exactSortedStrings = (value, expected) => Array.isArray(value) && value.every((entry) => typeof entry === "string") && | |
| JSON.stringify([...value].sort()) === JSON.stringify([...expected].sort()); | |
| const expectedIncludes = ["refs/tags/pylon-build-*", "refs/tags/pylon-stable-*"]; | |
| const restConditions = restRuleset?.conditions; | |
| const restRefName = restConditions?.ref_name; | |
| const restRules = restRuleset?.rules; | |
| const restUpdateRules = Array.isArray(restRules) ? restRules.filter((rule) => rule?.type === "update") : []; | |
| const restDeletionRules = Array.isArray(restRules) ? restRules.filter((rule) => rule?.type === "deletion") : []; | |
| if ( | |
| response?.status !== 200 || !restRuleset || restRuleset.id !== 21950766 || | |
| restRuleset.node_id !== "RRS_lACqUmVwb3NpdG9yec5QaCQtzgFO8S4" || restRuleset.name !== "Pylon immutable publication tags" || | |
| restRuleset.source_type !== "Repository" || restRuleset.source !== "pylon-code/prime-agent" || | |
| restRuleset.target !== "tag" || restRuleset.enforcement !== "active" || | |
| Object.hasOwn(restRuleset, "bypass_actors") && (!Array.isArray(restRuleset.bypass_actors) || restRuleset.bypass_actors.length !== 0) || | |
| Object.hasOwn(restRuleset, "current_user_can_bypass") && restRuleset.current_user_can_bypass !== "never" || | |
| !exactKeys(restConditions, ["ref_name"]) || !exactKeys(restRefName, ["exclude", "include"]) || | |
| !exactSortedStrings(restRefName.exclude, []) || !exactSortedStrings(restRefName.include, expectedIncludes) || | |
| !Array.isArray(restRules) || restRules.length !== 2 || restUpdateRules.length !== 1 || restDeletionRules.length !== 1 || | |
| !exactKeys(restUpdateRules[0], ["parameters", "type"]) || | |
| !exactKeys(restUpdateRules[0]?.parameters, ["update_allows_fetch_and_merge"]) || | |
| restUpdateRules[0]?.parameters?.update_allows_fetch_and_merge !== false || !exactKeys(restDeletionRules[0], ["type"]) | |
| ) throw new Error("REST ruleset-auditor response differs from the exact active immutable tag ruleset."); | |
| const query = `query PylonPublicationRulesetAudit($owner: String!, $repo: String!, $rulesetDatabaseId: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| id | |
| databaseId | |
| nameWithOwner | |
| ruleset(databaseId: $rulesetDatabaseId, includeParents: false) { | |
| id | |
| databaseId | |
| name | |
| enforcement | |
| target | |
| source { | |
| __typename | |
| ... on Repository { | |
| id | |
| databaseId | |
| nameWithOwner | |
| } | |
| } | |
| bypassActors { totalCount } | |
| conditions { | |
| refName { include exclude } | |
| organizationProperty { __typename } | |
| repositoryId { __typename } | |
| repositoryName { __typename } | |
| repositoryProperty { __typename } | |
| } | |
| rules(first: 100) { | |
| totalCount | |
| nodes { | |
| type | |
| parameters { | |
| __typename | |
| ... on UpdateParameters { updateAllowsFetchAndMerge } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }`; | |
| const authoritative = await github.graphql(query, { | |
| owner: "pylon-code", repo: "prime-agent", rulesetDatabaseId: 21950766, | |
| }); | |
| const repository = authoritative?.repository; | |
| const ruleset = repository?.ruleset; | |
| const source = ruleset?.source; | |
| const bypassActors = ruleset?.bypassActors; | |
| const conditions = ruleset?.conditions; | |
| const refName = conditions?.refName; | |
| const rules = ruleset?.rules; | |
| const nodes = rules?.nodes; | |
| const updateRules = Array.isArray(nodes) ? nodes.filter((rule) => rule?.type === "UPDATE") : []; | |
| const deletionRules = Array.isArray(nodes) ? nodes.filter((rule) => rule?.type === "DELETION") : []; | |
| if ( | |
| Object.hasOwn(authoritative ?? {}, "errors") || !repository || repository.id !== "R_kgDOUGgkLQ" || | |
| repository.databaseId !== 1349002285 || repository.nameWithOwner !== "pylon-code/prime-agent" || | |
| !ruleset || ruleset.id !== restRuleset.node_id || ruleset.id !== "RRS_lACqUmVwb3NpdG9yec5QaCQtzgFO8S4" || | |
| ruleset.databaseId !== 21950766 || ruleset.name !== "Pylon immutable publication tags" || | |
| ruleset.enforcement !== "ACTIVE" || ruleset.target !== "TAG" || | |
| !exactKeys(source, ["__typename", "databaseId", "id", "nameWithOwner"]) || source.__typename !== "Repository" || | |
| source.id !== repository.id || source.databaseId !== repository.databaseId || source.nameWithOwner !== repository.nameWithOwner || | |
| !exactKeys(bypassActors, ["totalCount"]) || !Number.isInteger(bypassActors.totalCount) || bypassActors.totalCount !== 0 || | |
| !exactKeys(conditions, ["organizationProperty", "refName", "repositoryId", "repositoryName", "repositoryProperty"]) || | |
| conditions.organizationProperty !== null || conditions.repositoryId !== null || conditions.repositoryName !== null || | |
| conditions.repositoryProperty !== null || !exactKeys(refName, ["exclude", "include"]) || | |
| !exactSortedStrings(refName.exclude, []) || !exactSortedStrings(refName.include, expectedIncludes) || | |
| !exactKeys(rules, ["nodes", "totalCount"]) || !Number.isInteger(rules.totalCount) || rules.totalCount !== 2 || | |
| !Array.isArray(nodes) || nodes.length !== 2 || nodes.some((node) => node === null) || | |
| updateRules.length !== 1 || deletionRules.length !== 1 || !exactKeys(updateRules[0], ["parameters", "type"]) || | |
| !exactKeys(updateRules[0].parameters, ["__typename", "updateAllowsFetchAndMerge"]) || | |
| updateRules[0].parameters.__typename !== "UpdateParameters" || updateRules[0].parameters.updateAllowsFetchAndMerge !== false || | |
| !exactKeys(deletionRules[0], ["parameters", "type"]) || deletionRules[0].parameters !== null | |
| ) throw new Error("GraphQL ruleset-auditor response is null, partial, redacted, or differs from the exact non-bypassable target contract."); | |
| - name: Create or refetch the exact protected preview tag | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| TAG: ${{ steps.preview-tag.outputs.tag }} | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const tag = process.env.TAG; | |
| if (`${owner}/${repo}` !== "pylon-code/prime-agent" || context.eventName !== "push" || context.ref !== "refs/heads/pylon" || | |
| !/^pylon-build-g[0-9a-f]{12}-r[1-9][0-9]*$/.test(tag ?? "")) { | |
| throw new Error("Protected preview tag CAS identity is malformed."); | |
| } | |
| try { | |
| await github.rest.git.createRef({ owner, repo, ref: `refs/tags/${tag}`, sha: context.sha }); | |
| } catch (error) { | |
| if (error.status !== 422) throw error; | |
| } | |
| const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); | |
| if (ref.data.object.type !== "commit" || ref.data.object.sha !== context.sha) { | |
| throw new Error("Protected preview tag is annotated or targets a different commit."); | |
| } | |
| - name: Create or finish the exact durable draft | |
| id: stage | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ARTIFACT_DIR: publication | |
| with: | |
| script: | | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const crypto = require("node:crypto"); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| if (`${owner}/${repo}` !== "pylon-code/prime-agent" || context.eventName !== "push" || context.ref !== "refs/heads/pylon") { | |
| throw new Error("Preview draft staging requires the canonical pylon push."); | |
| } | |
| const requireLivePylon = async () => { | |
| const pylon = await github.rest.git.getRef({ owner, repo, ref: "heads/pylon" }); | |
| if (pylon.data.object.type !== "commit" || pylon.data.object.sha !== context.sha) throw new Error("Preview draft staging is stale."); | |
| }; | |
| await requireLivePylon(); | |
| const dir = process.env.ARTIFACT_DIR; | |
| const names = fs.readdirSync(dir).sort(); | |
| if (names.length !== 6 || names.some((name) => !fs.lstatSync(path.join(dir, name)).isFile())) { | |
| throw new Error("Preview draft requires exactly six regular files."); | |
| } | |
| const releaseBytes = fs.readFileSync(path.join(dir, "pylon-prime-agent-release-v1.json")); | |
| const previewBytes = fs.readFileSync(path.join(dir, "pylon-preview-channel-v1.json")); | |
| const release = JSON.parse(releaseBytes); | |
| const preview = JSON.parse(previewBytes); | |
| const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); | |
| const tag = `pylon-build-g${context.sha.slice(0, 12)}-r${release.build?.recipeRevision}`; | |
| if ( | |
| release.source?.commit !== context.sha || release.source?.tree !== preview.build?.source?.tree || | |
| release.build?.id !== tag || preview.build?.tag !== tag || preview.build?.releaseManifest?.sha256 !== sha256(releaseBytes) || | |
| preview.publicationPolicyRevision !== 1 || preview.sequenceEpoch !== 1 || | |
| preview.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || preview.workflowRunId !== process.env.GITHUB_RUN_ID | |
| ) throw new Error("Preview draft manifests do not bind the exact source and workflow sequence."); | |
| const expectedNames = [...release.assets.map((asset) => asset.file), "pylon-prime-agent-release-v1.json", "pylon-preview-channel-v1.json"].sort(); | |
| if (names.join("\n") !== expectedNames.join("\n")) throw new Error("Preview draft file set differs."); | |
| const assets = names.map((name) => { | |
| const bytes = fs.readFileSync(path.join(dir, name)); | |
| return { name, bytes, size: bytes.length, sha256: sha256(bytes) }; | |
| }); | |
| for (const receipt of release.assets) { | |
| const actual = assets.find((asset) => asset.name === receipt.file); | |
| if (!actual || actual.size !== receipt.size || actual.sha256 !== receipt.sha256) throw new Error(`Preview draft asset differs: ${receipt.file}`); | |
| } | |
| const name = `Pylon Prime preview ${tag}`; | |
| const body = [ | |
| "Pylon Prime preview publication.", "", `Tag: ${tag}`, `Source: ${context.sha}`, | |
| `Tree: ${release.source.tree}`, `Recipe: r${release.build.recipeRevision}`, "", | |
| "Verify the immutable release and artifact attestations before use.", | |
| ].join("\n"); | |
| const requireExactTag = async () => { | |
| const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); | |
| if (ref.data.object.type !== "commit" || ref.data.object.sha !== context.sha) { | |
| throw new Error("Preview publication tag is annotated or targets a different commit."); | |
| } | |
| return ref.data; | |
| }; | |
| await requireExactTag(); | |
| const releases = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 }); | |
| const matching = releases.filter((candidate) => candidate.tag_name === tag); | |
| if (matching.length > 1) throw new Error("Preview draft tag is ambiguous."); | |
| let draft = matching[0]; | |
| if (draft && !draft.draft) { | |
| if ( | |
| draft.immutable !== true || draft.tag_name !== tag || draft.name !== name || draft.body !== body || | |
| draft.prerelease !== true || draft.target_commitish !== context.sha || draft.assets.length !== assets.length | |
| ) throw new Error("Existing preview publication differs from this exact rerun."); | |
| for (const expected of assets) { | |
| const actual = draft.assets.find((asset) => asset.name === expected.name); | |
| if (!actual || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Existing preview publication differs: ${expected.name}`); | |
| } | |
| } | |
| const tagRef = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); | |
| if (tagRef.data.object.type !== "commit" || tagRef.data.object.sha !== context.sha) throw new Error("Existing preview tag differs."); | |
| return draft.id; | |
| } | |
| let createdDraft = false; | |
| if (!draft) { | |
| try { | |
| // Final live-tip read immediately precedes the first release mutation. | |
| await requireLivePylon(); | |
| draft = (await github.rest.repos.createRelease({ | |
| owner, repo, tag_name: tag, target_commitish: context.sha, name, body, | |
| draft: true, prerelease: true, make_latest: "false", | |
| })).data; | |
| createdDraft = true; | |
| } catch (error) { | |
| if (error.status !== 422) throw error; | |
| const raced = (await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 })) | |
| .filter((candidate) => candidate.tag_name === tag); | |
| if (raced.length !== 1 || !raced[0].draft) { | |
| throw new Error("Preview draft creation raced (422) without one exact recoverable draft."); | |
| } | |
| draft = raced[0]; | |
| } | |
| } | |
| if ( | |
| draft.tag_name !== tag || draft.name !== name || draft.body !== body || draft.target_commitish !== context.sha || | |
| draft.prerelease !== true || draft.immutable === true | |
| ) throw new Error("Existing preview draft identity differs."); | |
| for (const actual of draft.assets) { | |
| const expected = assets.find((asset) => asset.name === actual.name); | |
| if (!expected || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Existing preview draft asset differs: ${actual.name}`); | |
| } | |
| } | |
| const present = new Set(draft.assets.map((asset) => asset.name)); | |
| const missing = assets.filter((asset) => !present.has(asset.name)); | |
| if (!createdDraft && missing.length > 0) { | |
| // A resumed draft gets a fresh point-in-time authorization before its first mutation. | |
| await requireLivePylon(); | |
| } | |
| for (const asset of missing) { | |
| await github.request("POST /repos/{owner}/{repo}/releases/{release_id}/assets", { | |
| owner, repo, release_id: draft.id, name: asset.name, data: asset.bytes, | |
| headers: { "content-type": "application/octet-stream", "content-length": asset.size }, | |
| }); | |
| } | |
| const staged = (await github.rest.repos.getRelease({ owner, repo, release_id: draft.id })).data; | |
| if (staged.assets.length !== assets.length) throw new Error("Preview draft was not fully staged."); | |
| for (const expected of assets) { | |
| const actual = staged.assets.find((asset) => asset.name === expected.name); | |
| if (!actual || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) throw new Error(`Staged preview asset differs: ${expected.name}`); | |
| } | |
| return draft.id; | |
| attest: | |
| name: Approve and attest six preview subjects | |
| needs: [pack, reproducibility, install] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| environment: pylon-preview | |
| permissions: | |
| actions: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download byte-identical preview subjects | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: .npm/pylon-release/artifacts | |
| - name: Validate exact subjects before signing | |
| shell: bash | |
| run: | | |
| node <<'NODE' | |
| const crypto = require("node:crypto"); | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const dir = ".npm/pylon-release/artifacts"; | |
| const canonical = (value) => { | |
| if (value === null || ["string", "boolean"].includes(typeof value)) return value; | |
| if (typeof value === "number" && Number.isFinite(value)) return value; | |
| if (Array.isArray(value)) return value.map(canonical); | |
| if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) throw new Error("Unsupported manifest value."); | |
| return Object.fromEntries(Object.keys(value).sort().map((key) => { | |
| if (value[key] === undefined) throw new Error("Undefined manifest value."); | |
| return [key, canonical(value[key])]; | |
| })); | |
| }; | |
| const canonicalJson = (value) => `${JSON.stringify(canonical(value), null, 2)}\n`; | |
| const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); | |
| const releaseName = "pylon-prime-agent-release-v1.json"; | |
| const previewName = "pylon-preview-channel-v1.json"; | |
| const releaseBytes = fs.readFileSync(path.join(dir, releaseName)); | |
| const previewBytes = fs.readFileSync(path.join(dir, previewName)); | |
| const release = JSON.parse(releaseBytes); | |
| const preview = JSON.parse(previewBytes); | |
| if (previewBytes.toString("utf8") !== canonicalJson(preview)) throw new Error("Preview manifest is not canonical JSON."); | |
| if ( | |
| preview.sequenceEpoch !== 1 || preview.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| preview.workflowRunId !== process.env.GITHUB_RUN_ID | |
| ) throw new Error("Preview manifest workflow sequence differs from this approved attestation run."); | |
| if (preview.build?.releaseManifest?.file !== releaseName || preview.build.releaseManifest.sha256 !== sha256(releaseBytes)) { | |
| throw new Error("Preview does not bind the exact build manifest."); | |
| } | |
| if (!Array.isArray(release.assets) || release.assets.length !== 4 || !Array.isArray(preview.assets) || preview.assets.length !== 4) { | |
| throw new Error("Preview must describe exactly four archives."); | |
| } | |
| const expected = new Map([[releaseName, { size: releaseBytes.length, sha256: sha256(releaseBytes) }], [previewName, { size: previewBytes.length, sha256: sha256(previewBytes) }]]); | |
| for (const asset of release.assets) { | |
| if ( | |
| !asset || Object.keys(asset).sort().join(",") !== "file,package,sha256,sha512,size" || | |
| path.basename(asset.file) !== asset.file || !/^pylon-prime-agent(?:-(?:ai|core|tui))?-\d+\.\d+\.\d+\.tgz$/.test(asset.file) || | |
| !Number.isSafeInteger(asset.size) || asset.size < 1 || !/^[0-9a-f]{64}$/.test(asset.sha256) | |
| ) throw new Error("Build manifest contains an unsafe archive receipt."); | |
| expected.set(asset.file, { size: asset.size, sha256: asset.sha256 }); | |
| } | |
| if (expected.size !== 6) throw new Error("Subject names are not unique."); | |
| const previewAssets = new Map(preview.assets.map((asset) => [asset.file, asset])); | |
| for (const asset of release.assets) { | |
| const channel = previewAssets.get(asset.file); | |
| if (!channel || channel.size !== asset.size || channel.sha256 !== asset.sha256 || channel.sha512 !== asset.sha512) { | |
| throw new Error(`Preview receipt differs for ${asset.file}.`); | |
| } | |
| } | |
| const names = fs.readdirSync(dir).sort(); | |
| if (names.length !== 6 || names.join("\n") !== [...expected.keys()].sort().join("\n")) throw new Error("Attestation subject set has an extra or missing file."); | |
| for (const name of names) { | |
| const file = path.join(dir, name); | |
| const stat = fs.lstatSync(file); | |
| if (!stat.isFile()) throw new Error(`Attestation subject is not a regular file: ${name}`); | |
| const bytes = fs.readFileSync(file); | |
| const receipt = expected.get(name); | |
| if (bytes.length !== receipt.size || sha256(bytes) !== receipt.sha256) throw new Error(`Attestation subject bytes differ: ${name}`); | |
| } | |
| NODE | |
| - name: Generate build provenance for exactly six subjects | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 | |
| with: | |
| subject-path: .npm/pylon-release/artifacts/* | |
| verify-attestation: | |
| name: Verify preview provenance | |
| needs: [admission, pack, reproducibility, install, attest] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Checkout protected verification policy | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.sha }} | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - name: Verify workflow artifact provenance | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const run = (await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ ...context.repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| context.repo.owner !== "pylon-code" || context.repo.repo !== "prime-agent" || | |
| run.id !== context.runId || run.run_number !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| String(run.run_attempt) !== process.env.GITHUB_RUN_ATTEMPT || run.repository?.id !== 1349002285 || | |
| run.repository?.full_name !== "pylon-code/prime-agent" || run.head_repository?.id !== 1349002285 || | |
| run.head_repository?.full_name !== "pylon-code/prime-agent" || run.event !== context.eventName || | |
| run.head_sha !== context.sha || run.head_branch !== "pylon" || context.ref !== "refs/heads/pylon" || | |
| workflow.path !== ".github/workflows/pylon-preview-release.yml" | |
| ) throw new Error("Artifact workflow provenance is not canonical."); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| ...context.repo, run_id: context.runId, per_page: 100, | |
| }); | |
| const matches = artifacts.filter((artifact) => artifact.name === "pylon-preview-pack-a"); | |
| if (matches.length !== 1 || matches[0].expired || !/^sha256:[0-9a-f]{64}$/.test(matches[0].digest ?? "")) { | |
| throw new Error("Artifact is ambiguous, expired, or lacks a SHA-256 transport digest."); | |
| } | |
| - name: Download six attested subjects | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: publication | |
| - name: Verify exact signer, subjects, source, and Rekor evidence | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| source_tree="$(git rev-parse 'HEAD^{tree}')" | |
| npm run release:pylon:verify-attestations -- \ | |
| --artifact-dir publication \ | |
| --source-sha "${{ github.sha }}" \ | |
| --source-tree "$source_tree" | |
| publish: | |
| name: Publish immutable preview | |
| needs: [stage-draft, verify-attestation] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| environment: pylon-preview | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: write | |
| steps: | |
| - name: Verify workflow artifact provenance | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const run = (await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ ...context.repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| context.repo.owner !== "pylon-code" || context.repo.repo !== "prime-agent" || | |
| run.id !== context.runId || run.run_number !== Number(process.env.GITHUB_RUN_NUMBER) || | |
| String(run.run_attempt) !== process.env.GITHUB_RUN_ATTEMPT || run.repository?.id !== 1349002285 || | |
| run.repository?.full_name !== "pylon-code/prime-agent" || run.head_repository?.id !== 1349002285 || | |
| run.head_repository?.full_name !== "pylon-code/prime-agent" || run.event !== context.eventName || | |
| run.head_sha !== context.sha || run.head_branch !== "pylon" || context.ref !== "refs/heads/pylon" || | |
| workflow.path !== ".github/workflows/pylon-preview-release.yml" | |
| ) throw new Error("Artifact workflow provenance is not canonical."); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| ...context.repo, run_id: context.runId, per_page: 100, | |
| }); | |
| const matches = artifacts.filter((artifact) => artifact.name === "pylon-preview-pack-a"); | |
| if (matches.length !== 1 || matches[0].expired || !/^sha256:[0-9a-f]{64}$/.test(matches[0].digest ?? "")) { | |
| throw new Error("Artifact is ambiguous, expired, or lacks a SHA-256 transport digest."); | |
| } | |
| - name: Download attested preview subjects | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pylon-preview-pack-a | |
| path: publication | |
| - name: Verify exact checks and freeze the approved preview draft | |
| id: finalize | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ARTIFACT_DIR: publication | |
| DRAFT_ID: ${{ needs.stage-draft.outputs.draft_id }} | |
| with: | |
| script: | | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const crypto = require("node:crypto"); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const repository = `${owner}/${repo}`; | |
| const refName = "heads/pylon"; | |
| const sourceSha = context.sha; | |
| if ( | |
| repository !== "pylon-code/prime-agent" || | |
| context.eventName !== "push" || | |
| context.ref !== "refs/heads/pylon" || | |
| !/^[0-9a-f]{40}$/.test(sourceSha) | |
| ) { | |
| throw new Error("Preview publisher requires the canonical exact pylon push."); | |
| } | |
| const requireLivePylon = async () => { | |
| const livePylon = await github.rest.git.getRef({ owner, repo, ref: refName }); | |
| if (livePylon.data.object.type !== "commit" || livePylon.data.object.sha !== sourceSha) { | |
| throw new Error("Preview publication became stale while verification ran."); | |
| } | |
| }; | |
| await requireLivePylon(); | |
| const releaseBytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-prime-agent-release-v1.json")); | |
| const previewBytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-preview-channel-v1.json")); | |
| const releaseManifest = JSON.parse(releaseBytes); | |
| const previewManifest = JSON.parse(previewBytes); | |
| const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); | |
| const expectedTag = `pylon-build-g${sourceSha.slice(0, 12)}-r${releaseManifest.build.recipeRevision}`; | |
| if ( | |
| releaseManifest.source.repository !== "https://github.com/pylon-code/prime-agent" || | |
| releaseManifest.source.commit !== sourceSha || | |
| releaseManifest.build.id !== expectedTag || | |
| previewManifest.channel !== "preview" || | |
| previewManifest.repository !== "https://github.com/pylon-code/prime-agent" || | |
| previewManifest.build.tag !== expectedTag || | |
| previewManifest.build.source.commit !== sourceSha || | |
| previewManifest.build.source.tree !== releaseManifest.source.tree || | |
| previewManifest.build.releaseManifest.sha256 !== sha256(releaseBytes) || | |
| previewManifest.publicationPolicyRevision !== 1 || previewManifest.sequenceEpoch !== 1 || | |
| previewManifest.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || previewManifest.workflowRunId !== process.env.GITHUB_RUN_ID | |
| ) { | |
| throw new Error("Downloaded preview metadata is not bound to this exact push and workflow sequence."); | |
| } | |
| const commit = await github.rest.git.getCommit({ owner, repo, commit_sha: sourceSha }); | |
| if (commit.data.tree.sha !== releaseManifest.source.tree) throw new Error("GitHub source tree differs from the build manifest."); | |
| const protection = await github.graphql( | |
| `query($owner:String!,$repo:String!,$ref:String!){repository(owner:$owner,name:$repo){ref(qualifiedName:$ref){branchProtectionRule{requiresStatusChecks requiredStatusChecks{context app{databaseId}}}}}}`, | |
| { owner, repo, ref: "refs/heads/pylon" }, | |
| ); | |
| const rule = protection.repository?.ref?.branchProtectionRule; | |
| const required = rule?.requiredStatusChecks; | |
| const expectedPolicy = [ | |
| { context: "Check changelog fragment", appId: 15368, workflowPath: ".github/workflows/changelog-merged-proof.yml" }, | |
| { context: "build-check-test", appId: 15368, workflowPath: ".github/workflows/ci.yml" }, | |
| ]; | |
| const actualPolicy = Array.isArray(required) ? required.map((requirement) => ({ | |
| context: requirement.context, appId: requirement.app?.databaseId ?? null, | |
| })).sort((left, right) => left.context < right.context ? -1 : left.context > right.context ? 1 : 0) : null; | |
| if (!rule?.requiresStatusChecks || JSON.stringify(actualPolicy) !== JSON.stringify(expectedPolicy.map(({ context, appId }) => ({ context, appId })))) { | |
| throw new Error("Protected pylon must require exactly the two app-bound publication checks."); | |
| } | |
| const checks = await github.paginate(github.rest.checks.listForRef, { | |
| owner, repo, ref: sourceSha, filter: "latest", per_page: 100, | |
| }); | |
| for (const requirement of expectedPolicy) { | |
| const candidates = checks.filter((check) => | |
| check.name === requirement.context && check.head_sha === sourceSha && check.app?.id === requirement.appId && | |
| check.status === "completed" && check.conclusion === "success" | |
| ); | |
| let proved = false; | |
| for (const check of candidates) { | |
| const runId = /^https:\/\/github\.com\/pylon-code\/prime-agent\/actions\/runs\/([0-9]+)(?:\/job\/[0-9]+)?$/.exec(check.details_url ?? "")?.[1]; | |
| if (!runId) continue; | |
| const suite = (await github.rest.checks.getSuite({ owner, repo, check_suite_id: check.check_suite.id })).data; | |
| const run = (await github.rest.actions.getWorkflowRun({ owner, repo, run_id: Number(runId) })).data; | |
| const workflow = (await github.rest.actions.getWorkflow({ owner, repo, workflow_id: run.workflow_id })).data; | |
| if ( | |
| suite.app?.id === requirement.appId && suite.head_sha === sourceSha && suite.status === "completed" && suite.conclusion === "success" && | |
| run.check_suite_id === suite.id && run.repository?.id === 1349002285 && run.repository?.full_name === repository && | |
| run.head_repository?.id === 1349002285 && run.head_repository?.full_name === repository && run.event === "push" && | |
| run.head_branch === "pylon" && run.head_sha === sourceSha && run.status === "completed" && run.conclusion === "success" && | |
| workflow.path === requirement.workflowPath | |
| ) { proved = true; break; } | |
| } | |
| if (!proved) throw new Error(`Required check ${requirement.context} lacks an exact canonical push workflow proof.`); | |
| } | |
| const files = fs.readdirSync(process.env.ARTIFACT_DIR).sort(); | |
| const expectedFiles = [ | |
| ...releaseManifest.assets.map((asset) => asset.file), | |
| "pylon-preview-channel-v1.json", | |
| "pylon-prime-agent-release-v1.json", | |
| ].sort(); | |
| if (JSON.stringify(files) !== JSON.stringify(expectedFiles) || files.length !== 6) { | |
| throw new Error("Preview publisher received a subject set other than the exact six files."); | |
| } | |
| const assets = files.map((name) => { | |
| const bytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, name)); | |
| return { name, bytes, size: bytes.byteLength, sha256: sha256(bytes) }; | |
| }); | |
| for (const asset of releaseManifest.assets) { | |
| const actual = assets.find((candidate) => candidate.name === asset.file); | |
| if (!actual || actual.size !== asset.size || actual.sha256 !== asset.sha256) { | |
| throw new Error(`Preview subject differs from build manifest: ${asset.file}`); | |
| } | |
| } | |
| const tag = expectedTag; | |
| core.setOutput("tag", tag); | |
| const name = `Pylon Prime preview ${tag}`; | |
| const body = [ | |
| "Pylon Prime preview publication.", "", `Tag: ${tag}`, `Source: ${sourceSha}`, | |
| `Tree: ${releaseManifest.source.tree}`, `Recipe: r${releaseManifest.build.recipeRevision}`, "", | |
| "Verify the immutable release and artifact attestations before use.", | |
| ].join("\n"); | |
| const requireExactTag = async () => { | |
| const tagRef = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); | |
| if (tagRef.data.object.type !== "commit" || tagRef.data.object.sha !== sourceSha) { | |
| throw new Error("Preview tag does not target the exact source commit."); | |
| } | |
| }; | |
| const assertExact = async (release) => { | |
| if ( | |
| release.immutable !== true || release.draft !== false || release.tag_name !== tag || release.name !== name || | |
| release.body !== body || release.prerelease !== true || release.target_commitish !== sourceSha || | |
| release.assets.length !== assets.length | |
| ) throw new Error("Existing preview release is mutable or has different metadata."); | |
| for (const expected of assets) { | |
| const actual = release.assets.find((candidate) => candidate.name === expected.name); | |
| if (!actual || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Existing preview asset differs: ${expected.name}`); | |
| } | |
| } | |
| await requireExactTag(); | |
| }; | |
| let existing; | |
| try { | |
| existing = (await github.rest.repos.getReleaseByTag({ owner, repo, tag })).data; | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| if (!existing) { | |
| const matching = (await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 })) | |
| .filter((release) => release.tag_name === tag); | |
| if (matching.length > 1) throw new Error("Preview tag resolves to multiple releases."); | |
| existing = matching[0]; | |
| } | |
| if (existing && !existing.draft) { | |
| if (String(existing.id) !== process.env.DRAFT_ID) throw new Error("Idempotent preview release id differs from approved staging."); | |
| await assertExact(existing); | |
| core.info(`Immutable preview ${tag} already contains identical bytes and metadata.`); | |
| core.setOutput("release_id", ""); | |
| return; | |
| } | |
| let draft = existing; | |
| if (draft) { | |
| if ( | |
| draft.immutable === true || draft.tag_name !== tag || draft.name !== name || draft.body !== body || | |
| draft.prerelease !== true || draft.target_commitish !== sourceSha | |
| ) throw new Error("Partial preview draft identity differs; refusing to edit it."); | |
| for (const actual of draft.assets) { | |
| const expected = assets.find((candidate) => candidate.name === actual.name); | |
| if (!expected || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Partial preview draft contains a changed asset: ${actual.name}`); | |
| } | |
| } | |
| } else { | |
| throw new Error("Approved preview draft is missing; publisher will not recreate it after attestation."); | |
| } | |
| if (String(draft.id) !== process.env.DRAFT_ID) throw new Error("Approved preview draft id changed after attestation."); | |
| if (draft.assets.length !== assets.length) throw new Error("Approved preview draft is not fully staged."); | |
| for (const expected of assets) { | |
| const actual = draft.assets.find((asset) => asset.name === expected.name); | |
| if (!actual || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Approved preview draft asset differs: ${expected.name}`); | |
| } | |
| } | |
| core.setOutput("release_id", String(draft.id)); | |
| - name: Mint repository-scoped ruleset auditor token | |
| id: ruleset-auditor | |
| if: steps.finalize.outputs.release_id != '' | |
| uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 | |
| with: | |
| app-id: ${{ vars.PYLON_RULESET_AUDITOR_APP_ID }} | |
| private-key: ${{ secrets.PYLON_RULESET_AUDITOR_PRIVATE_KEY }} | |
| owner: pylon-code | |
| repositories: prime-agent | |
| permission-administration: read | |
| - name: Require live pylon, exact tag, and exact draft before final ruleset audit | |
| if: steps.finalize.outputs.release_id != '' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ARTIFACT_DIR: publication | |
| DRAFT_ID: ${{ steps.finalize.outputs.release_id }} | |
| TAG: ${{ steps.finalize.outputs.tag }} | |
| with: | |
| script: | | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const crypto = require("node:crypto"); | |
| const pylon = await github.rest.git.getRef({ ...context.repo, ref: "heads/pylon" }); | |
| if (pylon.data.object.type !== "commit" || pylon.data.object.sha !== context.sha) { | |
| throw new Error("Preview publication became stale before its final authoritative ruleset audit."); | |
| } | |
| const tag = await github.rest.git.getRef({ ...context.repo, ref: `tags/${process.env.TAG}` }); | |
| if (tag.data.object.type !== "commit" || tag.data.object.sha !== context.sha) { | |
| throw new Error("Preview publication tag changed before its final authoritative ruleset audit."); | |
| } | |
| const release = JSON.parse(fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-prime-agent-release-v1.json"))); | |
| const body = [ | |
| "Pylon Prime preview publication.", "", `Tag: ${process.env.TAG}`, `Source: ${context.sha}`, | |
| `Tree: ${release.source?.tree}`, `Recipe: r${release.build?.recipeRevision}`, "", | |
| "Verify the immutable release and artifact attestations before use.", | |
| ].join("\n"); | |
| const expected = fs.readdirSync(process.env.ARTIFACT_DIR).map((name) => { | |
| const bytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, name)); | |
| return { name, size: bytes.length, digest: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}` }; | |
| }); | |
| const draft = (await github.rest.repos.getRelease({ ...context.repo, release_id: Number(process.env.DRAFT_ID) })).data; | |
| if ( | |
| !draft.draft || draft.immutable === true || draft.tag_name !== process.env.TAG || | |
| draft.name !== `Pylon Prime preview ${process.env.TAG}` || draft.body !== body || draft.prerelease !== true || | |
| draft.target_commitish !== context.sha || draft.assets?.length !== expected.length | |
| ) throw new Error("Preview draft changed before final authoritative ruleset audit."); | |
| for (const asset of expected) { | |
| const actual = draft.assets.find((candidate) => candidate.name === asset.name); | |
| if (!actual || actual.size !== asset.size || actual.digest !== asset.digest) { | |
| throw new Error(`Preview draft asset changed before final ruleset audit: ${asset.name}`); | |
| } | |
| } | |
| - name: Require authoritative publication tag ruleset before immutable preview publish | |
| if: steps.finalize.outputs.release_id != '' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| github-token: ${{ steps.ruleset-auditor.outputs.token }} | |
| script: | | |
| const response = await github.request("GET /repos/{owner}/{repo}/rulesets/{ruleset_id}", { | |
| owner: "pylon-code", repo: "prime-agent", ruleset_id: 21950766, includes_parents: false, | |
| headers: { accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" }, | |
| }); | |
| const restRuleset = response?.data; | |
| const exactKeys = (value, keys) => value !== null && typeof value === "object" && !Array.isArray(value) && | |
| JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort()); | |
| const exactSortedStrings = (value, expected) => Array.isArray(value) && value.every((entry) => typeof entry === "string") && | |
| JSON.stringify([...value].sort()) === JSON.stringify([...expected].sort()); | |
| const expectedIncludes = ["refs/tags/pylon-build-*", "refs/tags/pylon-stable-*"]; | |
| const restConditions = restRuleset?.conditions; | |
| const restRefName = restConditions?.ref_name; | |
| const restRules = restRuleset?.rules; | |
| const restUpdateRules = Array.isArray(restRules) ? restRules.filter((rule) => rule?.type === "update") : []; | |
| const restDeletionRules = Array.isArray(restRules) ? restRules.filter((rule) => rule?.type === "deletion") : []; | |
| if ( | |
| response?.status !== 200 || !restRuleset || restRuleset.id !== 21950766 || | |
| restRuleset.node_id !== "RRS_lACqUmVwb3NpdG9yec5QaCQtzgFO8S4" || restRuleset.name !== "Pylon immutable publication tags" || | |
| restRuleset.source_type !== "Repository" || restRuleset.source !== "pylon-code/prime-agent" || | |
| restRuleset.target !== "tag" || restRuleset.enforcement !== "active" || | |
| Object.hasOwn(restRuleset, "bypass_actors") && (!Array.isArray(restRuleset.bypass_actors) || restRuleset.bypass_actors.length !== 0) || | |
| Object.hasOwn(restRuleset, "current_user_can_bypass") && restRuleset.current_user_can_bypass !== "never" || | |
| !exactKeys(restConditions, ["ref_name"]) || !exactKeys(restRefName, ["exclude", "include"]) || | |
| !exactSortedStrings(restRefName.exclude, []) || !exactSortedStrings(restRefName.include, expectedIncludes) || | |
| !Array.isArray(restRules) || restRules.length !== 2 || restUpdateRules.length !== 1 || restDeletionRules.length !== 1 || | |
| !exactKeys(restUpdateRules[0], ["parameters", "type"]) || | |
| !exactKeys(restUpdateRules[0]?.parameters, ["update_allows_fetch_and_merge"]) || | |
| restUpdateRules[0]?.parameters?.update_allows_fetch_and_merge !== false || !exactKeys(restDeletionRules[0], ["type"]) | |
| ) throw new Error("REST ruleset-auditor response differs from the exact active immutable tag ruleset."); | |
| const query = `query PylonPublicationRulesetAudit($owner: String!, $repo: String!, $rulesetDatabaseId: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| id | |
| databaseId | |
| nameWithOwner | |
| ruleset(databaseId: $rulesetDatabaseId, includeParents: false) { | |
| id | |
| databaseId | |
| name | |
| enforcement | |
| target | |
| source { | |
| __typename | |
| ... on Repository { | |
| id | |
| databaseId | |
| nameWithOwner | |
| } | |
| } | |
| bypassActors { totalCount } | |
| conditions { | |
| refName { include exclude } | |
| organizationProperty { __typename } | |
| repositoryId { __typename } | |
| repositoryName { __typename } | |
| repositoryProperty { __typename } | |
| } | |
| rules(first: 100) { | |
| totalCount | |
| nodes { | |
| type | |
| parameters { | |
| __typename | |
| ... on UpdateParameters { updateAllowsFetchAndMerge } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }`; | |
| const authoritative = await github.graphql(query, { | |
| owner: "pylon-code", repo: "prime-agent", rulesetDatabaseId: 21950766, | |
| }); | |
| const repository = authoritative?.repository; | |
| const ruleset = repository?.ruleset; | |
| const source = ruleset?.source; | |
| const bypassActors = ruleset?.bypassActors; | |
| const conditions = ruleset?.conditions; | |
| const refName = conditions?.refName; | |
| const rules = ruleset?.rules; | |
| const nodes = rules?.nodes; | |
| const updateRules = Array.isArray(nodes) ? nodes.filter((rule) => rule?.type === "UPDATE") : []; | |
| const deletionRules = Array.isArray(nodes) ? nodes.filter((rule) => rule?.type === "DELETION") : []; | |
| if ( | |
| Object.hasOwn(authoritative ?? {}, "errors") || !repository || repository.id !== "R_kgDOUGgkLQ" || | |
| repository.databaseId !== 1349002285 || repository.nameWithOwner !== "pylon-code/prime-agent" || | |
| !ruleset || ruleset.id !== restRuleset.node_id || ruleset.id !== "RRS_lACqUmVwb3NpdG9yec5QaCQtzgFO8S4" || | |
| ruleset.databaseId !== 21950766 || ruleset.name !== "Pylon immutable publication tags" || | |
| ruleset.enforcement !== "ACTIVE" || ruleset.target !== "TAG" || | |
| !exactKeys(source, ["__typename", "databaseId", "id", "nameWithOwner"]) || source.__typename !== "Repository" || | |
| source.id !== repository.id || source.databaseId !== repository.databaseId || source.nameWithOwner !== repository.nameWithOwner || | |
| !exactKeys(bypassActors, ["totalCount"]) || !Number.isInteger(bypassActors.totalCount) || bypassActors.totalCount !== 0 || | |
| !exactKeys(conditions, ["organizationProperty", "refName", "repositoryId", "repositoryName", "repositoryProperty"]) || | |
| conditions.organizationProperty !== null || conditions.repositoryId !== null || conditions.repositoryName !== null || | |
| conditions.repositoryProperty !== null || !exactKeys(refName, ["exclude", "include"]) || | |
| !exactSortedStrings(refName.exclude, []) || !exactSortedStrings(refName.include, expectedIncludes) || | |
| !exactKeys(rules, ["nodes", "totalCount"]) || !Number.isInteger(rules.totalCount) || rules.totalCount !== 2 || | |
| !Array.isArray(nodes) || nodes.length !== 2 || nodes.some((node) => node === null) || | |
| updateRules.length !== 1 || deletionRules.length !== 1 || !exactKeys(updateRules[0], ["parameters", "type"]) || | |
| !exactKeys(updateRules[0].parameters, ["__typename", "updateAllowsFetchAndMerge"]) || | |
| updateRules[0].parameters.__typename !== "UpdateParameters" || updateRules[0].parameters.updateAllowsFetchAndMerge !== false || | |
| !exactKeys(deletionRules[0], ["parameters", "type"]) || deletionRules[0].parameters !== null | |
| ) throw new Error("GraphQL ruleset-auditor response is null, partial, redacted, or differs from the exact non-bypassable target contract."); | |
| - name: Publish the exact approved preview draft | |
| if: steps.finalize.outputs.release_id != '' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ARTIFACT_DIR: publication | |
| DRAFT_ID: ${{ steps.finalize.outputs.release_id }} | |
| with: | |
| script: | | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const crypto = require("node:crypto"); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const releaseBytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, "pylon-prime-agent-release-v1.json")); | |
| const release = JSON.parse(releaseBytes); | |
| const draftId = Number(process.env.DRAFT_ID); | |
| const tag = `pylon-build-g${context.sha.slice(0, 12)}-r${release.build?.recipeRevision}`; | |
| const name = `Pylon Prime preview ${tag}`; | |
| const body = [ | |
| "Pylon Prime preview publication.", "", `Tag: ${tag}`, `Source: ${context.sha}`, | |
| `Tree: ${release.source?.tree}`, `Recipe: r${release.build?.recipeRevision}`, "", | |
| "Verify the immutable release and artifact attestations before use.", | |
| ].join("\n"); | |
| const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); | |
| const assets = fs.readdirSync(process.env.ARTIFACT_DIR).sort().map((assetName) => { | |
| const bytes = fs.readFileSync(path.join(process.env.ARTIFACT_DIR, assetName)); | |
| return { name: assetName, size: bytes.length, sha256: sha256(bytes) }; | |
| }); | |
| if ( | |
| `${owner}/${repo}` !== "pylon-code/prime-agent" || context.eventName !== "push" || context.ref !== "refs/heads/pylon" || | |
| !Number.isSafeInteger(draftId) || draftId < 1 || release.source?.commit !== context.sha || release.build?.id !== tag || assets.length !== 6 | |
| ) throw new Error("Approved preview publication identity is malformed."); | |
| await github.rest.repos.updateRelease({ owner, repo, release_id: draftId, draft: false }); | |
| const published = (await github.rest.repos.getRelease({ owner, repo, release_id: draftId })).data; | |
| if ( | |
| published.immutable !== true || published.draft !== false || published.tag_name !== tag || published.name !== name || | |
| published.body !== body || published.prerelease !== true || published.target_commitish !== context.sha || | |
| published.assets.length !== assets.length | |
| ) throw new Error("Published preview release differs from the exact approved draft."); | |
| for (const expected of assets) { | |
| const actual = published.assets.find((asset) => asset.name === expected.name); | |
| if (!actual || actual.size !== expected.size || actual.digest !== `sha256:${expected.sha256}`) { | |
| throw new Error(`Published preview asset differs: ${expected.name}`); | |
| } | |
| } | |
| const tagRef = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }); | |
| if (tagRef.data.object.type !== "commit" || tagRef.data.object.sha !== context.sha) { | |
| throw new Error("Published preview tag differs from the exact source commit."); | |
| } | |
| - name: Verify GitHub immutable-release attestation | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="$(node -e "console.log(JSON.parse(require('node:fs').readFileSync('publication/pylon-preview-channel-v1.json')).build.tag)")" | |
| verified=false | |
| for attempt in 1 2 3 4 5 6; do | |
| if gh release verify "$tag" --repo pylon-code/prime-agent; then | |
| verified=true | |
| break | |
| fi | |
| if [ "$attempt" = 6 ]; then | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| test "$verified" = true | |
| for asset in publication/*; do | |
| gh release verify-asset "$tag" "$asset" --repo pylon-code/prime-agent | |
| done |