Skip to content

Latest commit

 

History

History
50 lines (33 loc) · 13.6 KB

RELEASING.md

File metadata and controls

50 lines (33 loc) · 13.6 KB

Releasing

Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. @scriptc/runtime, every platform helper/runtime pack, the four @scriptc/libnode-<platform> packs, @scriptc/compiler, @scriptc/experimental, @scriptc/node, @scriptc/threads, and scriptc always publish together at the same version.

To prepare a release:

  1. Bump the version in packages/cli/package.json
  2. Run node scripts/sync-versions.mjs to stamp the same version into the runtime, helper, compiler, experimental, node, and threads packages, then pnpm manifest to restamp packages/compiler/surface-manifest.json with the new version, and commit both (the test suite's staleness guard fails on a version drift)
  3. Fold the ## Unreleased section of CHANGELOG.md into a new ## <version> entry (newest first, below ## Unreleased), and leave ## Unreleased empty for the next cycle
  4. Wrap the new entry in <!-- release:start --> and <!-- release:end --> markers; this marked block is also the GitHub release body
  5. Remove the <!-- release:start --> and <!-- release:end --> markers from the previous release entry; only the latest release should have markers
  6. With Zig on PATH, run SCRIPTC_CROSS=1 pnpm exec vitest run tests/harness/library-cross.test.ts and require the cross-target library conformance lane to pass
  7. Refresh the runtime profile on a quiet Linux x64 host: node scripts/refresh-runtime-profile.mjs --tsc-ts <tsc-ts checkout> --project <TypeORM>:tsconfig.bench.json --project <T3 server>:tsconfig.bench.json --project <VS Code>/src:tsconfig.bench.json, and include the new packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz (see the runtime profile paragraph below)
  8. Commit to main

CI (.github/workflows/release.yml) compares the version in packages/cli/package.json to what scriptc has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag v<version> and the GitHub release with the marked changelog entry as its body, and attaches surface-manifest.json — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see packages/compiler/src/coverage/surface-manifest.ts for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the @scriptc/compiler package as @scriptc/compiler/surface-manifest.json.

The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. Each LLVM helper package also carries the comptime evaluator, bin/scriptc-comptime, built with the helper. The native CLI package bundles its TypeScript checker and declarations, and depends on its platform's LLVM helper and runtime pack packages at the same version; the compiler resolves them beside itself. The standalone archives bundle the helper and runtime pack too. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of @scriptc/compiler and scriptc. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds.

Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The cli-profile job runs node scripts/refresh-cli-profile.mjs on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged profile. The profile keeps the functions that training never ran, with zero counts, so the CLI builds optimize them as cold code and place them after the code that ran. Every platform's CLI build uses that profile (SCRIPTC_NATIVE_PROFILE_USE), and the GitHub release attaches it as scriptc-<version>-cli.profdata.gz. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile packages/compiler/native/cli.profdata.gz, which local and CI builds of the native compiler also use. The nightly profile-refresh workflow (profile-refresh.yml) refreshes the committed profile from main: it collects one with the committed layout, builds the native compiler with it to report the match, and opens a pull request that replaces packages/compiler/native/cli.profdata.gz. To refresh it by hand, run node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout> on a quiet Linux x64 host and commit the new profile on its own. CI's native_profile job builds the native compiler with the committed layout and profile and reports the share of the profiled functions that match, with a warning below 95%; a stale profile does not fail it. It fails when the profile cannot be judged: missing, unreadable, or describing a build laid out differently from the one that collected it.

Builds of the native compiler also order its functions by the committed layout packages/compiler/native/cli.layout.gz: the functions that --version and rebuilds of unchanged programs run come first, then the ones other builds run, ordered by the share of the layout's training builds that run each, so a run maps fewer pages of the executable. Functions the layout does not list keep their usual order. Function names of closures and generated helpers change with nearby edits, so the cli-profile job also collects a layout on the release commit (scripts/refresh-cli-layout.mjs), and the CLI builds use it (SCRIPTC_NATIVE_LAYOUT). Instrumented builds use the same layout as optimized builds, because a function laid out in a different partition than in the instrumented build no longer matches its profile record. The job therefore collects the layout first and then builds the instrumented compiler for the profile with that layout. Refresh the committed layout the same way, with node scripts/refresh-cli-layout.mjs, before the profile, and commit each on its own.

The Linux x64 runtime pack optimizes its speed-flavor units with the committed runtime profile packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz. Runtime functions whose code changed since the profile was collected are optimized as without a profile. The pack manifest records how the profile matched (flavors.speed.runtime_profile): the matched and changed functions, per unit object (annotated, mismatched) and by name (annotated_names, mismatched_names), and the share of the profile's counts on changed functions. The pack compiles some units more than once, and a function whose control flow differs between two of those builds matches the profile's record in one of them only, however fresh the profile is, so a function counts as changed by name only when no object's build of it matches. packages/runtime-pack-common/test/runtime-profile.test.ts fails when more than 10% of the functions the profile describes changed, counted by name, or when changed functions hold more than 5% of the counts, and names the functions with the most counts. Refresh the profile in each release commit (step 7), and between releases when that test fails, and commit the new profile on its own.

The refresh runs in a built checkout with its Linux x64 LLVM helper and runtime pack, and needs a tsc-ts checkout (https://github.com/vercel-labs/tsc-ts) after npm ci and npm run fetch:upstream. The three projects of step 7 are the typeorm, t3code-server and vscode entries of packages/tsc-ts/profiles/projects.json in that checkout, which pins each one's repository, commit, install command, directory and the tsconfig.bench.json to write. Without a libnode pack in SCRIPTC_LIBNODE, the benchmark run leaves out the island start-up workloads, and the profile then holds no counts from them. Only Linux x64 executables built with --optimization=speed change with the profile, so measure a refresh with node scripts/bench-runtime.mjs --baseline-optimization=speed --candidate-optimization=speed: the zero-cost gate builds at release and shows only the corpus programs that ask for speed.

Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. Each published package must have a GitHub Actions trusted publisher for release.yml and the Release environment. A missing configuration fails before upload. Re-runs skip package versions already present on npm, so a partially published release can be resumed safely.

libnode packs

The @scriptc/libnode-<platform> packs (linux-x64-gnu, linux-arm64-gnu, darwin-arm64, darwin-x64) carry the Node.js shared library that programs with Node islands load in process, plus the scriptc bridge compiled against it. They version in lockstep with the other platform packages (sync-versions.mjs stamps them); the Node release they contain is pinned separately in packages/libnode-common/libnode.json, which must match .node-version and internal/compatibility/node-v24.json. Each pack's manifest.json records the Node version, NODE_MODULE_VERSION, the bridge ABI and the size and SHA-256 of both libraries; a Linux pack also records that it links the C++ runtime statically.

A Node --shared build takes about half an hour on 8 cores, so packs never build in pnpm -r build or the test suites. .github/workflows/libnode-packs.yml builds them on matching hosts when the pin, the bridge (packages/libnode-common/bridge/, packages/runtime/src/scr_node_bridge.h) or the pack packages change, or on manual dispatch; the release job calls the same workflow, which restores every platform from its cache. Linux packs build in the manylinux_2_28 images (glibc 2.28, gcc-toolset-14) on native x64 and arm64 runners; Node disables its startup snapshot when cross-compiling, so arm64 is never cross-built. Each platform's libnode build is cached by Node release, configure flags, platform and toolchain; a bridge-only change restores libnode from the cache and rebuilds only the bridge. Every pack is smoke-tested by packages/libnode-common/smoke/smoke.c, which loads it the way the runtime does, and uploaded as the libnode-<platform> artifact, with zstd copies of both libraries as libnode-<platform>-zst. The Linux x64 and macOS test lanes of ci.yml restore the cached build for their platform through .github/actions/libnode, assemble the pack (build.mjs --from-cache, which never builds Node) and set SCRIPTC_LIBNODE, so the Node island tests run; without a cached build they fall back to the pinned development pack (packages/libnode-common/dev-pack.json). packages/libnode-common/build-pack.mjs assembles a pack from any matching libnode and headers, the bridge-only rebuild path.

Linux packs link the C++ runtime statically, because the manylinux gcc-toolset's libstdc++ is newer than the one on the oldest host scriptc supports (glibc 2.34, and the libstdc++ of the GCC 11 it ships). A pack that inherited the toolset's dependency would install and then fail to load there. packages/libnode-common/libnode.json adds Node's --partly-static to the Linux configure flags, which links -static-libstdc++ and -static-libgcc into libnode.so, and build-pack.mjs links the bridge the same way (hiding the archive's symbols) whenever its libnode carries its own C++ runtime. build.mjs fails the build if config.gypi lacks those link flags or if the staged libnode still references a GLIBCXX_/CXXABI_ symbol version or needs libstdc++.so, and verify.mjs refuses a Linux pack whose manifest does not record the static link. After the build, floor.mjs loads the pack in an AlmaLinux 9 container (glibc 2.34, the oldest runtime docs/content/docs/platforms.mdx states), compiles and runs smoke/smoke.c there with that distro's compiler, and requires both libraries to need no C++ runtime, so CI proves a pack runs on a supported host and not only on the runner that built it. The dev-pack.json fallback is CI-only and never published; its prebuilt libnode keeps the bridge dynamically linked.

To build one pack by hand on its own platform (it needs Python 3.9+, Ninja, a C++20 compiler and zstd; on Linux, build in a manylinux_2_28 container for a distributable pack):

pnpm --filter @scriptc/libnode-linux-x64-gnu build:native
node packages/libnode-common/scripts/verify.mjs packages/libnode-linux-x64-gnu
node packages/libnode-common/scripts/smoke.mjs

The packs publish with every release as opt-in packages. scriptc does not depend on them: only a program that calls into Node.js installs the pack for its host (npm i @scriptc/libnode-<platform>), and SC8005's hint names the package and that install line. Each pack needs its own npm trusted publisher for release.yml and the Release environment, like the other packages. The release job calls libnode-packs, downloads the four libnode-<platform> artifacts with the native packages, and publishes them ahead of the CLI. ci.yml installs a scratch scriptc from the release-shaped tarballs with and without the host pack, and requires the Node island program to build and run with it and to fail with SC8005 without it (scripts/smoke-libnode-opt-in.mjs). The installed command is the natively compiled compiler, so the script skips, with that reason in the job log, for as long as the native driver under packages/compiler/src/native has no libnode step.