Release with Attestations #186
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release with Attestations | |
| # ---------------------------------------------------------------------------- | |
| # Architecture (PR: workflow performance overhaul — May 2026) | |
| # ---------------------------------------------------------------------------- | |
| # Previous design ran a single monolithic "Prepare Release" job (~41 min) that | |
| # built the app, then ran unit tests, then E2E tests sequentially — followed | |
| # by a "Build Release Package" job that REBUILT the app from scratch (~10 min) | |
| # and a "Create Release and Deploy" job that built+tested ONCE MORE for npm | |
| # publish. Total wall-clock: ~68 min. | |
| # | |
| # This version splits the workflow into parallel jobs, each ≤ 10 min: | |
| # | |
| # prepare → version + commit/tag (no build/test) | |
| # ├── build → npm run build (single source of truth for dist) | |
| # ├── unit-tests → vitest + coverage (parallel) | |
| # └── typedoc → API docs (parallel) | |
| # ├── e2e (matrix) → cypress shards (parallel; needs build) | |
| # ├── package → SBOM + attestations (needs build) | |
| # │ └── release → GitHub Release (needs prepare, build, package, unit-tests, e2e) | |
| # ├── deploy-docs → docs/ → GitHub Pages (needs build, tests, typedoc) | |
| # └── npm-publish → build:lib + publish (runs LAST; only if every | |
| # upstream job succeeded) | |
| # | |
| # Wall-clock target: ~25 min critical path; every individual job ≤ 10 min. | |
| # ---------------------------------------------------------------------------- | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to release (vX.Y.Z format)" | |
| required: true | |
| default: "v1.0.0" | |
| prerelease: | |
| description: "Is this a pre-release?" | |
| type: boolean | |
| default: false | |
| push: | |
| tags: | |
| - "v*" | |
| # Restrict top-level permissions to minimum required defaults | |
| permissions: read-all | |
| jobs: | |
| # -------------------------------------------------------------------------- | |
| # 1. prepare — version + commit/tag only (≤2 min) | |
| # -------------------------------------------------------------------------- | |
| prepare: | |
| name: Prepare Release | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: write # Required for git auto-commit | |
| outputs: | |
| version: ${{ steps.get-version.outputs.version }} | |
| npm_version: ${{ steps.get-version.outputs.npm_version }} | |
| is_prerelease: ${{ github.event.inputs.prerelease || 'false' }} | |
| # Commit SHA to check out in downstream jobs — picks up the version-bump | |
| # commit when triggered via workflow_dispatch. | |
| release_sha: ${{ steps.resolve-sha.outputs.sha }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| # No fetch-depth: 0 needed for the version bump — saves ~2 min. | |
| - name: Get version | |
| id: get-version | |
| run: | | |
| if [[ "${{ github.event_name }}" == "push" ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| VERSION=${{ github.event.inputs.version }} | |
| fi | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| NPM_VERSION="${VERSION#v}" | |
| echo "npm_version=${NPM_VERSION}" >> $GITHUB_OUTPUT | |
| echo "Version: ${VERSION}" | |
| echo "npm version: ${NPM_VERSION}" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Set Version for release | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| PLAIN_VERSION="${{ github.event.inputs.version }}" | |
| PLAIN_VERSION="${PLAIN_VERSION#v}" | |
| npm version $PLAIN_VERSION --no-git-tag-version | |
| - uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 | |
| if: github.event_name == 'workflow_dispatch' | |
| with: | |
| commit_message: "chore(release): bump version to ${{ github.event.inputs.version }}" | |
| tagging_message: "${{ github.event.inputs.version }}" | |
| - name: Resolve release SHA | |
| id: resolve-sha | |
| run: | | |
| SHA=$(git rev-parse HEAD) | |
| echo "sha=${SHA}" >> $GITHUB_OUTPUT | |
| echo "Release SHA: ${SHA}" | |
| # -------------------------------------------------------------------------- | |
| # 2. build — single canonical `npm run build` (≤12 min cap, target ≤10) | |
| # Uploads `dist/` + the release zip + checksum as artifacts. ALL | |
| # downstream jobs consume these instead of rebuilding. | |
| # -------------------------------------------------------------------------- | |
| build: | |
| name: Build Application | |
| needs: [prepare] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build application | |
| run: npm run build | |
| - name: Create release artifacts | |
| run: | | |
| echo "📦 Creating release zip + checksum from dist/..." | |
| cd dist | |
| zip -r ../riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip . | |
| cd .. | |
| sha256sum riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip \ | |
| > riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.sha256 | |
| echo "✅ Release artifacts created" | |
| - name: Upload dist artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 1 | |
| if-no-files-found: error | |
| - name: Upload release zip | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-zip | |
| path: | | |
| riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip | |
| riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.sha256 | |
| retention-days: 7 | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # 3. unit-tests — vitest + coverage (parallel with build, ≤8 min) | |
| # -------------------------------------------------------------------------- | |
| unit-tests: | |
| name: Unit Tests | |
| needs: [prepare] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run unit tests with coverage | |
| # Coverage `reportsDirectory`, coverage reporters, and test | |
| # reporter `outputFile`s are all configured in vitest.config.js | |
| # (see test.coverage.reportsDirectory / test.reporters). Do not | |
| # re-specify them on the CLI — passing `--reporter` here would | |
| # replace the configured reporter array, silently dropping the | |
| # html and json-with-outputFile reporters. | |
| # | |
| # `coverage.reportOnFailure: true` in vitest.config.js ensures | |
| # `builds/coverage/` survives a failing run; without it Vitest | |
| # calls `cleanAfterRun()` on failure and wipes the directory. | |
| # | |
| # NODE_OPTIONS: v8 coverage with `all: true` across 500+ source | |
| # files requires extra heap; without this the process is OOM-killed | |
| # mid-report-generation (exits 0 but writes nothing to builds/). | |
| env: | |
| NODE_OPTIONS: "--max-old-space-size=4096" | |
| run: | | |
| mkdir -p builds/test-results builds/coverage | |
| set -o pipefail | |
| npx vitest run --coverage 2>&1 | tee builds/test-results/vitest-console.log | |
| # Defense-in-depth: fail attribution stays on Unit Tests rather | |
| # than the downstream deploy-docs job ("Artifact not found"). | |
| if [ -z "$(find builds -type f -print -quit)" ]; then | |
| echo "::error::No unit-test or coverage files found under builds/ — check vitest/coverage configuration." | |
| exit 1 | |
| fi | |
| - name: Upload coverage + test reports | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: unit-test-reports | |
| path: builds/ | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| # -------------------------------------------------------------------------- | |
| # 4. typedoc — API docs (parallel with build, ≤5 min) | |
| # -------------------------------------------------------------------------- | |
| typedoc: | |
| name: Generate TypeDoc | |
| needs: [prepare] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Generate API documentation | |
| run: npm run typedoc | |
| - name: Upload typedoc artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: typedoc | |
| path: api/ | |
| retention-days: 7 | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # 5. e2e — Cypress, sharded across spec files (parallel; ≤10 min/shard) | |
| # Consumes the pre-built dist artifact, so there is no inner `npm run | |
| # build` (the previous `npm run e2e` script did one redundantly). | |
| # -------------------------------------------------------------------------- | |
| e2e: | |
| name: E2E (${{ matrix.shard.name }}) | |
| needs: [prepare, build] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false # All shards report, even if one fails | |
| matrix: | |
| shard: | |
| - name: critical | |
| specs: "cypress/e2e/homepage.cy.js,cypress/e2e/accessibility.cy.js,cypress/e2e/sitemap.cy.js" | |
| - name: dashboards | |
| specs: "cypress/e2e/dashboard-page.cy.js,cypress/e2e/dashboards.cy.js,cypress/e2e/dashboards-no-scroll.cy.js" | |
| - name: dashboards-extra | |
| specs: "cypress/e2e/all-dashboards.cy.js,cypress/e2e/politician-dashboard.cy.js" | |
| - name: news | |
| specs: "cypress/e2e/news-page.cy.js,cypress/e2e/news-articles.cy.js" | |
| - name: i18n | |
| specs: "cypress/e2e/multi-language-sanity.cy.js" | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Download dist artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Run Cypress (${{ matrix.shard.name }} shard) | |
| # Mirror the original `npm run e2e` semantics exactly: | |
| # start-server-and-test preview http://localhost:4173 cypress:run | |
| # where cypress:run = `cypress run --config video=false` (default | |
| # Electron browser). We capture stdout via `tee` so the existing | |
| # `scripts/generate-e2e-results-html.cjs` can rebuild the | |
| # docs/cypress/index.html report from the log. | |
| shell: bash | |
| env: | |
| NODE_OPTIONS: "--max-old-space-size=8192" | |
| CYPRESS_VIDEO: "false" | |
| run: | | |
| mkdir -p builds/cypress | |
| set -o pipefail | |
| npx start-server-and-test preview http://localhost:4173 \ | |
| "npx cypress run --config video=false --spec '${{ matrix.shard.specs }}'" \ | |
| 2>&1 | tee "builds/cypress/e2e-output-${{ matrix.shard.name }}.log" | |
| TEST_EXIT_CODE=${PIPESTATUS[0]} | |
| # Per-shard screenshot snapshot (only present on failure) | |
| if [ -d cypress/screenshots ]; then | |
| mkdir -p "builds/cypress/screenshots" | |
| cp -r cypress/screenshots/. "builds/cypress/screenshots/" || true | |
| fi | |
| exit ${TEST_EXIT_CODE} | |
| # Upload per-shard cypress artifacts (log + any screenshots). The | |
| # deploy-docs job downloads ALL shards and merges them into a single | |
| # builds/cypress/ so the existing report generator + screenshot | |
| # gallery work identically to the pre-refactor pipeline. | |
| - name: Upload cypress artifacts | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cypress-artifacts-${{ matrix.shard.name }} | |
| path: builds/cypress/ | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| # -------------------------------------------------------------------------- | |
| # 6. package — SBOM + SLSA attestations (≤5 min) | |
| # Consumes the release-zip artifact built by `build`, so no rebuild. | |
| # -------------------------------------------------------------------------- | |
| package: | |
| name: Build Release Package | |
| needs: [prepare, build] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| id-token: write # Required for OIDC | |
| attestations: write # Required for SBOM + build attestations | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Download release zip | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-zip | |
| path: . | |
| - name: Generate SBOM | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| id: sbom | |
| with: | |
| format: spdx-json | |
| output-file: riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json | |
| artifact-name: riksdagsmonitor-${{ needs.prepare.outputs.version }} | |
| - name: Generate artifact attestation | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| id: attest | |
| with: | |
| subject-path: riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip | |
| - name: Copy artifact attestation for zip | |
| run: cp ${{ steps.attest.outputs.bundle-path }} riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| - name: Generate SBOM attestation | |
| id: attestsbom | |
| uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 | |
| with: | |
| subject-path: riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip | |
| sbom-path: riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json | |
| - name: Copy SBOM attestation for zip | |
| run: cp ${{ steps.attestsbom.outputs.bundle-path }} riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json.intoto.jsonl | |
| - name: Upload security artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: security-artifacts | |
| path: | | |
| riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json | |
| riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json.intoto.jsonl | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # 7. deploy-docs — assemble docs/ and push to GitHub Pages (≤8 min) | |
| # Depends on all reports being ready (typedoc + tests + e2e). | |
| # -------------------------------------------------------------------------- | |
| deploy-docs: | |
| name: Deploy Documentation | |
| needs: [prepare, build, unit-tests, typedoc, e2e] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: write # Required for github-pages-deploy-action | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Download unit-test reports | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: unit-test-reports | |
| path: builds/ | |
| # Download EVERY e2e shard artifact and merge into builds/cypress/. | |
| # `merge-multiple: true` flattens cypress-artifacts-critical/..., | |
| # cypress-artifacts-news/... etc. into the same builds/cypress/ | |
| # tree, so per-shard logs + screenshots all land in one folder. | |
| - name: Download e2e (cypress) artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: cypress-artifacts-* | |
| path: builds/cypress/ | |
| merge-multiple: true | |
| # The HTML generator (scripts/generate-e2e-results-html.cjs) reads a | |
| # single `e2e-output.log`. Concatenate the per-shard logs in a | |
| # deterministic order so the rendered report covers every shard. | |
| - name: Aggregate cypress shard logs | |
| shell: bash | |
| run: | | |
| mkdir -p builds/cypress | |
| if compgen -G "builds/cypress/e2e-output-*.log" > /dev/null; then | |
| for f in $(ls builds/cypress/e2e-output-*.log | sort); do | |
| echo "===== $(basename "$f") =====" | |
| cat "$f" | |
| echo | |
| done > builds/cypress/e2e-output.log | |
| echo "✅ Aggregated $(ls builds/cypress/e2e-output-*.log | wc -l) shard log(s) → builds/cypress/e2e-output.log" | |
| else | |
| echo "⚠️ No shard logs found — index.html will be a placeholder." | |
| fi | |
| - name: Download TypeDoc | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: typedoc | |
| path: api/ | |
| - name: Clean old docs | |
| run: | | |
| rm -rf docs/api/* docs/cypress/* docs/test-results/* docs/coverage/* docs/dependencies/* || true | |
| - name: Copy API documentation to docs | |
| run: | | |
| mkdir -p docs/api | |
| if [ -d "api" ]; then | |
| cp -r api/* docs/api/ || true | |
| fi | |
| - name: Generate dependencies documentation | |
| run: npm run docs:dependencies | |
| - name: Copy test reports to docs | |
| run: | | |
| npm run build:test-reports | |
| node scripts/generate-e2e-results-html.cjs || echo "⚠️ Failed to generate E2E test results HTML" | |
| node scripts/generate-dependency-report-html.cjs || echo "⚠️ Failed to generate dependency report HTML" | |
| - name: Create documentation index | |
| run: | | |
| cat > docs/index.html << 'DOCSEOF' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Riksdagsmonitor Documentation Hub</title> | |
| <style> | |
| :root{--bg:#0a0e27;--bg2:#1a1e3d;--cyan:#00d9ff;--green:#00ff88;--red:#ff006e;--yellow:#ffbe0b;--text:#e0e0e0;--muted:#808080;--card:rgba(26,30,61,.6);--border:rgba(0,217,255,.3)} | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;line-height:1.6;color:var(--text);background:linear-gradient(135deg,var(--bg) 0%,var(--bg2) 100%);min-height:100vh;padding:2rem} | |
| .container{max-width:1200px;margin:0 auto} | |
| h1{color:var(--cyan);font-size:2.5rem;margin-bottom:.5rem;text-shadow:0 0 10px rgba(0,217,255,.5)} | |
| .subtitle{color:var(--red);font-size:1.2rem;margin-bottom:1rem} | |
| .release-info{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:1rem 1.5rem;margin-bottom:2rem;font-size:.9rem;color:var(--muted)} | |
| .release-info strong{color:var(--cyan)} | |
| h2{color:var(--yellow);font-size:1.3rem;margin:2rem 0 1rem;padding-bottom:.5rem;border-bottom:1px solid rgba(0,217,255,.15)} | |
| .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.5rem;margin-bottom:2rem} | |
| .card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:1.5rem;transition:all .3s ease} | |
| .card:hover{border-color:var(--cyan);box-shadow:0 0 20px rgba(0,217,255,.4);transform:translateY(-4px)} | |
| .card h3{color:var(--yellow);font-size:1.2rem;margin-bottom:.75rem} | |
| .card p{margin-bottom:1rem;color:#b0b0b0;font-size:.9rem} | |
| .card .links{display:flex;flex-wrap:wrap;gap:.5rem} | |
| .card .links a{display:inline-block;padding:.5rem 1rem;background:linear-gradient(135deg,#00d9ff 0%,#0099cc 100%);color:#0a0e27;text-decoration:none;border-radius:4px;font-weight:700;font-size:.85rem;transition:all .3s ease} | |
| .card .links a:hover{background:linear-gradient(135deg,#00ffff 0%,#00d9ff 100%);box-shadow:0 0 10px rgba(0,217,255,.6)} | |
| .card .links a.secondary{background:rgba(0,217,255,.1);color:var(--cyan);border:1px solid var(--border)} | |
| .card .links a.secondary:hover{background:rgba(0,217,255,.2)} | |
| .footer{text-align:center;color:var(--muted);margin-top:3rem;padding-top:2rem;border-top:1px solid rgba(0,217,255,.2);font-size:.85rem} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>📚 Riksdagsmonitor Documentation Hub</h1> | |
| <p class="subtitle">Swedish Parliament Intelligence Platform — Technical Documentation</p> | |
| <div class="release-info"> | |
| 📅 Generated during release workflow · <strong>Riksdagsmonitor</strong> by Hack23 AB | |
| </div> | |
| <h2>📖 API & Architecture</h2> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>📖 API Documentation</h3> | |
| <p>Comprehensive TypeDoc-generated API reference for all TypeScript modules, dashboard components, and utility functions.</p> | |
| <div class="links"> | |
| <a href="api/index.html">View API Docs →</a> | |
| </div> | |
| </div> | |
| </div> | |
| <h2>🧪 Testing & Quality</h2> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>🧪 Unit Test Results</h3> | |
| <p>Interactive Vitest HTML report with test tree, filtering, duration breakdown, and re-run capability (built-in @vitest/ui reporter).</p> | |
| <div class="links"> | |
| <a href="test-results/html/index.html">Vitest HTML Report →</a> | |
| <a href="test-results/vitest-results.json" class="secondary">JSON</a> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>🎭 E2E Test Results</h3> | |
| <p>Cypress end-to-end test reports with spec-level breakdown, timing per spec, screenshot gallery, and searchable log.</p> | |
| <div class="links"> | |
| <a href="cypress/index.html">View E2E Reports →</a> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>📊 Test Coverage</h3> | |
| <p>Istanbul HTML coverage report with per-file line/branch/function/statement metrics, source annotation, and directory breakdown (built-in @vitest/coverage-v8).</p> | |
| <div class="links"> | |
| <a href="coverage/index.html">Istanbul Report →</a> | |
| <a href="coverage/lcov-report/index.html" class="secondary">LCOV</a> | |
| <a href="coverage/coverage-final.json" class="secondary">JSON</a> | |
| </div> | |
| </div> | |
| </div> | |
| <h2>📦 Dependencies & Supply Chain</h2> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>📦 Dependency Report</h3> | |
| <p>Interactive dependency report with package counts, search, prod/dev badges, and expandable full tree view.</p> | |
| <div class="links"> | |
| <a href="dependencies/index.html">View Dependencies →</a> | |
| <a href="dependencies/dependency-tree.json" class="secondary">JSON</a> | |
| <a href="dependencies/dependency-tree.txt" class="secondary">TXT</a> | |
| </div> | |
| </div> | |
| </div> | |
| <h2>🔗 Quick Links</h2> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>🏠 Main Site</h3> | |
| <p>Return to the main Riksdagsmonitor platform to explore Swedish Parliament intelligence dashboards.</p> | |
| <div class="links"> | |
| <a href="../index.html">Go to Main Site →</a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <p>Generated during release workflow · Riksdagsmonitor by Hack23 AB · Licensed under Apache License 2.0</p> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| DOCSEOF | |
| echo "✅ Documentation index created at docs/index.html" | |
| - name: Create .nojekyll file | |
| run: touch docs/.nojekyll | |
| - name: Update sitemap | |
| run: npm run docs:sitemap | |
| - name: Deploy Documentation to GitHub Pages | |
| uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0 | |
| with: | |
| folder: docs | |
| target-folder: docs | |
| branch: main | |
| clean: false | |
| commit-message: "docs: update documentation for ${{ needs.prepare.outputs.version || 'development' }}" | |
| # -------------------------------------------------------------------------- | |
| # 8. release — create GitHub Release with artifacts (≤3 min) | |
| # -------------------------------------------------------------------------- | |
| release: | |
| name: Create GitHub Release | |
| needs: [prepare, build, package, unit-tests, e2e] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: write # Required to create releases | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| # release-drafter reads .github/release-drafter.yml from the default | |
| # branch (api driven), so a shallow checkout is sufficient here. | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Download release zip | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-zip | |
| path: artifacts/build | |
| - name: Download security artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: security-artifacts | |
| path: artifacts/security | |
| # NOTE: release-drafter emits benign warnings about "pull_request_target.*" | |
| # not being known webhook names (release-drafter/release-drafter#1369). | |
| - name: Draft Release Notes | |
| id: release-drafter | |
| uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1 | |
| with: | |
| version: ${{ needs.prepare.outputs.version }} | |
| tag: ${{ needs.prepare.outputs.version }} | |
| name: Riksdagsmonitor ${{ needs.prepare.outputs.version }} | |
| publish: false | |
| prerelease: ${{ needs.prepare.outputs.is_prerelease }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create GitHub Release | |
| uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 | |
| with: | |
| tag: ${{ needs.prepare.outputs.version }} | |
| name: Riksdagsmonitor ${{ needs.prepare.outputs.version }} | |
| body: | | |
| ${{ steps.release-drafter.outputs.body }} | |
| ## 📦 Release Artifacts | |
| - `riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip` - Production build | |
| - `riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.sha256` - Checksum for verification | |
| - `riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json` - SBOM (Software Bill of Materials) | |
| - `*.intoto.jsonl` - SLSA Build Provenance Attestations | |
| ## 📦 npm Package | |
| Shared types, theme system, and utilities are available as an npm package: | |
| ```bash | |
| npm install riksdagsmonitor | |
| ``` | |
| ## 📚 Documentation | |
| - [API Documentation](https://riksdagsmonitor.com/docs/api/) | |
| - [Test Coverage Report](https://riksdagsmonitor.com/docs/coverage/) | |
| - [E2E Test Reports](https://riksdagsmonitor.com/docs/cypress/) | |
| - [Documentation Hub](https://riksdagsmonitor.com/docs/) | |
| ## 🔐 Security | |
| All artifacts include SLSA Build Provenance attestations and SBOM for supply chain security. | |
| Verify attestations using the GitHub CLI: | |
| ```bash | |
| gh attestation verify riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip -R Hack23/riksdagsmonitor | |
| ``` | |
| generateReleaseNotes: false | |
| immutableCreate: true | |
| draft: false | |
| prerelease: ${{ needs.prepare.outputs.is_prerelease }} | |
| artifacts: | | |
| artifacts/build/riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip | |
| artifacts/build/riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.sha256 | |
| artifacts/security/riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json | |
| artifacts/security/riksdagsmonitor-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| artifacts/security/riksdagsmonitor-${{ needs.prepare.outputs.version }}.spdx.json.intoto.jsonl | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # -------------------------------------------------------------------------- | |
| # 9. npm-publish — build:lib + publish (≤5 min) — RUNS LAST | |
| # Only executes after every upstream job (build, unit-tests, typedoc, | |
| # e2e, package, deploy-docs, release) has succeeded, so a broken | |
| # build / failing tests / failed deployment never leak a tag onto npm. | |
| # Skips the heavy `npm run build` (that's only needed for the website, | |
| # not the library package) AND the duplicate `npm test` (already | |
| # passed in unit-tests). | |
| # -------------------------------------------------------------------------- | |
| npm-publish: | |
| name: Publish to npm | |
| needs: [prepare, build, unit-tests, typedoc, e2e, package, deploy-docs, release] | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| contents: read | |
| id-token: write # Required for npm provenance | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| ref: ${{ needs.prepare.outputs.release_sha }} | |
| - name: Setup Node.js for npm publish | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "26" | |
| cache: "npm" | |
| cache-dependency-path: | | |
| package-lock.json | |
| .github/workflows/release.yml | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Lint library source | |
| run: npm run lint | |
| - name: Build library package | |
| run: npm run build:lib | |
| - name: Verify package contents | |
| run: | | |
| echo "📦 Package contents preview:" | |
| PACK_OUTPUT="$(npm pack --dry-run 2>&1)" | |
| printf '%s\n' "$PACK_OUTPUT" | |
| echo "" | |
| echo "📊 Package size summary:" | |
| printf '%s\n' "$PACK_OUTPUT" | grep -E "^npm notice (package size|unpacked size|total files)" || true | |
| - name: Publish to npm with provenance | |
| run: npm publish --ignore-scripts --provenance --access public | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| - name: Verify npm package | |
| run: | | |
| echo "✅ Package published successfully!" | |
| echo "📦 Package: riksdagsmonitor@${{ needs.prepare.outputs.npm_version }}" | |
| echo "🔗 npm URL: https://www.npmjs.com/package/riksdagsmonitor" | |
| # -------------------------------------------------------------------------- | |
| # 10. summary — fan-in status job (≤1 min) | |
| # -------------------------------------------------------------------------- | |
| summary: | |
| name: Deployment Summary | |
| needs: [prepare, release, deploy-docs, npm-publish] | |
| if: always() | |
| runs-on: ubuntu-26.04 | |
| steps: | |
| - name: Print summary | |
| run: | | |
| echo "🎉 Release ${{ needs.prepare.outputs.version }} pipeline complete" | |
| echo "" | |
| echo "Job results:" | |
| echo " - release: ${{ needs.release.result }}" | |
| echo " - deploy-docs: ${{ needs.deploy-docs.result }}" | |
| echo " - npm-publish: ${{ needs.npm-publish.result }}" | |
| echo "" | |
| echo "📍 Deployments:" | |
| echo " - GitHub Pages (main branch)" | |
| echo "" | |
| echo "📚 Documentation:" | |
| echo " - https://riksdagsmonitor.com/docs/" |