name: Release on: push: branches: - main workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: check-release: name: Check for new version # The private mirror carries this file too; only the public repo # publishes (the trusted publisher is pinned to it). if: github.repository == 'vercel-labs/scriptc' runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read actions: read outputs: should_release: ${{ steps.check.outputs.should_release }} version: ${{ steps.check.outputs.version }} artifact_run: ${{ steps.reuse.outputs.artifact_run || github.run_id }} reuse_artifacts: ${{ steps.reuse.outputs.artifact_run != '' }} receipt_run: ${{ steps.reuse.outputs.receipt_run }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "24" registry-url: "https://registry.npmjs.org" - name: Check GitHub release completion id: check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | LOCAL_VERSION=$(node -p "require('./packages/cli/package.json').version") echo "Local version: $LOCAL_VERSION" RELEASE_DRAFT=$(gh release view "v$LOCAL_VERSION" --json isDraft --jq .isDraft 2>/dev/null || echo missing) if [ "$RELEASE_DRAFT" = false ]; then echo "Release is complete" echo "should_release=false" >> "$GITHUB_OUTPUT" else echo "Release needs publishing or completion" echo "should_release=true" >> "$GITHUB_OUTPUT" fi echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT" - name: Find reusable builds and accepted upload receipts id: reuse if: steps.check.outputs.should_release == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.check.outputs.version }} run: | node --input-type=module <<'NODE' import { execFileSync, spawnSync } from 'node:child_process'; import { appendFileSync } from 'node:fs'; const api = (path) => JSON.parse(execFileSync('gh', ['api', path], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 })); const repo = process.env.GITHUB_REPOSITORY; const expected = [ 'darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', 'linux-x64-musl', 'linux-arm64-musl', 'windows-x64', 'wasm32-wasi', 'runtime-ios-arm64', 'runtime-ios-simulator-arm64', 'runtime-android-arm64', ].map((platform) => `native-${platform}`); const runs = api(`repos/${repo}/actions/workflows/release.yml/runs?branch=main&status=completed&per_page=100`).workflow_runs; let artifactRun; let receiptRun; for (const run of runs) { if (String(run.id) === process.env.GITHUB_RUN_ID || run.head_branch !== 'main' || run.head_repository?.full_name !== repo || !['push', 'workflow_dispatch'].includes(run.event)) continue; // Release orchestration, packaging validation, and their tests do // not change the compiled payloads in native build artifacts. if (spawnSync('git', ['merge-base', '--is-ancestor', run.head_sha, 'HEAD']).status !== 0 || spawnSync('git', ['diff', '--quiet', run.head_sha, 'HEAD', '--', '.', ':(exclude).github/workflows/release.yml', ':(exclude)scripts/verify-native-cli.mjs', ':(exclude)scripts/package-native-cli.mjs', ':(exclude)tests/harness/native-cli-packaging.test.ts', ':(exclude)scripts/wait-npm-available.mjs', ':(exclude)tests/harness/npm-available.test.ts', ]).status !== 0) continue; const artifacts = api(`repos/${repo}/actions/runs/${run.id}/artifacts?per_page=100`).artifacts .filter((artifact) => !artifact.expired && artifact.size_in_bytes > 0); if (!artifactRun && expected.every((name) => artifacts.filter((artifact) => artifact.name === name).length === 1)) { artifactRun = run.id; console.log(`Reusing native builds from run ${run.id} (${run.head_sha})`); } if (!receiptRun && artifacts.some((artifact) => artifact.name.startsWith(`npm-accepted-${process.env.VERSION}-`))) { receiptRun = run.id; console.log(`Restoring accepted uploads from run ${run.id}`); } if (artifactRun && receiptRun) break; } if (artifactRun) appendFileSync(process.env.GITHUB_OUTPUT, `artifact_run=${artifactRun}\n`); if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`); NODE # Collects the native compiler's function layout and profile on this commit # (scripts/refresh-cli-layout.mjs, scripts/refresh-cli-profile.mjs), so every # platform's CLI build optimizes and lays out the code it compiles. The # committed ones fall behind main within a day or two. If this job fails, # the CLI builds use the committed profile and layout. cli-profile: name: Collect the native compiler profile needs: check-release if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true' runs-on: ubuntu-24.04 # The layout and the profile's two phases each build an instrumented compiler. timeout-minutes: 120 continue-on-error: true permissions: contents: read steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: version: 11.1.3 - uses: actions/setup-node@v4 with: node-version: "24" cache: pnpm - uses: ./.github/actions/llvm-linux - run: pnpm install --frozen-lockfile - uses: ./.github/actions/zig - uses: ./.github/actions/native-packages with: target: linux-x64-gnu - run: pnpm --filter @scriptc/compiler --filter scriptc build - name: Check out tsc-ts run: | git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts" echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)" npm ci --prefix "$RUNNER_TEMP/tsc-ts" # The layout comes first: the profile matches the functions of builds # with the layout its instrumented compiler was built with, so the # profile is collected with a second instrumented compiler built with # this layout. - name: Collect the function layout run: >- node scripts/refresh-cli-layout.mjs --jobs 4 --output "$RUNNER_TEMP/cli-profile/cli.layout.gz" --work "$RUNNER_TEMP/cli-layout-work" - name: Collect the profile env: SCRIPTC_NATIVE_LAYOUT: ${{ runner.temp }}/cli-profile/cli.layout.gz run: >- node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4 --context-sensitive --output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" --work "$RUNNER_TEMP/cli-profile-work" - uses: actions/upload-artifact@v4 with: name: cli-profile path: | ${{ runner.temp }}/cli-profile/cli.profdata.gz ${{ runner.temp }}/cli-profile/cli.layout.gz retention-days: 7 if-no-files-found: error build-native-packages: name: Build native package (${{ matrix.platform }}) needs: [check-release, cli-profile] # Runs when the profile job fails; the CLI builds then use the committed profile. if: >- !cancelled() && needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true' strategy: fail-fast: false matrix: include: - platform: darwin-arm64 runner: macos-15 helper: llvm-darwin-arm64 runtime: runtime-darwin-arm64 cli: cli-darwin-arm64 - platform: darwin-x64 runner: macos-15-intel helper: llvm-darwin-x64 runtime: runtime-darwin-x64 cli: cli-darwin-x64 - platform: linux-x64 runner: ubuntu-24.04 helper: llvm-linux-x64-gnu runtime: runtime-linux-x64-gnu cli: cli-linux-x64-gnu llvm_asset: LLVM-22.1.8-Linux-X64 llvm_dist: x64 use_zig: true - platform: linux-arm64 runner: ubuntu-24.04-arm helper: llvm-linux-arm64-gnu runtime: runtime-linux-arm64-gnu cli: cli-linux-arm64-gnu llvm_asset: LLVM-22.1.8-Linux-ARM64 llvm_dist: arm64 use_zig: true - platform: windows-x64 runner: windows-2022 helper: llvm-win32-x64-msvc runtime: runtime-win32-x64-msvc cli: cli-win32-x64-msvc use_zig: true - platform: linux-x64-musl runner: ubuntu-24.04 helper: llvm-linux-x64-musl runtime: runtime-linux-x64-musl cli: cli-linux-x64-musl seed_helper: llvm-linux-x64-gnu cli_target: x86_64-linux-musl llvm_asset: LLVM-22.1.8-Linux-X64 llvm_dist: x64 use_zig: true - platform: linux-arm64-musl runner: ubuntu-24.04-arm helper: llvm-linux-arm64-musl runtime: runtime-linux-arm64-musl cli: cli-linux-arm64-musl seed_helper: llvm-linux-arm64-gnu cli_target: aarch64-linux-musl llvm_asset: LLVM-22.1.8-Linux-ARM64 llvm_dist: arm64 use_zig: true - platform: wasm32-wasi runner: ubuntu-24.04 helper: llvm-linux-x64-gnu runtime: runtime-wasm32-wasi llvm_asset: LLVM-22.1.8-Linux-X64 llvm_dist: x64 use_zig: true runs-on: ${{ matrix.runner }} # The Intel macOS build took 24 minutes for 0.3.2, 40 for 0.4.5, and passed 45 for 0.4.6. timeout-minutes: 90 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: version: 11.1.3 - uses: actions/setup-node@v4 with: node-version: "24" cache: pnpm - name: Install toolchain (macOS) if: startsWith(matrix.platform, 'darwin') run: | brew install llvm@22 llvm_prefix=$(brew --prefix llvm@22) echo "$llvm_prefix/bin" >> "$GITHUB_PATH" echo "LLVM_DIR=$llvm_prefix/lib/cmake/llvm" >> "$GITHUB_ENV" - name: Install pinned LLVM development distribution (Linux) if: startsWith(matrix.platform, 'linux') || matrix.platform == 'wasm32-wasi' run: | sudo apt-get update sudo apt-get install --yes g++ zlib1g-dev libzstd-dev curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/${{ matrix.llvm_asset }}.tar.xz" tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP" mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8" echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV" echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH" # The Linux helpers link LLVM built with ThinLTO and a profile of the # helper's code generation (native/llvm-codegen/llvm-dist.mjs), from a # release of this repository. Without that archive they link LLVM's # release archive, which gives the same objects more slowly. The # wasm32-wasi job also uploads the x64 GNU helper, so it selects the same. - name: Select the profiled LLVM distribution (Linux) if: matrix.llvm_dist != '' env: ARCH: ${{ matrix.llvm_dist }} run: | tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)") asset=$(node --input-type=module -e "import { llvmDistAsset } from './native/llvm-codegen/llvm-dist.mjs'; console.log(llvmDistAsset(process.env.ARCH))") base="https://github.com/$GITHUB_REPOSITORY/releases/download/$tag" if curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "$base/$asset" && curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset.sha256" "$base/$asset.sha256" && (cd "$RUNNER_TEMP" && sha256sum --check "$asset.sha256"); then tar -xJf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP" echo "SCRIPTC_HELPER_LLVM_DIR=$RUNNER_TEMP/${asset%.tar.xz}/lib/cmake/llvm" >> "$GITHUB_ENV" echo "The helper links $asset from release $tag" else echo "::warning::$asset is not available from release $tag; the helper links LLVM's release archive" fi - uses: vercel-labs/setup-zig@v1 if: matrix.use_zig == true with: version: 0.16.0 - name: Install pinned LLVM development distribution (Windows) if: matrix.platform == 'windows-x64' shell: pwsh run: | # The complete archive supplies LLVM's exported CMake targets and # static libraries. windows-2022 includes CMake, Ninja, VS 2022, # and 7-Zip; unlike tar.exe, 7-Zip expands this archive quickly. $archive = "$env:RUNNER_TEMP\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar.xz" $llvm = "$env:RUNNER_TEMP\llvm-22.1.8" $expanded = "$env:RUNNER_TEMP\llvm-expanded" $sevenZip = "$env:ProgramFiles\7-Zip\7z.exe" if (-not (Test-Path $sevenZip)) { choco install 7zip --no-progress -y if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } if (-not (Test-Path $sevenZip)) { throw "7-Zip is unavailable" } curl.exe -fL --retry 3 -o $archive https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/clang%2Bllvm-22.1.8-x86_64-pc-windows-msvc.tar.xz & $sevenZip x -y "-o$expanded" $archive if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & $sevenZip x -y "-o$expanded" "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc.tar" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Move-Item "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc" $llvm echo "LLVM_DIR=$llvm\lib\cmake\llvm" >> $env:GITHUB_ENV echo "$llvm\bin" >> $env:GITHUB_PATH # A musl CLI ships tsc-ts's musl executable, and these jobs run on glibc # runners, where pnpm installs only the glibc platform packages. - name: Let pnpm install musl platform packages if: endsWith(matrix.cli_target, '-musl') run: | printf 'supportedArchitectures:\n libc: [current, musl]\n' >> pnpm-workspace.yaml - run: pnpm install --frozen-lockfile - name: Build native packages (POSIX) if: matrix.platform != 'windows-x64' run: | if [ -n "${SCRIPTC_HELPER_LLVM_DIR:-}" ]; then LLVM_DIR="$SCRIPTC_HELPER_LLVM_DIR" SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/${{ matrix.helper }} build:native || { echo "::warning::the helper did not build against the profiled LLVM; it links LLVM's release archive" rm -rf node_modules/.cache/scriptc-${{ matrix.helper }} pnpm --filter @scriptc/${{ matrix.helper }} build:native } else pnpm --filter @scriptc/${{ matrix.helper }} build:native fi if [ "${{ matrix.use_zig }}" = true ]; then CC=zig AR=zig pnpm --filter @scriptc/${{ matrix.runtime }} build:native else CC=clang AR=llvm-ar pnpm --filter @scriptc/${{ matrix.runtime }} build:native fi - name: Build native packages (Windows) if: matrix.platform == 'windows-x64' run: | pnpm --filter @scriptc/${{ matrix.helper }} build:native pnpm --filter @scriptc/${{ matrix.runtime }} build:native - name: Build host helper for cross-libc bootstrap if: matrix.seed_helper != '' run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native - name: Download the profile collected on this commit id: cli-profile if: matrix.cli != '' continue-on-error: true uses: actions/download-artifact@v4 with: name: cli-profile path: ${{ runner.temp }}/cli-profile # Without the artifact, SCRIPTC_NATIVE_PROFILE_USE and SCRIPTC_NATIVE_LAYOUT # stay unset and build-native-cli.mts uses the committed profile and layout. - name: Select the native compiler profile if: matrix.cli != '' shell: bash env: COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }} run: | if [ -n "$COLLECTED" ]; then echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV" echo "The native CLI builds with the profile collected on $GITHUB_SHA" layout="$(dirname "$COLLECTED")/cli.layout.gz" if [ -f "$layout" ]; then echo "SCRIPTC_NATIVE_LAYOUT=$layout" >> "$GITHUB_ENV" echo "The native CLI builds with the function layout collected on $GITHUB_SHA" fi else echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile" fi - name: Build native CLI distribution if: matrix.cli != '' env: SCRIPTC_TARGET: ${{ matrix.cli_target }} run: | pnpm --filter @scriptc/compiler --filter scriptc build pnpm --filter @scriptc/${{ matrix.cli }} build:native node scripts/verify-native-cli.mjs packages/${{ matrix.cli }} --run - name: Verify GNU npm distribution on glibc 2.34 if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64' run: | NODE_ROOT=$(dirname "$(dirname "$(command -v node)")") docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" --volume "$NODE_ROOT:/node:ro" \ --env PATH=/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin \ --workdir /work amazonlinux:2023 sh -c ' set -eu dnf install --assumeyes clang tar gzip test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.34" node scripts/smoke-native-install.mjs packages/${{ matrix.cli }} ' - name: Verify packed Darwin helper on its build host if: matrix.platform == 'darwin-arm64' run: | TARBALL=$(pnpm --dir packages/llvm-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent) node scripts/verify-llvm-package.mjs "$RUNNER_TEMP/$(basename "$TARBALL")" - name: Upload native packages uses: actions/upload-artifact@v4 with: name: native-${{ matrix.platform }} path: | packages/${{ matrix.helper }} packages/${{ matrix.runtime }} ${{ matrix.cli != '' && format('packages/{0}/dist', matrix.cli) || '' }} retention-days: 7 build-mobile-runtime-packs: name: Build ${{ matrix.runtime }} needs: check-release if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true' strategy: fail-fast: false matrix: include: - runtime: runtime-ios-arm64 runner: macos-15 - runtime: runtime-ios-simulator-arm64 runner: macos-15 - runtime: runtime-android-arm64 runner: ubuntu-24.04 runs-on: ${{ matrix.runner }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/mobile-runtime-pack with: runtime: ${{ matrix.runtime }} - name: Stage mobile runtime pack run: | mkdir -p "$RUNNER_TEMP/mobile-pack/${{ matrix.runtime }}" cp -R packages/${{ matrix.runtime }}/. "$RUNNER_TEMP/mobile-pack/${{ matrix.runtime }}/" - uses: actions/upload-artifact@v4 with: name: native-${{ matrix.runtime }} path: ${{ runner.temp }}/mobile-pack retention-days: 7 publish: name: Publish to npm needs: [check-release, build-native-packages, build-mobile-runtime-packs] if: >- !cancelled() && needs.check-release.outputs.should_release == 'true' && (needs.check-release.outputs.reuse_artifacts == 'true' || (needs.build-native-packages.result == 'success' && needs.build-mobile-runtime-packs.result == 'success')) runs-on: ubuntu-latest # Two waits for npm availability of at most 25 minutes each. timeout-minutes: 70 environment: Release permissions: contents: read actions: read id-token: write outputs: uploads: ${{ steps.publish.outputs.uploads }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Download native packages uses: actions/download-artifact@v4 with: pattern: native-* path: native-artifacts merge-multiple: true run-id: ${{ needs.check-release.outputs.artifact_run }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Restore accepted upload receipts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.check-release.outputs.version }} RECEIPT_RUN: ${{ needs.check-release.outputs.receipt_run }} run: | touch "$RUNNER_TEMP/npm-accepted.txt" "$RUNNER_TEMP/npm-accepted-times.txt" # Include this run so re-running only failed jobs restores its receipts. for run in "$GITHUB_RUN_ID" "$RECEIPT_RUN"; do if [ -z "$run" ]; then continue; fi count=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run/artifacts?per_page=100" \ --jq "[.artifacts[] | select(.expired == false and (.name | startswith(\"npm-accepted-$VERSION-\")))] | length") if [ "$count" -eq 0 ]; then continue; fi gh run download "$run" --repo "$GITHUB_REPOSITORY" --pattern "npm-accepted-$VERSION-*" --dir "$RUNNER_TEMP/npm-receipts/$run" find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted.txt" find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted-times.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted-times.txt" done - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 11.1.3 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "24" registry-url: "https://registry.npmjs.org" # Publishing uses npm trusted publishing (OIDC): the job's id-token # permission lets npm mint short-lived credentials, so no npm token # secret exists anywhere in this repo. The runtime, every shipped # platform runtime/helper package, compiler, and CLI must each be # configured on npmjs.com with a GitHub Actions trusted publisher # pointing at repository vercel-labs/scriptc, workflow release.yml, # environment Release. A package missing that configuration fails # with an OIDC authentication error before anything uploads. # Trusted publishing requires npm >= 11.5.1 (bundled with Node 24). - name: Install and build run: | pnpm install --frozen-lockfile cp -R native-artifacts/. packages/ # upload-artifact does not preserve executable bits. Restore the # helper mode before pnpm runs its prepack checks. find packages -type f -path '*/bin/scriptc-llvm-codegen' -exec chmod 755 {} + find packages -type f \( -path '*/dist/bin/scriptc' -o -path '*/dist/lib/typescript/lib/tsc' -o -path '*/dist/lib/scriptc-comptime' \) -exec chmod 755 {} + pnpm -r build - name: Check version sync run: | VERSION="${{ needs.check-release.outputs.version }}" for pkg in packages/runtime packages/runtime-* packages/llvm-* packages/compiler packages/experimental packages/node packages/threads packages/types packages/cli packages/cli-*; do if [ "$(node -p "require('./$pkg/package.json').private === true")" = "true" ]; then continue fi V=$(node -p "require('./$pkg/package.json').version") if [ "$V" != "$VERSION" ]; then echo "Version mismatch: $pkg is $V, expected $VERSION" echo "Run 'node scripts/sync-versions.mjs' to stamp runtime and compiler from the CLI version, then commit" exit 1 fi done - name: Publish to npm id: publish shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.check-release.outputs.version }} run: | # npm accepts --provenance only from PUBLIC source repositories; # while this repo is internal the flag is dropped, and the same # step starts attaching provenance the moment the repo goes # public — no workflow edit. VISIBILITY=$(gh api "repos/${{ github.repository }}" --jq .visibility) if [ "$VISIBILITY" = "public" ]; then PROVENANCE="--provenance" else PROVENANCE="" echo "repository visibility is '$VISIBILITY': publishing without provenance" fi # Upload acceptance and public availability are separate states. # Save acceptance immediately so retries never depend on registry lag. # npm-release.txt lists every package of the release, and # npm-accepted-times.txt the time npm accepted each upload, for the # npm-available job. publish_dir() { dir="$1" packed="${2:-}" name=$(node -p "require('./$dir/package.json').name") echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-release.txt" if grep -Fxq "$name@$VERSION" "$RUNNER_TEMP/npm-accepted.txt"; then echo "$name@$VERSION upload already accepted, skipping" return 0 fi if [ -z "$packed" ]; then tarball=$(cd "$dir" && pnpm pack --silent | tail -1) packed="$dir/$tarball" fi case "$dir" in packages/llvm-darwin-*|packages/llvm-linux-*) node scripts/verify-llvm-package-mode.mjs "$packed" "$name" ;; esac publish_log="$RUNNER_TEMP/npm-publish.log" if npm publish "$packed" $PROVENANCE --access public 2>&1 | tee "$publish_log"; then echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt" echo "$name@$VERSION $(date +%s)" >> "$RUNNER_TEMP/npm-accepted-times.txt" return 0 else publish_status=${PIPESTATUS[0]} fi if [ "$publish_status" -eq 0 ]; then return 1; fi # Older runs have no receipts. This specific conflict means npm # already accepted this version; other publish failures still fail. if grep -Fq 'npm error code E409' "$publish_log" && \ grep -Fq "Cannot publish over previously staged version \"$VERSION\"." "$publish_log"; then echo "$name@$VERSION is already accepted and awaiting npm processing" echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt" return 0 fi if npm view "$name@$VERSION" version >/dev/null 2>&1; then echo "$name@$VERSION is now public" echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt" return 0 fi return "$publish_status" } # pnpm pack resolves workspace dependencies; npm handles OIDC. # Publish dependencies first. The npm-available job waits for the # packages that nothing in this release depends on. publish_dir packages/runtime publish_dir packages/runtime-darwin-arm64 publish_dir packages/llvm-darwin-arm64 publish_dir packages/runtime-darwin-x64 publish_dir packages/llvm-darwin-x64 publish_dir packages/runtime-linux-x64-gnu publish_dir packages/llvm-linux-x64-gnu publish_dir packages/runtime-linux-arm64-gnu publish_dir packages/llvm-linux-arm64-gnu publish_dir packages/runtime-linux-x64-musl publish_dir packages/llvm-linux-x64-musl publish_dir packages/runtime-linux-arm64-musl publish_dir packages/llvm-linux-arm64-musl publish_dir packages/runtime-ios-arm64 publish_dir packages/runtime-ios-simulator-arm64 publish_dir packages/runtime-android-arm64 publish_dir packages/runtime-wasm32-wasi publish_dir packages/runtime-win32-x64-msvc publish_dir packages/llvm-win32-x64-msvc # A native CLI package depends on its platform's LLVM helper and # runtime pack packages, and scriptc on the native CLI packages. # Each group publishes only once the packages it depends on # resolve, so an install never finds a package whose dependencies # npm is still processing. A rerun skips accepted uploads and waits # again. wait_available() { awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \ "$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt" > "$RUNNER_TEMP/npm-wait.txt" node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-wait.txt" --interval 30 --timeout 1500 } wait_available for pkg in packages/cli-*; do publish_dir "$pkg"; done publish_dir packages/compiler publish_dir packages/experimental publish_dir packages/node publish_dir packages/threads publish_dir packages/types wait_available publish_dir packages/cli echo "All package uploads accepted; npm registry propagation may continue after this job." >> "$GITHUB_STEP_SUMMARY" # Each package of the release with the time its upload was accepted, # when a run recorded it. { echo "uploads<> "$GITHUB_OUTPUT" - name: Save accepted upload receipts if: always() uses: actions/upload-artifact@v4 with: name: npm-accepted-${{ needs.check-release.outputs.version }}-${{ github.run_attempt }} path: | ${{ runner.temp }}/npm-accepted.txt ${{ runner.temp }}/npm-accepted-times.txt if-no-files-found: ignore retention-days: 90 # Waits until every package of the release resolves on the npm registry, # and reports each package's time from upload to available. Fails with the # packages that are still missing after 2 hours. It only reads from the # registry; the next release run retries it, and publishing skips uploads # npm already accepted. npm-available: name: Wait for npm availability needs: [check-release, publish] if: >- !cancelled() && needs.check-release.outputs.should_release == 'true' && needs.publish.result == 'success' runs-on: ubuntu-latest timeout-minutes: 135 permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "24" registry-url: "https://registry.npmjs.org" - name: Wait for every package to resolve env: UPLOADS: ${{ needs.publish.outputs.uploads }} run: | printf '%s\n' "$UPLOADS" > "$RUNNER_TEMP/npm-uploads.txt" node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-uploads.txt" # Publish standalone native distributions and the surface manifest after # every npm package resolves. The release body comes from the marked # changelog. github-release: name: Create GitHub Release needs: [check-release, publish, npm-available] if: >- !cancelled() && needs.check-release.outputs.should_release == 'true' && needs.npm-available.result == 'success' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write actions: read steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 11.1.3 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "24" # Regenerate the surface manifest from this tree and require it to # match the committed file exactly — the same staleness guard # the test suite runs — so the attached asset is provably the # manifest of the code being released. - name: Generate surface manifest run: | pnpm install --frozen-lockfile pnpm manifest --check - name: Extract changelog entry run: | VERSION="${{ needs.check-release.outputs.version }}" awk '//{found=1; next} //{exit} found{print}' CHANGELOG.md > /tmp/release-notes.md LINES=$(wc -l < /tmp/release-notes.md | tr -d ' ') if [ "$LINES" -lt 2 ]; then echo "Error: No release notes found between and markers in CHANGELOG.md" exit 1 fi echo "Extracted release notes for $VERSION ($LINES lines)" - name: Download native distributions and runtime packs uses: actions/download-artifact@v4 with: pattern: native-* path: packages merge-multiple: true run-id: ${{ needs.check-release.outputs.artifact_run }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Package standalone compilers run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets # The profile the native CLIs were built with. Runs that reuse native # builds from before the cli-profile job, or whose profile job failed, # have none; their CLIs used the committed profile. - name: Download the native compiler profile env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUN: ${{ needs.check-release.outputs.artifact_run }} VERSION: ${{ needs.check-release.outputs.version }} run: | if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz" else echo "Run $RUN has no cli-profile artifact" fi - name: Create GitHub Release run: | VERSION="${{ needs.check-release.outputs.version }}" TAG="v$VERSION" if gh release view "$TAG" &>/dev/null; then echo "Release $TAG already exists, skipping creation" else echo "Creating release $TAG..." gh release create "$TAG" \ --target "$GITHUB_SHA" \ --title "$TAG" \ --draft \ --notes-file /tmp/release-notes.md fi # Attach the surface manifest (idempotent: --clobber makes # re-runs replace the asset instead of failing). gh release upload "$TAG" packages/compiler/surface-manifest.json --clobber gh release upload "$TAG" /tmp/scriptc-release-assets/* --clobber gh release edit "$TAG" --draft=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}