Skip to content

Push frontend images to ghcr (pr-N/staging from PRs, main/sha from main) - #22

Merged
matthewberry merged 3 commits into
mainfrom
feat/13-ghcr
Oct 20, 2026
Merged

matthewberry merged 3 commits into
mainfrom
feat/13-ghcr

Conversation

@matthewberry

Copy link
Copy Markdown
Contributor

Closes #13.

What

  • docker.yml pushes to ghcr using GITHUB_TOKEN (permissions: packages: write). No Docker Hub secrets are needed.

  • Tags:

    Event Tags pushed
    pull request pr-<N>, staging
    push to main main (prod pins it), sha-<short>

    Nothing pushes latest (flavor: latest=false). Prod still deploys straight from main, as decided. Tag rules checked against the docker/metadata-action v5 README: type=ref,event=pr gives pr-2, and enable=${{ … }} is supported.

  • app:sync derives the image as ghcr.io/<org>/<id>-frontend: moleculemaker for alphasynthesis, ibiofoundry for alphasynbio. The chart values and the managed line in docker.yml follow.

  • Removed:

    • the release trigger, whose builds never pushed because neither build step's condition matched a release;
    • the develop branch trigger;
    • the configuration= build-arg, which the Dockerfile has no ARG for.

Constraint

GITHUB_TOKEN can only push packages to its own org, so an app's repo must live in its family's org. That's what we agreed; the workflow header says so.

Verification

  • test:scripts 14/14. The ezspecificity and mutagenesis fixture comparisons now treat the image as the only expected difference for both families. app:sync --check passes.
  • This PR's own docker run is the first push of ghcr.io/moleculemaker/test-frontend:pr-<N> and :staging. I'll check after it runs that the package exists, is public and pulls anonymously.

Not in scope

… from main

Images are ghcr.io/<org>/<id>-frontend for both families (moleculemaker
for alphasynthesis, ibiofoundry for alphasynbio), derived by app:sync and
logged in with GITHUB_TOKEN, so no registry secrets. Tags follow the agreed
scheme: a PR build pushes pr-<N> and staging; a main build pushes main
(prod) and sha-<short>; nothing pushes latest. The release trigger, whose
builds never pushed, and the ignored configuration build-arg are gone.

The chart-values tests still compare against ezspecificity and
mutagenesis; the image is now the only expected difference for both.

Closes #13

@matthewberry matthewberry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of 2005ea6.

Verified

  • The tag rules match the table. I read is_default_branch in metadata-action v5.10.0 (src/meta.ts:444). It compares context.ref without the refs/heads/ prefix to the repo's default branch. On pull_request the ref is refs/pull/N/merge, so main and sha-<short> are off, and staging plus pr-N are on. On a push to main it's the reverse. flavor: latest=false stops the automatic latest.
  • This PR's run. docker and E2E on Chrome both pass. The package ghcr.io/moleculemaker/test-frontend exists, with one index tagged staging,pr-22.
  • Checks. test:scripts passes 14/14, and app:sync --check passes.
  • Leftover references. I searched the repo (excluding the lockfile and documentation.json) for latest, develop, Docker Hub, docker.io and the bare moleculemaker/test-frontend. Nothing expects latest or Docker Hub. The remaining latest hits are runs-on: ubuntu-latest, commented-out nightwatch config, and the mutagenesis fixture.

Findings

  1. The package is private, and an anonymous pull returns 403 (inline, docker.yml:9). This is the one to settle before #15.
  2. e2e.yml:4 still triggers on develop. It's not in the diff, so it isn't an inline comment. It's harmless, but it's the last develop reference left after this PR drops the branch from docker.yml.
  3. Nit: the MAIN_REPO key in a test (inline).

Note: staging is whichever PR run finished last. There is no concurrency group, so two PRs building at once race for the tag, and re-running an old PR's workflow moves staging back to it. That matches the header's "most recently built PR", so it isn't a defect. Mentioning it in case "last finished" isn't what you meant.

Didn't investigate: the push to main (no run exists yet), the iBioFoundry org's package settings, and fork and Dependabot PRs. Dependabot gets a read-only token too; I didn't check whether this repo has Dependabot enabled.

Comment thread .github/workflows/docker.yml Outdated
# main main (prod runs it) and sha-<short>
# The repo must live in the org its family deploys from (moleculemaker for
# alphasynthesis, iBioFoundry for alphasynbio), because GITHUB_TOKEN can only push
# packages to its own org. Packages are public, so the cluster needs no pull secret.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package this run created is private, so "Packages are public, so the cluster needs no pull secret" doesn't hold yet. (Reproduced.) The docker run (39348164629) pushed pr-22 and staging, but:

  • gh api /orgs/moleculemaker/packages/container/test-frontend returns "visibility": "private", linked to moleculemaker/frontend-template;
  • https://ghcr.io/token?scope=repository:moleculemaker/test-frontend:pull gives no anonymous token, and the manifest request for both tags returns 403.

The first push of a new package creates it private, and nothing in this workflow changes that. So every app generated from the template starts with a private image, and a cluster without a pull secret will fail with ImagePullBackOff once #15 deploys it. The org does allow public containers (job-template is public), so this looks like a one-time manual step per app (Package settings → Change visibility, done by an org admin). I haven't checked whether a package linked to an internal repo, which this one is, can be made public.

Possible fixes: document that step (README / the workflow header) and make it part of creating an app, or keep imagePullSecrets in the chart, as mutagenesis does today. Either way, the comment should match what actually happens.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it explains itself: this repo is INTERNAL, and the package inherited that as private. job-template is a PUBLIC repo and its first push created a public package. Fixed the claim in fe3d8ad: the header now says a package takes its repo's visibility on first push. New apps get public repos (as agreed), so they need no pull secret. An internal or private repo's package needs an org admin to make it public, or a pull secret.

I haven't changed the visibility of test-frontend or this repo; I'm asking the user, since it matters for the #15 canary. The existing frontends (ezspecificity, aceretro, mutagenesis) are also INTERNAL, so moving them onto this workflow would hit the same thing.

Also in fe3d8ad: e2e.yml no longer triggers on develop, and the test example's key is now IMAGE. Staging-tag race: noted and left as is. It's the known last-PR-built-wins tradeoff, and a concurrency group would only change it to last-started-wins.

Comment thread scripts/app-sync.test.mjs Outdated
assert.equal(out, [
'env:',
' MAIN_REPO: moleculemaker/ezspecificity-frontend # app:sync image',
' MAIN_REPO: ghcr.io/moleculemaker/ezspecificity-frontend # app:sync image',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this managed-line example still uses MAIN_REPO, which #21 removed. It now holds an image ref, which reads like the old mis-tag. Renaming the key to something like IMAGE (here and on line 53) would avoid confusion.

Review of #22 (5579414557), reproduced: the first push created
ghcr.io/moleculemaker/test-frontend as a private package because this repo
is internal (job-template, a public repo, got a public package). The header
claimed packages are public; it now says a package takes its repo's
visibility on first push, and what an internal repo's app needs instead.
Also: e2e.yml no longer triggers on the nonexistent develop branch, and
the managed-line test uses a neutral key.

@matthewberry matthewberry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of fe3d8ad. Nothing blocking.

  • The private-package finding is addressed. The header now matches what I saw:

    • frontend-template is INTERNAL, and its package came out private;
    • job-template is PUBLIC, and so is its package.

    That's two data points, not a reading of GitHub's docs, but they agree with the stated rule. The "admin makes it public or add a pull secret" path is now written where the next app will read it. The current test-frontend package is still private, which is expected until someone changes it.

  • develop is gone from e2e.yml, and the test key is now IMAGE. I searched .github for develop: no hits.

  • Checks. test:scripts passes 14/14, and app:sync --check passes. E2E on Chrome passes on this head. docker was still running when I checked.

  • Staging race. Leaving it as a known tradeoff is fine.

Leftover, optional: e2e.yml still has the release trigger, which this PR removed from docker.yml. It's harmless (E2E on a release is reasonable), so only change it if you want the two workflows to match.

@matthewberry matthewberry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of fe3d8ad. Nothing blocking. Two new findings, both about the image the chart pulls.

Findings (inline)

  1. The header's "or a pull secret in the chart" fallback isn't supported by the chart (docker.yml:14). Reproduced by reading the template.
  2. The local values.yaml image has no tag, so it resolves to :latest, which this workflow no longer pushes (chart/values.yaml:7). Reproduced against the registry.

Verified

  • test:scripts passes 14/14, and app:sync --check passes.
  • The Dockerfile has no ARG configuration, so dropping that build-arg is correct.
  • App ids are lowercase (^[a-z][a-z0-9-]*[a-z0-9]$) and both family orgs are lowercase, as ghcr requires.

Didn't investigate: whether anyone installs the chart with values.yaml alone. I found no README or script that does.

Comment thread .github/workflows/docker.yml Outdated
# The first push creates the package with the repo's visibility: public for a public
# repo (as new apps are, so the cluster needs no pull secret), private for an internal
# or private one. A private package needs an org admin to make it public in the
# package settings, or a pull secret in the chart, before the cluster can pull it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chart has no pull-secret support, so this fallback doesn't work. (Reproduced by reading the template.) chart/templates/deployment.yaml sets image: and imagePullPolicy: but has no imagePullSecrets, and nothing under chart/ mentions a pull secret (git grep -i pullsecret -- chart finds nothing). Putting a secret in the values would change nothing, and the pod would still hit ImagePullBackOff.

This matters for the #15 canary: frontend-template is internal and test-frontend is still private, so making the package public is currently the only option that works.

Possible fixes: add imagePullSecrets to the deployment template (mutagenesis's values already use one), or drop "or a pull secret in the chart" from the header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right: deployment.yaml has no imagePullSecrets, so that clause offered something the chart can't do. I dropped it in 8aa991e; the header now says a private package must be made public. I'll correct the same option where I offered it to the user.

Comment thread chart/values.yaml Outdated
annotations: {}
controller:
image: moleculemaker/test-frontend
image: ghcr.io/moleculemaker/test-frontend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: this untagged image resolves to :latest, which nothing pushes any more. (Reproduced against the registry.)

  • gh api /orgs/moleculemaker/packages/container/test-frontend/versions lists only staging and pr-22. flavor: latest=false means latest will never appear.
  • Before this PR, the same untagged name resolved to Docker Hub's moleculemaker/test-frontend:latest, which exists (updated today).
  • imagePullPolicy: Always means a locally built image wouldn't be used instead.

So installing the chart with only these local defaults now fails to pull. I found no README or script that does this, so it may have no users. The fix is one line in render() (app-sync.mjs:147): controller: { image: `${d.image}:main` }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced: an untagged image means :latest, which nothing pushes now. Fixed in 8aa991e: values.yaml renders ${image}:main, and a new test pins it. Script tests: 15/15.

… can't use

Review of #22 (5579540211):
- chart/values.yaml's untagged image resolved to :latest, which nothing
  pushes any more (reproduced: ghcr has only staging and pr-N so far). It
  now pins :main.
- The header offered a pull secret in the chart as a fallback for private
  packages, but the deployment template has no imagePullSecrets, so a
  private package must be made public.

@matthewberry matthewberry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 8aa991e. Nothing blocking; both findings are fixed.

  • Pull secret: the header no longer offers it and says the chart has no imagePullSecrets. A private package must be made public.
  • Local image: render() and chart/values.yaml now use :main, and a new test pins it. The tag will exist once this merges and main builds.
  • Checks: test:scripts passes 15/15, and app:sync --check passes. I did not wait for CI on this head.

@matthewberry
matthewberry merged commit abcf851 into main Oct 20, 2026
2 checks passed
@matthewberry
matthewberry deleted the feat/13-ghcr branch October 20, 2026 01:12
matthewberry added a commit that referenced this pull request Oct 20, 2026
Conflicts kept both sides: the local chart image pins :main (#22) and the
config is runtimeConfig with the banner keys (#23); the fixture tests
assert the ghcr images and strip the banner keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ghcr CI with staging/prod tags, e2e, and nightly cron

1 participant