House release workflow (.github/workflows/house-release.yml). A maintainer starts it by hand from the Actions tab, on main.
The GitHub Action, the CLI and the MCP server (@resnovas/action, @resnovas/smartcloud and @resnovas/smartcloud-mcp) form one fixed release group, so they always share a version. The libraries under packages/ are bundled into them and are never released on their own.
Release configuration
The workflows and the tools undertools/release/ are synced from Resnovas/.github, the same in every repository that ships releases. What this repository ships is written once, in release.config.json at the root:
Change the file, not the workflows: the workflows are managed by the house sync.
tools/release/prepare-cli.ts is this repository’s own, since it writes the npm package’s manifest and README.
Action bundle budget
pnpm bundle:check builds the action and checks dist/index.js against the raw and gzip byte limits in bundle-size.json. CI runs the same command in its build job, which feeds the required check context on pull requests and merge queue groups. Changes that skip the build job also skip the bundle check.
The checked-in baseline is 3,881,093 raw bytes and 796,299 gzip bytes, with limits of 4,269,203 and 875,929 bytes respectively (10% headroom, rounded up). Gzip uses level 9. Only dist/index.js counts, including its linked source-map comment; the source map and other release assets do not. This measures the CI bundle before release version stamping and PostHog processing.
The command prints a table of baseline, current size, signed change, limit and remaining budget, and appends it to the GitHub job summary. Either limit exceeded fails the build with the exact overage. The delta is against the reviewed baseline in the JSON file, not a freshly rebuilt base branch, so it needs no extra checkout, network access, secrets or write permissions.
Investigate unexpected growth before changing a limit. For intentional growth, include the measured sizes and the reason in the pull request, then update the baseline and limits explicitly in bundle-size.json; CI never rewrites them. Run pnpm bundle:check again to verify the reviewed limits.
What a release does
- Nx reads the conventional commits since the last
v*tag and picks the bump:featis minor,fix,perfandrevertare patch, and a breaking change is major. Other types appear in the notes, or are hidden, without bumping. Aspecifierinput overrides this. - Nx writes the new version to the three apps’
package.jsonfiles, and the action is bundled intodist/index.jsfrom that source. - The workflow commits both, removes the vendored
externals/directory (every run of the action downloads the whole tagged tree), and signs the commit off asgithub-actions[bot]. The commit is on no branch: its parent is the head ofmain. - Only the
v<version>tag is pushed.mainnever receives a release commit or the bundle, so no branch protection is bypassed. - The
v<major>tag moves to the new tag, which is what workflows pin, and thedraft-releasejob writes the release notes to a draft GitHub release on it. Nx writes the same notes to the changelog files, below. - The
changelogsjob opens a pull request that brings the changelog files tomain. - Another job checks out the tag, rebuilds the action and stops unless the bundle matches the tagged one. It then bundles the CLI and MCP server, with the released version stamped in, and publishes
@resnovas/smartcloudto npm with provenance, through trusted publishing. - A last job, the house
attest.ymlworkflow, signs build provenance for the checkeddist/index.js, binds the action’s SBOM to it, and attaches an SPDX SBOM of each app (resnovas-action-<version>.spdx.json,resnovas-smartcloud-<version>.spdx.jsonandresnovas-smartcloud-mcp-<version>.spdx.json) to the draft release. - The
publish-releasejob then publishes the release. It stays a draft until the SBOMs are attached, because Resnovas repositories use immutable releases: once published, a release’s assets and tag are locked. If a job before it fails, the release stays a draft; fix the cause and re-run the failed jobs.
tools/release/sourcemaps.ts has PostHog’s CLI inject a chunk id into the bundle and upload its map to error tracking as the release smartcloud@<version>, then deletes the map. Neither the tag nor the npm package ships a map: the tagged tree is downloaded by every run of the action, and PostHog resolves stack traces from its own copy. The maps name sources relative to the bundle, never a path on the machine that built it.
The tag is the only record of the current version: the apps’ versions on main stay 0.0.0, and a local build reports 0.0.0.
Release notes and changelogs
The GitHub release on each tag holds the main release notes. Nx writes the same notes, rendered bytools/release/changelog-renderer.ts (Nx’s default renderer with its emoji replaced by words), to:
The libraries under
packages/ get no changelog of their own: they are never published, and their changes appear under the apps that bundle them.
The release commit is never on main, and main takes changes only through pull requests, with signed commits, through the merge queue. So the changelogs job, after the release job has tagged and released, commits the files to a release/changelogs-v<version> branch through the GitHub API as the Resnovas Bot app, which makes GitHub sign the commit, and opens a pull request titled chore(release): changelogs for v<version>. The commit is signed off by the app’s bot account for the DCO check, and the house preset trusts that account. Review and merge it like any other pull request, before the next release: each release adds its entry to the files as main holds them.
The job mints the app’s token itself and runs none of the repository’s code or dependencies; the release job, which installs and runs them, hands it the files as an artifact and never sees the app’s key. If the pull request cannot be opened, the files stay in the run’s changelogs artifact for a week.
Verifying a release
Every release’sdist/index.js carries a signed build provenance attestation, which names the workflow run that built it. Check a copy of the bundle, for example from a v<version> tag, with the GitHub CLI:
pnpm sbom from the tag’s lockfile. The action’s SBOM is also attested against dist/index.js; add --predicate-type https://spdx.dev/Document/v2.3 to the command above to check it. To generate the same SBOMs locally:
npm audit signatures checks.
Running a release
Open Actions, choose release, then Run workflow onmain.
Run it once as a dry run, check the notes, then run it again with
dry-run cleared. When no commit since the last tag calls for a release, the workflow says so and stops.
Release preview on pull requests
A release is cut frommain long after most pull requests merge, so it is easy to lose track of what a change will do to the next version. The release preview answers that on the pull request itself: every pull request gets a comment saying which version the next release would be if it were merged now, what its own commit adds, and the release notes that release would carry. Nothing is released by it; it is the same dry run as pnpm release:dry-run, run for you.
What you see
TheHouse release preview workflow (.github/workflows/house-release-preview.yml) runs when a pull request is opened, reopened or pushed to, and when its title or base branch changes. It leaves one comment headed Release preview, and edits that same comment on later runs rather than adding more. The same report is in the job summary of the run (open Checks, then release preview).
A comment for a pull request titled feat(labels): colour aliases reads like this:
- The next version counts every commit on
mainsince the last stablev*tag, plus this pull request. If another merged pull request already calls for a minor release, afixhere still shows the minor version: the release takes the biggest bump of all of them. - Would land as is the commit the merge queue would put on
main. Pull requests are squash-merged, so it is the pull request’s title with its number added, over its commits’ messages. That is why the title matters:featis a minor release,fix,perfandrevertare patch,!after the type orBREAKING CHANGE:in any commit message is major, and other types (docs,chore,ci,refactor,testand so on) change nothing. - Nx counts a commit only for the files it changes. A pull request that changes only workflows,
tools/, docs or tests releases nothing whatever its title, and the preview shows the version the other commits call for. - No commit calls for a new version means that, with this pull request merged, there would be nothing to release yet.
- The first release is shown until a stable v2 release exists. Its version,
2.0.0, is set by hand when the release workflow runs (see below), so the preview shows that version and the notes it would carry. - Release notes are what the GitHub release and the changelog files would say. Very long notes are cut short in the comment; the job summary has them in full.
When the comment is missing or says something went wrong
The preview never blocks a pull request: it is not part of the required
check, a failure becomes a warning, and the workflow reports success either way.
How it works
The workflow checks out the pull request’s merge commit with the full history and tags, and runstools/release/release-preview.ts. The script swaps the merge commit for the squash commit that would land, then asks Nx release for the version and notes in dry-run mode, exactly as the release workflow would, without building, tagging or writing anything. The job that runs this code has a read-only token. A second job, which runs no code from the pull request, posts or updates the comment with the only write access in the workflow (pull-requests: write).
Run the same preview locally with pnpm release:preview. Locally it previews the checked-out commit as it is, since it has no pull request title to squash with.
The first v2 release
v1’s tags, up to1.0.0-beta.8, have no v prefix, so Nx finds no earlier v* tag to count from. Run the first release with:
specifier:2.0.0first-release: ticked
1.0.0-beta.8), so they cover the v2 work rather than the whole history. From then on, leave both inputs empty.
To preview it locally:
Before the first release
- The workflow reads the Resnovas organization secret
POSTHOG_CLI_API_KEY: a PostHog personal API key with theerror tracking writeandorganization readscopes, in PostHog’s EU cloud. It passes the key to PostHog’s CLI under the same name. The key names no project:tools/release/sourcemaps.tssetsPOSTHOG_CLI_PROJECT_IDto the Smartcloud project (285077), so one organization key serves every repository that uploads to its own project. Scope the key to the organization, or list each project that uploads with it; a key limited to other projects fails the upload. Without the secret the maps are still deleted, but none is uploaded, so errors from that release show bundle lines rather than source lines. A dry run never uploads. - The
changelogsjob authenticates as the Resnovas Bot GitHub app (resnovas-smartcloud), from the organisation variableRESNOVAS_BOT_APP_IDand secretRESNOVAS_BOT_PRIVATE_KEY. The app must be installed on this repository with contents and pull requests write access. A real release stops before tagging when either is missing. - The workflow pushes tags with its own token. If a tag ruleset protects
v*, let GitHub Actions bypass it for tag creation and updates. - Trusted publishing needs
@resnovas/smartcloudon npm to trust this repository’shouse-release.ymlworkflow.
Nightly pre-releases
TheHouse nightly workflow (.github/workflows/house-nightly.yml, running tools/release/nightly.ts) cuts a pre-release from main every night at 02:30 UTC, and can be started by hand from the Actions tab (tick dry-run to see the version only). It cuts nothing on a night when main has not moved: every release commit, nightly or stable, has the main commit it was cut from as its parent, so a commit that already has one is skipped.
A nightly is named v<version>-nightly.<yyyymmdd>, with .1, .2 for a second or third that day. Before the first stable 2.x release the version is 2.0.0; after it, the next patch of the newest stable release, so a nightly always sorts before the release it previews. It is built like a release, a commit on no branch holding the bundled action with the version stamped in, and gets a GitHub pre-release with generated notes that is never marked latest. Nothing is published to npm and no changelog is written.
Until the first stable 2.x release exists, v2 moves to each nightly, so workflows pinned to resnovas/smartcloud@v2 run the newest nightly. From then on only stable releases move it.
Nightlies never decide a stable release’s version or notes: releaseTag.strictPreid in nx.json makes nx release without a preid resolve only stable v* tags, so the next release still counts, and writes its notes, from the last stable release, and its own tag never clashes with a nightly’s.