feat: publish the container image and the PyPI package - #86
Merged
yi-here merged 3 commits intoAug 27, 2026
Merged
Conversation
`pip install orcarouter-lite` produced nothing usable: no console script, and
`packages.find` shipped only `app/` + `packages/`, leaving `scripts/start.py`
(the boot path) and `design/` (the entire dashboard) out of the wheel. Anyone
installing from PyPI would have got a library with no way to start it and no UI
behind `/`.
* `app/cli.py` holds the boot logic and is exposed as the `orcarouter-lite`
console script (`--host/--port/--log-level/--reload/--version`).
`scripts/start.py` and `app/__main__.py` delegate to it, so a checkout, the
container and the wheel all take one code path. `scripts/` cannot hold the
logic — it is not part of the distribution.
* Build backend moves to hatchling for `force-include`, which maps the
repo-root `design/` tree to `app/design/` inside the wheel. No file moves
(nothing conflicts with the open PRs) and no explicit package list to drift
out of sync when a subpackage is added.
* `_find_design_dir()` replaces the hardcoded `../design`, checking
`$ORCA_DESIGN_DIR`, then `app/design` (wheel), then `../design` (checkout
and Docker image).
* Metadata for a real PyPI listing: readme, SPDX license + license-files,
authors, keywords, classifiers, project.urls.
The Dockerfile builder had to change with it. It pre-installed dependencies via
`mkdir -p app packages && pip install "."`, which now fails outright because
`readme = "README.md"` is not in the build context. It reads the dependency list
straight out of pyproject instead — clearer about the intent (the project itself
is never installed there; the runtime stage uses PYTHONPATH) and the cached
layer no longer breaks on a README edit.
Verified end to end: `python -m build`, `twine check --strict`, install the
wheel into a clean venv, boot it — `/health` 200, `/` serves the dashboard,
`/static/style.css` 200 (42561 bytes).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither artifact the README points at exists. There is no workflow that pushes
anything: `ci.yml` builds an image only to `docker rm -f` it, and nothing ever
touches PyPI. The v0.1.0 tag has zero assets.
`release.yml`:
* push to main -> ghcr.io/<repo>:edge (amd64 — arm64 is emulated and slow,
not worth it on every merge)
* push a v* tag -> :latest, :X.Y.Z, :X.Y, :sha-xxxx on amd64 + arm64, a
provenance attestation, the PyPI release, and both dists
attached to the GitHub release
* the pushed image is pulled back by digest and curl'd on /health before the
job passes
* PyPI uses trusted publishing (OIDC), so there is no token to store; the tag
has to match the pyproject version or the job refuses to publish. The job is
scoped to this repository so a fork can rehearse a release against its own
ghcr namespace without dying at the OIDC exchange.
`ci.yml` gains a `package` job so packaging breaks on the PR, not at release
time: build, `twine check --strict`, install the wheel into a clean venv, boot
it, and assert `/` still answers with HTML. That last assertion is the point —
drop the design force-include and everything still builds and boots, only the
dashboard silently vanishes. `design/**` joins the path filters for the same
reason.
RELEASING.md covers the one-time setup each artifact needs. Note that the
existing v0.1.0 tag cannot be released with a workflow_dispatch: a dispatch runs
the workflow file from the ref you select, and that tag predates release.yml.
Verified end to end on a fork before this PR: the image job built and pushed
ghcr.io/<fork>/orcarouter-lite:edge (linux/amd64, non-root, CMD unchanged),
signed provenance, pulled the image back by digest and got a healthy /health;
the pypi job skipped as designed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🐳 OrcaCode Review
Found 4 issues in this PR: 🟡 4 P2.
Reviewed via OrcaRouter — Route Smarter. Ship Safer. Spend Less.
…e-run safe Four review findings on release.yml, all of them real: * The build pushed `latest`/`X.Y.Z`/`X.Y` and only then smoke-tested. A failed smoke could not unpublish them. The build now pushes only the immutable `sha-<short>` tag; a `promote` step moves the release tags onto that digest with `docker buildx imagetools create` after the smoke passes. * The smoke was a plain `docker run`, which executes the host (amd64) variant only — the arm64 image was published without ever having been booted. It now loops over every platform in the manifest, with a longer timeout for the emulated one. * `attest build provenance` moved after `promote` and now attests the digest the release tags actually resolve to, read back rather than assumed: imagetools copies a multi-platform index verbatim but wraps a single-platform manifest in a fresh one. That keeps `gh attestation verify oci://…:latest` pointing at the image people pull. The step also asserts every tag landed on one digest. * The concurrency group was per-ref, so a v0.1.1 and a v0.1.2 push ran in parallel and both wrote the shared `latest` tag — last writer wins, and the slower older release wins it. All tag releases now share one group. * The `pypi` job published to PyPI before attaching the dists to the GitHub release. A failure in between left the release half-done and unretryable: a published PyPI version cannot be replaced, and without `skip-existing` the re-run aborts at the upload before reaching the attach. The GitHub release now goes first, `skip-existing: true` lets a re-run pass through, and `needs: image` stops PyPI from shipping a version whose container never booted. RELEASING.md gains a "What gates what" section describing the new ordering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🐳 OrcaCode Review
✅ No findings — nothing to flag in this PR. Great work!
OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 149 calls · 6.2M tokens · 96% cached
❤️ Share · Install OrcaCode Review
Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.
Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Orca-Code-Review — push 2
✅ no blocking findings
Why
The README advertises two artifacts that do not exist:
maintodayghcr.io/...placeholder, "(image coming soon)"orcarouter-litereturns 404There is no workflow that publishes anything.
ci.ymlbuilds an image only todocker rm -fit, and nothing has ever touched PyPI; thev0.1.0tag has zero assets.pip install orcarouter-litewould not have produced anything usable even if it were published: there is no console script, andpackages.findships onlyapp/+packages/— leavingscripts/start.py(the boot path) anddesign/(the entire dashboard) out of the wheel. You would get a library with no way to start it and nothing behind/.What
feat(packaging)— make the wheel installable and self-containedapp/cli.pyholds the boot logic and is exposed as theorcarouter-liteconsole script (--host/--port/--log-level/--reload/--version).scripts/start.pyandapp/__main__.pydelegate to it, so a checkout, the container and the wheel all take one code path. The logic cannot stay inscripts/— that directory is not part of the distribution.force-include, which maps the repo-rootdesign/tree toapp/design/inside the wheel. No file moves (nothing conflicts with open PRs) and no explicit package list to drift out of sync when a subpackage is added._find_design_dir()replaces the hardcoded../design:$ORCA_DESIGN_DIR, thenapp/design(wheel), then../design(checkout and Docker image).license-files, authors, keywords, classifiers,project.urls.The Dockerfile builder had to change with it. It pre-installed dependencies via
mkdir -p app packages && pip install ".", which now fails outright becausereadme = "README.md"is not in that build context. It reads the dependency list straight out ofpyproject.tomlinstead — clearer about the intent (the project itself is never installed there; the runtime stage puts the source onPYTHONPATH) and the cached layer no longer breaks on a code or README edit.ci(release)— publish the container image and the PyPI packagemain→ghcr.io/…:edge(amd64 only; arm64 is emulated and slow, not worth it on every merge)v*tag →:latest,:X.Y.Z,:X.Y,:sha-xxxxon amd64 + arm64, a provenance attestation, the PyPI release, and both dists attached to the GitHub release/healthbefore the job passespyprojectversion or the job refuses to publish. The job is scoped to this repository, so a fork can rehearse a release against its own ghcr namespace instead of dying at the OIDC exchange.ci.ymlgains apackagejob so packaging breaks on the PR rather than at release time: build,twine check --strict, install the wheel into a clean venv, boot it, and assert/still answers with HTML. That last assertion is the point — drop the design force-include and everything still builds and boots, only the dashboard silently vanishes.design/**joins the path filters for the same reason.Verification
Rehearsed end to end on a fork before opening this, so every step below has actually run:
docker smokedocker build+ boot +/health✅packagetwine check --strict, clean-venv install, boot, dashboard served ✅releaseghcr.io/<fork>/orcarouter-lite:edge— linux/amd64, 12 layers, non-rootorca,CMD ["python","scripts/start.py"]unchanged — signed provenance, pulled the image back by digest, healthy/health;pypijob skipped as designed ✅Locally: 312 passed, ruff clean,
twine check --strictPASSED on both wheel and sdist.Before the first tag
RELEASING.mdhas the detail. Two things need a human once:Continuum-AI-Corp, repoOrcaRouter-Lite, workflowrelease.yml, environmentpypi. Until it exists av*tag fails at the OIDC exchange. No rush: merging this and pushing tomainruns only the image job.Note the existing
v0.1.0tag cannot be released viaworkflow_dispatch: a dispatch runs the workflow file from the ref you select, and that tag predatesrelease.yml. Cutv0.1.1instead.Not included
The README is untouched here, so after merge it will still say
ghcr.io/...(image coming soon), and its Railway button still points athttps://railway.app/new/template— a URL with no template behind it, which lands visitors on the generic marketplace page and deploys nothing (Railway only serves one-click deploys for a published template code; the legacy?template=<github-url>form no longer resolves). Both are doc-only changes across 12 files and can follow separately.🤖 Generated with Claude Code