Publishing a release¶
The Release workflow publishes three container images (ghcr.io/sanderdw/iceberg-data-platform-{portal,users,notebook}), the Helm chart (oci://ghcr.io/sanderdw/charts/iceberg-data-platform) and the source and installation archives. Local tooling only prepares artifacts.
| Channel | Published by | Release | Image tags | Installer |
|---|---|---|---|---|
| Stable | vX.Y.Z tag on main |
vX.Y.Z, marked Latest if highest |
:X.Y.Z, :latest |
/releases/latest/download/install.sh or /releases/download/vX.Y.Z/install.sh |
| Branch preview | Run workflow on the branch | Prerelease SLUG-RUN_ID-ATTEMPT plus rolling SLUG-preview |
:SLUG-RUN_ID-ATTEMPT |
/releases/download/SLUG-preview/install.sh |
Every installer is pinned to its own release and image tag. Previews never become Latest and never write :latest. A preview also pushes the chart as version X.Y.Z-SLUG-RUN_ID-ATTEMPT, with any . or _ in the slug turned into - because chart versions are SemVer. Its images default to the preview's tag: helm install ... --version X.Y.Z-SLUG-RUN_ID-ATTEMPT.
Publish a branch for testers¶
A branch publishes on demand, either through Actions → Release → Run workflow or with gh workflow run Release --ref BRANCH. To publish a branch on every push, add it under on.push.branches in .github/workflows/release.yml.
The workflow runs the full CI suite and builds AMD64 and ARM64 images. Only then does it publish the exact prerelease and update the SLUG-preview installers. A failed run leaves the previous preview in place.
A preview also builds the Conversational BI, dbt and Ingestion (dlt) images from the same commit, tagged :SLUG-RUN_ID-ATTEMPT, so its installer can add all three extensions without their own releases. The platform installer doesn't add OpenMetadata, so a core preview doesn't build its image; an OpenMetadata preview publishes its own installer.
SLUG is the branch name in lowercase, with characters other than letters, digits, ., _ and - replaced by -, up to 60 characters. Branch names that start with v plus a digit are rejected. The workflow keeps the five newest builds per branch. Delete the SLUG-preview release after merging.
To build a pinned bundle locally without publishing:
uv run python -m scripts.release --build --install --release-tag BRANCH-123-1 --image-tag BRANCH-123-1 --extension-image-tag BRANCH-123-1
Publish a stable release¶
- Update the version in
pyproject.toml,package.json, both fields inpackage-lock.json,uv.lock(uv lock) and the API versions inserver/app.pyanduser_portal/app.py, andversionandappVersionindeploy/helm/iceberg-data-platform/Chart.yaml.npm run check:releaserejects any mismatch. Updatedocs/changelog.md, add the release notes indocs/releases/X.Y.Z.mdand tonavinzensical.tomland update the version in the install examples, including the chart versions (the platform's and the extensions') indocs/install-kubernetes.md, the release notes and.agents/skills/kubernetes-install/SKILL.md. Record Bridge contract changes incontracts/bridge/CHANGELOG.mdand the version inserver/bridge.py(see COMPATIBILITY.md); the Bridge has no release of its own. - Run the checks and the dependency audits:
uv export --locked --all-groups --no-hashes --no-emit-project --format requirements-txt -o /tmp/iceberg-requirements.txt
uvx pip-audit==2.10.1 -r /tmp/iceberg-requirements.txt --no-deps --disable-pip
npm audit
- Merge into
main, wait for CI, then release each extension whose version changed. The installation bundle installs the extensions from their own releases, so a stable core release requires, for each one, the tag<id>-v<version>of the version in itspyproject.toml, unchanged bundled files since that tag, and public images: - Conversational BI:
conversationalbi-v<version>, withcompose.yamlandscripts/setup.pyinextensions/conversationalbi. - dbt:
dbt-v<version>, withcompose.yaml,scripts/setup.pyandskills/inextensions/dbt. - Ingestion (dlt):
ingestion-v<version>, withcompose.yaml,scripts/setup.pyandskills/inextensions/ingestion.
A branch preview skips this check, because it builds the extension images itself. OpenMetadata isn't in the installation bundle; it has its own installer, so this check doesn't cover it. Release it before the core when its version changed, so the core's release notes can point to it.
4. Tag the core commit on main:
The workflow rejects a tag that doesn't match the project version or isn't on main. It then:
- runs the checks and a Gitleaks scan of the source archive
- builds the images on native AMD64 and ARM64
- packages the Helm chart, pushes it to
oci://ghcr.io/sanderdw/charts/iceberg-data-platformand publishes the GitHub release with the archives, the chart andSHA256SUMS. A new chart package starts private on GHCR, like a new image: make it public once and rerun the job - confirms that the
latestinstaller URLs serve the new version
To check a release before merging, run Actions → Release candidate → Run workflow. It runs npm run verify, builds the source archive, scans it with Gitleaks and publishes nothing.
Never move a published tag. Fix the cause and rerun the workflow instead.
To inspect the artifacts locally, run uv run python -m scripts.release --build --install --release-tag vX.Y.Z --image-tag X.Y.Z. This writes a deterministic source archive, an install bundle and checksums to dist/. check:release enforces a source allowlist, matching versions, license notices and valid local Markdown links. It also rejects known local secrets.
Release an extension¶
Extensions are versioned and released on their own, with tags <id>-vX.Y.Z that never mark the latest release. For Conversational BI:
- In
extensions/conversationalbi, update the version inpyproject.toml,uv.lock(uv lock),conversationalbi/config.py,web/package.jsonandruntime/package.jsonwith their lock files, the two image tags incompose.yamland the API contract (uv run python -m scripts.api_contract), andversionandappVersionindeploy/helm/iceberg-conversationalbi/Chart.yaml. Date itsCHANGELOG.mdsection.uv run python -m scripts.releaserejects a mismatch. - After the merge into
main, tag that commit:
git tag -a conversationalbi-vX.Y.Z -m "Conversational BI X.Y.Z"
git push origin conversationalbi-vX.Y.Z
The Conversational BI release workflow runs its CI, builds ghcr.io/sanderdw/iceberg-conversationalbi and iceberg-conversationalbi-runtime for AMD64 and ARM64, checks that both are public and publishes the release with its source archive and its Helm chart, which it also pushes to oci://ghcr.io/sanderdw/charts/iceberg-conversationalbi. On the first publication, set both packages to Public and rerun the failed job. Then release the core.
To publish Conversational BI from a branch without a release, run Actions → Conversational BI release → Run workflow on that branch, or gh workflow run "Conversational BI release" --ref BRANCH. It tags both images :SLUG-RUN_ID-ATTEMPT and publishes a prerelease conversationalbi-SLUG-RUN_ID-ATTEMPT with the source archive, never the version tag. It keeps the five newest previews per branch.
The dbt extension works the same way:
- In
extensions/dbt, update the version inpyproject.toml,uv.lock(uv lock),dbt_portal/config.py, the image tags incompose.yaml(bothiceberg-dbtlines, the runner image and theRUNNER_IMAGEdefault) and the API contract (uv run python -m scripts.api_contract), andversionandappVersionindeploy/helm/iceberg-dbt/Chart.yaml. Date itsCHANGELOG.mdsection.uv run python -m scripts.releaserejects a mismatch. - After the merge into
main, tag that commit:
The dbt extension release workflow runs its CI, builds ghcr.io/sanderdw/iceberg-dbt and iceberg-dbt-runner for AMD64 and ARM64, checks that both are public and publishes the release with its source archive and its Helm chart, which it also pushes to oci://ghcr.io/sanderdw/charts/iceberg-dbt. On the first publication, set both packages to Public and rerun the failed job. Its branch previews are named dbt-SLUG-RUN_ID-ATTEMPT.
The Ingestion extension works the same way:
- In
extensions/ingestion, update the version inpyproject.toml,uv.lock(uv lock),ingestion/config.py, the image tags incompose.yaml(bothiceberg-ingestionlines, the runner image and theRUNNER_IMAGEdefault) and the API contract (uv run python -m scripts.api_contract), andversionandappVersionindeploy/helm/iceberg-ingestion/Chart.yaml. Date itsCHANGELOG.mdsection.uv run python -m scripts.releaserejects a mismatch. - After the merge into
main, tag that commit:
The Ingestion release workflow runs its CI, builds ghcr.io/sanderdw/iceberg-ingestion and iceberg-ingestion-runner for AMD64 and ARM64, checks that both are public and publishes the release with its source archive and its Helm chart, which it also pushes to oci://ghcr.io/sanderdw/charts/iceberg-ingestion. On the first publication, set both packages to Public and rerun the failed job. Its branch previews are named ingestion-SLUG-RUN_ID-ATTEMPT.
The OpenMetadata extension works the same way, with one image and no API contract:
- In
extensions/openmetadata, update the version inpyproject.toml,uv.lock(uv lock),VERSIONinopenmetadata_sync/config.pyand the image tag incompose.yaml. Date itsCHANGELOG.mdsection, and update the version in the install commands ofdocs/openmetadata.mdand the extension's README.uv run python -m scripts.releaserejects a mismatch. - After the merge into
main, tag that commit right away, because the published documentation already names the new installer:
The OpenMetadata extension release workflow runs its CI, including the end-to-end test against a local OpenMetadata server, builds ghcr.io/sanderdw/iceberg-openmetadata for AMD64 and ARM64, checks that it is public and publishes the release with its source archive, its installation bundle (iceberg-openmetadata-X.Y.Z-install.tar.gz: compose.yaml on the published image, the demo's compose.openmetadata.yaml, scripts/setup.py and scripts/om_token.py), install.sh and SHA256SUMS. install.sh downloads from its own release, never from releases/latest, which stays the platform's. On the first publication, set the package to Public and rerun the failed job. Its branch previews are named openmetadata-SLUG-RUN_ID-ATTEMPT and carry an installer of their own. To build them locally: uv run --no-project python -m scripts.release --build --install in extensions/openmetadata.
Package visibility¶
On the first publication, open each package in GitHub Packages and set it to Public, including iceberg-conversationalbi, iceberg-conversationalbi-runtime, iceberg-dbt, iceberg-dbt-runner, iceberg-ingestion, iceberg-ingestion-runner and iceberg-openmetadata, and the charts charts/iceberg-data-platform, charts/iceberg-conversationalbi, charts/iceberg-dbt and charts/iceberg-ingestion. The workflow checks for anonymous pull access before publishing a preview. Publishing uses the workflow's GITHUB_TOKEN, so no registry secret is needed.