From 2005ea6a41520e676ad1a934259894830c886735 Mon Sep 17 00:00:00 2001 From: matthewberry Date: Mon, 19 Oct 2026 19:31:30 -0500 Subject: [PATCH 1/3] Push frontend images to ghcr: pr-N and staging from PRs, main and sha from main Images are ghcr.io//-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- and staging; a main build pushes main (prod) and sha-; 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 --- .github/workflows/docker.yml | 81 +++++++++++------------------------- chart/values.prod.yaml | 2 +- chart/values.staging.yaml | 2 +- chart/values.yaml | 2 +- scripts/app-sync.mjs | 6 ++- scripts/app-sync.test.mjs | 27 ++++++++---- 6 files changed, 50 insertions(+), 70 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d310d75..48b9284 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,85 +1,54 @@ name: Docker -# This will run when: -# - when new code is pushed to main/develop to push the tags -# latest and develop -# - when a pull request is created and updated to make sure the -# Dockerfile is still valid. -# To be able to push to dockerhub, this execpts the following -# secrets to be set in the project: -# - DOCKERHUB_USERNAME : username that can push to the org -# - DOCKERHUB_PASSWORD : password asscoaited with the username +# Builds the frontend image and pushes it to GitHub Container Registry, as +# ghcr.io//-frontend (derived from app.yaml): +# pull requests pr- and staging (staging runs the most recently built PR) +# main main (prod runs it) and sha- +# 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. on: push: branches: - main - - develop - pull_request: - # Trigger the workflow on release activity - release: - # Only use the types keyword to narrow down the activity types that will trigger your workflow. - types: - - published - - edited - - created - -env: - DOCKERHUB_ORG: moleculemaker +permissions: + contents: read + packages: write jobs: docker: runs-on: ubuntu-latest steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - uses: docker/setup-buildx-action@v3 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: - images: moleculemaker/test-frontend # app:sync image + images: ghcr.io/moleculemaker/test-frontend # app:sync image + flavor: latest=false tags: | - # set latest tag for default branch - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch type=ref,event=pr - type=raw,value=staging,enable=true - type=ref,event=tag - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - + type=raw,value=staging,enable=${{ github.event_name == 'pull_request' }} + type=raw,value=main,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} - - name: Build and push Docker image (production) - if: github.ref == 'refs/heads/main' - uses: docker/build-push-action@v3 + - name: Log in to ghcr + uses: docker/login-action@v3 with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: - configuration=production - + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image (staging) - if: github.event_name == 'pull_request' - uses: docker/build-push-action@v3 + - name: Build and push + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: - configuration=staging diff --git a/chart/values.prod.yaml b/chart/values.prod.yaml index e625f77..603e634 100644 --- a/chart/values.prod.yaml +++ b/chart/values.prod.yaml @@ -10,7 +10,7 @@ ingress: traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.middlewares: alphasynthesis-test-prod-domain-redirect@kubernetescrd controller: - image: moleculemaker/test-frontend:main + image: ghcr.io/moleculemaker/test-frontend:main config: hostname: https://mmli.fastapi.mmli2.ncsa.illinois.edu basePath: "" diff --git a/chart/values.staging.yaml b/chart/values.staging.yaml index 1ef76b0..d81b8c3 100644 --- a/chart/values.staging.yaml +++ b/chart/values.staging.yaml @@ -7,7 +7,7 @@ ingress: kubernetes.io/tls-acme: "true" traefik.ingress.kubernetes.io/router.tls: "true" controller: - image: moleculemaker/test-frontend:staging + image: ghcr.io/moleculemaker/test-frontend:staging config: hostname: https://mmli.fastapi.staging.mmli2.ncsa.illinois.edu basePath: "" diff --git a/chart/values.yaml b/chart/values.yaml index 4532918..89e8ac8 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -4,7 +4,7 @@ ingress: tls: false annotations: {} controller: - image: moleculemaker/test-frontend + image: ghcr.io/moleculemaker/test-frontend config: hostname: https://mmli.fastapi.staging.mmli2.ncsa.illinois.edu basePath: "" diff --git a/scripts/app-sync.mjs b/scripts/app-sync.mjs index 8ff8322..173f850 100644 --- a/scripts/app-sync.mjs +++ b/scripts/app-sync.mjs @@ -21,11 +21,14 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); export const FAMILIES = { alphasynthesis: { + // GitHub org the app's repo and ghcr package live in + ghcrOrg: 'moleculemaker', namespace: 'alphasynthesis', prodDomain: 'platform.moleculemaker.org', feedbackDomain: 'moleculemaker.org', }, alphasynbio: { + ghcrOrg: 'ibiofoundry', namespace: 'ibiofoundry', prodDomain: 'platform.ibiofoundry.illinois.edu', feedbackDomain: 'ibiofoundry.illinois.edu', @@ -88,8 +91,7 @@ export function derive(app) { prod_host: prodHost, // The cluster's own name for the prod app, redirected to prod_host prod_alias_host: `${app.id}.frontend.mmli2.ncsa.illinois.edu`, - // Docker Hub for every family until the template moves to ghcr (#13) - image: `moleculemaker/${app.id}-frontend`, + image: `ghcr.io/${family.ghcrOrg}/${app.id}-frontend`, feedback_email: `${app.id}-feedback@${family.feedbackDomain}`, }; } diff --git a/scripts/app-sync.test.mjs b/scripts/app-sync.test.mjs index 3ce3457..1665083 100644 --- a/scripts/app-sync.test.mjs +++ b/scripts/app-sync.test.mjs @@ -11,21 +11,30 @@ const generated = (app, file) => parse(render(app)[file]); const EZSPEC = { id: 'ezspecificity', name: 'EZSpecificity', family: 'alphasynthesis', matomo_site_id: 1 }; const MUTAGENESIS = { id: 'mutagenesis', name: 'Mutagenesis', family: 'alphasynbio', matomo_site_id: 21 }; -test('alphasynthesis chart values match ezspecificity-frontend as deployed', () => { - assert.deepEqual(generated(EZSPEC, 'chart/values.staging.yaml'), fixture('ezspecificity.values.staging.yaml')); - assert.deepEqual(generated(EZSPEC, 'chart/values.prod.yaml'), fixture('ezspecificity.values.prod.yaml')); +// Images are the expected difference from the live apps: the template pushes every app +// to public ghcr with pr-N/staging and main tags. ezspecificity is still on Docker Hub, +// and mutagenesis on private ghcr with :latest and a pull secret. +const TAG = { staging: 'staging', prod: 'main' }; + +test('alphasynthesis chart values match ezspecificity-frontend as deployed, apart from the image', () => { + for (const env of ['staging', 'prod']) { + const expected = fixture(`ezspecificity.values.${env}.yaml`); + const actual = generated(EZSPEC, `chart/values.${env}.yaml`); + assert.equal(actual.controller.image, `ghcr.io/moleculemaker/ezspecificity-frontend:${TAG[env]}`); + delete expected.controller; + delete actual.controller; + assert.deepEqual(actual, expected, env); + } }); -test('alphasynbio chart values match mutagenesis-frontend except the registry', () => { - // Expected differences: mutagenesis pushes to private ghcr with a pull secret; the - // template pushes to Docker Hub until #13 and needs no pull secret for public images. +test('alphasynbio chart values match mutagenesis-frontend, apart from the image and pull secret', () => { for (const env of ['staging', 'prod']) { const expected = fixture(`mutagenesis.values.${env}.yaml`); delete expected.imagePullSecrets; delete expected.imagePullSecret; delete expected.controller; const actual = generated(MUTAGENESIS, `chart/values.${env}.yaml`); - assert.equal(actual.controller.image, `moleculemaker/mutagenesis-frontend:${env === 'prod' ? 'main' : 'staging'}`); + assert.equal(actual.controller.image, `ghcr.io/ibiofoundry/mutagenesis-frontend:${TAG[env]}`); delete actual.controller; assert.deepEqual(actual, expected, env); } @@ -50,7 +59,7 @@ test('managed lines take the derived value and keep their marker', () => { assert.equal(out, [ 'env:', - ' MAIN_REPO: moleculemaker/ezspecificity-frontend # app:sync image', + ' MAIN_REPO: ghcr.io/moleculemaker/ezspecificity-frontend # app:sync image', ' OTHER: untouched', ' - ezspecificity.frontend.staging.mmli2.ncsa.illinois.edu # app:sync staging_host', ].join('\n')); @@ -86,7 +95,7 @@ test('a tagged line the rewriter cannot parse is an error, not a silent skip', ( test('a "- key: value" list item keeps its key', () => { const out = updateManagedLines(' - image: moleculemaker/old # app:sync image', derive(EZSPEC), 'x.yml'); - assert.equal(out, ' - image: moleculemaker/ezspecificity-frontend # app:sync image'); + assert.equal(out, ' - image: ghcr.io/moleculemaker/ezspecificity-frontend # app:sync image'); }); test('replacement patterns in the name are inserted literally', () => { From fe3d8ad4383ef50fc43e58ebdade95f534dcf998 Mon Sep 17 00:00:00 2001 From: matthewberry Date: Mon, 19 Oct 2026 19:36:17 -0500 Subject: [PATCH 2/3] docker.yml: say when ghcr packages are public; drop the develop trigger 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. --- .github/workflows/docker.yml | 7 ++++++- .github/workflows/e2e.yml | 2 +- scripts/app-sync.test.mjs | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 48b9284..ccf7bb3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,7 +6,12 @@ name: Docker # main main (prod runs it) and sha- # 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. +# packages to its own org. +# +# 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. on: push: branches: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e07f91e..6efa5c5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,7 +1,7 @@ name: End-to-end Tests on: push: - branches: [main, develop] + branches: [main] pull_request: release: types: [published, edited, created] diff --git a/scripts/app-sync.test.mjs b/scripts/app-sync.test.mjs index 1665083..61c0a1a 100644 --- a/scripts/app-sync.test.mjs +++ b/scripts/app-sync.test.mjs @@ -50,7 +50,7 @@ test('the identity module carries the family and the derived feedback address', test('managed lines take the derived value and keep their marker', () => { const text = [ 'env:', - ' MAIN_REPO: moleculemaker/old-frontend # app:sync image', + ' IMAGE: moleculemaker/old-frontend # app:sync image', ' OTHER: untouched', ' - old.frontend.staging.example # app:sync staging_host', ].join('\n'); @@ -59,7 +59,7 @@ test('managed lines take the derived value and keep their marker', () => { assert.equal(out, [ 'env:', - ' MAIN_REPO: ghcr.io/moleculemaker/ezspecificity-frontend # app:sync image', + ' IMAGE: ghcr.io/moleculemaker/ezspecificity-frontend # app:sync image', ' OTHER: untouched', ' - ezspecificity.frontend.staging.mmli2.ncsa.illinois.edu # app:sync staging_host', ].join('\n')); From 8aa991e551904a7705349769ad278ac87867f738 Mon Sep 17 00:00:00 2001 From: matthewberry Date: Mon, 19 Oct 2026 19:55:46 -0500 Subject: [PATCH 3/3] Local chart image pins :main; drop the pull-secret fallback the chart 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. --- .github/workflows/docker.yml | 2 +- chart/values.yaml | 2 +- scripts/app-sync.mjs | 2 +- scripts/app-sync.test.mjs | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ccf7bb3..14890cc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,7 +11,7 @@ name: Docker # 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. +# package settings before the cluster can pull it (the chart has no imagePullSecrets). on: push: branches: diff --git a/chart/values.yaml b/chart/values.yaml index 89e8ac8..33cec01 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -4,7 +4,7 @@ ingress: tls: false annotations: {} controller: - image: ghcr.io/moleculemaker/test-frontend + image: ghcr.io/moleculemaker/test-frontend:main config: hostname: https://mmli.fastapi.staging.mmli2.ncsa.illinois.edu basePath: "" diff --git a/scripts/app-sync.mjs b/scripts/app-sync.mjs index 173f850..bda7862 100644 --- a/scripts/app-sync.mjs +++ b/scripts/app-sync.mjs @@ -144,7 +144,7 @@ export function render(app) { // Local cluster defaults; staging and prod override all of it 'chart/values.yaml': toYaml({ ingress: { hostname: d.local_host, tls: false, annotations: {} }, - controller: { image: d.image }, + controller: { image: `${d.image}:main` }, config: oauthConfig('staging', d.local_host, BACKEND.staging), extraDeploy: [], }), diff --git a/scripts/app-sync.test.mjs b/scripts/app-sync.test.mjs index 61c0a1a..2327abb 100644 --- a/scripts/app-sync.test.mjs +++ b/scripts/app-sync.test.mjs @@ -129,3 +129,7 @@ test('no tagged files is an empty list, not an error; other git failures still t const notARepo = () => { throw Object.assign(new Error('not a git repository'), { status: 128 }); }; assert.throws(() => managedFiles(notARepo), /not a git repository/); }); + +test('local chart values pin a tag that is actually pushed', () => { + assert.equal(generated(EZSPEC, 'chart/values.yaml').controller.image, 'ghcr.io/moleculemaker/ezspecificity-frontend:main'); +});