From d0d69b8f0fd64c023f666431e05334bfb1526e6d Mon Sep 17 00:00:00 2001 From: Daniel Niasoff <4070285+dniasoff@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:35:01 +0000 Subject: [PATCH 1/3] Accept the MCP OAuth bearer on staged upload file writes An MCP upload plan tells the agent to PUT each file with the bearer it used on /mcp ("reuse_the_mcp_bearer_credential"). With a hosted OAuth provider (generic OIDC or WorkOS), /mcp accepts the end-user access token, but the upload URL is under /api, whose bearer check accepts only managed API keys. So an agent signed in only through OAuth could plan an upload but never write its files, and could not publish. The staged-upload file write (PUT /api/v1/uploads/:uploadId/files/:token) now authenticates its bearer exactly as /mcp does. That is a superset of the API-credential path for that route: the same local token and managed keys, plus the hosted access token for the exact /mcp resource. Upload ownership is unchanged, because StagedUploadService still binds every file to the principal that created the plan. Every other API route keeps the API-credential contract. Tests (generic OIDC): - an OIDC access token plans, uploads and publishes over MCP (fails before this change with 401 on the file write); - the same token gets 401 on other API routes (api-keys, REST upload creation), cannot write another principal's upload, and wrong-audience, ID, and expired tokens are refused on the file write. --- docs/deployment.md | 4 + docs/mcp.md | 2 +- src/http/create-http-app.ts | 22 ++- .../oidc-mcp-authorization.test.ts | 158 ++++++++++++++++++ 4 files changed, 182 insertions(+), 4 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 873fcbdb..5fc7e048 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -43,6 +43,10 @@ Remote deployments use WorkOS or a generic OIDC provider. Network access and app The issuer configured for browser login also protects `/mcp`. Agents present an end-user access token, and the server records the person who obtained it. The +same access token also writes the files of the agent's own MCP upload plan +(`PUT /api/v1/uploads//files/`, the URLs `artifact_create_upload` +returns), so an agent signed in only through OAuth can publish. Every other API +route still requires an API key or a browser session. The server never issues client credentials and never runs an authorization server of its own. diff --git a/docs/mcp.md b/docs/mcp.md index 85a3faf2..66930681 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -50,7 +50,7 @@ An agent uses this sequence: 1. Call `artifact_capabilities` to read limits and available features. 2. Call `artifact_create_upload` with each path, size, media type, and SHA-256 fingerprint. -3. Upload each file to its returned opaque upload URL. +3. Upload each file to its returned opaque upload URL with the same bearer credential the agent uses for MCP: an API key or the OAuth access token. 4. Call `artifact_commit_upload` with the publication target and idempotency key. When the agent publishes a new version, it first reads the current version ID. It sends that ID as `expectedCurrentVersionId`. diff --git a/src/http/create-http-app.ts b/src/http/create-http-app.ts index 45aa95cc..8a0c2955 100644 --- a/src/http/create-http-app.ts +++ b/src/http/create-http-app.ts @@ -806,13 +806,21 @@ export function createHttpApp( message: "A valid Artifact Server API key is required.", }); } + const credential = Redacted.make(parsed.data, {label: "bearer-credential"}); const principal = await runHttpApplicationEffect( context, dependencies, AuthenticationService.use((authentication) => - authentication.authenticateApiBearer( - Redacted.make(parsed.data, {label: "bearer-credential"}), - ) + isStagedUploadFileRequest(context) + // An MCP upload plan tells the agent to PUT each file with the + // bearer it used on /mcp, so this one route accepts exactly the + // credentials /mcp accepts, including a hosted OAuth access token + // for the /mcp resource. Every other API route keeps the + // API-credential contract. + ? authentication.authenticateMcpBearer(credential).pipe( + Effect.map((authenticated) => authenticated.principal), + ) + : authentication.authenticateApiBearer(credential) ), ); context.set("authenticationMethod", "bearer"); @@ -4093,3 +4101,11 @@ function redirectToReview( } return context.redirect(`/review?${requestUrl.searchParams.toString()}`, 308); } + +const stagedUploadFilePath = /^\/api\/v1\/uploads\/[^/]+\/files\/[^/]+$/u; + +/** Whether this request writes one file of a staged upload plan. */ +function isStagedUploadFileRequest(context: Context): boolean { + return context.req.method === "PUT" + && stagedUploadFilePath.test(context.req.path); +} diff --git a/tests/conformance/oidc-mcp-authorization.test.ts b/tests/conformance/oidc-mcp-authorization.test.ts index fab15723..aec32705 100644 --- a/tests/conformance/oidc-mcp-authorization.test.ts +++ b/tests/conformance/oidc-mcp-authorization.test.ts @@ -1,3 +1,4 @@ +import {createHash} from "node:crypto"; import {createServer, type Server} from "node:http"; import { @@ -15,6 +16,7 @@ import { } from "jose"; import {Effect, Predicate, Redacted} from "effect"; import {afterEach, beforeEach, describe, expect, test} from "vitest"; +import {z} from "zod"; import {createOidcHostedAuthentication} from "../../src/identity/oidc-hosted-authentication.js"; @@ -337,6 +339,131 @@ describe("generic OIDC MCP authorization", () => { issueToken({idToken: true}))).rejects.toThrow("ID token is not"); }); + test("an OIDC access token uploads and publishes the files of its own MCP upload plan", async () => { + const token = await issueToken({}); + const bytes = new TextEncoder().encode("oidc\n"); + const plan = await callTool(token, "artifact_create_upload", { + entryPath: "index.html", + files: [declaredFile("index.html", "text/html", bytes)], + }, uploadPlanSchema); + + const [file] = plan.files; + expect(file?.authorization.credential).toBe("reuse_the_mcp_bearer_credential"); + const uploaded = await putPlanFile(plan.files[0]?.uploadUrl, token, bytes); + expect(uploaded.status).toBe(200); + + const published = await callTool(token, "artifact_commit_upload", { + idempotencyKey: `oidc-upload-${crypto.randomUUID()}`, + target: {kind: "new_artifact", name: "OIDC upload"}, + uploadId: plan.uploadId, + }, publishedVersionSchema); + expect(published.links.review).toContain("/review"); + }); + + test("an OIDC access token opens no other API route, no other principal's upload, and no wrong resource", async () => { + const token = await issueToken({}); + const bytes = new TextEncoder().encode("hostile\n"); + const declared = { + entryPath: "index.html", + files: [declaredFile("index.html", "text/plain", bytes)], + }; + + // Only the staged-file write takes the MCP credential; the rest of the + // API keeps its own contract. + const keys = await fetch(`${server.baseUrl}/api/v1/api-keys`, { + headers: {Authorization: `Bearer ${token}`}, + }); + expect(keys.status).toBe(401); + const restCreate = await fetch(`${server.baseUrl}/api/v1/uploads`, { + body: JSON.stringify(declared), + headers: {Authorization: `Bearer ${token}`, "Content-Type": "application/json"}, + method: "POST", + }); + expect(restCreate.status).toBe(401); + + // Another principal's upload stays closed to this person. + const foreign = await callTool( + installation.apiToken, + "artifact_create_upload", + declared, + uploadPlanSchema, + ); + const crossed = await putPlanFile(foreign.files[0]?.uploadUrl, token, bytes); + expect(crossed.ok).toBe(false); + expect([401, 403, 404]).toContain(crossed.status); + + // Tokens /mcp refuses are refused here too. + const own = await callTool(token, "artifact_create_upload", declared, uploadPlanSchema); + const hostileTokens = await Promise.all([ + issueToken({audience: "artifact-server"}), + issueToken({audience: "https://attacker.example/mcp"}), + issueToken({idToken: true}), + issueToken({expiresAt: Math.floor(Date.now() / 1_000) - 60}), + ]); + const hostileResponses = await Promise.all(hostileTokens.map((hostile) => + putPlanFile(own.files[0]?.uploadUrl, hostile, bytes) + )); + expect(hostileResponses.map((response) => response.status)) + .toEqual([401, 401, 401, 401]); + }); + + async function callTool( + token: string, + name: string, + parameters: ToolArguments, + output: z.ZodType, + ): Promise { + const response = await fetch(`${server.baseUrl}/mcp`, { + body: JSON.stringify({ + id: crypto.randomUUID(), + jsonrpc: "2.0", + method: "tools/call", + params: { + _meta: { + [CLIENT_CAPABILITIES_META_KEY]: {}, + [CLIENT_INFO_META_KEY]: {name: "oidc-auth-test", version: "1"}, + [PROTOCOL_VERSION_META_KEY]: protocolVersion, + }, + arguments: parameters, + name, + }, + }), + headers: { + Accept: "application/json, text/event-stream", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "MCP-Protocol-Version": protocolVersion, + "Mcp-Method": "tools/call", + "Mcp-Name": name, + }, + method: "POST", + }); + expect(response.status).toBe(200); + const body = z.object({ + result: z.object({ + isError: z.boolean().optional(), + structuredContent: z.unknown(), + }).loose(), + }).loose().parse(await response.json()); + expect(body.result.isError ?? false).toBe(false); + return output.parse(body.result.structuredContent); + } + + /** PUT to the plan's URL path on this test server, whatever its origin. */ + function putPlanFile( + uploadUrl: string | undefined, + token: string, + bytes: Uint8Array, + ): Promise { + if (uploadUrl === undefined) throw new Error("The upload plan has no file."); + const planned = new URL(uploadUrl); + return fetch(new URL(`${planned.pathname}${planned.search}`, server.baseUrl), { + body: Buffer.from(bytes), + headers: {Authorization: `Bearer ${token}`}, + method: "PUT", + }); + } + async function issueToken(options: { readonly audience?: string | string[]; /** `null` leaves the claim out. */ @@ -419,6 +546,37 @@ describe("generic OIDC MCP authorization", () => { } }); +type ToolArgument = + | number + | string + | readonly ToolArgument[] + | {readonly [key: string]: ToolArgument}; + +interface ToolArguments { + readonly [key: string]: ToolArgument; +} + +const publishedVersionSchema = z.object({ + links: z.object({review: z.string()}).loose(), +}).loose(); + +const uploadPlanSchema = z.object({ + files: z.array(z.object({ + authorization: z.object({credential: z.string()}).loose(), + uploadUrl: z.string(), + }).loose()).min(1), + uploadId: z.string(), +}).loose(); + +function declaredFile(manifestPath: string, mediaType: string, bytes: Uint8Array) { + return { + mediaType, + path: manifestPath, + sha256: createHash("sha256").update(bytes).digest("hex"), + size: bytes.byteLength, + }; +} + function verifiedSubject( verifier: ExternalMcpBearerVerifier, token: string, From 29209af904d465a93801c23ffabc2c3eeac56fcc Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 25 Oct 2026 18:15:06 +0000 Subject: [PATCH 2/3] ci: restrict triggers to PR/tag/dispatch [skip ci] --- .github/workflows/ci.yml | 523 +++++++++--------- .github/workflows/install-verify.yml | 516 +++++++++--------- .github/workflows/release.yml | 786 +++++++++++++-------------- .github/workflows/security.yml | 205 ++++--- 4 files changed, 1011 insertions(+), 1019 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4cdcfd3..025d2edb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,9 @@ on: type: string pull_request: branches: [main] - merge_group: - types: [checks_requested] push: - branches: [main] - schedule: - - cron: "17 9 * * *" + tags: + - '**' workflow_dispatch: inputs: tier: @@ -24,8 +21,8 @@ on: default: full type: choice options: - - full - - pr + - full + - pr permissions: contents: read @@ -41,50 +38,50 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 8 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Verify CI full-gate definition - run: pnpm check:ci - - name: Lint - run: pnpm lint - - name: Typecheck - run: pnpm typecheck - - name: Build application - run: pnpm build - - name: Verify public site - run: pnpm verify:site - - name: Validate conformance ledger - run: pnpm conformance:validate - - name: Validate conformance test IDs - run: pnpm conformance:tests - - name: Verify AWS Pulumi project - run: pnpm test:aws-pulumi - - name: Verify GCP Pulumi project - run: pnpm test:gcp-pulumi - - name: Upload compiled application - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist/ - if-no-files-found: error - retention-days: 2 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Verify CI full-gate definition + run: pnpm check:ci + - name: Lint + run: pnpm lint + - name: Typecheck + run: pnpm typecheck + - name: Build application + run: pnpm build + - name: Verify public site + run: pnpm verify:site + - name: Validate conformance ledger + run: pnpm conformance:validate + - name: Validate conformance test IDs + run: pnpm conformance:tests + - name: Verify AWS Pulumi project + run: pnpm test:aws-pulumi + - name: Verify GCP Pulumi project + run: pnpm test:gcp-pulumi + - name: Upload compiled application + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist/ + if-no-files-found: error + retention-days: 2 unit: name: PR / Unit and conformance behavior @@ -93,32 +90,32 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 12 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled application - run: test -f dist/cli/main.js && test -f dist/web/review.html - - name: Run root behavior and conformance tests - env: - NODE_OPTIONS: --max-old-space-size=4096 - run: pnpm test + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled application + run: test -f dist/cli/main.js && test -f dist/web/review.html + - name: Run root behavior and conformance tests + env: + NODE_OPTIONS: --max-old-space-size=4096 + run: pnpm test browser: name: PR / Browser behavior @@ -127,40 +124,40 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Install Chromium - run: pnpm exec playwright install --with-deps chromium - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled web application - run: test -f dist/web/review.html - - name: Run browser tests - run: pnpm test:web:prebuilt - - name: Upload browser diagnostics - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-browser-${{ github.run_id }}-${{ github.run_attempt }} - path: test-results/browser/ - if-no-files-found: warn - retention-days: 7 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled web application + run: test -f dist/web/review.html + - name: Run browser tests + run: pnpm test:web:prebuilt + - name: Upload browser diagnostics + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-browser-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/browser/ + if-no-files-found: warn + retention-days: 7 cloudflare: name: PR / Cloudflare deployment @@ -169,30 +166,30 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 8 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled web application - run: test -f dist/web/review.html - - name: Run Cloudflare checks - run: pnpm check:cloudflare:prebuilt + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled web application + run: test -f dist/web/review.html + - name: Run Cloudflare checks + run: pnpm check:cloudflare:prebuilt portable: name: macOS portability @@ -200,27 +197,27 @@ jobs: runs-on: macos-14 timeout-minutes: 12 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Verify portable code paths - run: pnpm check + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Verify portable code paths + run: pnpm check full: name: Full / Linux iteration gate @@ -230,126 +227,126 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=4096 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Reclaim unused runner disk - run: | - df -h / - sudo rm -rf -- \ - /opt/ghc \ - /usr/local/lib/android \ - /usr/local/share/boost \ - /usr/share/dotnet - df -h / - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js without a release cache - if: inputs.tier == 'release' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install Node.js with the pnpm cache - if: inputs.tier != 'release' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Install Chromium - run: pnpm exec playwright install --with-deps chromium - - name: Enable the containerd image store - run: | - artifactserver_daemon_config=$(mktemp) - if [[ -s /etc/docker/daemon.json ]]; then - jq \ - '.features = ((.features // {}) + {"containerd-snapshotter": true})' \ - /etc/docker/daemon.json > "$artifactserver_daemon_config" - else - printf '%s\n' \ - '{"features":{"containerd-snapshotter":true}}' \ - > "$artifactserver_daemon_config" - fi - sudo install -d -m 0755 /etc/docker - sudo install -m 0644 \ - "$artifactserver_daemon_config" \ - /etc/docker/daemon.json - rm -f -- "$artifactserver_daemon_config" - sudo systemctl restart docker - docker info --format '{{json .DriverStatus}}' \ - | grep -F 'io.containerd.snapshotter.v1' - - name: Prepare attestation-capable BuildKit - run: | - docker buildx create \ - --driver docker-container \ - --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ - --name artifact-server-ci \ - --use - docker buildx inspect --bootstrap - - name: Verify CI full-gate definition - run: pnpm check:ci + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Reclaim unused runner disk + run: | + df -h / + sudo rm -rf -- \ + /opt/ghc \ + /usr/local/lib/android \ + /usr/local/share/boost \ + /usr/share/dotnet + df -h / + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js without a release cache + if: inputs.tier == 'release' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install Node.js with the pnpm cache + if: inputs.tier != 'release' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + - name: Enable the containerd image store + run: | + artifactserver_daemon_config=$(mktemp) + if [[ -s /etc/docker/daemon.json ]]; then + jq \ + '.features = ((.features // {}) + {"containerd-snapshotter": true})' \ + /etc/docker/daemon.json > "$artifactserver_daemon_config" + else + printf '%s\n' \ + '{"features":{"containerd-snapshotter":true}}' \ + > "$artifactserver_daemon_config" + fi + sudo install -d -m 0755 /etc/docker + sudo install -m 0644 \ + "$artifactserver_daemon_config" \ + /etc/docker/daemon.json + rm -f -- "$artifactserver_daemon_config" + sudo systemctl restart docker + docker info --format '{{json .DriverStatus}}' \ + | grep -F 'io.containerd.snapshotter.v1' + - name: Prepare attestation-capable BuildKit + run: | + docker buildx create \ + --driver docker-container \ + --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ + --name artifact-server-ci \ + --use + docker buildx inspect --bootstrap + - name: Verify CI full-gate definition + run: pnpm check:ci # full-gate-command: pnpm check - - name: Check correctness and static contracts - run: pnpm check + - name: Check correctness and static contracts + run: pnpm check # full-gate-command: pnpm test:web - - name: Test browser behavior - run: pnpm test:web + - name: Test browser behavior + run: pnpm test:web # full-gate-command: pnpm test:aws-pulumi - - name: Test AWS Pulumi project - run: pnpm test:aws-pulumi + - name: Test AWS Pulumi project + run: pnpm test:aws-pulumi # full-gate-command: pnpm test:gcp-pulumi - - name: Test GCP Pulumi project - run: pnpm test:gcp-pulumi + - name: Test GCP Pulumi project + run: pnpm test:gcp-pulumi # full-gate-command: pnpm verify:object-storage - - name: Verify object storage - run: pnpm verify:object-storage + - name: Verify object storage + run: pnpm verify:object-storage # full-gate-command: pnpm test:external-storage-runtime - - name: Test external-storage runtime - run: pnpm test:external-storage-runtime + - name: Test external-storage runtime + run: pnpm test:external-storage-runtime # full-gate-command: pnpm test:coverage - - name: Report coverage - run: pnpm test:coverage + - name: Report coverage + run: pnpm test:coverage # full-gate-command: pnpm test:local-package - - name: Test local package - run: pnpm test:local-package + - name: Test local package + run: pnpm test:local-package # full-gate-command: pnpm perf:baseline - - name: Record local performance baseline - run: pnpm perf:baseline + - name: Record local performance baseline + run: pnpm perf:baseline # full-gate-command: pnpm perf:capacity - - name: Record server capacity baseline - run: pnpm perf:capacity + - name: Record server capacity baseline + run: pnpm perf:capacity # full-gate-command: pnpm test:compact-compose - - name: Test compact Compose - run: pnpm test:compact-compose + - name: Test compact Compose + run: pnpm test:compact-compose # full-gate-command: pnpm test:external-storage-compose - - name: Test external-storage Compose - run: pnpm test:external-storage-compose + - name: Test external-storage Compose + run: pnpm test:external-storage-compose # full-gate-command: pnpm verify:helm - - name: Verify Helm release - run: pnpm verify:helm + - name: Verify Helm release + run: pnpm verify:helm # full-gate-command: pnpm test:oidc - - name: Test generic OIDC - run: pnpm test:oidc + - name: Test generic OIDC + run: pnpm test:oidc - - name: Upload full-gate evidence - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: full-gate-evidence-${{ github.run_id }}-${{ github.run_attempt }} - path: | - project/evidence/*.json - coverage/ - test-results/ - release/*.manifest.json - release/*.tgz - if-no-files-found: error - retention-days: 14 + - name: Upload full-gate evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: full-gate-evidence-${{ github.run_id }}-${{ github.run_attempt }} + path: | + project/evidence/*.json + coverage/ + test-results/ + release/*.manifest.json + release/*.tgz + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/install-verify.yml b/.github/workflows/install-verify.yml index 9377ffa2..bae87db8 100644 --- a/.github/workflows/install-verify.yml +++ b/.github/workflows/install-verify.yml @@ -1,8 +1,6 @@ name: Verify Published Installation on: - release: - types: [published] workflow_dispatch: inputs: release_tag: @@ -37,137 +35,137 @@ jobs: matrix: os: [ubuntu-latest, macos-14] steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Download, verify, and exercise the Node package - env: - ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-install" - assets="$work/assets" - install="$work/install" - data="$work/data" - mkdir -p "$assets" "$install" + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Download, verify, and exercise the Node package + env: + ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-install" + assets="$work/assets" + install="$work/install" + data="$work/data" + mkdir -p "$assets" "$install" - if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern 'release-public-*' \ - --dir "$assets" - else - gh release download "$ARTIFACTSERVER_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --dir "$assets" - fi + if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern 'release-public-*' \ + --dir "$assets" + else + gh release download "$ARTIFACTSERVER_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --dir "$assets" + fi - checksums=$(find "$assets" -type f -name SHA256SUMS -print -quit) - if [[ -z "$checksums" ]]; then - printf 'SHA256SUMS is missing from the downloaded release assets.\n' >&2 - exit 1 + checksums=$(find "$assets" -type f -name SHA256SUMS -print -quit) + if [[ -z "$checksums" ]]; then + printf 'SHA256SUMS is missing from the downloaded release assets.\n' >&2 + exit 1 + fi + release_directory=$(dirname "$checksums") + if command -v sha256sum >/dev/null 2>&1; then + (cd "$release_directory" && sha256sum --check --strict SHA256SUMS) + else + (cd "$release_directory" && shasum -a 256 --check SHA256SUMS) + fi + + version=${ARTIFACTSERVER_TAG#v} + archive="$release_directory/artifact-server-$version-node.tar.gz" + if [[ ! -f "$archive" ]]; then + printf 'The Node package for %s is missing.\n' "$ARTIFACTSERVER_TAG" >&2 + exit 1 + fi + if [[ -z "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + gh attestation verify "$archive" \ + --repo "$GITHUB_REPOSITORY" \ + --deny-self-hosted-runners + else + printf '%s\n' \ + '::notice::Dry-run assets are checksum-verified; attestations exist only for tagged releases.' + fi + + tar -xzf "$archive" -C "$install" + artifactserver="$install/artifactserver/bin/artifactserver" + test -x "$artifactserver" + test "$("$artifactserver" --version)" = "$version" + + fixture="$work/install-proof.html" + printf '%s\n' 'Clean install proof' > "$fixture" + "$artifactserver" start --data "$data" --port 8787 > "$work/server.log" 2>&1 & + server_pid=$! + cleanup() { + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + if [[ -f "$data/local-service.json" ]]; then + managed_pid=$(node -p \ + "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).pid" \ + "$data/local-service.json") + kill "$managed_pid" 2>/dev/null || true fi - release_directory=$(dirname "$checksums") - if command -v sha256sum >/dev/null 2>&1; then - (cd "$release_directory" && sha256sum --check --strict SHA256SUMS) - else - (cd "$release_directory" && shasum -a 256 --check SHA256SUMS) + } + trap cleanup EXIT + for attempt in {1..40}; do + if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then + break fi - - version=${ARTIFACTSERVER_TAG#v} - archive="$release_directory/artifact-server-$version-node.tar.gz" - if [[ ! -f "$archive" ]]; then - printf 'The Node package for %s is missing.\n' "$ARTIFACTSERVER_TAG" >&2 + if [[ "$attempt" == 40 ]]; then + cat "$work/server.log" >&2 exit 1 fi - if [[ -z "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - gh attestation verify "$archive" \ - --repo "$GITHUB_REPOSITORY" \ - --deny-self-hosted-runners - else - printf '%s\n' \ - '::notice::Dry-run assets are checksum-verified; attestations exist only for tagged releases.' - fi + sleep 0.25 + done - tar -xzf "$archive" -C "$install" - artifactserver="$install/artifactserver/bin/artifactserver" - test -x "$artifactserver" - test "$("$artifactserver" --version)" = "$version" - - fixture="$work/install-proof.html" - printf '%s\n' 'Clean install proof' > "$fixture" - "$artifactserver" start --data "$data" --port 8787 > "$work/server.log" 2>&1 & - server_pid=$! - cleanup() { - kill "$server_pid" 2>/dev/null || true - wait "$server_pid" 2>/dev/null || true - if [[ -f "$data/local-service.json" ]]; then - managed_pid=$(node -p \ - "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).pid" \ - "$data/local-service.json") - kill "$managed_pid" 2>/dev/null || true - fi - } - trap cleanup EXIT - for attempt in {1..40}; do - if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then - break - fi - if [[ "$attempt" == 40 ]]; then - cat "$work/server.log" >&2 - exit 1 - fi - sleep 0.25 - done - - publication=$("$artifactserver" publish "$fixture" \ - --data "$data" \ - --server http://127.0.0.1:8787 \ - --token-file "$data/local-api-token" \ - --public) - ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' - import {request} from "node:http"; - const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); - const published = new URL(publication.links.version); - const content = await new Promise((resolve, reject) => { - const incoming = request({ - headers: {host: published.host}, - hostname: "127.0.0.1", - path: `${published.pathname}${published.search}`, - port: 8787, - }, (response) => { - let body = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { body += chunk; }); - response.on("end", () => { - if (response.statusCode !== 200) { - reject(new Error(`Published content returned ${response.statusCode}.`)); - return; - } - resolve(body); - }); + publication=$("$artifactserver" publish "$fixture" \ + --data "$data" \ + --server http://127.0.0.1:8787 \ + --token-file "$data/local-api-token" \ + --public) + ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' + import {request} from "node:http"; + const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); + const published = new URL(publication.links.version); + const content = await new Promise((resolve, reject) => { + const incoming = request({ + headers: {host: published.host}, + hostname: "127.0.0.1", + path: `${published.pathname}${published.search}`, + port: 8787, + }, (response) => { + let body = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { body += chunk; }); + response.on("end", () => { + if (response.statusCode !== 200) { + reject(new Error(`Published content returned ${response.statusCode}.`)); + return; + } + resolve(body); }); - incoming.on("error", reject); - incoming.end(); }); - if (!content.includes("Clean install proof")) { - throw new Error("Published content did not match the clean-install fixture."); - } - NODE + incoming.on("error", reject); + incoming.end(); + }); + if (!content.includes("Clean install proof")) { + throw new Error("Published content did not match the clean-install fixture."); + } + NODE - kill "$server_pid" - wait "$server_pid" || true - browser_command="$work/browser-command" - printf '%s\n' '#!/bin/sh' 'exit 0' > "$browser_command" - chmod 0700 "$browser_command" - open_output=$(ARTIFACT_SERVER_BROWSER_COMMAND="$browser_command" \ - "$artifactserver" open --data "$data") - printf '%s\n' "$open_output" | grep -Eq 'https?://[^[:space:]]+' + kill "$server_pid" + wait "$server_pid" || true + browser_command="$work/browser-command" + printf '%s\n' '#!/bin/sh' 'exit 0' > "$browser_command" + chmod 0700 "$browser_command" + open_output=$(ARTIFACT_SERVER_BROWSER_COMMAND="$browser_command" \ + "$artifactserver" open --data "$data") + printf '%s\n' "$open_output" | grep -Eq 'https?://[^[:space:]]+' image: name: Immutable GHCR image / compact profile @@ -179,101 +177,101 @@ jobs: contents: read packages: read steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Run the immutable image through the compact profile - env: - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-image-install" - mkdir -p "$work" - gh release download "$ARTIFACTSERVER_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern image-reference.txt \ - --dir "$work" - image_reference=$(cat "$work/image-reference.txt") - printf '%s' "$GH_TOKEN" | docker login ghcr.io \ - --username "$GITHUB_ACTOR" \ - --password-stdin - gh attestation verify "oci://$image_reference" \ - --repo "$GITHUB_REPOSITORY" \ - --bundle-from-oci \ - --deny-self-hosted-runners + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Run the immutable image through the compact profile + env: + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-image-install" + mkdir -p "$work" + gh release download "$ARTIFACTSERVER_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern image-reference.txt \ + --dir "$work" + image_reference=$(cat "$work/image-reference.txt") + printf '%s' "$GH_TOKEN" | docker login ghcr.io \ + --username "$GITHUB_ACTOR" \ + --password-stdin + gh attestation verify "oci://$image_reference" \ + --repo "$GITHUB_REPOSITORY" \ + --bundle-from-oci \ + --deny-self-hosted-runners - gh api \ - "repos/$GITHUB_REPOSITORY/contents/packaging/compose/compose.yaml?ref=$ARTIFACTSERVER_TAG" \ - --jq .content | base64 --decode > "$work/compose.yaml" - export ARTIFACT_SERVER_CONTENT_DOMAIN=content.example.net - export ARTIFACT_SERVER_IMAGE="$image_reference" - export ARTIFACT_SERVER_ORIGIN=https://artifacts.example.com - export ARTIFACT_SERVER_PORT=8787 - export ARTIFACT_SERVER_READINESS_WITHDRAWAL_MS=0 - export ARTIFACT_SERVER_REQUEST_LOG_SAMPLE_RATE=0 - export COMPOSE_PROJECT_NAME=artifact-server-install-verify - cleanup() { - docker compose --file "$work/compose.yaml" down --volumes \ - --remove-orphans >/dev/null 2>&1 || true - } - trap cleanup EXIT + gh api \ + "repos/$GITHUB_REPOSITORY/contents/packaging/compose/compose.yaml?ref=$ARTIFACTSERVER_TAG" \ + --jq .content | base64 --decode > "$work/compose.yaml" + export ARTIFACT_SERVER_CONTENT_DOMAIN=content.example.net + export ARTIFACT_SERVER_IMAGE="$image_reference" + export ARTIFACT_SERVER_ORIGIN=https://artifacts.example.com + export ARTIFACT_SERVER_PORT=8787 + export ARTIFACT_SERVER_READINESS_WITHDRAWAL_MS=0 + export ARTIFACT_SERVER_REQUEST_LOG_SAMPLE_RATE=0 + export COMPOSE_PROJECT_NAME=artifact-server-install-verify + cleanup() { + docker compose --file "$work/compose.yaml" down --volumes \ + --remove-orphans >/dev/null 2>&1 || true + } + trap cleanup EXIT - docker compose --file "$work/compose.yaml" run --rm --no-deps \ - artifact-server init \ - --admin-email admin@example.test \ - --data /var/lib/artifact-server/data - docker compose --file "$work/compose.yaml" up --detach - for attempt in {1..60}; do - if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then - break - fi - if [[ "$attempt" == 60 ]]; then - docker compose --file "$work/compose.yaml" logs >&2 - exit 1 - fi - sleep 0.5 - done + docker compose --file "$work/compose.yaml" run --rm --no-deps \ + artifact-server init \ + --admin-email admin@example.test \ + --data /var/lib/artifact-server/data + docker compose --file "$work/compose.yaml" up --detach + for attempt in {1..60}; do + if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then + break + fi + if [[ "$attempt" == 60 ]]; then + docker compose --file "$work/compose.yaml" logs >&2 + exit 1 + fi + sleep 0.5 + done - docker compose --file "$work/compose.yaml" exec -T artifact-server \ - sh -c "printf '%s\\n' 'OCI install proof' > /tmp/install-proof.html" - publication=$(docker compose --file "$work/compose.yaml" exec -T \ - artifact-server node dist/cli/main.js publish /tmp/install-proof.html \ - --server http://127.0.0.1:8787 \ - --token-file /var/lib/artifact-server/data/secrets/api-token \ - --public) - ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' - import {request} from "node:http"; - const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); - const published = new URL(publication.links.version); - const body = await new Promise((resolve, reject) => { - const incoming = request({ - headers: {host: published.host}, - hostname: "127.0.0.1", - path: `${published.pathname}${published.search}`, - port: 8787, - }, (response) => { - let content = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { content += chunk; }); - response.on("end", () => { - if (response.statusCode !== 200) { - reject(new Error(`Published content returned ${response.statusCode}.`)); - return; - } - resolve(content); - }); + docker compose --file "$work/compose.yaml" exec -T artifact-server \ + sh -c "printf '%s\\n' 'OCI install proof' > /tmp/install-proof.html" + publication=$(docker compose --file "$work/compose.yaml" exec -T \ + artifact-server node dist/cli/main.js publish /tmp/install-proof.html \ + --server http://127.0.0.1:8787 \ + --token-file /var/lib/artifact-server/data/secrets/api-token \ + --public) + ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' + import {request} from "node:http"; + const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); + const published = new URL(publication.links.version); + const body = await new Promise((resolve, reject) => { + const incoming = request({ + headers: {host: published.host}, + hostname: "127.0.0.1", + path: `${published.pathname}${published.search}`, + port: 8787, + }, (response) => { + let content = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { content += chunk; }); + response.on("end", () => { + if (response.statusCode !== 200) { + reject(new Error(`Published content returned ${response.statusCode}.`)); + return; + } + resolve(content); }); - incoming.on("error", reject); - incoming.end(); }); - if (!body.includes("OCI install proof")) { - throw new Error("Published content did not match the OCI fixture."); - } - NODE + incoming.on("error", reject); + incoming.end(); + }); + if (!body.includes("OCI install proof")) { + throw new Error("Published content did not match the OCI fixture."); + } + NODE adapter: name: Public npm adapter @@ -283,50 +281,50 @@ jobs: actions: read contents: read steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - registry-url: https://registry.npmjs.org - package-manager-cache: false - - name: Install and load the OpenCode adapter - env: - ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | # zizmor: ignore[adhoc-packages] This job proves the published package installs without the source lockfile. - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-adapter-install" - prefix="$work/prefix" - mkdir -p "$prefix" - version=${ARTIFACTSERVER_TAG#v} - if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - assets="$work/assets" - mkdir -p "$assets" - gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern 'release-public-*' \ - --dir "$assets" - package_spec=$(find "$assets" -type f \ - -name "plannotator-artifact-server-opencode-$version.tgz" \ - -print -quit) - if [[ -z "$package_spec" ]]; then - printf 'The OpenCode adapter tarball is missing.\n' >&2 - exit 1 - fi - else - package_spec="@plannotator/artifact-server-opencode@$version" + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Install and load the OpenCode adapter + env: + ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | # zizmor: ignore[adhoc-packages] This job proves the published package installs without the source lockfile. + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-adapter-install" + prefix="$work/prefix" + mkdir -p "$prefix" + version=${ARTIFACTSERVER_TAG#v} + if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + assets="$work/assets" + mkdir -p "$assets" + gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern 'release-public-*' \ + --dir "$assets" + package_spec=$(find "$assets" -type f \ + -name "plannotator-artifact-server-opencode-$version.tgz" \ + -print -quit) + if [[ -z "$package_spec" ]]; then + printf 'The OpenCode adapter tarball is missing.\n' >&2 + exit 1 fi - npm install --global --prefix "$prefix" "$package_spec" tsx@4.23.12 - adapter_entry="$prefix/lib/node_modules/@plannotator/artifact-server-opencode/index.ts" - test -f "$adapter_entry" - ARTIFACTSERVER_ADAPTER_ENTRY="$adapter_entry" node \ - --import "$prefix/lib/node_modules/tsx/dist/loader.mjs" \ - --input-type=module <<'NODE' - import {pathToFileURL} from "node:url"; - const loaded = await import(pathToFileURL(process.env.ARTIFACTSERVER_ADAPTER_ENTRY)); - if (typeof loaded.ArtifactServerBridge !== "function") { - throw new Error("The installed OpenCode adapter did not expose its plugin entry."); - } - NODE + else + package_spec="@plannotator/artifact-server-opencode@$version" + fi + npm install --global --prefix "$prefix" "$package_spec" tsx@4.23.12 + adapter_entry="$prefix/lib/node_modules/@plannotator/artifact-server-opencode/index.ts" + test -f "$adapter_entry" + ARTIFACTSERVER_ADAPTER_ENTRY="$adapter_entry" node \ + --import "$prefix/lib/node_modules/tsx/dist/loader.mjs" \ + --input-type=module <<'NODE' + import {pathToFileURL} from "node:url"; + const loaded = await import(pathToFileURL(process.env.ARTIFACTSERVER_ADAPTER_ENTRY)); + if (typeof loaded.ArtifactServerBridge !== "function") { + throw new Error("The installed OpenCode adapter did not expose its plugin entry."); + } + NODE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9f1971f..4ec5afc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,18 +4,18 @@ on: pull_request: branches: [main] paths: - - .github/workflows/ci.yml - - .github/workflows/release.yml - - scripts/assemble-release-assets.mjs - - scripts/build-oci-image.sh - - scripts/check-release-ref.mjs - - scripts/check-release-version.mjs - - scripts/extract-oci-sboms.mjs - - scripts/generate-release-sboms.sh - - scripts/install-release-syft.sh + - .github/workflows/ci.yml + - .github/workflows/release.yml + - scripts/assemble-release-assets.mjs + - scripts/build-oci-image.sh + - scripts/check-release-ref.mjs + - scripts/check-release-version.mjs + - scripts/extract-oci-sboms.mjs + - scripts/generate-release-sboms.sh + - scripts/install-release-syft.sh push: tags: - - "v*" + - '**' workflow_dispatch: inputs: dry_run: @@ -51,69 +51,69 @@ jobs: tag: ${{ steps.metadata.outputs.tag }} version: ${{ steps.metadata.outputs.version }} steps: - - name: Checkout complete repository history - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Verify release metadata and ref - id: metadata - env: - ARTIFACTSERVER_EVENT_NAME: ${{ github.event_name }} - ARTIFACTSERVER_PRIVATE_REGISTRY_TEST: ${{ inputs.private_registry_test || false }} - ARTIFACTSERVER_REF_NAME: ${{ github.ref_name }} - ARTIFACTSERVER_REF_TYPE: ${{ github.ref_type }} - ARTIFACTSERVER_RUN_ATTEMPT: ${{ github.run_attempt }} - ARTIFACTSERVER_RUN_ID: ${{ github.run_id }} - shell: bash - run: | - set -euo pipefail - version=$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version") - revision=$(git rev-parse HEAD) - series=${version%.*} - tag="v$version" - publish=false - publish_image=false + - name: Checkout complete repository history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Verify release metadata and ref + id: metadata + env: + ARTIFACTSERVER_EVENT_NAME: ${{ github.event_name }} + ARTIFACTSERVER_PRIVATE_REGISTRY_TEST: ${{ inputs.private_registry_test || false }} + ARTIFACTSERVER_REF_NAME: ${{ github.ref_name }} + ARTIFACTSERVER_REF_TYPE: ${{ github.ref_type }} + ARTIFACTSERVER_RUN_ATTEMPT: ${{ github.run_attempt }} + ARTIFACTSERVER_RUN_ID: ${{ github.run_id }} + shell: bash + run: | + set -euo pipefail + version=$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version") + revision=$(git rev-parse HEAD) + series=${version%.*} + tag="v$version" + publish=false + publish_image=false + image_primary_tag="$tag" + image_secondary_tag="$series" + if [[ "$ARTIFACTSERVER_EVENT_NAME" == push && "$ARTIFACTSERVER_REF_TYPE" == tag ]]; then + tag="$ARTIFACTSERVER_REF_NAME" + publish=true + publish_image=true image_primary_tag="$tag" image_secondary_tag="$series" - if [[ "$ARTIFACTSERVER_EVENT_NAME" == push && "$ARTIFACTSERVER_REF_TYPE" == tag ]]; then - tag="$ARTIFACTSERVER_REF_NAME" - publish=true + node scripts/check-release-version.mjs --tag "$tag" + node scripts/check-release-ref.mjs --tag "$tag" --main-ref origin/main + else + node scripts/check-release-version.mjs + node scripts/check-release-ref.mjs --dry-run + if [[ "$ARTIFACTSERVER_PRIVATE_REGISTRY_TEST" == true ]]; then publish_image=true - image_primary_tag="$tag" - image_secondary_tag="$series" - node scripts/check-release-version.mjs --tag "$tag" - node scripts/check-release-ref.mjs --tag "$tag" --main-ref origin/main - else - node scripts/check-release-version.mjs - node scripts/check-release-ref.mjs --dry-run - if [[ "$ARTIFACTSERVER_PRIVATE_REGISTRY_TEST" == true ]]; then - publish_image=true - image_primary_tag="ci-$ARTIFACTSERVER_RUN_ID-$ARTIFACTSERVER_RUN_ATTEMPT" - image_secondary_tag="ci-${revision:0:12}" - fi + image_primary_tag="ci-$ARTIFACTSERVER_RUN_ID-$ARTIFACTSERVER_RUN_ATTEMPT" + image_secondary_tag="ci-${revision:0:12}" fi - { - printf 'image_primary_tag=%s\n' "$image_primary_tag" - printf 'image_secondary_tag=%s\n' "$image_secondary_tag" - printf 'publish=%s\n' "$publish" - printf 'publish_image=%s\n' "$publish_image" - printf 'revision=%s\n' "$revision" - printf 'series=%s\n' "$series" - printf 'tag=%s\n' "$tag" - printf 'version=%s\n' "$version" - } >> "$GITHUB_OUTPUT" + fi + { + printf 'image_primary_tag=%s\n' "$image_primary_tag" + printf 'image_secondary_tag=%s\n' "$image_secondary_tag" + printf 'publish=%s\n' "$publish" + printf 'publish_image=%s\n' "$publish_image" + printf 'revision=%s\n' "$revision" + printf 'series=%s\n' "$series" + printf 'tag=%s\n' "$tag" + printf 'version=%s\n' "$version" + } >> "$GITHUB_OUTPUT" gate: name: Canonical release gate @@ -128,53 +128,53 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 18 steps: - - name: Checkout release source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Build portable local package - env: - ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle - run: pnpm package:local "$ARTIFACTSERVER_OUTPUT" - - name: Pack public adapters - env: - ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle - shell: bash - run: | - set -euo pipefail - pnpm --dir integrations/pi pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - pnpm --dir integrations/opencode pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - pnpm --dir integrations/claude-channel pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - - name: Install verified Syft - run: scripts/install-release-syft.sh "$RUNNER_TEMP/syft" - - name: Generate package SPDX SBOMs - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - scripts/generate-release-sboms.sh - "$RUNNER_TEMP/syft/syft" - release/package-bundle - release/package-bundle - "$ARTIFACTSERVER_VERSION" - packages - - name: Upload package release inputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} - path: release/package-bundle/ - if-no-files-found: error - retention-days: 7 + - name: Checkout release source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Build portable local package + env: + ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle + run: pnpm package:local "$ARTIFACTSERVER_OUTPUT" + - name: Pack public adapters + env: + ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle + shell: bash + run: | + set -euo pipefail + pnpm --dir integrations/pi pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + pnpm --dir integrations/opencode pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + pnpm --dir integrations/claude-channel pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + - name: Install verified Syft + run: scripts/install-release-syft.sh "$RUNNER_TEMP/syft" + - name: Generate package SPDX SBOMs + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + scripts/generate-release-sboms.sh + "$RUNNER_TEMP/syft/syft" + release/package-bundle + release/package-bundle + "$ARTIFACTSERVER_VERSION" + packages + - name: Upload package release inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} + path: release/package-bundle/ + if-no-files-found: error + retention-days: 7 image: name: Build and optionally publish OCI image @@ -185,99 +185,99 @@ jobs: contents: read packages: write steps: - - name: Checkout release source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Prepare attestation-capable BuildKit - shell: bash - run: | - set -euo pipefail - docker buildx create \ - --driver docker-container \ - --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ - --name artifact-server-release \ - --use - docker buildx inspect --bootstrap - - name: Sign in to private GHCR for image publication - if: needs.verify.outputs.publish_image == 'true' - env: - ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} - ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} - run: >- - docker login ghcr.io - --username "$ARTIFACTSERVER_GITHUB_ACTOR" - --password-stdin - <<< "$ARTIFACTSERVER_GITHUB_TOKEN" - - name: Build release image - env: - ARTIFACT_SERVER_IMAGE_PUSH: ${{ needs.verify.outputs.publish_image }} - ARTIFACT_SERVER_IMAGE_TAGS: ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_primary_tag }},ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_secondary_tag }} - run: pnpm package:oci release/image-work - - name: Extract verified image SPDX SBOMs - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - scripts/generate-release-sboms.sh - unused - release/image-work - release/image-bundle - "$ARTIFACTSERVER_VERSION" - image - - name: Record immutable image reference - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - manifest="release/image-work/artifact-server-$ARTIFACTSERVER_VERSION.oci.tar.manifest.json" - digest=$(jq --exit-status --raw-output '.imageIndexDigest' "$manifest") - mkdir -p release/image-bundle - cp -- "$manifest" \ - "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json" - printf '%s\n' "ghcr.io/plannotator/artifact-server@$digest" \ - > release/image-bundle/image-reference.txt - - name: Verify published image tags resolve to the built digest - if: needs.verify.outputs.publish_image == 'true' - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.image_primary_tag }} - ARTIFACTSERVER_SERIES: ${{ needs.verify.outputs.image_secondary_tag }} - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - expected=$(jq --exit-status --raw-output \ - '.imageIndexDigest' \ - "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json") - for tag in "$ARTIFACTSERVER_TAG" "$ARTIFACTSERVER_SERIES"; do - actual="sha256:$(docker buildx imagetools inspect --raw \ - "ghcr.io/plannotator/artifact-server:$tag" | sha256sum | cut -d ' ' -f 1)" - if [[ "$actual" != "$expected" ]]; then - printf 'Published tag %s resolved to %s, expected %s.\n' \ - "$tag" "$actual" "$expected" >&2 - exit 1 - fi - done - - name: Upload image release inputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-image-${{ github.run_id }}-${{ github.run_attempt }} - path: | - release/image-work/artifact-server-${{ needs.verify.outputs.version }}.oci.tar - release/image-bundle/ - if-no-files-found: error - retention-days: 7 + - name: Checkout release source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Prepare attestation-capable BuildKit + shell: bash + run: | + set -euo pipefail + docker buildx create \ + --driver docker-container \ + --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ + --name artifact-server-release \ + --use + docker buildx inspect --bootstrap + - name: Sign in to private GHCR for image publication + if: needs.verify.outputs.publish_image == 'true' + env: + ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} + ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} + run: >- + docker login ghcr.io + --username "$ARTIFACTSERVER_GITHUB_ACTOR" + --password-stdin + <<< "$ARTIFACTSERVER_GITHUB_TOKEN" + - name: Build release image + env: + ARTIFACT_SERVER_IMAGE_PUSH: ${{ needs.verify.outputs.publish_image }} + ARTIFACT_SERVER_IMAGE_TAGS: ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_primary_tag }},ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_secondary_tag }} + run: pnpm package:oci release/image-work + - name: Extract verified image SPDX SBOMs + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + scripts/generate-release-sboms.sh + unused + release/image-work + release/image-bundle + "$ARTIFACTSERVER_VERSION" + image + - name: Record immutable image reference + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + manifest="release/image-work/artifact-server-$ARTIFACTSERVER_VERSION.oci.tar.manifest.json" + digest=$(jq --exit-status --raw-output '.imageIndexDigest' "$manifest") + mkdir -p release/image-bundle + cp -- "$manifest" \ + "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json" + printf '%s\n' "ghcr.io/plannotator/artifact-server@$digest" \ + > release/image-bundle/image-reference.txt + - name: Verify published image tags resolve to the built digest + if: needs.verify.outputs.publish_image == 'true' + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.image_primary_tag }} + ARTIFACTSERVER_SERIES: ${{ needs.verify.outputs.image_secondary_tag }} + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + expected=$(jq --exit-status --raw-output \ + '.imageIndexDigest' \ + "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json") + for tag in "$ARTIFACTSERVER_TAG" "$ARTIFACTSERVER_SERIES"; do + actual="sha256:$(docker buildx imagetools inspect --raw \ + "ghcr.io/plannotator/artifact-server:$tag" | sha256sum | cut -d ' ' -f 1)" + if [[ "$actual" != "$expected" ]]; then + printf 'Published tag %s resolved to %s, expected %s.\n' \ + "$tag" "$actual" "$expected" >&2 + exit 1 + fi + done + - name: Upload image release inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-image-${{ github.run_id }}-${{ github.run_attempt }} + path: | + release/image-work/artifact-server-${{ needs.verify.outputs.version }}.oci.tar + release/image-bundle/ + if-no-files-found: error + retention-days: 7 assemble: name: Assemble downloadable release assets @@ -285,38 +285,38 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - name: Checkout release tooling - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Download package release inputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} - path: release/incoming/packages - - name: Download image release inputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-image-${{ github.run_id }}-${{ github.run_attempt }} - path: release/incoming/image - - name: Assemble assets and checksums - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - node scripts/assemble-release-assets.mjs - release/incoming - release/public - "$ARTIFACTSERVER_VERSION" - - name: Verify every downloadable asset checksum - working-directory: release/public - run: sha256sum --check --strict SHA256SUMS - - name: Upload downloadable release assets - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public/ - if-no-files-found: error - retention-days: 14 + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Download package release inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} + path: release/incoming/packages + - name: Download image release inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-image-${{ github.run_id }}-${{ github.run_attempt }} + path: release/incoming/image + - name: Assemble assets and checksums + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + node scripts/assemble-release-assets.mjs + release/incoming + release/public + "$ARTIFACTSERVER_VERSION" + - name: Verify every downloadable asset checksum + working-directory: release/public + run: sha256sum --check --strict SHA256SUMS + - name: Upload downloadable release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public/ + if-no-files-found: error + retention-days: 14 attest: name: Attest release assets @@ -330,75 +330,75 @@ jobs: id-token: write packages: write steps: - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Read immutable image subject - id: image - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - reference=$(cat release/public/image-reference.txt) - digest=${reference##*@} - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - printf 'name=%s\n' "${reference%@*}" >> "$GITHUB_OUTPUT" - - name: Sign in to private GHCR for image attestations - env: - ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} - ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} - run: >- - docker login ghcr.io - --username "$ARTIFACTSERVER_GITHUB_ACTOR" - --password-stdin - <<< "$ARTIFACTSERVER_GITHUB_TOKEN" - - name: Attest downloadable asset provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: release/public/* - - name: Attest local package SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.tar.gz - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.spdx.json - - name: Attest Pi adapter SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest OpenCode adapter SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest Claude channel SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest image provenance in GHCR - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - push-to-registry: true - - name: Attest image SBOM in GHCR - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-amd64.spdx.json - push-to-registry: true - - name: Attest arm64 image SBOM in GHCR - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-arm64.spdx.json - push-to-registry: true + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Read immutable image subject + id: image + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + reference=$(cat release/public/image-reference.txt) + digest=${reference##*@} + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + printf 'name=%s\n' "${reference%@*}" >> "$GITHUB_OUTPUT" + - name: Sign in to private GHCR for image attestations + env: + ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} + ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} + run: >- + docker login ghcr.io + --username "$ARTIFACTSERVER_GITHUB_ACTOR" + --password-stdin + <<< "$ARTIFACTSERVER_GITHUB_TOKEN" + - name: Attest downloadable asset provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: release/public/* + - name: Attest local package SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.tar.gz + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.spdx.json + - name: Attest Pi adapter SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest OpenCode adapter SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest Claude channel SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest image provenance in GHCR + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + push-to-registry: true + - name: Attest image SBOM in GHCR + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-amd64.spdx.json + push-to-registry: true + - name: Attest arm64 image SBOM in GHCR + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-arm64.spdx.json + push-to-registry: true publish-npm: name: Publish public npm adapters @@ -410,65 +410,65 @@ jobs: contents: read id-token: write steps: - - name: Install Node.js for npm provenance - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - registry-url: https://registry.npmjs.org - package-manager-cache: false - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Publish adapters - env: - ARTIFACTSERVER_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - shell: bash - run: | - set -euo pipefail - if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then - printf '%s\n' \ - '::notice::npm publication is skipped because NPM_TOKEN is not configured. Publish the adapter tarballs from the release assets locally with npm publish --access public.' - exit 0 - fi - provenance_args=() - if [[ "$ARTIFACTSERVER_REPOSITORY_VISIBILITY" == public ]]; then - provenance_args+=(--provenance) - else - printf '%s\n' \ - '::notice::npm provenance is deferred until the source repository is public.' - fi - publish_adapter() { - package_name=$1 - archive=$2 - local_integrity=$(node --input-type=module -e \ - 'import {createHash} from "node:crypto"; import {readFileSync} from "node:fs"; const bytes = readFileSync(process.argv[1]); process.stdout.write(`sha512-${createHash("sha512").update(bytes).digest("base64")}`);' \ - "$archive") - if remote_integrity=$(npm view \ - "$package_name@$ARTIFACTSERVER_VERSION" dist.integrity 2>/dev/null); then - if [[ "$remote_integrity" != "$local_integrity" ]]; then - printf 'npm already contains different bytes for %s@%s.\n' \ - "$package_name" "$ARTIFACTSERVER_VERSION" >&2 - exit 1 - fi - printf '::notice::%s@%s already contains the verified release bytes.\n' \ - "$package_name" "$ARTIFACTSERVER_VERSION" - return + - name: Install Node.js for npm provenance + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Publish adapters + env: + ARTIFACTSERVER_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then + printf '%s\n' \ + '::notice::npm publication is skipped because NPM_TOKEN is not configured. Publish the adapter tarballs from the release assets locally with npm publish --access public.' + exit 0 + fi + provenance_args=() + if [[ "$ARTIFACTSERVER_REPOSITORY_VISIBILITY" == public ]]; then + provenance_args+=(--provenance) + else + printf '%s\n' \ + '::notice::npm provenance is deferred until the source repository is public.' + fi + publish_adapter() { + package_name=$1 + archive=$2 + local_integrity=$(node --input-type=module -e \ + 'import {createHash} from "node:crypto"; import {readFileSync} from "node:fs"; const bytes = readFileSync(process.argv[1]); process.stdout.write(`sha512-${createHash("sha512").update(bytes).digest("base64")}`);' \ + "$archive") + if remote_integrity=$(npm view \ + "$package_name@$ARTIFACTSERVER_VERSION" dist.integrity 2>/dev/null); then + if [[ "$remote_integrity" != "$local_integrity" ]]; then + printf 'npm already contains different bytes for %s@%s.\n' \ + "$package_name" "$ARTIFACTSERVER_VERSION" >&2 + exit 1 fi - npm publish "$archive" "${provenance_args[@]}" --access public - } - publish_adapter \ - @plannotator/artifact-server-pi \ - "release/public/plannotator-artifact-server-pi-$ARTIFACTSERVER_VERSION.tgz" - publish_adapter \ - @plannotator/artifact-server-opencode \ - "release/public/plannotator-artifact-server-opencode-$ARTIFACTSERVER_VERSION.tgz" - publish_adapter \ - @plannotator/artifact-server-claude-channel \ - "release/public/plannotator-artifact-server-claude-channel-$ARTIFACTSERVER_VERSION.tgz" + printf '::notice::%s@%s already contains the verified release bytes.\n' \ + "$package_name" "$ARTIFACTSERVER_VERSION" + return + fi + npm publish "$archive" "${provenance_args[@]}" --access public + } + publish_adapter \ + @plannotator/artifact-server-pi \ + "release/public/plannotator-artifact-server-pi-$ARTIFACTSERVER_VERSION.tgz" + publish_adapter \ + @plannotator/artifact-server-opencode \ + "release/public/plannotator-artifact-server-opencode-$ARTIFACTSERVER_VERSION.tgz" + publish_adapter \ + @plannotator/artifact-server-claude-channel \ + "release/public/plannotator-artifact-server-claude-channel-$ARTIFACTSERVER_VERSION.tgz" release: name: Create GitHub release @@ -480,28 +480,28 @@ jobs: actions: write contents: write steps: - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Create verified GitHub release - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} - GH_TOKEN: ${{ github.token }} - run: >- - gh release create "$ARTIFACTSERVER_TAG" - release/public/* - --repo "$GITHUB_REPOSITORY" - --verify-tag - --generate-notes - --title "Artifact Server $ARTIFACTSERVER_TAG" - - name: Dispatch published-install verification - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} - GH_TOKEN: ${{ github.token }} - run: >- - gh workflow run install-verify.yml - --repo "$GITHUB_REPOSITORY" - --ref main - --field release_tag="$ARTIFACTSERVER_TAG" + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Create verified GitHub release + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} + GH_TOKEN: ${{ github.token }} + run: >- + gh release create "$ARTIFACTSERVER_TAG" + release/public/* + --repo "$GITHUB_REPOSITORY" + --verify-tag + --generate-notes + --title "Artifact Server $ARTIFACTSERVER_TAG" + - name: Dispatch published-install verification + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} + GH_TOKEN: ${{ github.token }} + run: >- + gh workflow run install-verify.yml + --repo "$GITHUB_REPOSITORY" + --ref main + --field release_tag="$ARTIFACTSERVER_TAG" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 33068936..f167e6ef 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,12 +3,9 @@ name: Security Scanning on: pull_request: branches: [main] - merge_group: - types: [checks_requested] push: - branches: [main] - schedule: - - cron: "23 10 * * 1" + tags: + - '**' workflow_dispatch: permissions: @@ -30,114 +27,114 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - name: Checkout repository history - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false + - name: Checkout repository history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install verified Gitleaks - shell: bash - run: | - set -euo pipefail - archive="$RUNNER_TEMP/gitleaks.tar.gz" - install_dir="$RUNNER_TEMP/gitleaks-bin" - curl --fail --silent --show-error --location \ - --proto '=https' --tlsv1.2 \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - --output "$archive" - printf '%s %s\n' "$GITLEAKS_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict - mkdir -p "$install_dir" - tar -xzf "$archive" -C "$install_dir" gitleaks - "$install_dir/gitleaks" version - echo "$install_dir" >> "$GITHUB_PATH" + - name: Install verified Gitleaks + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/gitleaks.tar.gz" + install_dir="$RUNNER_TEMP/gitleaks-bin" + curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + --output "$archive" + printf '%s %s\n' "$GITLEAKS_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict + mkdir -p "$install_dir" + tar -xzf "$archive" -C "$install_dir" gitleaks + "$install_dir/gitleaks" version + echo "$install_dir" >> "$GITHUB_PATH" - - name: Verify Gitleaks detector health - shell: bash - run: | - set -euo pipefail - synthetic_prefix='glpat' - synthetic_body='abcdefghijklmnopqrst' - set +e - printf 'token = "%s-%s"\n' "$synthetic_prefix" "$synthetic_body" \ - | gitleaks stdin --config .gitleaks.toml --no-banner --redact=100 >/dev/null 2>&1 - detector_exit=$? - set -e - if [[ "$detector_exit" -ne 1 ]]; then - echo "Gitleaks failed its synthetic detection test (exit $detector_exit)." >&2 - exit 1 - fi + - name: Verify Gitleaks detector health + shell: bash + run: | + set -euo pipefail + synthetic_prefix='glpat' + synthetic_body='abcdefghijklmnopqrst' + set +e + printf 'token = "%s-%s"\n' "$synthetic_prefix" "$synthetic_body" \ + | gitleaks stdin --config .gitleaks.toml --no-banner --redact=100 >/dev/null 2>&1 + detector_exit=$? + set -e + if [[ "$detector_exit" -ne 1 ]]; then + echo "Gitleaks failed its synthetic detection test (exit $detector_exit)." >&2 + exit 1 + fi - - name: Scan complete repository history - shell: bash - run: | - set -euo pipefail - gitleaks git . --config .gitleaks.toml --log-opts='--all' --redact=100 --no-banner - { - echo '### Gitleaks' - echo '- Scope: complete repository history' - echo '- Detector health: synthetic token detected before analysis' - echo '- Result: no secret findings' - } >> "$GITHUB_STEP_SUMMARY" + - name: Scan complete repository history + shell: bash + run: | + set -euo pipefail + gitleaks git . --config .gitleaks.toml --log-opts='--all' --redact=100 --no-banner + { + echo '### Gitleaks' + echo '- Scope: complete repository history' + echo '- Detector health: synthetic token detected before analysis' + echo '- Result: no secret findings' + } >> "$GITHUB_STEP_SUMMARY" zizmor: name: zizmor runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Install verified zizmor - shell: bash - run: | - set -euo pipefail - archive="$RUNNER_TEMP/zizmor.tar.gz" - install_dir="$RUNNER_TEMP/zizmor-bin" - curl --fail --silent --show-error --location \ - --proto '=https' --tlsv1.2 \ - "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" \ - --output "$archive" - printf '%s %s\n' "$ZIZMOR_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict - mkdir -p "$install_dir" - tar -xzf "$archive" -C "$install_dir" zizmor - "$install_dir/zizmor" --version - echo "$install_dir" >> "$GITHUB_PATH" + - name: Install verified zizmor + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/zizmor.tar.gz" + install_dir="$RUNNER_TEMP/zizmor-bin" + curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" \ + --output "$archive" + printf '%s %s\n' "$ZIZMOR_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict + mkdir -p "$install_dir" + tar -xzf "$archive" -C "$install_dir" zizmor + "$install_dir/zizmor" --version + echo "$install_dir" >> "$GITHUB_PATH" - - name: Verify zizmor detector health - shell: bash - run: | - set -euo pipefail - unsafe_expression='$'"{{ github.event.issue.title }}" - set +e - printf '%s\n' \ - 'name: Synthetic unsafe workflow' \ - 'on: issues' \ - 'jobs:' \ - ' test:' \ - ' runs-on: ubuntu-latest' \ - ' steps:' \ - " - run: echo \"$unsafe_expression\"" \ - | zizmor --offline --persona regular --strict-collection --format=json --no-progress --color=never - \ - > "$RUNNER_TEMP/zizmor-health.json" 2>/dev/null - detector_exit=$? - set -e - if [[ "$detector_exit" -lt 11 || "$detector_exit" -gt 14 ]] \ - || ! jq -e 'any(.ident == "template-injection")' "$RUNNER_TEMP/zizmor-health.json" >/dev/null; then - echo "zizmor failed its synthetic detection test (exit $detector_exit)." >&2 - exit 1 - fi + - name: Verify zizmor detector health + shell: bash + run: | + set -euo pipefail + unsafe_expression='$'"{{ github.event.issue.title }}" + set +e + printf '%s\n' \ + 'name: Synthetic unsafe workflow' \ + 'on: issues' \ + 'jobs:' \ + ' test:' \ + ' runs-on: ubuntu-latest' \ + ' steps:' \ + " - run: echo \"$unsafe_expression\"" \ + | zizmor --offline --persona regular --strict-collection --format=json --no-progress --color=never - \ + > "$RUNNER_TEMP/zizmor-health.json" 2>/dev/null + detector_exit=$? + set -e + if [[ "$detector_exit" -lt 11 || "$detector_exit" -gt 14 ]] \ + || ! jq -e 'any(.ident == "template-injection")' "$RUNNER_TEMP/zizmor-health.json" >/dev/null; then + echo "zizmor failed its synthetic detection test (exit $detector_exit)." >&2 + exit 1 + fi - - name: Audit GitHub configuration - shell: bash - run: | - set -euo pipefail - zizmor --offline --persona regular --strict-collection --no-progress --color=never . - { - echo '### zizmor' - echo '- Scope: GitHub workflows and Dependabot configuration' - echo '- Detector health: synthetic template injection detected before analysis' - echo '- Result: no findings' - } >> "$GITHUB_STEP_SUMMARY" + - name: Audit GitHub configuration + shell: bash + run: | + set -euo pipefail + zizmor --offline --persona regular --strict-collection --no-progress --color=never . + { + echo '### zizmor' + echo '- Scope: GitHub workflows and Dependabot configuration' + echo '- Detector health: synthetic template injection detected before analysis' + echo '- Result: no findings' + } >> "$GITHUB_STEP_SUMMARY" From d85930cafe8a58f25ef8f210d39c9ec34af53053 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 25 Oct 2026 18:30:44 +0000 Subject: [PATCH 3/3] ci: preserve workflow jobs after trigger rewrite [skip ci] --- .github/workflows/ci.yml | 518 +++++++++--------- .github/workflows/install-verify.yml | 514 +++++++++--------- .github/workflows/release.yml | 786 +++++++++++++-------------- .github/workflows/security.yml | 200 +++---- 4 files changed, 1009 insertions(+), 1009 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 025d2edb..c1ede6bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ on: branches: [main] push: tags: - - '**' + - '**' workflow_dispatch: inputs: tier: @@ -21,8 +21,8 @@ on: default: full type: choice options: - - full - - pr + - full + - pr permissions: contents: read @@ -38,50 +38,50 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 8 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Verify CI full-gate definition - run: pnpm check:ci - - name: Lint - run: pnpm lint - - name: Typecheck - run: pnpm typecheck - - name: Build application - run: pnpm build - - name: Verify public site - run: pnpm verify:site - - name: Validate conformance ledger - run: pnpm conformance:validate - - name: Validate conformance test IDs - run: pnpm conformance:tests - - name: Verify AWS Pulumi project - run: pnpm test:aws-pulumi - - name: Verify GCP Pulumi project - run: pnpm test:gcp-pulumi - - name: Upload compiled application - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist/ - if-no-files-found: error - retention-days: 2 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Verify CI full-gate definition + run: pnpm check:ci + - name: Lint + run: pnpm lint + - name: Typecheck + run: pnpm typecheck + - name: Build application + run: pnpm build + - name: Verify public site + run: pnpm verify:site + - name: Validate conformance ledger + run: pnpm conformance:validate + - name: Validate conformance test IDs + run: pnpm conformance:tests + - name: Verify AWS Pulumi project + run: pnpm test:aws-pulumi + - name: Verify GCP Pulumi project + run: pnpm test:gcp-pulumi + - name: Upload compiled application + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist/ + if-no-files-found: error + retention-days: 2 unit: name: PR / Unit and conformance behavior @@ -90,32 +90,32 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 12 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled application - run: test -f dist/cli/main.js && test -f dist/web/review.html - - name: Run root behavior and conformance tests - env: - NODE_OPTIONS: --max-old-space-size=4096 - run: pnpm test + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled application + run: test -f dist/cli/main.js && test -f dist/web/review.html + - name: Run root behavior and conformance tests + env: + NODE_OPTIONS: --max-old-space-size=4096 + run: pnpm test browser: name: PR / Browser behavior @@ -124,40 +124,40 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Install Chromium - run: pnpm exec playwright install --with-deps chromium - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled web application - run: test -f dist/web/review.html - - name: Run browser tests - run: pnpm test:web:prebuilt - - name: Upload browser diagnostics - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: pr-browser-${{ github.run_id }}-${{ github.run_attempt }} - path: test-results/browser/ - if-no-files-found: warn - retention-days: 7 + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled web application + run: test -f dist/web/review.html + - name: Run browser tests + run: pnpm test:web:prebuilt + - name: Upload browser diagnostics + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-browser-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/browser/ + if-no-files-found: warn + retention-days: 7 cloudflare: name: PR / Cloudflare deployment @@ -166,30 +166,30 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 8 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Download compiled application - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} - path: dist - - name: Verify compiled web application - run: test -f dist/web/review.html - - name: Run Cloudflare checks - run: pnpm check:cloudflare:prebuilt + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Download compiled application + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-compiled-${{ github.run_id }}-${{ github.run_attempt }} + path: dist + - name: Verify compiled web application + run: test -f dist/web/review.html + - name: Run Cloudflare checks + run: pnpm check:cloudflare:prebuilt portable: name: macOS portability @@ -197,27 +197,27 @@ jobs: runs-on: macos-14 timeout-minutes: 12 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Verify portable code paths - run: pnpm check + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Verify portable code paths + run: pnpm check full: name: Full / Linux iteration gate @@ -227,126 +227,126 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=4096 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Reclaim unused runner disk - run: | - df -h / - sudo rm -rf -- \ - /opt/ghc \ - /usr/local/lib/android \ - /usr/local/share/boost \ - /usr/share/dotnet - df -h / - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js without a release cache - if: inputs.tier == 'release' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install Node.js with the pnpm cache - if: inputs.tier != 'release' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - cache: pnpm - - name: Install Ruby - uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 - with: - ruby-version: "3.3" - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Install Chromium - run: pnpm exec playwright install --with-deps chromium - - name: Enable the containerd image store - run: | - artifactserver_daemon_config=$(mktemp) - if [[ -s /etc/docker/daemon.json ]]; then - jq \ - '.features = ((.features // {}) + {"containerd-snapshotter": true})' \ - /etc/docker/daemon.json > "$artifactserver_daemon_config" - else - printf '%s\n' \ - '{"features":{"containerd-snapshotter":true}}' \ - > "$artifactserver_daemon_config" - fi - sudo install -d -m 0755 /etc/docker - sudo install -m 0644 \ - "$artifactserver_daemon_config" \ - /etc/docker/daemon.json - rm -f -- "$artifactserver_daemon_config" - sudo systemctl restart docker - docker info --format '{{json .DriverStatus}}' \ - | grep -F 'io.containerd.snapshotter.v1' - - name: Prepare attestation-capable BuildKit - run: | - docker buildx create \ - --driver docker-container \ - --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ - --name artifact-server-ci \ - --use - docker buildx inspect --bootstrap - - name: Verify CI full-gate definition - run: pnpm check:ci + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Reclaim unused runner disk + run: | + df -h / + sudo rm -rf -- \ + /opt/ghc \ + /usr/local/lib/android \ + /usr/local/share/boost \ + /usr/share/dotnet + df -h / + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js without a release cache + if: inputs.tier == 'release' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install Node.js with the pnpm cache + if: inputs.tier != 'release' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + cache: pnpm + - name: Install Ruby + uses: ruby/setup-ruby@bec3f19a76460dbe12f60def7d1a77585f07516c # v1.322.0 + with: + ruby-version: "3.3" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + - name: Enable the containerd image store + run: | + artifactserver_daemon_config=$(mktemp) + if [[ -s /etc/docker/daemon.json ]]; then + jq \ + '.features = ((.features // {}) + {"containerd-snapshotter": true})' \ + /etc/docker/daemon.json > "$artifactserver_daemon_config" + else + printf '%s\n' \ + '{"features":{"containerd-snapshotter":true}}' \ + > "$artifactserver_daemon_config" + fi + sudo install -d -m 0755 /etc/docker + sudo install -m 0644 \ + "$artifactserver_daemon_config" \ + /etc/docker/daemon.json + rm -f -- "$artifactserver_daemon_config" + sudo systemctl restart docker + docker info --format '{{json .DriverStatus}}' \ + | grep -F 'io.containerd.snapshotter.v1' + - name: Prepare attestation-capable BuildKit + run: | + docker buildx create \ + --driver docker-container \ + --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ + --name artifact-server-ci \ + --use + docker buildx inspect --bootstrap + - name: Verify CI full-gate definition + run: pnpm check:ci # full-gate-command: pnpm check - - name: Check correctness and static contracts - run: pnpm check + - name: Check correctness and static contracts + run: pnpm check # full-gate-command: pnpm test:web - - name: Test browser behavior - run: pnpm test:web + - name: Test browser behavior + run: pnpm test:web # full-gate-command: pnpm test:aws-pulumi - - name: Test AWS Pulumi project - run: pnpm test:aws-pulumi + - name: Test AWS Pulumi project + run: pnpm test:aws-pulumi # full-gate-command: pnpm test:gcp-pulumi - - name: Test GCP Pulumi project - run: pnpm test:gcp-pulumi + - name: Test GCP Pulumi project + run: pnpm test:gcp-pulumi # full-gate-command: pnpm verify:object-storage - - name: Verify object storage - run: pnpm verify:object-storage + - name: Verify object storage + run: pnpm verify:object-storage # full-gate-command: pnpm test:external-storage-runtime - - name: Test external-storage runtime - run: pnpm test:external-storage-runtime + - name: Test external-storage runtime + run: pnpm test:external-storage-runtime # full-gate-command: pnpm test:coverage - - name: Report coverage - run: pnpm test:coverage + - name: Report coverage + run: pnpm test:coverage # full-gate-command: pnpm test:local-package - - name: Test local package - run: pnpm test:local-package + - name: Test local package + run: pnpm test:local-package # full-gate-command: pnpm perf:baseline - - name: Record local performance baseline - run: pnpm perf:baseline + - name: Record local performance baseline + run: pnpm perf:baseline # full-gate-command: pnpm perf:capacity - - name: Record server capacity baseline - run: pnpm perf:capacity + - name: Record server capacity baseline + run: pnpm perf:capacity # full-gate-command: pnpm test:compact-compose - - name: Test compact Compose - run: pnpm test:compact-compose + - name: Test compact Compose + run: pnpm test:compact-compose # full-gate-command: pnpm test:external-storage-compose - - name: Test external-storage Compose - run: pnpm test:external-storage-compose + - name: Test external-storage Compose + run: pnpm test:external-storage-compose # full-gate-command: pnpm verify:helm - - name: Verify Helm release - run: pnpm verify:helm + - name: Verify Helm release + run: pnpm verify:helm # full-gate-command: pnpm test:oidc - - name: Test generic OIDC - run: pnpm test:oidc + - name: Test generic OIDC + run: pnpm test:oidc - - name: Upload full-gate evidence - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: full-gate-evidence-${{ github.run_id }}-${{ github.run_attempt }} - path: | - project/evidence/*.json - coverage/ - test-results/ - release/*.manifest.json - release/*.tgz - if-no-files-found: error - retention-days: 14 + - name: Upload full-gate evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: full-gate-evidence-${{ github.run_id }}-${{ github.run_attempt }} + path: | + project/evidence/*.json + coverage/ + test-results/ + release/*.manifest.json + release/*.tgz + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/install-verify.yml b/.github/workflows/install-verify.yml index bae87db8..82890f5a 100644 --- a/.github/workflows/install-verify.yml +++ b/.github/workflows/install-verify.yml @@ -35,137 +35,137 @@ jobs: matrix: os: [ubuntu-latest, macos-14] steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Download, verify, and exercise the Node package - env: - ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-install" - assets="$work/assets" - install="$work/install" - data="$work/data" - mkdir -p "$assets" "$install" - - if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern 'release-public-*' \ - --dir "$assets" - else - gh release download "$ARTIFACTSERVER_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --dir "$assets" - fi - - checksums=$(find "$assets" -type f -name SHA256SUMS -print -quit) - if [[ -z "$checksums" ]]; then - printf 'SHA256SUMS is missing from the downloaded release assets.\n' >&2 - exit 1 - fi - release_directory=$(dirname "$checksums") - if command -v sha256sum >/dev/null 2>&1; then - (cd "$release_directory" && sha256sum --check --strict SHA256SUMS) - else - (cd "$release_directory" && shasum -a 256 --check SHA256SUMS) - fi - - version=${ARTIFACTSERVER_TAG#v} - archive="$release_directory/artifact-server-$version-node.tar.gz" - if [[ ! -f "$archive" ]]; then - printf 'The Node package for %s is missing.\n' "$ARTIFACTSERVER_TAG" >&2 - exit 1 - fi - if [[ -z "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - gh attestation verify "$archive" \ - --repo "$GITHUB_REPOSITORY" \ - --deny-self-hosted-runners - else - printf '%s\n' \ - '::notice::Dry-run assets are checksum-verified; attestations exist only for tagged releases.' - fi + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Download, verify, and exercise the Node package + env: + ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-install" + assets="$work/assets" + install="$work/install" + data="$work/data" + mkdir -p "$assets" "$install" - tar -xzf "$archive" -C "$install" - artifactserver="$install/artifactserver/bin/artifactserver" - test -x "$artifactserver" - test "$("$artifactserver" --version)" = "$version" + if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern 'release-public-*' \ + --dir "$assets" + else + gh release download "$ARTIFACTSERVER_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --dir "$assets" + fi - fixture="$work/install-proof.html" - printf '%s\n' 'Clean install proof' > "$fixture" - "$artifactserver" start --data "$data" --port 8787 > "$work/server.log" 2>&1 & - server_pid=$! - cleanup() { - kill "$server_pid" 2>/dev/null || true - wait "$server_pid" 2>/dev/null || true - if [[ -f "$data/local-service.json" ]]; then - managed_pid=$(node -p \ - "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).pid" \ - "$data/local-service.json") - kill "$managed_pid" 2>/dev/null || true + checksums=$(find "$assets" -type f -name SHA256SUMS -print -quit) + if [[ -z "$checksums" ]]; then + printf 'SHA256SUMS is missing from the downloaded release assets.\n' >&2 + exit 1 fi - } - trap cleanup EXIT - for attempt in {1..40}; do - if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then - break + release_directory=$(dirname "$checksums") + if command -v sha256sum >/dev/null 2>&1; then + (cd "$release_directory" && sha256sum --check --strict SHA256SUMS) + else + (cd "$release_directory" && shasum -a 256 --check SHA256SUMS) fi - if [[ "$attempt" == 40 ]]; then - cat "$work/server.log" >&2 + + version=${ARTIFACTSERVER_TAG#v} + archive="$release_directory/artifact-server-$version-node.tar.gz" + if [[ ! -f "$archive" ]]; then + printf 'The Node package for %s is missing.\n' "$ARTIFACTSERVER_TAG" >&2 exit 1 fi - sleep 0.25 - done + if [[ -z "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + gh attestation verify "$archive" \ + --repo "$GITHUB_REPOSITORY" \ + --deny-self-hosted-runners + else + printf '%s\n' \ + '::notice::Dry-run assets are checksum-verified; attestations exist only for tagged releases.' + fi - publication=$("$artifactserver" publish "$fixture" \ - --data "$data" \ - --server http://127.0.0.1:8787 \ - --token-file "$data/local-api-token" \ - --public) - ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' - import {request} from "node:http"; - const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); - const published = new URL(publication.links.version); - const content = await new Promise((resolve, reject) => { - const incoming = request({ - headers: {host: published.host}, - hostname: "127.0.0.1", - path: `${published.pathname}${published.search}`, - port: 8787, - }, (response) => { - let body = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { body += chunk; }); - response.on("end", () => { - if (response.statusCode !== 200) { - reject(new Error(`Published content returned ${response.statusCode}.`)); - return; - } - resolve(body); + tar -xzf "$archive" -C "$install" + artifactserver="$install/artifactserver/bin/artifactserver" + test -x "$artifactserver" + test "$("$artifactserver" --version)" = "$version" + + fixture="$work/install-proof.html" + printf '%s\n' 'Clean install proof' > "$fixture" + "$artifactserver" start --data "$data" --port 8787 > "$work/server.log" 2>&1 & + server_pid=$! + cleanup() { + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + if [[ -f "$data/local-service.json" ]]; then + managed_pid=$(node -p \ + "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).pid" \ + "$data/local-service.json") + kill "$managed_pid" 2>/dev/null || true + fi + } + trap cleanup EXIT + for attempt in {1..40}; do + if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then + break + fi + if [[ "$attempt" == 40 ]]; then + cat "$work/server.log" >&2 + exit 1 + fi + sleep 0.25 + done + + publication=$("$artifactserver" publish "$fixture" \ + --data "$data" \ + --server http://127.0.0.1:8787 \ + --token-file "$data/local-api-token" \ + --public) + ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' + import {request} from "node:http"; + const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); + const published = new URL(publication.links.version); + const content = await new Promise((resolve, reject) => { + const incoming = request({ + headers: {host: published.host}, + hostname: "127.0.0.1", + path: `${published.pathname}${published.search}`, + port: 8787, + }, (response) => { + let body = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { body += chunk; }); + response.on("end", () => { + if (response.statusCode !== 200) { + reject(new Error(`Published content returned ${response.statusCode}.`)); + return; + } + resolve(body); + }); }); + incoming.on("error", reject); + incoming.end(); }); - incoming.on("error", reject); - incoming.end(); - }); - if (!content.includes("Clean install proof")) { - throw new Error("Published content did not match the clean-install fixture."); - } - NODE + if (!content.includes("Clean install proof")) { + throw new Error("Published content did not match the clean-install fixture."); + } + NODE - kill "$server_pid" - wait "$server_pid" || true - browser_command="$work/browser-command" - printf '%s\n' '#!/bin/sh' 'exit 0' > "$browser_command" - chmod 0700 "$browser_command" - open_output=$(ARTIFACT_SERVER_BROWSER_COMMAND="$browser_command" \ - "$artifactserver" open --data "$data") - printf '%s\n' "$open_output" | grep -Eq 'https?://[^[:space:]]+' + kill "$server_pid" + wait "$server_pid" || true + browser_command="$work/browser-command" + printf '%s\n' '#!/bin/sh' 'exit 0' > "$browser_command" + chmod 0700 "$browser_command" + open_output=$(ARTIFACT_SERVER_BROWSER_COMMAND="$browser_command" \ + "$artifactserver" open --data "$data") + printf '%s\n' "$open_output" | grep -Eq 'https?://[^[:space:]]+' image: name: Immutable GHCR image / compact profile @@ -177,101 +177,101 @@ jobs: contents: read packages: read steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Run the immutable image through the compact profile - env: - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-image-install" - mkdir -p "$work" - gh release download "$ARTIFACTSERVER_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern image-reference.txt \ - --dir "$work" - image_reference=$(cat "$work/image-reference.txt") - printf '%s' "$GH_TOKEN" | docker login ghcr.io \ - --username "$GITHUB_ACTOR" \ - --password-stdin - gh attestation verify "oci://$image_reference" \ - --repo "$GITHUB_REPOSITORY" \ - --bundle-from-oci \ - --deny-self-hosted-runners + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Run the immutable image through the compact profile + env: + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-image-install" + mkdir -p "$work" + gh release download "$ARTIFACTSERVER_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern image-reference.txt \ + --dir "$work" + image_reference=$(cat "$work/image-reference.txt") + printf '%s' "$GH_TOKEN" | docker login ghcr.io \ + --username "$GITHUB_ACTOR" \ + --password-stdin + gh attestation verify "oci://$image_reference" \ + --repo "$GITHUB_REPOSITORY" \ + --bundle-from-oci \ + --deny-self-hosted-runners - gh api \ - "repos/$GITHUB_REPOSITORY/contents/packaging/compose/compose.yaml?ref=$ARTIFACTSERVER_TAG" \ - --jq .content | base64 --decode > "$work/compose.yaml" - export ARTIFACT_SERVER_CONTENT_DOMAIN=content.example.net - export ARTIFACT_SERVER_IMAGE="$image_reference" - export ARTIFACT_SERVER_ORIGIN=https://artifacts.example.com - export ARTIFACT_SERVER_PORT=8787 - export ARTIFACT_SERVER_READINESS_WITHDRAWAL_MS=0 - export ARTIFACT_SERVER_REQUEST_LOG_SAMPLE_RATE=0 - export COMPOSE_PROJECT_NAME=artifact-server-install-verify - cleanup() { - docker compose --file "$work/compose.yaml" down --volumes \ - --remove-orphans >/dev/null 2>&1 || true - } - trap cleanup EXIT + gh api \ + "repos/$GITHUB_REPOSITORY/contents/packaging/compose/compose.yaml?ref=$ARTIFACTSERVER_TAG" \ + --jq .content | base64 --decode > "$work/compose.yaml" + export ARTIFACT_SERVER_CONTENT_DOMAIN=content.example.net + export ARTIFACT_SERVER_IMAGE="$image_reference" + export ARTIFACT_SERVER_ORIGIN=https://artifacts.example.com + export ARTIFACT_SERVER_PORT=8787 + export ARTIFACT_SERVER_READINESS_WITHDRAWAL_MS=0 + export ARTIFACT_SERVER_REQUEST_LOG_SAMPLE_RATE=0 + export COMPOSE_PROJECT_NAME=artifact-server-install-verify + cleanup() { + docker compose --file "$work/compose.yaml" down --volumes \ + --remove-orphans >/dev/null 2>&1 || true + } + trap cleanup EXIT - docker compose --file "$work/compose.yaml" run --rm --no-deps \ - artifact-server init \ - --admin-email admin@example.test \ - --data /var/lib/artifact-server/data - docker compose --file "$work/compose.yaml" up --detach - for attempt in {1..60}; do - if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then - break - fi - if [[ "$attempt" == 60 ]]; then - docker compose --file "$work/compose.yaml" logs >&2 - exit 1 - fi - sleep 0.5 - done + docker compose --file "$work/compose.yaml" run --rm --no-deps \ + artifact-server init \ + --admin-email admin@example.test \ + --data /var/lib/artifact-server/data + docker compose --file "$work/compose.yaml" up --detach + for attempt in {1..60}; do + if curl --fail --silent http://127.0.0.1:8787/ready >/dev/null; then + break + fi + if [[ "$attempt" == 60 ]]; then + docker compose --file "$work/compose.yaml" logs >&2 + exit 1 + fi + sleep 0.5 + done - docker compose --file "$work/compose.yaml" exec -T artifact-server \ - sh -c "printf '%s\\n' 'OCI install proof' > /tmp/install-proof.html" - publication=$(docker compose --file "$work/compose.yaml" exec -T \ - artifact-server node dist/cli/main.js publish /tmp/install-proof.html \ - --server http://127.0.0.1:8787 \ - --token-file /var/lib/artifact-server/data/secrets/api-token \ - --public) - ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' - import {request} from "node:http"; - const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); - const published = new URL(publication.links.version); - const body = await new Promise((resolve, reject) => { - const incoming = request({ - headers: {host: published.host}, - hostname: "127.0.0.1", - path: `${published.pathname}${published.search}`, - port: 8787, - }, (response) => { - let content = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { content += chunk; }); - response.on("end", () => { - if (response.statusCode !== 200) { - reject(new Error(`Published content returned ${response.statusCode}.`)); - return; - } - resolve(content); + docker compose --file "$work/compose.yaml" exec -T artifact-server \ + sh -c "printf '%s\\n' 'OCI install proof' > /tmp/install-proof.html" + publication=$(docker compose --file "$work/compose.yaml" exec -T \ + artifact-server node dist/cli/main.js publish /tmp/install-proof.html \ + --server http://127.0.0.1:8787 \ + --token-file /var/lib/artifact-server/data/secrets/api-token \ + --public) + ARTIFACTSERVER_PUBLICATION="$publication" node --input-type=module <<'NODE' + import {request} from "node:http"; + const publication = JSON.parse(process.env.ARTIFACTSERVER_PUBLICATION); + const published = new URL(publication.links.version); + const body = await new Promise((resolve, reject) => { + const incoming = request({ + headers: {host: published.host}, + hostname: "127.0.0.1", + path: `${published.pathname}${published.search}`, + port: 8787, + }, (response) => { + let content = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { content += chunk; }); + response.on("end", () => { + if (response.statusCode !== 200) { + reject(new Error(`Published content returned ${response.statusCode}.`)); + return; + } + resolve(content); + }); }); + incoming.on("error", reject); + incoming.end(); }); - incoming.on("error", reject); - incoming.end(); - }); - if (!body.includes("OCI install proof")) { - throw new Error("Published content did not match the OCI fixture."); - } - NODE + if (!body.includes("OCI install proof")) { + throw new Error("Published content did not match the OCI fixture."); + } + NODE adapter: name: Public npm adapter @@ -281,50 +281,50 @@ jobs: actions: read contents: read steps: - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - registry-url: https://registry.npmjs.org - package-manager-cache: false - - name: Install and load the OpenCode adapter - env: - ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} - ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - GH_TOKEN: ${{ github.token }} - shell: bash - run: | # zizmor: ignore[adhoc-packages] This job proves the published package installs without the source lockfile. - set -euo pipefail - work="$RUNNER_TEMP/artifact-server-adapter-install" - prefix="$work/prefix" - mkdir -p "$prefix" - version=${ARTIFACTSERVER_TAG#v} - if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then - assets="$work/assets" - mkdir -p "$assets" - gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern 'release-public-*' \ - --dir "$assets" - package_spec=$(find "$assets" -type f \ - -name "plannotator-artifact-server-opencode-$version.tgz" \ - -print -quit) - if [[ -z "$package_spec" ]]; then - printf 'The OpenCode adapter tarball is missing.\n' >&2 - exit 1 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Install and load the OpenCode adapter + env: + ARTIFACTSERVER_SOURCE_RUN_ID: ${{ inputs.source_run_id }} + ARTIFACTSERVER_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | # zizmor: ignore[adhoc-packages] This job proves the published package installs without the source lockfile. + set -euo pipefail + work="$RUNNER_TEMP/artifact-server-adapter-install" + prefix="$work/prefix" + mkdir -p "$prefix" + version=${ARTIFACTSERVER_TAG#v} + if [[ -n "$ARTIFACTSERVER_SOURCE_RUN_ID" ]]; then + assets="$work/assets" + mkdir -p "$assets" + gh run download "$ARTIFACTSERVER_SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern 'release-public-*' \ + --dir "$assets" + package_spec=$(find "$assets" -type f \ + -name "plannotator-artifact-server-opencode-$version.tgz" \ + -print -quit) + if [[ -z "$package_spec" ]]; then + printf 'The OpenCode adapter tarball is missing.\n' >&2 + exit 1 + fi + else + package_spec="@plannotator/artifact-server-opencode@$version" fi - else - package_spec="@plannotator/artifact-server-opencode@$version" - fi - npm install --global --prefix "$prefix" "$package_spec" tsx@4.23.12 - adapter_entry="$prefix/lib/node_modules/@plannotator/artifact-server-opencode/index.ts" - test -f "$adapter_entry" - ARTIFACTSERVER_ADAPTER_ENTRY="$adapter_entry" node \ - --import "$prefix/lib/node_modules/tsx/dist/loader.mjs" \ - --input-type=module <<'NODE' - import {pathToFileURL} from "node:url"; - const loaded = await import(pathToFileURL(process.env.ARTIFACTSERVER_ADAPTER_ENTRY)); - if (typeof loaded.ArtifactServerBridge !== "function") { - throw new Error("The installed OpenCode adapter did not expose its plugin entry."); - } - NODE + npm install --global --prefix "$prefix" "$package_spec" tsx@4.23.12 + adapter_entry="$prefix/lib/node_modules/@plannotator/artifact-server-opencode/index.ts" + test -f "$adapter_entry" + ARTIFACTSERVER_ADAPTER_ENTRY="$adapter_entry" node \ + --import "$prefix/lib/node_modules/tsx/dist/loader.mjs" \ + --input-type=module <<'NODE' + import {pathToFileURL} from "node:url"; + const loaded = await import(pathToFileURL(process.env.ARTIFACTSERVER_ADAPTER_ENTRY)); + if (typeof loaded.ArtifactServerBridge !== "function") { + throw new Error("The installed OpenCode adapter did not expose its plugin entry."); + } + NODE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ec5afc2..fb4cb0c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,18 +4,18 @@ on: pull_request: branches: [main] paths: - - .github/workflows/ci.yml - - .github/workflows/release.yml - - scripts/assemble-release-assets.mjs - - scripts/build-oci-image.sh - - scripts/check-release-ref.mjs - - scripts/check-release-version.mjs - - scripts/extract-oci-sboms.mjs - - scripts/generate-release-sboms.sh - - scripts/install-release-syft.sh + - .github/workflows/ci.yml + - .github/workflows/release.yml + - scripts/assemble-release-assets.mjs + - scripts/build-oci-image.sh + - scripts/check-release-ref.mjs + - scripts/check-release-version.mjs + - scripts/extract-oci-sboms.mjs + - scripts/generate-release-sboms.sh + - scripts/install-release-syft.sh push: tags: - - '**' + - '**' workflow_dispatch: inputs: dry_run: @@ -51,69 +51,69 @@ jobs: tag: ${{ steps.metadata.outputs.tag }} version: ${{ steps.metadata.outputs.version }} steps: - - name: Checkout complete repository history - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Verify release metadata and ref - id: metadata - env: - ARTIFACTSERVER_EVENT_NAME: ${{ github.event_name }} - ARTIFACTSERVER_PRIVATE_REGISTRY_TEST: ${{ inputs.private_registry_test || false }} - ARTIFACTSERVER_REF_NAME: ${{ github.ref_name }} - ARTIFACTSERVER_REF_TYPE: ${{ github.ref_type }} - ARTIFACTSERVER_RUN_ATTEMPT: ${{ github.run_attempt }} - ARTIFACTSERVER_RUN_ID: ${{ github.run_id }} - shell: bash - run: | - set -euo pipefail - version=$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version") - revision=$(git rev-parse HEAD) - series=${version%.*} - tag="v$version" - publish=false - publish_image=false - image_primary_tag="$tag" - image_secondary_tag="$series" - if [[ "$ARTIFACTSERVER_EVENT_NAME" == push && "$ARTIFACTSERVER_REF_TYPE" == tag ]]; then - tag="$ARTIFACTSERVER_REF_NAME" - publish=true - publish_image=true + - name: Checkout complete repository history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Verify release metadata and ref + id: metadata + env: + ARTIFACTSERVER_EVENT_NAME: ${{ github.event_name }} + ARTIFACTSERVER_PRIVATE_REGISTRY_TEST: ${{ inputs.private_registry_test || false }} + ARTIFACTSERVER_REF_NAME: ${{ github.ref_name }} + ARTIFACTSERVER_REF_TYPE: ${{ github.ref_type }} + ARTIFACTSERVER_RUN_ATTEMPT: ${{ github.run_attempt }} + ARTIFACTSERVER_RUN_ID: ${{ github.run_id }} + shell: bash + run: | + set -euo pipefail + version=$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version") + revision=$(git rev-parse HEAD) + series=${version%.*} + tag="v$version" + publish=false + publish_image=false image_primary_tag="$tag" image_secondary_tag="$series" - node scripts/check-release-version.mjs --tag "$tag" - node scripts/check-release-ref.mjs --tag "$tag" --main-ref origin/main - else - node scripts/check-release-version.mjs - node scripts/check-release-ref.mjs --dry-run - if [[ "$ARTIFACTSERVER_PRIVATE_REGISTRY_TEST" == true ]]; then + if [[ "$ARTIFACTSERVER_EVENT_NAME" == push && "$ARTIFACTSERVER_REF_TYPE" == tag ]]; then + tag="$ARTIFACTSERVER_REF_NAME" + publish=true publish_image=true - image_primary_tag="ci-$ARTIFACTSERVER_RUN_ID-$ARTIFACTSERVER_RUN_ATTEMPT" - image_secondary_tag="ci-${revision:0:12}" + image_primary_tag="$tag" + image_secondary_tag="$series" + node scripts/check-release-version.mjs --tag "$tag" + node scripts/check-release-ref.mjs --tag "$tag" --main-ref origin/main + else + node scripts/check-release-version.mjs + node scripts/check-release-ref.mjs --dry-run + if [[ "$ARTIFACTSERVER_PRIVATE_REGISTRY_TEST" == true ]]; then + publish_image=true + image_primary_tag="ci-$ARTIFACTSERVER_RUN_ID-$ARTIFACTSERVER_RUN_ATTEMPT" + image_secondary_tag="ci-${revision:0:12}" + fi fi - fi - { - printf 'image_primary_tag=%s\n' "$image_primary_tag" - printf 'image_secondary_tag=%s\n' "$image_secondary_tag" - printf 'publish=%s\n' "$publish" - printf 'publish_image=%s\n' "$publish_image" - printf 'revision=%s\n' "$revision" - printf 'series=%s\n' "$series" - printf 'tag=%s\n' "$tag" - printf 'version=%s\n' "$version" - } >> "$GITHUB_OUTPUT" + { + printf 'image_primary_tag=%s\n' "$image_primary_tag" + printf 'image_secondary_tag=%s\n' "$image_secondary_tag" + printf 'publish=%s\n' "$publish" + printf 'publish_image=%s\n' "$publish_image" + printf 'revision=%s\n' "$revision" + printf 'series=%s\n' "$series" + printf 'tag=%s\n' "$tag" + printf 'version=%s\n' "$version" + } >> "$GITHUB_OUTPUT" gate: name: Canonical release gate @@ -128,53 +128,53 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 18 steps: - - name: Checkout release source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Build portable local package - env: - ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle - run: pnpm package:local "$ARTIFACTSERVER_OUTPUT" - - name: Pack public adapters - env: - ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle - shell: bash - run: | - set -euo pipefail - pnpm --dir integrations/pi pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - pnpm --dir integrations/opencode pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - pnpm --dir integrations/claude-channel pack --pack-destination "$ARTIFACTSERVER_OUTPUT" - - name: Install verified Syft - run: scripts/install-release-syft.sh "$RUNNER_TEMP/syft" - - name: Generate package SPDX SBOMs - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - scripts/generate-release-sboms.sh - "$RUNNER_TEMP/syft/syft" - release/package-bundle - release/package-bundle - "$ARTIFACTSERVER_VERSION" - packages - - name: Upload package release inputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} - path: release/package-bundle/ - if-no-files-found: error - retention-days: 7 + - name: Checkout release source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Build portable local package + env: + ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle + run: pnpm package:local "$ARTIFACTSERVER_OUTPUT" + - name: Pack public adapters + env: + ARTIFACTSERVER_OUTPUT: ${{ github.workspace }}/release/package-bundle + shell: bash + run: | + set -euo pipefail + pnpm --dir integrations/pi pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + pnpm --dir integrations/opencode pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + pnpm --dir integrations/claude-channel pack --pack-destination "$ARTIFACTSERVER_OUTPUT" + - name: Install verified Syft + run: scripts/install-release-syft.sh "$RUNNER_TEMP/syft" + - name: Generate package SPDX SBOMs + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + scripts/generate-release-sboms.sh + "$RUNNER_TEMP/syft/syft" + release/package-bundle + release/package-bundle + "$ARTIFACTSERVER_VERSION" + packages + - name: Upload package release inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} + path: release/package-bundle/ + if-no-files-found: error + retention-days: 7 image: name: Build and optionally publish OCI image @@ -185,99 +185,99 @@ jobs: contents: read packages: write steps: - - name: Checkout release source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: 10.34.3 - - name: Install Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - package-manager-cache: false - - name: Install dependencies without a cache - run: pnpm install --frozen-lockfile - - name: Prepare attestation-capable BuildKit - shell: bash - run: | - set -euo pipefail - docker buildx create \ - --driver docker-container \ - --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ - --name artifact-server-release \ - --use - docker buildx inspect --bootstrap - - name: Sign in to private GHCR for image publication - if: needs.verify.outputs.publish_image == 'true' - env: - ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} - ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} - run: >- - docker login ghcr.io - --username "$ARTIFACTSERVER_GITHUB_ACTOR" - --password-stdin - <<< "$ARTIFACTSERVER_GITHUB_TOKEN" - - name: Build release image - env: - ARTIFACT_SERVER_IMAGE_PUSH: ${{ needs.verify.outputs.publish_image }} - ARTIFACT_SERVER_IMAGE_TAGS: ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_primary_tag }},ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_secondary_tag }} - run: pnpm package:oci release/image-work - - name: Extract verified image SPDX SBOMs - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - scripts/generate-release-sboms.sh - unused - release/image-work - release/image-bundle - "$ARTIFACTSERVER_VERSION" - image - - name: Record immutable image reference - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - manifest="release/image-work/artifact-server-$ARTIFACTSERVER_VERSION.oci.tar.manifest.json" - digest=$(jq --exit-status --raw-output '.imageIndexDigest' "$manifest") - mkdir -p release/image-bundle - cp -- "$manifest" \ - "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json" - printf '%s\n' "ghcr.io/plannotator/artifact-server@$digest" \ - > release/image-bundle/image-reference.txt - - name: Verify published image tags resolve to the built digest - if: needs.verify.outputs.publish_image == 'true' - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.image_primary_tag }} - ARTIFACTSERVER_SERIES: ${{ needs.verify.outputs.image_secondary_tag }} - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - expected=$(jq --exit-status --raw-output \ - '.imageIndexDigest' \ - "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json") - for tag in "$ARTIFACTSERVER_TAG" "$ARTIFACTSERVER_SERIES"; do - actual="sha256:$(docker buildx imagetools inspect --raw \ - "ghcr.io/plannotator/artifact-server:$tag" | sha256sum | cut -d ' ' -f 1)" - if [[ "$actual" != "$expected" ]]; then - printf 'Published tag %s resolved to %s, expected %s.\n' \ - "$tag" "$actual" "$expected" >&2 - exit 1 - fi - done - - name: Upload image release inputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-image-${{ github.run_id }}-${{ github.run_attempt }} - path: | - release/image-work/artifact-server-${{ needs.verify.outputs.version }}.oci.tar - release/image-bundle/ - if-no-files-found: error - retention-days: 7 + - name: Checkout release source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: 10.34.3 + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + package-manager-cache: false + - name: Install dependencies without a cache + run: pnpm install --frozen-lockfile + - name: Prepare attestation-capable BuildKit + shell: bash + run: | + set -euo pipefail + docker buildx create \ + --driver docker-container \ + --driver-opt image=moby/buildkit@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8 \ + --name artifact-server-release \ + --use + docker buildx inspect --bootstrap + - name: Sign in to private GHCR for image publication + if: needs.verify.outputs.publish_image == 'true' + env: + ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} + ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} + run: >- + docker login ghcr.io + --username "$ARTIFACTSERVER_GITHUB_ACTOR" + --password-stdin + <<< "$ARTIFACTSERVER_GITHUB_TOKEN" + - name: Build release image + env: + ARTIFACT_SERVER_IMAGE_PUSH: ${{ needs.verify.outputs.publish_image }} + ARTIFACT_SERVER_IMAGE_TAGS: ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_primary_tag }},ghcr.io/plannotator/artifact-server:${{ needs.verify.outputs.image_secondary_tag }} + run: pnpm package:oci release/image-work + - name: Extract verified image SPDX SBOMs + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + scripts/generate-release-sboms.sh + unused + release/image-work + release/image-bundle + "$ARTIFACTSERVER_VERSION" + image + - name: Record immutable image reference + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + manifest="release/image-work/artifact-server-$ARTIFACTSERVER_VERSION.oci.tar.manifest.json" + digest=$(jq --exit-status --raw-output '.imageIndexDigest' "$manifest") + mkdir -p release/image-bundle + cp -- "$manifest" \ + "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json" + printf '%s\n' "ghcr.io/plannotator/artifact-server@$digest" \ + > release/image-bundle/image-reference.txt + - name: Verify published image tags resolve to the built digest + if: needs.verify.outputs.publish_image == 'true' + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.image_primary_tag }} + ARTIFACTSERVER_SERIES: ${{ needs.verify.outputs.image_secondary_tag }} + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + expected=$(jq --exit-status --raw-output \ + '.imageIndexDigest' \ + "release/image-bundle/artifact-server-$ARTIFACTSERVER_VERSION-oci.manifest.json") + for tag in "$ARTIFACTSERVER_TAG" "$ARTIFACTSERVER_SERIES"; do + actual="sha256:$(docker buildx imagetools inspect --raw \ + "ghcr.io/plannotator/artifact-server:$tag" | sha256sum | cut -d ' ' -f 1)" + if [[ "$actual" != "$expected" ]]; then + printf 'Published tag %s resolved to %s, expected %s.\n' \ + "$tag" "$actual" "$expected" >&2 + exit 1 + fi + done + - name: Upload image release inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-image-${{ github.run_id }}-${{ github.run_attempt }} + path: | + release/image-work/artifact-server-${{ needs.verify.outputs.version }}.oci.tar + release/image-bundle/ + if-no-files-found: error + retention-days: 7 assemble: name: Assemble downloadable release assets @@ -285,38 +285,38 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - name: Checkout release tooling - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Download package release inputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} - path: release/incoming/packages - - name: Download image release inputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-image-${{ github.run_id }}-${{ github.run_attempt }} - path: release/incoming/image - - name: Assemble assets and checksums - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - run: >- - node scripts/assemble-release-assets.mjs - release/incoming - release/public - "$ARTIFACTSERVER_VERSION" - - name: Verify every downloadable asset checksum - working-directory: release/public - run: sha256sum --check --strict SHA256SUMS - - name: Upload downloadable release assets - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public/ - if-no-files-found: error - retention-days: 14 + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Download package release inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-packages-${{ github.run_id }}-${{ github.run_attempt }} + path: release/incoming/packages + - name: Download image release inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-image-${{ github.run_id }}-${{ github.run_attempt }} + path: release/incoming/image + - name: Assemble assets and checksums + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + run: >- + node scripts/assemble-release-assets.mjs + release/incoming + release/public + "$ARTIFACTSERVER_VERSION" + - name: Verify every downloadable asset checksum + working-directory: release/public + run: sha256sum --check --strict SHA256SUMS + - name: Upload downloadable release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public/ + if-no-files-found: error + retention-days: 14 attest: name: Attest release assets @@ -330,75 +330,75 @@ jobs: id-token: write packages: write steps: - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Read immutable image subject - id: image - env: - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - shell: bash - run: | - set -euo pipefail - reference=$(cat release/public/image-reference.txt) - digest=${reference##*@} - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - printf 'name=%s\n' "${reference%@*}" >> "$GITHUB_OUTPUT" - - name: Sign in to private GHCR for image attestations - env: - ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} - ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} - run: >- - docker login ghcr.io - --username "$ARTIFACTSERVER_GITHUB_ACTOR" - --password-stdin - <<< "$ARTIFACTSERVER_GITHUB_TOKEN" - - name: Attest downloadable asset provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: release/public/* - - name: Attest local package SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.tar.gz - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.spdx.json - - name: Attest Pi adapter SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest OpenCode adapter SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest Claude channel SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.tgz - sbom-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.spdx.json - - name: Attest image provenance in GHCR - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - push-to-registry: true - - name: Attest image SBOM in GHCR - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-amd64.spdx.json - push-to-registry: true - - name: Attest arm64 image SBOM in GHCR - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-name: ${{ steps.image.outputs.name }} - subject-digest: ${{ steps.image.outputs.digest }} - sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-arm64.spdx.json - push-to-registry: true + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Read immutable image subject + id: image + env: + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + shell: bash + run: | + set -euo pipefail + reference=$(cat release/public/image-reference.txt) + digest=${reference##*@} + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + printf 'name=%s\n' "${reference%@*}" >> "$GITHUB_OUTPUT" + - name: Sign in to private GHCR for image attestations + env: + ARTIFACTSERVER_GITHUB_ACTOR: ${{ github.actor }} + ARTIFACTSERVER_GITHUB_TOKEN: ${{ github.token }} + run: >- + docker login ghcr.io + --username "$ARTIFACTSERVER_GITHUB_ACTOR" + --password-stdin + <<< "$ARTIFACTSERVER_GITHUB_TOKEN" + - name: Attest downloadable asset provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: release/public/* + - name: Attest local package SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.tar.gz + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-node.spdx.json + - name: Attest Pi adapter SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-pi-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest OpenCode adapter SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-opencode-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest Claude channel SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.tgz + sbom-path: release/public/plannotator-artifact-server-claude-channel-${{ needs.verify.outputs.version }}.spdx.json + - name: Attest image provenance in GHCR + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + push-to-registry: true + - name: Attest image SBOM in GHCR + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-amd64.spdx.json + push-to-registry: true + - name: Attest arm64 image SBOM in GHCR + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.image.outputs.digest }} + sbom-path: release/public/artifact-server-${{ needs.verify.outputs.version }}-oci-linux-arm64.spdx.json + push-to-registry: true publish-npm: name: Publish public npm adapters @@ -410,65 +410,65 @@ jobs: contents: read id-token: write steps: - - name: Install Node.js for npm provenance - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24.15.0 - registry-url: https://registry.npmjs.org - package-manager-cache: false - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Publish adapters - env: - ARTIFACTSERVER_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} - ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - shell: bash - run: | - set -euo pipefail - if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then - printf '%s\n' \ - '::notice::npm publication is skipped because NPM_TOKEN is not configured. Publish the adapter tarballs from the release assets locally with npm publish --access public.' - exit 0 - fi - provenance_args=() - if [[ "$ARTIFACTSERVER_REPOSITORY_VISIBILITY" == public ]]; then - provenance_args+=(--provenance) - else - printf '%s\n' \ - '::notice::npm provenance is deferred until the source repository is public.' - fi - publish_adapter() { - package_name=$1 - archive=$2 - local_integrity=$(node --input-type=module -e \ - 'import {createHash} from "node:crypto"; import {readFileSync} from "node:fs"; const bytes = readFileSync(process.argv[1]); process.stdout.write(`sha512-${createHash("sha512").update(bytes).digest("base64")}`);' \ - "$archive") - if remote_integrity=$(npm view \ - "$package_name@$ARTIFACTSERVER_VERSION" dist.integrity 2>/dev/null); then - if [[ "$remote_integrity" != "$local_integrity" ]]; then - printf 'npm already contains different bytes for %s@%s.\n' \ - "$package_name" "$ARTIFACTSERVER_VERSION" >&2 - exit 1 - fi - printf '::notice::%s@%s already contains the verified release bytes.\n' \ - "$package_name" "$ARTIFACTSERVER_VERSION" - return + - name: Install Node.js for npm provenance + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.15.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Publish adapters + env: + ARTIFACTSERVER_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} + ARTIFACTSERVER_VERSION: ${{ needs.verify.outputs.version }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then + printf '%s\n' \ + '::notice::npm publication is skipped because NPM_TOKEN is not configured. Publish the adapter tarballs from the release assets locally with npm publish --access public.' + exit 0 fi - npm publish "$archive" "${provenance_args[@]}" --access public - } - publish_adapter \ - @plannotator/artifact-server-pi \ - "release/public/plannotator-artifact-server-pi-$ARTIFACTSERVER_VERSION.tgz" - publish_adapter \ - @plannotator/artifact-server-opencode \ - "release/public/plannotator-artifact-server-opencode-$ARTIFACTSERVER_VERSION.tgz" - publish_adapter \ - @plannotator/artifact-server-claude-channel \ - "release/public/plannotator-artifact-server-claude-channel-$ARTIFACTSERVER_VERSION.tgz" + provenance_args=() + if [[ "$ARTIFACTSERVER_REPOSITORY_VISIBILITY" == public ]]; then + provenance_args+=(--provenance) + else + printf '%s\n' \ + '::notice::npm provenance is deferred until the source repository is public.' + fi + publish_adapter() { + package_name=$1 + archive=$2 + local_integrity=$(node --input-type=module -e \ + 'import {createHash} from "node:crypto"; import {readFileSync} from "node:fs"; const bytes = readFileSync(process.argv[1]); process.stdout.write(`sha512-${createHash("sha512").update(bytes).digest("base64")}`);' \ + "$archive") + if remote_integrity=$(npm view \ + "$package_name@$ARTIFACTSERVER_VERSION" dist.integrity 2>/dev/null); then + if [[ "$remote_integrity" != "$local_integrity" ]]; then + printf 'npm already contains different bytes for %s@%s.\n' \ + "$package_name" "$ARTIFACTSERVER_VERSION" >&2 + exit 1 + fi + printf '::notice::%s@%s already contains the verified release bytes.\n' \ + "$package_name" "$ARTIFACTSERVER_VERSION" + return + fi + npm publish "$archive" "${provenance_args[@]}" --access public + } + publish_adapter \ + @plannotator/artifact-server-pi \ + "release/public/plannotator-artifact-server-pi-$ARTIFACTSERVER_VERSION.tgz" + publish_adapter \ + @plannotator/artifact-server-opencode \ + "release/public/plannotator-artifact-server-opencode-$ARTIFACTSERVER_VERSION.tgz" + publish_adapter \ + @plannotator/artifact-server-claude-channel \ + "release/public/plannotator-artifact-server-claude-channel-$ARTIFACTSERVER_VERSION.tgz" release: name: Create GitHub release @@ -480,28 +480,28 @@ jobs: actions: write contents: write steps: - - name: Download downloadable release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-public-${{ github.run_id }}-${{ github.run_attempt }} - path: release/public - - name: Create verified GitHub release - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} - GH_TOKEN: ${{ github.token }} - run: >- - gh release create "$ARTIFACTSERVER_TAG" - release/public/* - --repo "$GITHUB_REPOSITORY" - --verify-tag - --generate-notes - --title "Artifact Server $ARTIFACTSERVER_TAG" - - name: Dispatch published-install verification - env: - ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} - GH_TOKEN: ${{ github.token }} - run: >- - gh workflow run install-verify.yml - --repo "$GITHUB_REPOSITORY" - --ref main - --field release_tag="$ARTIFACTSERVER_TAG" + - name: Download downloadable release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-public-${{ github.run_id }}-${{ github.run_attempt }} + path: release/public + - name: Create verified GitHub release + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} + GH_TOKEN: ${{ github.token }} + run: >- + gh release create "$ARTIFACTSERVER_TAG" + release/public/* + --repo "$GITHUB_REPOSITORY" + --verify-tag + --generate-notes + --title "Artifact Server $ARTIFACTSERVER_TAG" + - name: Dispatch published-install verification + env: + ARTIFACTSERVER_TAG: ${{ needs.verify.outputs.tag }} + GH_TOKEN: ${{ github.token }} + run: >- + gh workflow run install-verify.yml + --repo "$GITHUB_REPOSITORY" + --ref main + --field release_tag="$ARTIFACTSERVER_TAG" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f167e6ef..0c0196b2 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -5,7 +5,7 @@ on: branches: [main] push: tags: - - '**' + - '**' workflow_dispatch: permissions: @@ -27,114 +27,114 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - name: Checkout repository history - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false + - name: Checkout repository history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install verified Gitleaks - shell: bash - run: | - set -euo pipefail - archive="$RUNNER_TEMP/gitleaks.tar.gz" - install_dir="$RUNNER_TEMP/gitleaks-bin" - curl --fail --silent --show-error --location \ - --proto '=https' --tlsv1.2 \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - --output "$archive" - printf '%s %s\n' "$GITLEAKS_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict - mkdir -p "$install_dir" - tar -xzf "$archive" -C "$install_dir" gitleaks - "$install_dir/gitleaks" version - echo "$install_dir" >> "$GITHUB_PATH" + - name: Install verified Gitleaks + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/gitleaks.tar.gz" + install_dir="$RUNNER_TEMP/gitleaks-bin" + curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + --output "$archive" + printf '%s %s\n' "$GITLEAKS_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict + mkdir -p "$install_dir" + tar -xzf "$archive" -C "$install_dir" gitleaks + "$install_dir/gitleaks" version + echo "$install_dir" >> "$GITHUB_PATH" - - name: Verify Gitleaks detector health - shell: bash - run: | - set -euo pipefail - synthetic_prefix='glpat' - synthetic_body='abcdefghijklmnopqrst' - set +e - printf 'token = "%s-%s"\n' "$synthetic_prefix" "$synthetic_body" \ - | gitleaks stdin --config .gitleaks.toml --no-banner --redact=100 >/dev/null 2>&1 - detector_exit=$? - set -e - if [[ "$detector_exit" -ne 1 ]]; then - echo "Gitleaks failed its synthetic detection test (exit $detector_exit)." >&2 - exit 1 - fi + - name: Verify Gitleaks detector health + shell: bash + run: | + set -euo pipefail + synthetic_prefix='glpat' + synthetic_body='abcdefghijklmnopqrst' + set +e + printf 'token = "%s-%s"\n' "$synthetic_prefix" "$synthetic_body" \ + | gitleaks stdin --config .gitleaks.toml --no-banner --redact=100 >/dev/null 2>&1 + detector_exit=$? + set -e + if [[ "$detector_exit" -ne 1 ]]; then + echo "Gitleaks failed its synthetic detection test (exit $detector_exit)." >&2 + exit 1 + fi - - name: Scan complete repository history - shell: bash - run: | - set -euo pipefail - gitleaks git . --config .gitleaks.toml --log-opts='--all' --redact=100 --no-banner - { - echo '### Gitleaks' - echo '- Scope: complete repository history' - echo '- Detector health: synthetic token detected before analysis' - echo '- Result: no secret findings' - } >> "$GITHUB_STEP_SUMMARY" + - name: Scan complete repository history + shell: bash + run: | + set -euo pipefail + gitleaks git . --config .gitleaks.toml --log-opts='--all' --redact=100 --no-banner + { + echo '### Gitleaks' + echo '- Scope: complete repository history' + echo '- Detector health: synthetic token detected before analysis' + echo '- Result: no secret findings' + } >> "$GITHUB_STEP_SUMMARY" zizmor: name: zizmor runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Install verified zizmor - shell: bash - run: | - set -euo pipefail - archive="$RUNNER_TEMP/zizmor.tar.gz" - install_dir="$RUNNER_TEMP/zizmor-bin" - curl --fail --silent --show-error --location \ - --proto '=https' --tlsv1.2 \ - "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" \ - --output "$archive" - printf '%s %s\n' "$ZIZMOR_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict - mkdir -p "$install_dir" - tar -xzf "$archive" -C "$install_dir" zizmor - "$install_dir/zizmor" --version - echo "$install_dir" >> "$GITHUB_PATH" + - name: Install verified zizmor + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/zizmor.tar.gz" + install_dir="$RUNNER_TEMP/zizmor-bin" + curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" \ + --output "$archive" + printf '%s %s\n' "$ZIZMOR_LINUX_X64_SHA256" "$archive" | sha256sum --check --strict + mkdir -p "$install_dir" + tar -xzf "$archive" -C "$install_dir" zizmor + "$install_dir/zizmor" --version + echo "$install_dir" >> "$GITHUB_PATH" - - name: Verify zizmor detector health - shell: bash - run: | - set -euo pipefail - unsafe_expression='$'"{{ github.event.issue.title }}" - set +e - printf '%s\n' \ - 'name: Synthetic unsafe workflow' \ - 'on: issues' \ - 'jobs:' \ - ' test:' \ - ' runs-on: ubuntu-latest' \ - ' steps:' \ - " - run: echo \"$unsafe_expression\"" \ - | zizmor --offline --persona regular --strict-collection --format=json --no-progress --color=never - \ - > "$RUNNER_TEMP/zizmor-health.json" 2>/dev/null - detector_exit=$? - set -e - if [[ "$detector_exit" -lt 11 || "$detector_exit" -gt 14 ]] \ - || ! jq -e 'any(.ident == "template-injection")' "$RUNNER_TEMP/zizmor-health.json" >/dev/null; then - echo "zizmor failed its synthetic detection test (exit $detector_exit)." >&2 - exit 1 - fi + - name: Verify zizmor detector health + shell: bash + run: | + set -euo pipefail + unsafe_expression='$'"{{ github.event.issue.title }}" + set +e + printf '%s\n' \ + 'name: Synthetic unsafe workflow' \ + 'on: issues' \ + 'jobs:' \ + ' test:' \ + ' runs-on: ubuntu-latest' \ + ' steps:' \ + " - run: echo \"$unsafe_expression\"" \ + | zizmor --offline --persona regular --strict-collection --format=json --no-progress --color=never - \ + > "$RUNNER_TEMP/zizmor-health.json" 2>/dev/null + detector_exit=$? + set -e + if [[ "$detector_exit" -lt 11 || "$detector_exit" -gt 14 ]] \ + || ! jq -e 'any(.ident == "template-injection")' "$RUNNER_TEMP/zizmor-health.json" >/dev/null; then + echo "zizmor failed its synthetic detection test (exit $detector_exit)." >&2 + exit 1 + fi - - name: Audit GitHub configuration - shell: bash - run: | - set -euo pipefail - zizmor --offline --persona regular --strict-collection --no-progress --color=never . - { - echo '### zizmor' - echo '- Scope: GitHub workflows and Dependabot configuration' - echo '- Detector health: synthetic template injection detected before analysis' - echo '- Result: no findings' - } >> "$GITHUB_STEP_SUMMARY" + - name: Audit GitHub configuration + shell: bash + run: | + set -euo pipefail + zizmor --offline --persona regular --strict-collection --no-progress --color=never . + { + echo '### zizmor' + echo '- Scope: GitHub workflows and Dependabot configuration' + echo '- Detector health: synthetic template injection detected before analysis' + echo '- Result: no findings' + } >> "$GITHUB_STEP_SUMMARY"