Build, Attest and Release #260
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: Build, Attest and Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to release (vX.Y.Z format)" | |
| required: true | |
| default: "v0.1.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: | |
| prepare: | |
| name: Prepare Release | |
| runs-on: ubuntu-26.04 | |
| # Run inside Cypress's official browsers image so Chrome/Firefox/Edge/Xvfb and all | |
| # GTK/NSS/font system libs are preinstalled. Node is normalised to the version pinned | |
| # in .nvmrc by actions/setup-node below. | |
| container: | |
| # Use cypress/browsers:latest โ keeps Chrome/Firefox/Edge/Xvfb on a | |
| # rolling release. We don't pin to a specific patch tag because .nvmrc is | |
| # a Node major (26) and `actions/setup-node` would override the | |
| # container's Node anyway, which would defeat the purpose of digest | |
| # pinning the container's Node. | |
| image: cypress/browsers:latest | |
| # Run as root so the in-job apt-get (graphviz/ffmpeg/zip/git) and | |
| # actions/setup-node cache writes don't hit permission errors. The | |
| # cypress/browsers image's default USER is non-root. | |
| options: --user root | |
| # Only prepare job needs write permissions for commit and tagging | |
| permissions: | |
| contents: write # Required for git auto-commit | |
| outputs: | |
| version: ${{ steps.get-version.outputs.version }} | |
| is_prerelease: ${{ github.event.inputs.prerelease || 'false' }} | |
| steps: | |
| # NOTE: step-security/harden-runner does not support container jobs and is | |
| # intentionally omitted from this job. | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - 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 | |
| echo "Version: ${VERSION}" | |
| - name: Setup Three.js Test Environment | |
| env: | |
| CYPRESS_VIDEO: true | |
| # Suppress X11 keyboard configuration warnings | |
| XKB_DEFAULT_RULES: evdev | |
| XKB_DEFAULT_MODEL: pc105 | |
| XKB_DEFAULT_LAYOUT: us | |
| timeout-minutes: 5 | |
| run: | | |
| echo "๐ง Verifying Three.js test environment for release validation..." | |
| # Chrome / Xvfb / GTK libs are baked into cypress/browsers image. | |
| # Only graphviz (for diagrams documentation), ffmpeg, zip (for | |
| # release artifact packaging), and rsync (required by | |
| # JamesIves/github-pages-deploy-action) are still required on top. | |
| apt-get update -qq | |
| DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends graphviz ffmpeg zip git rsync | |
| echo "โ Three.js environment ready" | |
| google-chrome --version | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: "npm" | |
| - name: Cache Cypress binary | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/Cypress | |
| key: v2-cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| v2-cypress-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: build | |
| run: npm run build | |
| - name: Set Version for release | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| PLAIN_VERSION="${{ github.event.inputs.version }}" | |
| # Remove 'v' prefix if present | |
| PLAIN_VERSION="${PLAIN_VERSION#v}" | |
| npm version $PLAIN_VERSION --no-git-tag-version | |
| # NOTE: We intentionally do not use stefanzweifel/git-auto-commit-action | |
| # here. Because this job runs inside the cypress/browsers container, | |
| # that Docker action runs in its own container and cannot read the | |
| # host's git safe.directory configuration. It hits | |
| # "fatal: detected dubious ownership in repository" and then silently | |
| # reports "Working tree clean. Nothing to commit.", skipping the commit, | |
| # tag, and push โ which causes the downstream build job to fail with | |
| # "A branch or tag with the name 'vX.Y.Z' could not be found". | |
| # Running git directly in the job's shell lets us set safe.directory | |
| # for the actual UID running git and reliably commit + tag + push. | |
| - name: Commit version bump and create tag | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| VERSION: ${{ github.event.inputs.version }} | |
| run: | | |
| set -euo pipefail | |
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit for version bump." | |
| else | |
| git commit -m "chore(release): bump version to ${VERSION}" | |
| fi | |
| # Create tag if it doesn't already exist | |
| if git rev-parse -q --verify "refs/tags/${VERSION}" >/dev/null; then | |
| echo "Tag ${VERSION} already exists locally." | |
| else | |
| git tag -a "${VERSION}" -m "${VERSION}" | |
| fi | |
| # actions/checkout leaves the repo in a detached HEAD state for | |
| # workflow_dispatch on a branch, so push HEAD explicitly to main. | |
| git push origin "HEAD:${GITHUB_REF_NAME}" | |
| git push origin "refs/tags/${VERSION}" | |
| - name: Verify Cypress | |
| run: npx cypress verify | |
| - name: Start app and run Cypress tests with Three.js optimization | |
| env: | |
| CYPRESS_VIDEO: false | |
| NODE_OPTIONS: "--max-old-space-size=4096" | |
| XKB_DEFAULT_RULES: evdev | |
| XKB_DEFAULT_MODEL: pc105 | |
| XKB_DEFAULT_LAYOUT: us | |
| TERM: dumb | |
| run: | | |
| echo "๐ Running Three.js E2E tests for release validation..." | |
| NODE_OPTIONS="--max-old-space-size=4096" npm run test:e2e || TEST_EXIT_CODE=$? | |
| exit ${TEST_EXIT_CODE:-0} | |
| - name: Clean old docs | |
| run: rm -rf docs/api/*; rm -rf docs/cypress/*; rm -rf docs/test-results/* | |
| - name: Run tests with coverage | |
| run: npm run coverage | |
| - name: Generate api html documentation | |
| run: npm run docs | |
| - name: Generate dependencies documentation | |
| run: npm run docs:dependencies | |
| - name: Generate diagrams documentation | |
| run: npm run docs:diagrams | |
| - name: Merge e2e test report | |
| run: npm run test:e2ereportmerge | |
| - name: Generate e2e html report | |
| run: npm run test:e2ereporthtmlall | |
| - name: Copy test reports to docs | |
| run: npm run build:test-reports | |
| - 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' }}" | |
| build: | |
| name: Build Release Package | |
| needs: [prepare] | |
| runs-on: ubuntu-26.04 | |
| # Build job needs specific permissions for attestations | |
| permissions: | |
| contents: read | |
| id-token: write # Required for OIDC | |
| attestations: write # Required for SBOM and build attestations | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| # Use GITHUB_REF directly for tag events | |
| ref: ${{ github.event_name == 'push' && github.ref || github.event_name == 'workflow_dispatch' && github.event.inputs.version || '' }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Cache Vite build cache | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: node_modules/.vite | |
| key: v2-${{ runner.os }}-vite-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| v2-${{ runner.os }}-vite- | |
| - name: Build application | |
| run: npm run build | |
| env: | |
| VITE_APP_VERSION: ${{ needs.prepare.outputs.version }} | |
| - name: Create release artifacts | |
| run: | | |
| # Create the zip file from the dist directory | |
| cd dist | |
| zip -r ../game-${{ needs.prepare.outputs.version }}.zip . | |
| cd .. | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: build-artifacts | |
| path: | | |
| dist/ | |
| game-${{ needs.prepare.outputs.version }}.zip | |
| if-no-files-found: error | |
| - name: Generate SBOM | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| id: sbom | |
| with: | |
| format: spdx-json | |
| output-file: game-${{ needs.prepare.outputs.version }}.spdx.json | |
| artifact-name: game-${{ needs.prepare.outputs.version }} | |
| - name: Generate artifact attestation | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| id: attest | |
| with: | |
| subject-path: game-${{ needs.prepare.outputs.version }}.zip | |
| - name: Copy artifact attestation for zip | |
| run: cp ${{ steps.attest.outputs.bundle-path }} game-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| - name: Generate SBOM attestation | |
| id: attestsbom | |
| uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 | |
| with: | |
| subject-path: game-${{ needs.prepare.outputs.version }}.zip | |
| sbom-path: game-${{ needs.prepare.outputs.version }}.spdx.json | |
| - name: Copy SBOM attestation for zip | |
| run: cp ${{ steps.attestsbom.outputs.bundle-path }} game-${{ 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: | | |
| game-${{ needs.prepare.outputs.version }}.spdx.json | |
| game-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| game-${{ needs.prepare.outputs.version }}.spdx.json.intoto.jsonl | |
| if-no-files-found: error | |
| release: | |
| name: Create Release | |
| needs: [prepare, build] | |
| runs-on: ubuntu-26.04 | |
| # Release job needs specific permissions to create GitHub releases | |
| permissions: | |
| contents: write # Required to create releases | |
| id-token: write # Required for OIDC | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| # Checkout main branch to get latest documentation | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| ref: main # Always use main branch for deployment | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: build-artifacts | |
| path: artifacts/build | |
| - name: Download security artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: security-artifacts | |
| path: artifacts/security | |
| - name: Draft Release Notes | |
| id: release-drafter | |
| # NOTE: release-drafter v6.2.0 emits cosmetic warnings about | |
| # "pull_request_target.X is not a known webhook name" due to | |
| # Probot framework limitations. These are harmless and tracked | |
| # upstream: https://github.com/release-drafter/release-drafter/issues/1369 | |
| uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1 | |
| with: | |
| version: ${{ needs.prepare.outputs.version }} | |
| tag: ${{ needs.prepare.outputs.version }} | |
| name: Game ${{ needs.prepare.outputs.version }} | |
| publish: false | |
| prerelease: ${{ needs.prepare.outputs.is_prerelease }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Create GitHub Release with all artifacts | |
| - name: Create GitHub Release | |
| uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 | |
| with: | |
| tag: ${{ needs.prepare.outputs.version }} | |
| name: Game ${{ needs.prepare.outputs.version }} | |
| body: ${{ steps.release-drafter.outputs.body }} | |
| generateReleaseNotes: false | |
| immutableCreate: true | |
| draft: false | |
| prerelease: ${{ needs.prepare.outputs.is_prerelease }} | |
| artifacts: | | |
| artifacts/build/game-${{ needs.prepare.outputs.version }}.zip | |
| artifacts/security/game-${{ needs.prepare.outputs.version }}.spdx.json | |
| artifacts/security/game-${{ needs.prepare.outputs.version }}.zip.intoto.jsonl | |
| artifacts/security/game-${{ needs.prepare.outputs.version }}.spdx.json.intoto.jsonl | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Prepare docs directory for GitHub Pages | |
| - name: Prepare docs directory | |
| run: | | |
| mkdir -p docs | |
| # Clean only application files, preserve documentation | |
| rm -rf docs/index.html docs/assets docs/*.js docs/*.css | |
| # Deploy new application version | |
| - name: Deploy new version | |
| run: | | |
| # Extract the built application to docs | |
| rm docs/assets/*.css docs/assets/*.js || true | |
| unzip -o artifacts/build/game-${{ needs.prepare.outputs.version }}.zip -d docs/ | |
| # Create version marker for traceability | |
| echo "Version ${{ needs.prepare.outputs.version }} deployed at $(date)" > docs/version.txt | |
| # Final deployment with all components | |
| - name: Deploy 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: "chore(release): deploy version ${{ needs.prepare.outputs.version }}" | |
| publish: | |
| name: Publish npm Package | |
| needs: [prepare, 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@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'push' && github.ref || format('refs/tags/{0}', needs.prepare.outputs.version) }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: "npm" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build library package | |
| run: npm run build:lib | |
| - name: Verify package contents | |
| run: | | |
| echo "๐ฆ Package contents preview:" | |
| npm pack --dry-run | |
| echo "" | |
| echo "๐ Package files:" | |
| TARBALL=$(npm pack) | |
| tar -tzf "$TARBALL" | head -50 | |
| rm -f "$TARBALL" | |
| - name: Publish to npm with provenance | |
| run: npm publish --provenance --access public --ignore-scripts | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| - name: Verify npm package | |
| run: | | |
| PLAIN_VERSION="${{ needs.prepare.outputs.version }}" | |
| PLAIN_VERSION="${PLAIN_VERSION#v}" | |
| echo "โ Package published successfully!" | |
| echo "๐ฆ Package: blacktrigram@${PLAIN_VERSION}" | |
| echo "๐ npm URL: https://www.npmjs.com/package/blacktrigram" | |
| echo "" | |
| echo "To install:" | |
| echo " npm install blacktrigram@${PLAIN_VERSION}" | |
| echo "" | |
| echo "To verify provenance:" | |
| echo " npm audit signatures" |