Release engineering¶
adk-fluent ships two packages from one monorepo — adk-fluent on PyPI
and adk-fluent-ts on npm. Releases are symmetric: same version
string, same tag, same workflow shape, same cadence. This page covers
how the pipeline is wired and how to cut a release.
Architecture at a glance¶
┌──────────┐ ┌──────────────────────────┐
│ VERSION │ ◀─────── │ single source of truth │
│ 0.14.1 │ │ (repo root, plain semver)│
└────┬─────┘ └──────────────────────────┘
│
├──▶ python/src/adk_fluent/_version.py ◀── docs/conf.py reads from here
└──▶ ts/package.json
The shared/scripts/release/ module owns propagation. Everything
else — the justfile recipes, the CI workflows, the docs build — reads
from these three files.
The release signal is the commit, not a tag¶
Pushing a commit to master that modifies VERSION (or the files
that mirror it) is the release. There is no manual git tag, no
gh workflow run, no dashboard button. CI sees the bump and takes
over.
GitHub Actions layout¶
release.yml ← orchestrator; triggered by VERSION bump
├─ plan (diff VERSION against HEAD~1; short-circuit if tag exists)
├─ python-pypi uses ./_publish-python.yml (target=pypi)
├─ python-testpypi uses ./_publish-python.yml (target=testpypi)
├─ ts-npm uses ./_publish-npm.yml (target=npm)
├─ ts-npm-dry uses ./_publish-npm.yml (target=npm-dry-run)
└─ tag-release (after both prod publishes succeed)
- creates v<VERSION> tag
- extracts CHANGELOG section
- drafts GitHub Release with built artifacts
_publish-python.yml ← reusable; build → inspect → publish
_publish-npm.yml ← reusable; build → inspect → publish
Both reusable workflows have the same three stages — build,
inspect, publish — so a failure in one is debugged the same
way you debug the other.
The orchestrator triggers on:
pushtomastertouchingVERSION,python/src/adk_fluent/_version.py, orts/package.json→ full auto-release.workflow_dispatch→ manual dry-run or targeted republish.
Idempotency: the plan job runs git ls-remote --tags origin v<VERSION>. If the tag already exists, the whole run short-circuits
(skip=true). Re-pushing the same VERSION is safe.
Cutting a release¶
All local commands live in justfile.release and are named rel-*.
Normal flow (patch, minor, major)¶
$ just rel-prepare minor # bumps, syncs, promotes [Unreleased], commits
$ git push origin master # CI publishes + tags
That’s it. CI handles PyPI, npm, the v0.15.0 tag, and the GitHub
Release page. Watch the run:
$ gh run watch
Dry-run¶
Rehearse the whole pipeline against TestPyPI + npm pack:
$ just rel-bump patch # stage the new version locally
$ just rel-preflight # verify readiness
$ just rel-dry-run # dispatches the CI workflow with target=dry-run
Because the dry-run targets are TestPyPI and npm-dry-run (a pack,
no publish), it is safe to run repeatedly. If it fails, fix forward
without publishing.
Individual commands¶
command |
what it does |
|---|---|
|
print the canonical version |
|
show VERSION vs consumers + changelog state |
|
bump + propagate to |
|
propagate current (or given) VERSION to consumers |
|
lint the release state (versions agree, changelog has entry, tag free, tree clean) |
|
same, but treats warnings as failures (used in CI) |
|
bump + sync + promote [Unreleased] + commit |
|
dispatch |
|
escape hatch — force-create the tag locally; only needed if CI can’t |
|
local escape hatch — prefer CI |
|
local escape hatch — prefer CI |
Old names (just version, just bump, just release,
just release-tag) keep working as aliases.
Preflight checks¶
just rel-preflight runs a short list of non-destructive checks and
prints [ok] / [warn] / [fail] per line. Checks are defined
in shared/scripts/release/preflight.py:CHECKS.
Current checks:
versions — VERSION,
_version.py, andpackage.jsonall agree.changelog — entry for the current version exists, or
[Unreleased]is populated and will be promoted on prepare.python/pyproject —
python/pyproject.tomlpresent.ts/package.json — TS package file present and has a
"files"allowlist.tag —
vX.Y.Zis free locally and onorigin(environment-aware: on a tag-push CI run, asserts the tag matches VERSION instead).tree — working tree is clean.
Add a check by appending a function to CHECKS; it runs in CI via
--strict.
Supply-chain hygiene¶
PyPI Trusted Publishing: no long-lived tokens. The
pypienvironment on GitHub is wired to the project on pypi.org (andtestpypion test.pypi.org) via OIDC.attestations: trueproduces PEP 740 provenance.npm provenance:
npm publish --provenanceemits a sigstore bundle and records the GitHub Actions run that produced the tarball. Requires a public repo andid-token: write— both in place.GitHub Release assets: every released version attaches the wheel, sdist, and npm tarball. Users can pin against a checksummed asset instead of (or in addition to) the registry.
Tag provenance: the
tag-releasejob commits asgithub-actions[bot]and pushesv<VERSION>from the same run that published the artifacts. Tags cannot drift from what was published because nothing else creates them.
Adding a new distribution target¶
Say you want to publish a container image or a Homebrew formula.
Because the orchestrator is a thin dispatcher, the cost of adding a
target is one reusable workflow plus a few lines in release.yml:
# .github/workflows/_publish-docker.yml (new)
on: { workflow_call: { inputs: { version: { required: true, type: string } } } }
jobs:
build: { ... }
publish: { needs: [build], ... }
# release.yml (orchestrator)
docker:
needs: [plan]
if: needs.plan.outputs.publish-docker == 'true'
uses: ./.github/workflows/_publish-docker.yml
with: { version: ${{ needs.plan.outputs.version }} }
Add publish-docker to the plan job’s output-resolving script
and a matching --target docker option on the dispatch input. Extend
the tag-release job’s needs: list if the new target should gate
tagging. The rel-* recipes need no changes — they are
language-neutral.
Debugging a failed release¶
symptom |
likely cause |
fix |
|---|---|---|
|
the commit you pushed didn’t actually touch VERSION (e.g. amended wrong file). |
re-run |
|
the tag exists remotely — this VERSION was already released. |
bump again: |
|
one of |
fix the publish job, re-run |
|
token lives on a different environment or only at the repo scope. |
re-add the secret under Settings → Environments → npm → Environment secrets. |
|
|
the reusable npm workflow runs |
|
long description has malformed RST/Markdown. |
open the offending dist file locally ( |
|
forgot to register the workflow on pypi.org. |
Add a pending publisher under the project’s Publishing settings on pypi.org. |
When in doubt, re-dispatch release.yml with
target=dry-run first. It exercises the same code path without
touching production registries.