Skip to content

ci(docker): add workflow_dispatch to build a SHA-tagged preview image - #11110

Open
tonio-alucema wants to merge 2 commits into
masterfrom
docker-dispatch-preview-image
Open

ci(docker): add workflow_dispatch to build a SHA-tagged preview image#11110
tonio-alucema wants to merge 2 commits into
masterfrom
docker-dispatch-preview-image

Conversation

@tonio-alucema

@tonio-alucema tonio-alucema commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work.
  • The docker.yml workflow builds and pushes the server image to ghcr.
  • Today it triggers only on push to master and on v* tags. It has no branch build.
  • Because of this, you cannot build an image from an unmerged branch. You cannot preview a change end to end before you merge it.
  • The concurrency group is docker-${{ github.ref }} with cancel-in-progress: false. A newer push still supersedes a pending run, so a given master commit is not guaranteed to have an image either.
  • This pull request adds a manual workflow_dispatch trigger with an optional ref input. The manual run builds a SHA-tagged image and does not publish a release tag.
  • The benefit is a reliable preview image for any ref. The deploy tooling accepts the resulting sha-<short> tag as an ordinary app ref, so nothing downstream changes.

Linked Issues or Issue Description

No public GitHub issue exists. The problem is described below.

Current behavior

docker.yml triggers only on push to master and on v* tags. There is no way to build an image from an unmerged branch. A preview of a change before merge has no image to deploy. In addition, the per-ref concurrency group supersedes a pending master run when the next push arrives, so an arbitrary master commit can have no image.

Proposed behavior

Add a workflow_dispatch trigger with an optional ref input. The manual run checks out that ref, builds it, and pushes an image tagged sha-<short> for the built commit. The manual run does not publish latest or a v*/semver release tag. The push and tag paths do not change.

Reason and benefit

An operator can build a preview image for any unmerged branch, tag, or commit. This is the reliable way to be sure a ref has an image before you pin it as an app ref. This uses a dedicated preview build. It does not use the release workflow, because the release workflow publishes a stable v* tag, which is a much bigger action than a review image.

What Changed

  • Added a workflow_dispatch trigger to docker.yml with an optional ref input (default: the dispatched ref).
  • Both build jobs now check out ${{ inputs.ref || github.sha }}. On push and tag events this resolves to github.sha, the checkout default.
  • Added a Resolve build ref step that reads the exact checked-out commit (git rev-parse HEAD) and its short SHA.
  • Kept type=sha unchanged for push and tag events. For a manual run it is disabled, and a raw sha-<short> tag of the resolved commit is used instead. This keeps the SHA tag correct even when the built ref differs from github.sha.
  • Set PAPERCLIP_BUILD_COMMIT from the resolved commit. On push and tag events this value equals github.sha, so those images do not change.

Verification

  • actionlint reports no issues on .github/workflows/docker.yml.
  • The workflow YAML parses.
  • The push and tag paths stay behaviorally identical. inputs.ref is empty on those events, so the checkout ref and PAPERCLIP_BUILD_COMMIT both resolve to github.sha. type=sha is enabled only when github.event_name != 'workflow_dispatch', so it is unchanged on push and tag events.
  • Manual test after merge: run the workflow against an unmerged branch and confirm the image gets a sha-<short> tag and no v* tag.

Risks

Low risk. The change adds a new trigger and does not alter the push or tag build behavior. A workflow_dispatch run never publishes a release pointer: latest, type=semver, and the stock type=sha tag are all gated enable=${{ github.event_name != 'workflow_dispatch' }}, and latest additionally replaces {{is_default_branch}} with an explicit github.ref_name == default_branch && github.event_name != 'workflow_dispatch' check. So even a manual run dispatched from master leaves latest and every v*/semver tag untouched and publishes only sha-<short>. Push and tag builds are byte-for-byte unchanged because inputs.ref is empty and github.event_name is push/tag on those events.

Model Used

  • Provider and model: Anthropic Claude (Opus 4.8).
  • Exact model ID: claude-opus-4-8.
  • Reasoning mode: extended thinking.
  • Capabilities: tool use, code execution (repo edits, actionlint, git).

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

`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-<short>` 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-<short>` 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-<short>` 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 <noreply@paperclip.ing>
@tonio-alucema

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

The workflow_dispatch `ref` input is meant to preview an unmerged branch,
and the only ref guaranteed to carry this workflow after merge is master —
so the expected way to run it is dispatch-from-master with `ref` set to the
branch. But on a workflow_dispatch dispatched from master, `github.ref` is
the default branch, so `type=raw,value=latest,enable={{is_default_branch}}`
still fired: a preview image would move `latest` and poison every host that
pulls it. `type=semver` was likewise ungated (a dispatch from a v* tag would
have minted a release tag pointing at the previewed ref).

Gate both off for workflow_dispatch so a manual run can only ever produce
the `sha-<short>` tag. The `latest` gate now checks the default branch
explicitly instead of `{{is_default_branch}}` so it can also exclude the
dispatch case; on push:master and tag:v* the emitted tags are unchanged.

Tested: js-yaml and ruby YAML both parse the file; grep confirms both the
self-hosted and cloud meta blocks are gated symmetrically.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@tonio-alucema

Copy link
Copy Markdown
Contributor Author

Pushed 0cec411: gate latest and v*/semver tags off for workflow_dispatch runs. The ref input's expected use is dispatch-from-master (the only ref carrying this workflow after merge) with ref pointing at an unmerged branch — but on a dispatch from master github.ref is the default branch, so {{is_default_branch}} was still true and a preview build would have moved latest. Now a manual run can only ever emit the sha-<short> tag; push:master and tag:v* tag sets are unchanged.

@greptile-apps review

@tonio-alucema

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

1 similar comment
@tonio-alucema

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

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.

1 participant