Release Process
This document covers the CI/CD pipeline, versioning policy, the Changesets-based release workflow, and the procedure for adding a new package to the workspace.
CI/CD & Release Flow
GitLab CI with stages: build → test → release → deploy.
Pipeline runs on MRs and main. Jobs include: build, test:unit, typecheck, lint, and the sharded integration jobs described below.
Integration test sharding
The old monolithic test:integration job (~45 min) is replaced by three parallel jobs that all inherit from .integration_base (full service set: Redis + Postgres + real Temporal, pinned to tags: [amd64]):
test:integ:e2e—@helix-agents/e2e, 4 vitest shards. The e2e shard runner (packages/e2e/scripts/run-both-vitest.mjs) forwards--shardto both of its vitest configs.test:integ:dbos—@helix-agents/runtime-dbos, 2 shards (runs serially viasingleFork).test:integ:rest— everything else, using turbo negative filters (--filter='!@helix-agents/e2e' --filter='!@helix-agents/runtime-dbos') so no package is dropped.
Each shard runs in its own pod. A test failure isolates to that shard; re-running one ~12-minute shard is far cheaper than re-running the old ~45-minute monolith. The old global TURBO_CONCURRENCY cap is gone.
test:integration:nightly-matrix (schedule-only, runs the full 9-backend parity matrix) is unchanged.
format:check (disabled)
A format:check job exists in the pipeline but is currently disabled (when: never) pending a repo-wide pnpm run format pass to clear existing prettier drift. To enable it: remove the when: never line and uncomment the two trigger rules.
Release
The old two-step flow (release:version opened a release/next MR; release:publish fired when CHANGELOG changed on merge) is replaced by a single manual release job in the main pipeline.
On a green main pipeline, click release. The job:
- Runs
changeset versionto consume changesets and bump package versions. - Runs
changeset publishto publish packages to the GitLab npm registry. Changesets detects pnpm and publishes withpnpm publish, which rewrites every internalworkspace:^dependency to^<current version>in the publishedpackage.json. - Commits
chore: version packages [skip ci]and pushes the bump + tags straight tomain.
Publish runs before the push, so a publish failure leaves main untouched and the job is safely re-runnable (changeset publish skips already-published versions).
One-time setup required: pushing to the protected main branch requires a project access token with write_repository scope, added to main's "Allowed to push" list and stored as the masked CI/CD variable RELEASE_PUSH_TOKEN. CI_JOB_TOKEN cannot push to a protected branch; the job's push step is inert until that variable exists.
Versioning policy (CC25, round 30)
Helix Agents is pre-1.0 and follows a "MINOR = observable behavior change" policy — stricter than the loose "pre-1.0 breaking changes are OK" convention many projects use:
MAJOR: not used for
0.xpackages. A package that is already at>= 1.0.0(several are, through Changesets' peer-dependency cascade:ai-sdk,agent-serverand theruntime-*packages) takesmajorfor a breaking change, as precedent changesets did (e.g. theruntime-cloudflaresandbox subpath move).MINOR: bumped for BOTH type-signature additions AND observable behavior changes (including runtime-rejection of previously-valid inputs, changed default semantics, removed reference-identity on returned objects, and metric/log schema changes). Consumers must read the changeset body before upgrading a minor version — "MINOR" does NOT imply "fully backwards compatible."
PATCH: bug fixes, doc-only changes, purely internal refactors with no consumer-observable effect.
Peer-dependency cascade. .changeset/config.json sets ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange: true. A package whose peer dependency is bumped is bumped itself only when the peer's new version falls outside the declared range. Without the option, Changesets gave every peer dependent of a bumped package a major, even for an in-range minor; that is how ai-sdk and the runtime-* packages reached their current major versions. With it:
- A peer at
>= 1.0.0that takes a minor or patch stays inside^x.y.z, so its peer dependents get no forced bump (only theupdateInternalDependenciespatch, when they also depend on it directly). - A peer at
0.xthat takes a minor leaves a caret range (^0.13.0excludes0.14.0), so a dependent that declares the peer asworkspace:^or^0.xstill takes a major. An open range such as>=0.13.0(whatruntime-jsandruntime-dbosdeclare for their store peers) stays in range. A0.xpatch stays in range either way. - A peer's major leaves a caret range, so a
workspace:^/^xdependent still takes a major. - An open range absorbs even a peer's major:
>=0.13.0accepts1.0.0, soruntime-js/runtime-dbosget no bump when a store they peer-depend on ships a breaking change. The author of that change must add an explicit changeset for the open-range dependents.
Run pnpm changeset status --verbose before merging a changeset that bumps a peer-depended-on package, and state any resulting major in the changeset body and the MR description. The option lives under ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, so a @changesets/cli upgrade may rename or drop it: re-run changeset status after bumping Changesets and confirm the cascade still behaves as described here.
The rationale: pre-1.0 we'd rather surface behavior changes via changeset body than force a MAJOR bump cadence that dilutes the signal when a truly breaking API change lands post-1.0. Each changeset's "Observable behavior changes" section is load-bearing — consumers who skim CHANGELOG.md for version numbers alone will miss these.
Contributors: if your changeset adds runtime rejection of input that used to be accepted, changes a default value, or alters the content/shape of a log event or metric, use minor and describe the change explicitly under an "Observable behavior changes" section in the changeset body.
Release Process (Changesets)
Releases use Changesets. The release flow is:
Contributors add changesets — Run
pnpm changesetin your MR, select affected packages, choose bump type (patch/minor/major), describe changes. This creates a.changeset/*.mdfile. Only published packages are versioned: private packages (@helix-agents/e2eand the examples) are skipped (privatePackages.version: false), and a changeset must not name a private package together with a published one; Changesets rejects that mix. Thechangesets:checkMR job (pnpm run changeset:check) catches it before merge.Changesets accumulate — Multiple MRs can add changesets before a release.
Maintainer triggers release — On the green
mainpipeline, manually click thereleasejob (see CI/CD section above).
Local Release Flow (Faster)
Running the release locally skips waiting for CI:
# 1. Ensure you're on latest main
git checkout main && git pull origin main
# 2. Consume changesets and bump versions
pnpm changeset version
# 3. Publish packages to the registry
pnpm changeset publish
# 4. Commit and push the bump + tags
git add .changeset/ packages/*/CHANGELOG.md packages/*/package.json
git commit -m "chore: version packages [skip ci]"
git push origin main --follow-tagsSee .gitlab-ci.yml and the files it includes under ci/ for the full pipeline configuration.
Adding a Package
- Create
packages/<name>/ - Add
package.jsonwith name@helix-agents/<name>, version0.0.1 - Add
publishConfigpointing to GitLab registry - Add
tsconfig.jsonandtsup.config.ts(copy from existing package) - Depend on other workspace packages with
"@helix-agents/<pkg>": "workspace:^"(never a bare version or*— pnpm only links workspace packages through theworkspace:protocol), then runpnpm installand commit the updatedpnpm-lock.yaml. - Add a changeset for the new package:
pnpm changesetand select the new package asminor. The Changesets CLI is the only supported way to ship a version bump for a new package; do not editpackage.jsonversions by hand. See the Release Process (Changesets) section above.