diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d310d75..14890cc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,85 +1,59 @@ 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. +# +# 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 before the cluster can pull it (the chart has no imagePullSecrets). 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/.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/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..33cec01 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: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 8ff8322..bda7862 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}`, }; } @@ -142,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 3ce3457..2327abb 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); } @@ -41,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'); @@ -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', + ' IMAGE: 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', () => { @@ -120,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'); +});