From b1440477734d826232eab35950b9c8939035e3ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:37:05 +0000 Subject: [PATCH 1/4] Initial plan From 6b6433f221a36382a9351fe379bc665b1b6f35fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:53:59 +0000 Subject: [PATCH 2/4] feat: add request timeout, dist alignment check, live integration test, and release checklist for verify-artifact action Co-authored-by: chrismaz11 <24700273+chrismaz11@users.noreply.github.com> --- .github/workflows/ci.yml | 23 ++ docs/integrations/github-action.md | 35 ++- .../CONTRIBUTING.md | 55 ++++- .../trustsignal-verify-artifact/README.md | 46 ++-- .../trustsignal-verify-artifact/dist/index.js | 30 ++- .../docs/integration.md | 73 +++++-- .../docs/release-checklist.md | 76 +++++++ .../trustsignal-verify-artifact/package.json | 5 +- .../scripts/check-dist.js | 26 +++ .../scripts/integration-test.js | 204 ++++++++++++++++++ .../trustsignal-verify-artifact/src/index.js | 30 ++- 11 files changed, 534 insertions(+), 69 deletions(-) create mode 100644 github-actions/trustsignal-verify-artifact/docs/release-checklist.md create mode 100644 github-actions/trustsignal-verify-artifact/scripts/check-dist.js create mode 100644 github-actions/trustsignal-verify-artifact/scripts/integration-test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39be64f..a1b472d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,3 +210,26 @@ jobs: - name: Audit production dependencies run: npm audit --omit=dev --audit-level=high + + verify-artifact-action: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Check source syntax + working-directory: github-actions/trustsignal-verify-artifact + run: npm run check + + - name: Verify dist alignment + working-directory: github-actions/trustsignal-verify-artifact + run: npm run check:dist + + - name: Run local contract tests + working-directory: github-actions/trustsignal-verify-artifact + run: npm run test:local diff --git a/docs/integrations/github-action.md b/docs/integrations/github-action.md index 02ee084..a41304f 100644 --- a/docs/integrations/github-action.md +++ b/docs/integrations/github-action.md @@ -11,7 +11,7 @@ The GitHub Action does not connect to Supabase directly. TrustSignal persists re 1. The workflow sends an artifact hash or local artifact path through the GitHub Action. 2. The action calls `POST /api/v1/verify` on `api.trustsignal.dev`. 3. TrustSignal validates the request, authenticates the caller, issues a signed receipt, and persists the receipt server-side. -4. The action stores `receiptId` and `receiptSignature` for later verification or audit use. +4. The action writes `verification_id`, `status`, `receipt_id`, and `receipt_signature` as GitHub Actions outputs. 5. Public consumers can inspect the stored receipt through `GET /api/v1/receipt/{receiptId}` or render a compact badge from `GET /api/v1/receipt/{receiptId}/summary`. 6. A later workflow can call `POST /api/v1/receipt/{receiptId}/verify` with an artifact hash to confirm integrity. @@ -26,7 +26,7 @@ x-api-key: content-type: application/json ``` -Request body: +Request body sent by the action: ```json { @@ -48,20 +48,25 @@ Request body: } ``` -Response fields used by the action: +Response fields used by the action (both snake_case and camelCase variants are accepted): -- `verificationId` -- `receiptId` -- `receiptSignature` -- `status` +| Action output | API field(s) read | +| --- | --- | +| `verification_id` | `verification_id`, `verificationId`, `id`, `receipt_id`, `receiptId` | +| `status` | `status`, `verificationStatus`, `result`, `verified`, `valid`, `match` | +| `receipt_id` | `receipt_id`, `receiptId` | +| `receipt_signature` | `receipt_signature` (string), `receiptSignature` (string or `{ signature }` object) | + +The action request enforces a 30-second timeout. An `AbortError` from the timeout is reported +as a clean error message without exposing raw headers or internal service details. ### `GET /api/v1/receipt/{receiptId}` -This public-safe endpoint returns a compact inspection view for artifact receipts. It is intended for receipt drill-down pages and audit references. +This endpoint returns a compact inspection view for artifact receipts. It is intended for receipt drill-down pages and audit references. ### `GET /api/v1/receipt/{receiptId}/summary` -This public-safe endpoint returns a compact display payload for trust centers, evidence panels, and partner dashboards. +This endpoint returns a compact display payload for trust centers, evidence panels, and partner dashboards. ### `POST /api/v1/receipt/{receiptId}/verify` @@ -96,8 +101,18 @@ Response fields: - Row Level Security is enabled on the artifact receipt table as defense in depth. - Public lookup and summary endpoints are read-only and return safe receipt fields only. - Later verification remains behind TrustSignal API authentication. +- `fail_on_mismatch: true` (default) provides fail-closed behavior for pipelines that require verified artifacts. + +## Validation + +- Local contract tests: `npm run test:local` (uses mock fetch, no live API required) +- Dist alignment check: `npm run check:dist` (SHA-256 comparison of `src` and `dist`) +- Live integration test: `npm run test:integration` (skips when credentials are absent) + +See `github-actions/trustsignal-verify-artifact/docs/integration.md` for the full integration guide. ## Current Limitations -- The repository includes a local smoke test, but a live deployed integration test remains pending. - The public verification contract currently accepts `sha256` only. +- GitHub Marketplace publication requires extracting this action into a dedicated public repository with `action.yml` at the repository root. + diff --git a/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md b/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md index 7eddfd9..dded722 100644 --- a/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md +++ b/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md @@ -2,33 +2,68 @@ ## Local Validation -Run the lightweight validation checks before opening a change: +Run the complete validation suite before opening a change: + +```bash +npm run validate +``` + +This runs the following checks in order: + +1. **`npm run check`** — Node.js syntax check for `src/index.js` and `dist/index.js`. +2. **`npm run check:dist`** — SHA-256 comparison to confirm `dist/index.js` matches `src/index.js`. +3. **`npm run test:local`** — Local action contract test using a mock fetch (no live API required). + +Or run each step individually: ```bash node --check src/index.js node --check dist/index.js +node scripts/check-dist.js node scripts/test-local.js ``` -Or use package scripts: +## Live Integration Test + +To run the end-to-end integration test against a deployed TrustSignal API: ```bash -npm run check -npm run test:local -npm run validate:local +export TRUSTSIGNAL_INTEGRATION_API_BASE_URL=https://api.trustsignal.dev +export TRUSTSIGNAL_INTEGRATION_API_KEY= +npm run test:integration ``` +The test skips cleanly when the environment variables are not set. + ## Repository Structure - `action.yml`: GitHub Action metadata - `src/`: source implementation - `dist/`: committed runtime entrypoint for action consumers - `scripts/`: local validation helpers + - `mock-fetch.js`: fetch mock used by `test-local.js` + - `test-local.js`: local contract test (mock-based) + - `check-dist.js`: dist alignment check + - `integration-test.js`: live integration test (skips without credentials) - `docs/`: integration-facing documentation + - `integration.md`: verification flow, request/response contract, security notes + - `release-checklist.md`: pre-release and tagging checklist + +## Building + +```bash +npm run build +``` + +This copies `src/index.js` to `dist/index.js`. Run `npm run check:dist` afterwards to +confirm alignment. + +## Release + +See `docs/release-checklist.md` for the complete release process, including: -## Release Basics +- dist alignment verification +- semantic version tagging +- stable major tag maintenance +- GitHub Marketplace publication steps -- Follow semantic versioning. -- Commit updated `dist/index.js` with each release. -- Publish immutable tags such as `v0.1.0` and maintain a major tag such as `v1`. -- GitHub Marketplace publication requires a public repository with `action.yml` at the repository root. diff --git a/github-actions/trustsignal-verify-artifact/README.md b/github-actions/trustsignal-verify-artifact/README.md index 496f6ae..efe29f8 100644 --- a/github-actions/trustsignal-verify-artifact/README.md +++ b/github-actions/trustsignal-verify-artifact/README.md @@ -168,42 +168,50 @@ TrustSignal gives security and release teams a consistent way to verify artifact ## Current Limitations -- Local validation uses a fetch mock rather than a live TrustSignal deployment. - GitHub Marketplace publication requires this action to be published from a dedicated public repository root with `action.yml` at the top level. -- Live end-to-end validation against a deployed TrustSignal API should remain part of the release process. ## Local Validation -Run the lightweight validation checks with: +Run the complete validation suite with: + +```bash +npm run validate +``` + +This runs a syntax check, a dist alignment check (SHA-256 comparison of `src` and `dist`), and the local contract test. No live API is required. + +Individual commands: ```bash node --check src/index.js node --check dist/index.js +node scripts/check-dist.js node scripts/test-local.js ``` -Or use the package scripts: +## Live Integration Test + +To validate against a deployed TrustSignal API: ```bash -npm run check -npm run test:local -npm run validate:local +export TRUSTSIGNAL_INTEGRATION_API_BASE_URL=https://api.trustsignal.dev +export TRUSTSIGNAL_INTEGRATION_API_KEY= +npm run test:integration ``` +The test skips cleanly when the environment variables are not set. See `docs/integration.md` for details. + ## Versioning Guidance - Follow semantic versioning. -- Publish immutable release tags for each shipped version. -- Maintain a major tag such as `v1` for stable consumers. - -## Release Checklist - -- Commit the built `dist/index.js` artifact with every release. -- Create signed or otherwise controlled release tags according to your release process. -- Update documentation when the public API contract or output mapping changes. +- Publish immutable release tags for each shipped version (e.g., `v0.2.0`). +- Maintain a stable major tag such as `v1` for consumers who want automatic non-breaking updates. +- See `docs/release-checklist.md` for the complete release process. -## Roadmap +## Release Checklist Summary -- Add a live integration test against a deployed TrustSignal verification endpoint -- Publish the action from a dedicated public repository root -- Add example workflows for release pipelines and provenance retention patterns +- Confirm `src/index.js` changes are intentional. +- Run `npm run build` and then `npm run validate` to confirm dist alignment. +- Create an immutable version tag and update the stable major tag. +- Confirm `action.yml` references `dist/index.js` as the action entrypoint. +- Update documentation when the API contract or output field mapping changes. diff --git a/github-actions/trustsignal-verify-artifact/dist/index.js b/github-actions/trustsignal-verify-artifact/dist/index.js index 05625ea..507a2d6 100644 --- a/github-actions/trustsignal-verify-artifact/dist/index.js +++ b/github-actions/trustsignal-verify-artifact/dist/index.js @@ -200,14 +200,28 @@ async function callVerificationApi({ apiBaseUrl, apiKey, artifactHash, artifactP const endpoint = `${apiBaseUrl}/api/v1/verify`; const payload = buildVerificationRequest({ artifactHash, artifactPath, source }); - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'x-api-key': apiKey - }, - body: JSON.stringify(payload) - }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); + + let response; + try { + response = await fetch(endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': apiKey + }, + body: JSON.stringify(payload), + signal: controller.signal + }); + } catch (error) { + if (error && error.name === 'AbortError') { + throw new Error('TrustSignal API request timed out after 30 seconds'); + } + throw error; + } finally { + clearTimeout(timeout); + } const responseBody = await parseJsonResponse(response); diff --git a/github-actions/trustsignal-verify-artifact/docs/integration.md b/github-actions/trustsignal-verify-artifact/docs/integration.md index 9483297..8542264 100644 --- a/github-actions/trustsignal-verify-artifact/docs/integration.md +++ b/github-actions/trustsignal-verify-artifact/docs/integration.md @@ -34,22 +34,69 @@ } ``` -## Outputs +GitHub workflow context values (`repository`, `workflow`, `runId`, `actor`, `commit`) are read from +standard GitHub Actions environment variables and included automatically when they are set. -- `verification_id` -- `status` -- `receipt_id` -- `receipt_signature` +## Response Contract -If the API omits a distinct verification identifier, the action uses `receipt_id` as a compatibility alias for `verification_id`. +The action reads the following fields from the API response. Both snake_case and camelCase variants +are accepted for compatibility: -## Current Limitations +| Output field | API field(s) read | +| --- | --- | +| `verification_id` | `verification_id`, `verificationId`, `id`, `receipt_id`, `receiptId` (first non-empty) | +| `status` | `status`, `verificationStatus`, `result`, `verified`, `valid`, `match` | +| `receipt_id` | `receipt_id`, `receiptId` | +| `receipt_signature` | `receipt_signature` (string), `receiptSignature` (string or `{ signature }` object) | -- The included test path uses a local fetch mock rather than a live TrustSignal deployment. -- Marketplace publication still requires extraction into a dedicated public repository. +If the API omits a distinct verification identifier, the action uses `receipt_id` as a compatibility +alias for `verification_id`. -## Next Steps +## Security Behavior + +- The API key is transmitted only in the `x-api-key` request header. It is never logged. +- Error messages include the HTTP status code and the API-provided message only. + Raw headers and internal service details are not surfaced. +- `callVerificationApi` enforces a 30-second AbortController timeout to prevent hangs. +- `fail_on_mismatch: true` (the default) causes the action to exit non-zero when the + TrustSignal response does not indicate a valid verification result. This provides + fail-closed behavior for pipelines that require verified artifacts. + +## Local Validation + +```bash +# Syntax check +node --check src/index.js && node --check dist/index.js + +# Dist alignment check (confirms dist matches src by SHA-256) +node scripts/check-dist.js + +# Local contract test (uses mock fetch — no live API required) +node scripts/test-local.js + +# Full local validation +npm run validate +``` + +## Live Integration Test + +To validate against a deployed TrustSignal API endpoint: + +```bash +export TRUSTSIGNAL_INTEGRATION_API_BASE_URL=https://api.trustsignal.dev +export TRUSTSIGNAL_INTEGRATION_API_KEY= +node scripts/integration-test.js +``` + +The integration test: +- Verifies a test artifact by file path and by precomputed hash. +- Confirms all four output fields are present in the response. +- Confirms that an invalid API key is rejected with a non-zero exit. +- Skips cleanly when the environment variables are not set. + +## Notes + +- Marketplace publication requires extracting this action into a dedicated public repository + with `action.yml` at the repository root. +- See `docs/release-checklist.md` for the complete pre-release and tagging checklist. -- Add a live integration test against a deployed TrustSignal API environment. -- Publish semantic version tags and maintain a stable major tag. -- Move this package to the repository root of a dedicated public action repository. diff --git a/github-actions/trustsignal-verify-artifact/docs/release-checklist.md b/github-actions/trustsignal-verify-artifact/docs/release-checklist.md new file mode 100644 index 0000000..32d17e4 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/docs/release-checklist.md @@ -0,0 +1,76 @@ +# Release Checklist + +This checklist must be completed before every version tag is pushed for `TrustSignal Verify Artifact`. + +--- + +## Pre-Release: Source and Dist Alignment + +- [ ] `src/index.js` reflects all intended changes for this release. +- [ ] `npm run build` was run to copy `src/index.js` to `dist/index.js`. +- [ ] `npm run check:dist` passes — confirms `dist/index.js` matches `src/index.js` by SHA-256. +- [ ] `npm run check` passes — no syntax errors in source or dist. + +## Pre-Release: Local Contract Validation + +- [ ] `npm run test:local` passes — all local action contract assertions succeed. +- [ ] Mock response fields (`verification_id`, `status`, `receipt_id`, `receipt_signature`) are still exercised. + +## Pre-Release: Integration Validation (when credentials available) + +- [ ] `npm run test:integration` passes against a deployed TrustSignal API endpoint. + - Set `TRUSTSIGNAL_INTEGRATION_API_BASE_URL` and `TRUSTSIGNAL_INTEGRATION_API_KEY` before running. + - Confirms live end-to-end output field contract. + - Confirms invalid API key is rejected with a non-zero exit. + +## Pre-Release: Documentation + +- [ ] `README.md` reflects the current input and output contract. +- [ ] `action.yml` output descriptions are accurate. +- [ ] `docs/integration.md` reflects any API contract changes. +- [ ] `CHANGELOG.md` has an entry for this release (if maintained). + +## Pre-Release: Security + +- [ ] No secrets, API keys, or tokens are hardcoded in source or dist. +- [ ] Error messages do not leak raw API responses, headers, or internal service details. +- [ ] `fail_on_mismatch` default remains `true` (fail-closed behavior preserved). +- [ ] Request timeout is present in `callVerificationApi` (AbortController). + +## Release Tag + +- [ ] Create an immutable semver tag, for example `v0.2.0`. + ```bash + git tag -a v0.2.0 -m "Release v0.2.0" + git push origin v0.2.0 + ``` +- [ ] Update the stable major tag to point to this release. + ```bash + git tag -f v1 + git push origin v1 --force + ``` +- [ ] Confirm the tag points to the correct commit. + ```bash + git show v0.2.0 --stat | head -5 + ``` + +## Post-Release: Verification + +- [ ] Confirm `dist/index.js` in the tagged commit is the intended entrypoint. +- [ ] Confirm `action.yml` in the tag references `dist/index.js` as `main`. +- [ ] Smoke-test the release tag in a sample workflow using `uses: trustsignal-dev/trustsignal-verify-artifact@v1`. + +## Marketplace Publication (when applicable) + +GitHub Marketplace publication requires the action repository to have `action.yml` at the repository root. +The current structure nests this action inside a monorepo. Steps for marketplace publication: + +1. Extract `github-actions/trustsignal-verify-artifact/` into a dedicated public repository. +2. Place `action.yml`, `dist/index.js`, `README.md`, and `LICENSE` at the repository root. +3. Push a version tag to the public repository. +4. Use GitHub's **Draft a release** flow to publish to the Marketplace. +5. Link the Marketplace listing from this monorepo's documentation. + +--- + +_See `CONTRIBUTING.md` for local validation commands._ diff --git a/github-actions/trustsignal-verify-artifact/package.json b/github-actions/trustsignal-verify-artifact/package.json index f2b1d7e..bf1db07 100644 --- a/github-actions/trustsignal-verify-artifact/package.json +++ b/github-actions/trustsignal-verify-artifact/package.json @@ -14,8 +14,11 @@ "build": "mkdir -p dist && cp src/index.js dist/index.js", "package": "npm run build", "check": "node --check src/index.js && node --check dist/index.js", + "check:dist": "node scripts/check-dist.js", "test:local": "node scripts/test-local.js", - "validate:local": "npm run check && npm run test:local" + "test:integration": "node scripts/integration-test.js", + "validate:local": "npm run check && npm run check:dist && npm run test:local", + "validate": "npm run check && npm run check:dist && npm run test:local" }, "keywords": [ "github-action", diff --git a/github-actions/trustsignal-verify-artifact/scripts/check-dist.js b/github-actions/trustsignal-verify-artifact/scripts/check-dist.js new file mode 100644 index 0000000..6c34285 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/scripts/check-dist.js @@ -0,0 +1,26 @@ +// Verifies that dist/index.js is in sync with src/index.js. +// Run as part of CI and before every release to confirm the committed dist +// entrypoint reflects the current source. + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +function sha256File(relPath) { + const absPath = path.resolve(__dirname, '..', relPath); + const content = fs.readFileSync(absPath); + return crypto.createHash('sha256').update(content).digest('hex'); +} + +const srcHash = sha256File('src/index.js'); +const distHash = sha256File('dist/index.js'); + +if (srcHash !== distHash) { + process.stderr.write('dist/index.js is out of sync with src/index.js\n'); + process.stderr.write(` src: ${srcHash}\n`); + process.stderr.write(` dist: ${distHash}\n`); + process.stderr.write('Run: npm run build\n'); + process.exit(1); +} + +process.stdout.write('dist/index.js is in sync with src/index.js\n'); diff --git a/github-actions/trustsignal-verify-artifact/scripts/integration-test.js b/github-actions/trustsignal-verify-artifact/scripts/integration-test.js new file mode 100644 index 0000000..4f59b12 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/scripts/integration-test.js @@ -0,0 +1,204 @@ +// Live integration test against a deployed TrustSignal verification endpoint. +// +// Required environment variables (test is skipped when absent): +// TRUSTSIGNAL_INTEGRATION_API_BASE_URL — e.g. https://api.trustsignal.dev +// TRUSTSIGNAL_INTEGRATION_API_KEY — scoped API key with verify scope +// +// Optional: +// TRUSTSIGNAL_INTEGRATION_ARTIFACT_PATH — path to a local artifact to hash +// (defaults to a temp file created by the test) +// +// Usage: +// node scripts/integration-test.js +// npm run test:integration + +'use strict'; + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const apiBaseUrl = process.env.TRUSTSIGNAL_INTEGRATION_API_BASE_URL; +const apiKey = process.env.TRUSTSIGNAL_INTEGRATION_API_KEY; + +if (!apiBaseUrl || !apiKey) { + process.stdout.write( + 'Integration test skipped: TRUSTSIGNAL_INTEGRATION_API_BASE_URL and ' + + 'TRUSTSIGNAL_INTEGRATION_API_KEY are not set.\n' + ); + process.exit(0); +} + +function sha256(content) { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +function readOutputs(filePath) { + if (!fs.existsSync(filePath)) return {}; + const raw = fs.readFileSync(filePath, 'utf8'); + return Object.fromEntries( + raw + .trim() + .split('\n') + .filter(Boolean) + .map((line) => { + const idx = line.indexOf('='); + return [line.slice(0, idx), line.slice(idx + 1)]; + }) + ); +} + +function runAction({ artifactPath, artifactHash, failOnMismatch = true } = {}) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustsignal-integration-')); + const outputPath = path.join(tempDir, 'github-output.txt'); + + const env = { + ...process.env, + INPUT_API_BASE_URL: apiBaseUrl, + INPUT_API_KEY: apiKey, + INPUT_SOURCE: 'integration-test', + INPUT_FAIL_ON_MISMATCH: String(failOnMismatch), + GITHUB_OUTPUT: outputPath, + GITHUB_RUN_ID: 'integration-test-001', + GITHUB_REPOSITORY: 'trustsignal-dev/trustsignal-verify-artifact', + GITHUB_WORKFLOW: 'Integration Test', + GITHUB_ACTOR: 'integration-test-runner', + GITHUB_SHA: sha256('integration-test') + }; + + if (artifactPath) { + env.INPUT_ARTIFACT_PATH = artifactPath; + } + if (artifactHash) { + env.INPUT_ARTIFACT_HASH = artifactHash; + } + + // Run dist/index.js without any fetch mock — this is a real HTTP call. + const result = spawnSync(process.execPath, ['dist/index.js'], { + cwd: path.resolve(__dirname, '..'), + env, + encoding: 'utf8', + timeout: 60_000 + }); + + const outputs = readOutputs(outputPath); + + return { + status: result.status, + stdout: result.stdout || '', + stderr: result.stderr || '', + outputs + }; +} + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function assertNonEmpty(value, name) { + assert(typeof value === 'string' && value.length > 0, `${name} must be a non-empty string`); +} + +function main() { + process.stdout.write(`Integration test running against: ${apiBaseUrl}\n`); + + // Create a deterministic test artifact so the test is repeatable. + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustsignal-artifact-')); + const artifactContent = `trustsignal-integration-test-artifact ${Date.now()}`; + const artifactPath = path.join(tempDir, 'artifact.txt'); + fs.writeFileSync(artifactPath, artifactContent, 'utf8'); + + // ── Test 1: verify by file path ────────────────────────────────────────── + process.stdout.write(' Test 1: verify by artifact_path ... '); + const pathRun = runAction({ artifactPath, failOnMismatch: false }); + + if (pathRun.status !== 0) { + process.stderr.write(`FAIL (exit ${pathRun.status})\n${pathRun.stderr}\n`); + process.exit(1); + } + + assertNonEmpty( + pathRun.outputs.verification_id || pathRun.outputs.receipt_id, + 'verification_id or receipt_id' + ); + assertNonEmpty(pathRun.outputs.status, 'status'); + + process.stdout.write(`OK (status=${pathRun.outputs.status})\n`); + + // ── Test 2: verify by precomputed hash ─────────────────────────────────── + process.stdout.write(' Test 2: verify by artifact_hash ... '); + const artifactHash = sha256(artifactContent); + const hashRun = runAction({ artifactHash, failOnMismatch: false }); + + if (hashRun.status !== 0) { + process.stderr.write(`FAIL (exit ${hashRun.status})\n${hashRun.stderr}\n`); + process.exit(1); + } + + assertNonEmpty( + hashRun.outputs.verification_id || hashRun.outputs.receipt_id, + 'verification_id or receipt_id' + ); + assertNonEmpty(hashRun.outputs.status, 'status'); + + process.stdout.write(`OK (status=${hashRun.outputs.status})\n`); + + // ── Test 3: invalid API key returns failure ─────────────────────────────── + process.stdout.write(' Test 3: invalid API key is rejected ... '); + const savedKey = process.env.TRUSTSIGNAL_INTEGRATION_API_KEY; + // Temporarily override via the INPUT env var used by the action + const badKeyEnv = { + ...process.env, + INPUT_API_BASE_URL: apiBaseUrl, + INPUT_API_KEY: 'INVALID_KEY_FOR_TEST', + INPUT_ARTIFACT_PATH: artifactPath, + INPUT_SOURCE: 'integration-test', + INPUT_FAIL_ON_MISMATCH: 'false' + }; + const badKeyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustsignal-badkey-')); + const badKeyOutput = path.join(badKeyDir, 'github-output.txt'); + badKeyEnv.GITHUB_OUTPUT = badKeyOutput; + const badKeyRun = spawnSync(process.execPath, ['dist/index.js'], { + cwd: path.resolve(__dirname, '..'), + env: badKeyEnv, + encoding: 'utf8', + timeout: 60_000 + }); + + assert( + badKeyRun.status !== 0 || badKeyRun.stderr.includes('::error::'), + 'expected action to fail or emit error on invalid API key' + ); + process.stdout.write('OK\n'); + + // ── Test 4: output field contract ──────────────────────────────────────── + process.stdout.write(' Test 4: output field contract ... '); + // verification_id is set (may be same as receipt_id per compat alias) + assert( + typeof pathRun.outputs.verification_id === 'string', + 'verification_id output must be present' + ); + assert(typeof pathRun.outputs.status === 'string', 'status output must be present'); + assert(typeof pathRun.outputs.receipt_id === 'string', 'receipt_id output must be present'); + assert( + typeof pathRun.outputs.receipt_signature === 'string', + 'receipt_signature output must be present' + ); + process.stdout.write('OK\n'); + + process.stdout.write('\nAll integration tests passed.\n'); + process.stdout.write(` verification_id: ${pathRun.outputs.verification_id}\n`); + process.stdout.write(` receipt_id: ${pathRun.outputs.receipt_id}\n`); + process.stdout.write(` status: ${pathRun.outputs.status}\n`); +} + +try { + main(); +} catch (error) { + process.stderr.write(`Integration test error: ${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +} diff --git a/github-actions/trustsignal-verify-artifact/src/index.js b/github-actions/trustsignal-verify-artifact/src/index.js index 05625ea..507a2d6 100644 --- a/github-actions/trustsignal-verify-artifact/src/index.js +++ b/github-actions/trustsignal-verify-artifact/src/index.js @@ -200,14 +200,28 @@ async function callVerificationApi({ apiBaseUrl, apiKey, artifactHash, artifactP const endpoint = `${apiBaseUrl}/api/v1/verify`; const payload = buildVerificationRequest({ artifactHash, artifactPath, source }); - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'x-api-key': apiKey - }, - body: JSON.stringify(payload) - }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); + + let response; + try { + response = await fetch(endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': apiKey + }, + body: JSON.stringify(payload), + signal: controller.signal + }); + } catch (error) { + if (error && error.name === 'AbortError') { + throw new Error('TrustSignal API request timed out after 30 seconds'); + } + throw error; + } finally { + clearTimeout(timeout); + } const responseBody = await parseJsonResponse(response); From c1c52302e21b5e8a66957978d7ad94340262edcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:58:31 +0000 Subject: [PATCH 3/4] fix: address code review comments and add explicit permissions to CI job Co-authored-by: chrismaz11 <24700273+chrismaz11@users.noreply.github.com> --- .github/workflows/ci.yml | 2 ++ .../trustsignal-verify-artifact/scripts/integration-test.js | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1b472d..2ca63a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,6 +213,8 @@ jobs: verify-artifact-action: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v6 diff --git a/github-actions/trustsignal-verify-artifact/scripts/integration-test.js b/github-actions/trustsignal-verify-artifact/scripts/integration-test.js index 4f59b12..2518007 100644 --- a/github-actions/trustsignal-verify-artifact/scripts/integration-test.js +++ b/github-actions/trustsignal-verify-artifact/scripts/integration-test.js @@ -65,7 +65,8 @@ function runAction({ artifactPath, artifactHash, failOnMismatch = true } = {}) { GITHUB_REPOSITORY: 'trustsignal-dev/trustsignal-verify-artifact', GITHUB_WORKFLOW: 'Integration Test', GITHUB_ACTOR: 'integration-test-runner', - GITHUB_SHA: sha256('integration-test') + // Use a realistic-looking 40-character hex string for the commit SHA context field. + GITHUB_SHA: sha256('integration-test').slice(0, 40) }; if (artifactPath) { @@ -106,7 +107,7 @@ function assertNonEmpty(value, name) { function main() { process.stdout.write(`Integration test running against: ${apiBaseUrl}\n`); - // Create a deterministic test artifact so the test is repeatable. + // Create a unique test artifact per run to avoid stale-cache edge cases on the API side. const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustsignal-artifact-')); const artifactContent = `trustsignal-integration-test-artifact ${Date.now()}`; const artifactPath = path.join(tempDir, 'artifact.txt'); From 93acb1682dfda930fce6582a6c99d3f55004df49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:26:07 +0000 Subject: [PATCH 4/4] feat: add complete TrustSignal Verify Artifact integration workflow (main.yml) Co-authored-by: chrismaz11 <24700273+chrismaz11@users.noreply.github.com> --- .github/workflows/main.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d9392a4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,37 @@ +name: TrustSignal Verify Artifact + +on: + workflow_dispatch: + push: + branches: ["master"] + +jobs: + verify-artifact: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build release artifact + run: | + mkdir -p dist + echo "release-$(git rev-parse --short HEAD)" > dist/release.txt + + - name: Verify artifact with TrustSignal + id: trustsignal + uses: ./github-actions/trustsignal-verify-artifact + with: + api_base_url: ${{ secrets.TRUSTSIGNAL_API_BASE_URL }} + api_key: ${{ secrets.TRUSTSIGNAL_API_KEY }} + artifact_path: dist/release.txt + source: github-actions + fail_on_mismatch: "true" + + - name: Record verification outputs + run: | + echo "Verification ID: ${{ steps.trustsignal.outputs.verification_id }}" + echo "Status: ${{ steps.trustsignal.outputs.status }}" + echo "Receipt ID: ${{ steps.trustsignal.outputs.receipt_id }}"