Releasing
Releases are lockstep: all eleven modules carry one version, because they ship as one
stack behind one Helm chart. A single v* tag builds and publishes everything.
Every component is pinned to that version explicitly, whether or not it changed in the
release. Nothing resolves a version at deploy time: the chart’s image tags are written out in
full rather than falling back to appVersion, and a values file copied out of the chart stays
on the version it was copied from. scripts/set-version.sh writes all 26 of those places, and
the release refuses to build unless they all agree with the tag.
How a release happens
Section titled “How a release happens”Releases are driven by release-please. You never tag by hand in the normal flow:
- You merge work into
mainusing Conventional Commit messages. .github/workflows/release-please.ymlkeeps a single open Release PR up to date. It accumulates every change since the last release, works out the next version from the commit prefixes, and rewritesCHANGELOG.mdalong with all eleven module manifests.- Nothing is published while that PR is open. Reviewing it is reviewing the release.
- Merging the Release PR is the release. release-please tags the merge commit, opens the
GitHub Release with the changelog, and hands off to
release.yml, which builds and publishes everything.
What the workflow needs to open that PR
Section titled “What the workflow needs to open that PR”release-please opens the Release PR as the workflow’s own identity, and by default that identity is not allowed to. A repository where Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests is unchecked fails the run with:
Error: release-please failed: GitHub Actions is not permitted to create or approve pull requests.Either of two things clears it:
- A
RELEASE_PLEASE_TOKENsecret holding a fine-grained PAT or GitHub App token withcontents: writeandpull-requests: writeon this repository. The workflow prefers it and falls back toGITHUB_TOKENwhen it is absent. This is the better option: a pull request opened byGITHUB_TOKENstarts no workflow run, so the Release PR would otherwise sit there with no CI against the release commit. - Ticking the setting, which lets
GITHUB_TOKENopen the PR. Simpler, but the Release PR still gets no CI run.
The setting is also available at organisation level, where it overrides the per-repository one.
Where the release runs
Section titled “Where the release runs”Every release job runs on the self-hosted fleet (runs-on: self-hosted), the same runners
ci.yml uses. This is not a preference. A GitHub-hosted job bills against the account, and when
that billing lapses the job is not queued — it is failed, in about four seconds, with:
The job was not started because recent account payments have failed or yourspending limit needs to be increased.The release workflows were left on ubuntu-latest when CI moved to the fleet, so every release
run after that died there with nothing to show for it. If the fleet is down, the release waits
for it rather than falling back — which is the right failure, because a release that waits is
recoverable and a release that half-published is not.
Commit prefixes
Section titled “Commit prefixes”| Prefix | Effect on the version | Appears in the changelog |
|---|---|---|
fix: | patch — 1.0.0 → 1.0.1 | Bug Fixes |
feat: | minor — 1.0.0 → 1.1.0 | Features |
feat!:, or a BREAKING CHANGE: footer | major — 1.0.0 → 2.0.0 | Breaking, at the top |
perf: refactor: deps: build: ci: docs: | none on their own | Its own section |
test: chore: | none | Hidden |
A stretch of docs:-only commits leaves the Release PR at the same version rather than inventing
one. The version only moves when something that changes behaviour lands.
Scopes are optional and free-form — feat(core): …, fix(dashboard): … — and show up as a prefix
on the changelog line.
Releasing by hand
Section titled “Releasing by hand”The tag trigger still works as an escape hatch, for a version release-please would not compute on its own:
scripts/set-version.sh 1.1.0git commit -am "chore: release 1.1.0"git pushgit tag v1.1.0 && git push origin v1.1.0scripts/set-version.sh stamps the version into every component:
| Kind | Places |
|---|---|
pyproject.toml | platform, core, the four analyzers, task-scheduler — seven files |
package.json | dashboard, packages/platform-ui, docs |
version.txt | the repository’s own version |
| Annotated lines | the chart’s version and appVersion, every image tag in values.yaml, values-dev.yaml and values-prod.yaml, and the terraform provider’s build-time default |
The last row is any line carrying an x-release-please-version comment, whatever the file’s
syntax — the same rule release-please’s own “generic” updater follows, applied by
scripts/version-annotated.py. values-eks.yaml has no image block of its own and inherits the
pins from values.yaml.
| Flag | What it does |
|---|---|
--show | list the current version of every place |
--check 1.1.0 | exit 1 unless they all already agree |
--audit | exit 1 if this script and release-please’s extra-files disagree about which files carry a version |
--audit exists because the two lists are maintained separately and drifting apart fails
silently in the worst direction: release-please stamps a file the check never looks at, so the
release gate passes on a release whose manifests disagree. That is not hypothetical —
OpenTremor-analyzer-python-code-change sat outside the check for its whole existence. CI runs
both --audit and --check on every pull request.
The terraform provider still takes its version at build time through
-ldflags "-X main.version=…", and the release workflow passes the tag there, so the ldflags
value always wins for a published binary. Its var version default is stamped too, which is
what makes a go build ./... out of a release tag report that release rather than dev.
What a release publishes
Section titled “What a release publishes”.github/workflows/release.yml runs once release-please has tagged — or on any v*.*.* tag
pushed by hand:
version— resolves the version and runsset-version.sh --checkagainst it. A release whose manifests disagree fails here, before anything is built or pushed. Under release-please they always agree; the gate matters for the hand-tagged path, and catches a botchedextra-filesconfig in either. It also catches a chart whose pinned image tags point at images this release is not going to build.ci— the full test suite, called as a reusable workflow. It is the same run a pull request gets, so a tag can never ship code that hasn’t passed it.images— builds and pushes four images to GHCR.chart—helm packageat the release version, pushed as an OCI artifact.release— cross-compiles the terraform provider for linux/darwin/windows on amd64/arm64 and attaches the binaries plus aSHA256SUMSfile. release-please has normally created the GitHub Release already, so this appends to it rather than replacing its changelog; a hand-pushed tag has no release yet, and one is created.
| Artifact | Location |
|---|---|
| Core (bundles platform + all three analyzers) | ghcr.io/tguisep/opentremor-core:<version> |
| Dashboard | ghcr.io/tguisep/opentremor-dashboard:<version> |
| Docs | ghcr.io/tguisep/opentremor-docs:<version> |
| Task scheduler | ghcr.io/tguisep/opentremor-task-scheduler:<version> |
| Helm chart | oci://ghcr.io/tguisep/charts/opentremor:<version> |
| Terraform provider binaries | GitHub Release assets |
A version with a pre-release suffix (v1.1.0-rc.1) publishes normally but never moves the
latest tag, and marks the GitHub Release as a pre-release.
Deploying a release
Section titled “Deploying a release”helm install opentremor oci://ghcr.io/tguisep/charts/opentremor --version 1.1.0 \ --set config.auth.apiKey="$(openssl rand -base64 32)" \ --set mongodb.enabled=trueImage tags are pinned in the chart’s values, so a chart at 1.1.0 pulls 1.1.0 images without any
extra --set — and helm show values oci://ghcr.io/tguisep/charts/opentremor --version 1.1.0
states outright which image every component runs, rather than leaving it to be inferred from
appVersion. The templates keep a | default .Chart.AppVersion fallback, so setting a tag back
to "" still resolves rather than rendering ":".
Pinning one component to something else is the usual --set:
helm upgrade opentremor oci://ghcr.io/tguisep/charts/opentremor --version 1.1.0 \ --set dashboard.image.tag=1.1.1The published images bake in the chart’s path-prefixed ingress shape — the dashboard at
/dashboard, docs at /docs, and same-origin API calls. That is a build-time decision, not
a runtime one: Next.js inlines NEXT_PUBLIC_* and its basePath when the image is built. A
deployment that puts each app on its own subdomain needs its own images built with different
arguments, which is exactly what the single-VM compose stack does locally through
scripts/autodeploy.sh.
Registry access
Section titled “Registry access”The repository is private, so its GHCR packages are private too. A cluster needs a pull secret before it can fetch them:
kubectl create secret docker-registry ghcr-pull-secret \ --namespace opentremor \ --docker-server=ghcr.io \ --docker-username=<github-username> \ --docker-password=<PAT with read:packages>The values-dev.yaml and values-prod.yaml profiles already reference this secret by name.