Skip to content

Build, Attest and Release #260

Build, Attest and Release

Build, Attest and Release #260

Workflow file for this run

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"