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 outputs: should_release: ${{ steps.check.outputs.should_release }} version: ${{ steps.check.outputs.version }} 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: Compare package.json version to npm id: check run: | LOCAL_VERSION=$(node -p "require('./packages/cli/package.json').version") echo "Local version: $LOCAL_VERSION" NPM_VERSION=$(npm view scriptc version 2>/dev/null || echo "0.0.0") echo "npm version: $NPM_VERSION" if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION" echo "should_release=true" >> "$GITHUB_OUTPUT" else echo "Version unchanged on npm, skipping publish" echo "should_release=false" >> "$GITHUB_OUTPUT" fi echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT" build-native-packages: name: Build native package (${{ matrix.platform }}) needs: check-release if: needs.check-release.outputs.should_release == 'true' strategy: fail-fast: false matrix: include: - platform: darwin-arm64 runner: macos-15 helper: llvm-darwin-arm64 runtime: runtime-darwin-arm64 - platform: darwin-x64 runner: macos-15-intel helper: llvm-darwin-x64 runtime: runtime-darwin-x64 - platform: linux-x64 runner: ubuntu-24.04 helper: llvm-linux-x64-gnu runtime: runtime-linux-x64-gnu llvm_asset: LLVM-22.1.8-Linux-X64 use_zig: true - platform: linux-arm64 runner: ubuntu-24.04-arm helper: llvm-linux-arm64-gnu runtime: runtime-linux-arm64-gnu llvm_asset: LLVM-22.1.8-Linux-ARM64 use_zig: true - platform: windows-x64 runner: windows-2022 helper: llvm-win32-x64-msvc runtime: runtime-win32-x64-msvc use_zig: true - platform: linux-x64-musl runner: ubuntu-24.04 helper: llvm-linux-x64-musl runtime: runtime-linux-x64-musl llvm_asset: LLVM-22.1.8-Linux-X64 use_zig: true - platform: linux-arm64-musl runner: ubuntu-24.04-arm helper: llvm-linux-arm64-musl runtime: runtime-linux-arm64-musl llvm_asset: LLVM-22.1.8-Linux-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 use_zig: true runs-on: ${{ matrix.runner }} timeout-minutes: 30 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" - 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 - run: pnpm install --frozen-lockfile - name: Build native packages (POSIX) if: matrix.platform != 'windows-x64' run: | pnpm --filter @scriptc/${{ matrix.helper }} build:native 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 - uses: actions/upload-artifact@v4 with: name: native-${{ matrix.platform }} path: | packages/${{ matrix.helper }} packages/${{ matrix.runtime }} retention-days: 1 publish: name: Publish to npm needs: [check-release, build-native-packages] if: needs.check-release.outputs.should_release == 'true' runs-on: macos-15 timeout-minutes: 15 environment: Release permissions: contents: read id-token: write 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 - 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 {} + pnpm -r build - name: Check version sync run: | VERSION="${{ needs.check-release.outputs.version }}" for pkg in packages/runtime packages/runtime-darwin-arm64 packages/llvm-darwin-arm64 packages/runtime-linux-x64-gnu packages/llvm-linux-x64-gnu packages/runtime-linux-arm64-gnu packages/llvm-linux-arm64-gnu packages/runtime-linux-x64-musl packages/llvm-linux-x64-musl packages/runtime-linux-arm64-musl packages/llvm-linux-arm64-musl packages/runtime-wasm32-wasi packages/runtime-win32-x64-msvc packages/llvm-win32-x64-msvc packages/compiler packages/cli; do 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: Package and verify LLVM helper run: | TARBALL=$(pnpm --dir packages/llvm-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent) HELPER_TARBALL="$RUNNER_TEMP/$(basename "$TARBALL")" node scripts/verify-llvm-package.mjs "$HELPER_TARBALL" echo "HELPER_TARBALL=$HELPER_TARBALL" >> "$GITHUB_ENV" - name: Publish to npm run: | VERSION="${{ needs.check-release.outputs.version }}" # 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 # Dependency order, so each package's deps are resolvable the # moment it lands. pnpm pack rewrites workspace:* to the real # version; npm publish on the tarball handles OIDC. # Re-runs skip anything already on the registry at this version. # npm may finish a trusted publication asynchronously. In that # window a retry can report "previously staged" even though the # same package/version is about to become publicly visible. wait_for_published_version() { name="$1" for attempt in $(seq 1 12); do if npm view "$name@$VERSION" version >/dev/null 2>&1; then return 0 fi if [ "$attempt" -lt 12 ]; then sleep 10 fi done return 1 } publish_dir() { dir="$1" packed="${2:-}" name=$(node -p "require('./$dir/package.json').name") if npm view "$name@$VERSION" version >/dev/null 2>&1; then echo "$name@$VERSION already published, skipping" return 0 fi if [ -z "$packed" ]; then tarball=$(cd "$dir" && pnpm pack --silent | tail -1) packed="$dir/$tarball" fi if npm publish "$packed" $PROVENANCE --access public; then return 0 else publish_status=$? fi echo "$name@$VERSION publish failed; waiting for registry visibility before retrying" if wait_for_published_version "$name"; then echo "$name@$VERSION is now published; treating the publish as successful" return 0 fi return "$publish_status" } publish_dir packages/runtime publish_dir packages/runtime-darwin-arm64 publish_dir packages/llvm-darwin-arm64 "$HELPER_TARBALL" 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-wasm32-wasi publish_dir packages/runtime-win32-x64-msvc publish_dir packages/llvm-win32-x64-msvc publish_dir packages/compiler publish_dir packages/cli env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GitHub release is a tag, notes, and one asset: the surface # manifest (packages/compiler/surface-manifest.json — the machine- # readable listing of the surface the static tier compiles at this # version, regenerated here and verified against the committed file). # The platform helper ships through its npm package rather than as a GitHub # release asset, so this job runs AFTER a successful npm publish and never # gates it. The body is the CHANGELOG.md block between the # release:start/release:end markers, which RELEASING.md keeps on the # latest entry only. github-release: name: Create GitHub Release needs: [check-release, publish] if: needs.check-release.outputs.should_release == 'true' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write 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 byte-for-byte — 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: 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" \ --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 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}