From 02c6fa1c6c186f757df45cb452ae98eaa7beadcc Mon Sep 17 00:00:00 2001 From: Tonio Date: Sat, 8 Aug 2026 17:29:53 -0700 Subject: [PATCH] ci(docker): add workflow_dispatch to build a SHA-tagged preview image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docker.yml` only triggered on `push` to `master` and on `v*` tags, so there was no way to build an image from an unmerged branch. Previewing a change end to end before merging therefore had no image to deploy, and because the concurrency group is `docker-${github.ref}` a *pending* master run can still be superseded by the next push — so an arbitrary master SHA is not guaranteed to have an image either. Add a `workflow_dispatch` trigger with an optional `ref` input: - The manual run checks out `inputs.ref` (falling back to `github.sha`), resolves the exact commit, and publishes a `sha-` image tag for that commit. The `latest` and `v*`/semver tags stay gated to the push and tag events, so a dispatch never publishes a release tag. - `type=sha` is kept exactly as-is for push/tag events; the dispatch tag is emitted as a raw `sha-` of the resolved commit so it is correct even when the built ref differs from `github.sha`. - The commit stamped into the image (`PAPERCLIP_BUILD_COMMIT`) is taken from the same resolved commit; on push/tag it equals `github.sha`, so those builds are unchanged. This is intentionally not the release workflow, which publishes a stable `v*` tag — a much bigger act than building a review image. The resulting `sha-` tag is consumed as an ordinary app ref by the fleet deploy tooling, so nothing downstream changes. Tested: `actionlint` clean on the workflow; YAML parses; verified the diff leaves the push/tag paths byte-for-byte behaviourally identical (`ref` and `PAPERCLIP_BUILD_COMMIT` both resolve to `github.sha`, and `type=sha` is unchanged when `github.event_name != 'workflow_dispatch'`). Co-Authored-By: Paperclip --- .github/workflows/docker.yml | 67 +++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ba9c25e4009..01d556be072 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,6 +6,19 @@ on: - "master" tags: - "v*" + # Manual builds so an unmerged ref can be previewed before it lands. This + # publishes a SHA-tagged image and no `latest`/`v*` (release) tag — it is a + # review image, not a release — and the deploy tooling accepts the resulting + # `sha-` tag as an ordinary app ref, so nothing downstream changes. + # Deliberately not the release workflow: that one publishes a stable `v*` + # release, a far bigger act than building a preview image. + workflow_dispatch: + inputs: + ref: + description: "Branch, tag, or commit SHA to build. Defaults to the ref this run was dispatched on." + required: false + type: string + default: "" permissions: contents: read @@ -26,10 +39,29 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: + # push/tag builds resolve to github.sha — the checkout default, so + # their behaviour is unchanged. A manual run may override it with the + # `ref` input to build an unmerged branch, tag, or commit. + ref: ${{ inputs.ref || github.sha }} # Full history and tags so `git describe` below can compute the # release version to stamp into the image. fetch-depth: 0 + # Pin every downstream reference to the exact commit that was checked + # out. On push/tag builds this equals github.sha; on a manual run it is + # the head of the `ref` input, which can differ from github.sha (the ref + # the dispatch ran on). The SHA image tag and the commit stamped into the + # image are both derived from this, so a manual build always tags the + # commit it actually built. + - name: Resolve build ref + id: resolve-ref + run: | + set -euo pipefail + sha="$(git rev-parse HEAD)" + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + echo "short_sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + echo "Building ${GITHUB_REF} at ${sha}" + # `.git` is dockerignored, so a running image cannot derive its own # version and otherwise reports the source package.json placeholder in # analytics and the debug panel. Compute it here from the pristine @@ -140,7 +172,11 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=sha + # push/tag builds keep the stock sha tag (sha- of github.sha). + # A manual run may build a ref other than github.sha, so it tags the + # resolved commit instead — same sha- format, correct commit. + type=sha,enable=${{ github.event_name != 'workflow_dispatch' }} + type=raw,value=sha-${{ steps.resolve-ref.outputs.short_sha }},enable=${{ github.event_name == 'workflow_dispatch' }} labels: | io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }} io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }} @@ -155,7 +191,7 @@ jobs: target: production build-args: | PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }} - PAPERCLIP_BUILD_COMMIT=${{ github.sha }} + PAPERCLIP_BUILD_COMMIT=${{ steps.resolve-ref.outputs.sha }} CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }} platforms: linux/amd64,linux/arm64 push: true @@ -183,10 +219,29 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: + # push/tag builds resolve to github.sha — the checkout default, so + # their behaviour is unchanged. A manual run may override it with the + # `ref` input to build an unmerged branch, tag, or commit. + ref: ${{ inputs.ref || github.sha }} # Full history and tags so `git describe` below can compute the # release version to stamp into the image. fetch-depth: 0 + # Pin every downstream reference to the exact commit that was checked + # out. On push/tag builds this equals github.sha; on a manual run it is + # the head of the `ref` input, which can differ from github.sha (the ref + # the dispatch ran on). The SHA image tag and the commit stamped into the + # image are both derived from this, so a manual build always tags the + # commit it actually built. + - name: Resolve build ref + id: resolve-ref + run: | + set -euo pipefail + sha="$(git rev-parse HEAD)" + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + echo "short_sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + echo "Building ${GITHUB_REF} at ${sha}" + # `.git` is dockerignored, so a running image cannot derive its own # version and otherwise reports the source package.json placeholder in # analytics and the debug panel. Compute it here from the pristine @@ -301,7 +356,11 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=sha + # push/tag builds keep the stock sha tag (sha- of github.sha). + # A manual run may build a ref other than github.sha, so it tags the + # resolved commit instead — same sha- format, correct commit. + type=sha,enable=${{ github.event_name != 'workflow_dispatch' }} + type=raw,value=sha-${{ steps.resolve-ref.outputs.short_sha }},enable=${{ github.event_name == 'workflow_dispatch' }} labels: | io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }} io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }} @@ -316,7 +375,7 @@ jobs: build-args: | CLOUD_BUNDLED_PLUGINS=daytona PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }} - PAPERCLIP_BUILD_COMMIT=${{ github.sha }} + PAPERCLIP_BUILD_COMMIT=${{ steps.resolve-ref.outputs.sha }} CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }} # amd64 only, unlike the self-hosted image above: the cloud variant # is consumed exclusively by managed-deployment hosts, which run