From be262879ff92c29ea84fe61f0e2767fcc857f8bd Mon Sep 17 00:00:00 2001 From: "Austin (via Claude Code)" Date: Tue, 29 Sep 2026 03:44:05 +0000 Subject: [PATCH 1/2] ci: add on-demand Publish images workflow to force-publish tip of main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Images are tagged by git tree hash. When a PR is merged while its branch is behind main, GitHub writes a merge commit whose tree differs from the PR-head tree that ci.yml built, so the tip-of-main image has no matching tag. cd.yml's build-missing job normally backfills it on the push to main, but there is no supported way to force a publish on demand when that does not happen (e.g. the push's CD run was superseded in the cd-deploy concurrency group). Add .github/workflows/publish-images.yml: a workflow_dispatch job that force-(re)builds and pushes all three images (frontend, api, collab) for a chosen ref, defaulting to the tip of main. Publishing only — it does not deploy; cd.yml still owns rollout to production. The matrix, arm64 runner, tree -hash tag scheme and registry login are kept in sync with ci.yml and cd.yml. Document the escape hatch in the README's Container images section. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BcAqhmKSU8rnQHKcRyyhQ6 --- .github/workflows/publish-images.yml | 85 ++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/publish-images.yml diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml new file mode 100644 index 0000000..f008886 --- /dev/null +++ b/.github/workflows/publish-images.yml @@ -0,0 +1,85 @@ +name: Publish images + +# Force-(re)build and push all three container images for a chosen ref — +# normally the tip of main. Publishing only; this does NOT deploy (cd.yml owns +# rollout to production). +# +# Why this exists: images are tagged by git tree hash, not commit SHA. On a PR +# whose branch is up to date with main the merge is effectively a fast-forward, +# so the merge commit's tree equals the PR-head tree that ci.yml's +# build-and-push already published, and cd.yml deploys that prebuilt image. But +# when a PR is merged while its branch is behind main (branch protection does +# not require branches to be up to date), GitHub writes a real merge commit +# whose tree differs from the PR-head tree. ci.yml built an image for the +# PR-head tree, so the tip-of-main tree has no matching image. cd.yml's +# build-missing job normally backfills it on the push to main, but this is the +# on-demand escape hatch for when it did not (e.g. its run was superseded by a +# newer push in the cd-deploy concurrency group), or when an image otherwise +# needs to be rebuilt. +# +# The matrix, runner, tag scheme and registry login are kept in sync with +# ci.yml's build-and-push and cd.yml's build-missing. + +on: + workflow_dispatch: + inputs: + ref: + description: "Git ref (branch, tag, or SHA) to build from. Defaults to the ref this workflow is dispatched on (tip of main)." + required: false + type: string + +permissions: + contents: read + +jobs: + publish: + if: github.repository == 'imaustink/glyph' + # Self-hosted, arm64 — matches the production cluster's nodes. Building on + # ubuntu-latest (amd64) produces images that fail with "exec format error" + # when the kubelet tries to run them. + runs-on: arc-runner-set + # One job per image so the three builds run in parallel on separate + # runners. Keep in sync with ci.yml's build-and-push and cd.yml's + # build-missing. + strategy: + fail-fast: false + matrix: + include: + - image: frontend + dockerfile: Dockerfile + context: . + build-args: --build-arg VITE_STORAGE_MODE=api --build-arg VITE_API_URL= + - image: api + dockerfile: api/Dockerfile + context: api/ + - image: collab + dockerfile: collab/Dockerfile + context: . + steps: + # An empty `ref` input falls back to the ref this workflow was dispatched + # on (the default branch, i.e. the tip of main, unless another is picked). + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Docker login + run: | + echo "$REGISTRY_PASSWORD" | docker login docker.io \ + -u "$REGISTRY_USERNAME" --password-stdin + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + + # Tagged by git tree hash (content) so the tag matches exactly what + # cd.yml resolves and deploys. Unconditional build + push (unlike + # cd.yml's build-missing, which skips when the tag already exists): this + # is a *force* publish, so it also overwrites a bad or stale image under + # the same tag. + - name: Build & push ${{ matrix.image }} (forced) + run: | + ref="docker.io/blackmarket/glyph-${{ matrix.image }}:$(git rev-parse HEAD^{tree})" + echo "Force-publishing $ref" + docker build ${{ matrix.build-args }} \ + -t "$ref" \ + -f ${{ matrix.dockerfile }} ${{ matrix.context }} + docker push "$ref" diff --git a/README.md b/README.md index 10e1ee0..26a1b36 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ CI publishes images to public Docker Hub repositories for pull requests from thi Images are built for **linux/arm64** and tagged with the git tree hash of the commit they were built from (`git rev-parse HEAD^{tree}`); there is no `latest` tag. These are the default repositories in the Helm chart (`helm/glyph/values.yaml`). +Because the tag is the tree hash, merging a PR whose branch is **behind `main`** produces a merge commit whose tree differs from the PR-head tree CI built — so the tip-of-main image can be missing. CD's build-missing job normally backfills it on the push to `main`; the **Publish images** workflow (`.github/workflows/publish-images.yml`, `workflow_dispatch`) is the on-demand escape hatch to force-(re)build and push all three images for a chosen ref (default: tip of `main`) without deploying. + ## Desktop app Glyph also ships as a native desktop app for macOS and Linux (`src-tauri/`) — a thin window that connects to your self-hosted Glyph server, with automatic updates. From b47e96f29d806860c94bb743d9d526f0e0581125 Mon Sep 17 00:00:00 2001 From: claude-code-swe Date: Tue, 29 Sep 2026 12:48:27 +0000 Subject: [PATCH 2/2] ci: build fresh (--pull --no-cache) in force-publish Address review: the "overwrites a bad or stale image" promise only held for a corrupt/mis-pushed tag. If the build itself produced the bad image (a base image that moved under a floating tag, or a poisoned local layer cache on a reused arc-runner-set runner), a plain docker build reuses those cached layers and reproduces the identical bad image, so the re-push fixes nothing. This is the deliberate force path, so bust the cache and re-pull the base for a genuinely fresh rebuild. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Bsb5Etje6GZdt1tCkF7xsS --- .github/workflows/publish-images.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index f008886..dd09c8d 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -74,12 +74,16 @@ jobs: # cd.yml resolves and deploys. Unconditional build + push (unlike # cd.yml's build-missing, which skips when the tag already exists): this # is a *force* publish, so it also overwrites a bad or stale image under - # the same tag. + # the same tag. `--pull --no-cache` guarantees a genuinely fresh build so + # the force path also fixes an image whose contents (not just the pushed + # tag) went bad — a base image that moved under a floating tag, or a + # poisoned local layer cache on a reused arc-runner-set runner — which a + # cache-reusing rebuild would otherwise reproduce byte-for-byte. - name: Build & push ${{ matrix.image }} (forced) run: | ref="docker.io/blackmarket/glyph-${{ matrix.image }}:$(git rev-parse HEAD^{tree})" echo "Force-publishing $ref" - docker build ${{ matrix.build-args }} \ + docker build --pull --no-cache ${{ matrix.build-args }} \ -t "$ref" \ -f ${{ matrix.dockerfile }} ${{ matrix.context }} docker push "$ref"