name: Release runtime artifacts on: push: tags: - v2.0.0 - v2.0.0-rc.* workflow_dispatch: permissions: contents: read concurrency: group: runtime-release-${{ github.ref }} cancel-in-progress: false jobs: build: name: Build ${{ matrix.target }} permissions: contents: read id-token: write attestations: write strategy: fail-fast: false matrix: include: - os: macos-15 target: darwin-arm64 capabilities: browser,browser-visible,design,pdf,diagram,ios - os: macos-15-intel target: darwin-x64 capabilities: browser,browser-visible,design,pdf,diagram,ios - os: ubuntu-24.04-arm target: linux-arm64 capabilities: browser,browser-visible,design,pdf,diagram - os: ubuntu-24.04 target: linux-x64 capabilities: browser,browser-visible,design,pdf,diagram - os: windows-11-arm target: windows-arm64 capabilities: browser,browser-visible,design,pdf,diagram - os: windows-2025 target: windows-x64 capabilities: browser,browser-visible,design,pdf,diagram runs-on: ${{ matrix.os }} timeout-minutes: 35 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22.23.1 - name: Install frozen dependencies run: bun install --frozen-lockfile --ignore-scripts shell: bash - name: Build and stage the complete managed runtime env: GSTACK_HOME: ${{ runner.temp }}/gstack-release-home TARGET: ${{ matrix.target }} CAPABILITIES: ${{ matrix.capabilities }} run: | set -euo pipefail node runtime/install.js \ --source "$GITHUB_WORKSPACE" \ --home "$GSTACK_HOME" \ --version 2.0.0 \ --install-now \ --yes \ --capabilities "$CAPABILITIES" active_slot=$(node -e 'const fs=require("fs"),p=process.argv[1];const v=JSON.parse(fs.readFileSync(p,"utf8")).current;if(typeof v!=="string"||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(v))process.exit(1);process.stdout.write(v)' "$GSTACK_HOME/versions/current.json") active="$GSTACK_HOME/versions/$active_slot" test -f "$active/.gstack-bundle.json" case "$TARGET" in windows-*) managed_bun_rel=".gstack-runtime-tools/bun.exe" ;; *) managed_bun_rel=".gstack-runtime-tools/bun" ;; esac managed_bun="$active/$managed_bun_rel" test -f "$managed_bun" test "$("$managed_bun" --version)" = "1.3.14" test -f "$active/runtime/licenses/BUN-LICENSE-1.3.14.md" test -f "$active/runtime/licenses/BUN-SOURCE.md" node -e 'const fs=require("fs"),c=require("crypto"),root=process.argv[1],rel=process.argv[2];const m=JSON.parse(fs.readFileSync(root+"/.gstack-bundle.json","utf8"));if(m.tools?.bun?.path!==rel||m.tools?.bun?.version!=="1.3.14")process.exit(1);const license=fs.readFileSync(root+"/runtime/licenses/BUN-LICENSE-1.3.14.md");if(c.createHash("sha256").update(license).digest("hex")!=="2cb858b2db8fc793bca2093489c5bc8eee615d002cc4924254904044c27a0afa")process.exit(1)' "$active" "$managed_bun_rel" test -d "$active/.gstack-runtime-browsers" ( cd "$active" PLAYWRIGHT_BROWSERS_PATH="$active/.gstack-runtime-browsers" \ node --input-type=module --eval \ 'const { chromium } = await import("./node_modules/playwright/index.mjs"); for (const options of [{ headless: true }, { headless: true, channel: "chromium" }]) { const browser = await chromium.launch(options); await browser.close(); }' ) node_command=$(node -p 'process.execPath') host_bun=$(command -v bun) host_bun_dir=$(cd "$(dirname "$host_bun")" && pwd -P) clean_path="" IFS=: read -r -a path_parts <<< "$PATH" for part in "${path_parts[@]}"; do physical=$(cd "$part" 2>/dev/null && pwd -P || printf '%s' "$part") if [ "$physical" != "$host_bun_dir" ]; then clean_path="${clean_path:+$clean_path:}$part" fi done if (PATH="$clean_path"; command -v bun >/dev/null 2>&1); then echo "Host-global Bun remained available after removing setup-bun from PATH" >&2 exit 1 fi test "$(PATH="$clean_path" GSTACK_NODE="$node_command" "$GSTACK_HOME/bin/bun" --version)" = "1.3.14" browser_cleanup() { PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \ "$GSTACK_HOME/bin/browse" stop >/dev/null 2>&1 || true } trap browser_cleanup EXIT smoke_url=$(node -e 'const fs=require("fs"),p=require("path").join(process.env.GITHUB_WORKSPACE,".gstack-runtime-smoke.html");fs.writeFileSync(p,"GStack runtime smoke\n");process.stdout.write(require("url").pathToFileURL(p).href)') PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \ "$GSTACK_HOME/bin/browse" goto "$smoke_url" PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \ "$GSTACK_HOME/bin/browse" status browser_cleanup trap - EXIT stage="$RUNNER_TEMP/runtime-components" # GNU tar treats a Windows drive colon in an archive path as a # remote-host separator. Keep archive output in Git Bash's POSIX view. release_dir="$(pwd -P)/release-output" mkdir -p "$stage" "$release_dir" node .github/scripts/stage-runtime-components.mjs "$active" "$stage" for component_dir in "$stage"/*; do test -d "$component_dir" || continue component=$(basename "$component_dir") archive="$release_dir/gstack-runtime-2.0.0-$TARGET-$component.tar.gz" tar -czf "$archive" -C "$component_dir" gstack node -e 'const fs=require("fs"),c=require("crypto"),p=process.argv[1];const b=fs.readFileSync(p);fs.writeFileSync(p+".sha256",c.createHash("sha256").update(b).digest("hex")+" "+require("path").basename(p)+"\n")' "$archive" done shell: bash - name: Attest component archive provenance uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2 with: subject-path: release-output/*.tar.gz - name: Upload signed archive uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: runtime-${{ matrix.target }} path: release-output/* if-no-files-found: error retention-days: 14 manifest: name: Assemble manifest and GitHub Release needs: build runs-on: ubuntu-24.04 if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write id-token: write attestations: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: runtime-* path: release-output merge-multiple: true - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - name: Keyless-sign component archives run: | set -euo pipefail for archive in release-output/*.tar.gz; do cosign sign-blob --yes --bundle "$archive.sigstore.json" "$archive" done shell: bash - name: Create strict six-target manifest run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0 "$GITHUB_REF_NAME" - name: Checksum and keyless-sign manifest run: | set -euo pipefail cd release-output sha256sum gstack-runtime-manifest.json > gstack-runtime-manifest.json.sha256 cosign sign-blob --yes --bundle gstack-runtime-manifest.json.sigstore.json gstack-runtime-manifest.json shell: bash - name: Attest manifest provenance uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2 with: subject-path: release-output/gstack-runtime-manifest.json - name: Publish immutable release assets env: GH_TOKEN: ${{ github.token }} PRERELEASE_FLAG: ${{ contains(github.ref_name, '-rc.') && '--prerelease' || '' }} run: | set -euo pipefail gh release create "$GITHUB_REF_NAME" \ --verify-tag \ $PRERELEASE_FLAG \ --title "GStack runtime $GITHUB_REF_NAME" \ --notes "Signed optional runtime artifacts for the six portable GStack skills." \ release-output/* shell: bash