ci: add CI and tag-driven release workflows - #4
Merged
Merged
Conversation
This repo had no GitHub Actions workflows, no releases, and no version marker, unlike every other repo we own. Bring it up to the house standard set by butterstack-connector. ci.yml runs on pushes and PRs to main: discovers every shell script in the repo (by *.sh and any extensionless file with a bash/sh shebang), syntax-checks each with bash -n, lints with shellcheck at -S error, and validates both compose files with docker compose config -q. release.yml triggers only on a v* tag push, reuses ci.yml via workflow_call so a tag can never cut a release from a broken tree, and then cuts a GitHub Release with gh release create --generate-notes --verify-tag. Merging to main still publishes nothing. README gets a short Releases section documenting the flow.
Add an `image` job to release.yml that builds prod/Dockerfile with docker/build-push-action (matching the house pattern from butterstack-connector's release workflow) and pushes it to ghcr.io/butterstack/perforce-docker, tagged with both the release version and `latest`. linux/amd64 only: Perforce's own apt repository has no arm64 build of helix-p4d (dists/noble/Release lists amd64 and i386 only, and the binary-arm64/Packages path 404s), confirmed by a failed local arm64 build attempt. Publishing both platforms would have silently produced an amd64-only manifest under a multi-arch tag, the exact failure mode that shipped as the connector's v0.1.0 issue #5. Also updates the README: the Production quick-start now shows pulling the published image as an alternative to building locally, and the Releases section documents the image, the pull command, and the amd64-only architecture honestly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This repo had no GitHub Actions workflows (
.github/only held issue/PR templates), no releases, and no version marker, unlike every other repo we own. This PR brings it up to the tag-driven release strategy and basic CI thatbutterstack-connectoralready uses, adapted for a shell-and-Docker repo instead of a Go binary repo.What's added
.github/workflows/ci.yml- runs on pushes and PRs tomain. One job,lint, with a 10 minute timeout:*.sh, plus any extensionless file whose first line matches a bash/sh shebang) rather than hardcoding a list.bash -n.shellcheck -S error.dev/docker-compose.ymlandprod/docker-compose.ymlwithdocker compose config -q. The prod file requiresP4PASSWDwith no default by design, so the validation step exports a CI-only placeholder value just to let the config parse; nothing is built or started.workflow_calltrigger sorelease.ymlcan invoke it directly instead of duplicating the steps..github/workflows/release.yml- triggers only on av*tag push (merging tomainpublishes nothing). Callsci.ymlas a reusable workflow first, so a tag can never cut a release from a tree that would fail its own CI, then cuts the release with the same invocation the connector repo uses:It also now builds and publishes the production container image to GHCR - see "Container image publishing" below.
README.md- a "Releases" section documenting the tag-driven release and image publish, plus a pull-the-published-image alternative alongside the existing local-build instructions in the Production quick start.No script, Dockerfile, or other documentation behavior changed.
Container image publishing
Ryan approved publishing a container image on 2026-09-09, so this now also adds an
imagejob torelease.ymlthat builds and pushes toghcr.io/butterstack/perforce-dockeron every tag push, following the house pattern frombutterstack-connector'srelease.yml(docker/setup-buildx-action,docker/login-actionagainst ghcr.io withgithub.actor/GITHUB_TOKEN, thendocker/build-push-actionwithpush: true,provenance: true,sbom: true, and both a version tag andlatest). Action major versions match the connector's exactly (setup-buildx-action@v4,login-action@v4,build-push-action@v7).Four decisions, each verified against this repo rather than assumed:
Which Dockerfile:
prod/Dockerfileonly, published asghcr.io/butterstack/perforce-docker. The repo has separatedev/(fast, no SSL, default creds - meant to stay local) andprod/(SSL, hardened, no default creds) trees. Only prod is fit to hand out as a pullable image; publishing dev's default-credentials image under a public tag would be a real footgun. Kept this to prod-only to keep the change small, per the task's default; the dev image isn't published under any tag.Build context and target:
context: prod,file: prod/Dockerfile, plus a named build contextshared=shared, no target. Readprod/docker-compose.yml, which builds withcontext: .(i.e. theprod/directory) andadditional_contexts: { shared: ../shared }- that's howCOPY --from=shared typemaps /shared/typemapsandCOPY --from=shared setup-typemap.sh ...in the Dockerfile resolve.docker/build-push-action@v7'sbuild-contextsinput is the equivalent of buildx's--build-contextflag, so the workflow reproduces the same layout:context: prod,build-contexts: shared=shared(both paths relative to the actions/checkout'd repo root). There is notarget:becauseprod/Dockerfileis a singleFROM ubuntu:24.04stage - no multi-stage build to name a stage in, so nothing for a Dockerfile reorder to silently swap.Architectures:
linux/amd64only, and I verified this is real, not assumed. I fetched Perforce's own apt repo Release file directly:curl -fsSL http://package.perforce.com/apt/ubuntu/dists/noble/ReleaseshowsArchitectures: amd64 i386- no arm64 at all - anddists/noble/release/binary-arm64/Packages404s. I then actually randocker buildx build --platform linux/arm64 -f prod/Dockerfile --build-context shared=shared prodlocally: it fails at theapt-get install helix-p4dstep withE: Unable to locate package helix-p4d. So arm64 is genuinely unbuildable today, not just untested. The workflow publishesplatforms: linux/amd64only, carries nodocker/setup-qemu-actionstep (nothing needs cross-arch emulation for a single native-arch build on an amd64 runner), and has an explicit comment citing this evidence plus a warning not to add arm64 back without re-verifying - this is exactly the failure class that shipped as the connector's v0.1.0 issue #5 (buildx silently emitting an amd64-only manifest under a platforms list that claimed both). README and this PR both say the same thing plainly.Build args:
P4D_VERSIONleft untouched. The Dockerfile'sARG P4D_VERSION=2026.1default is not wired togithub.ref_nameor overridden anywhere in the new job. The release tag versions this repo's packaging (the Dockerfile, compose files, scripts), not the Perforce server version bundled inside - those are independent axes and conflating them would make a packaging-only release accidentally bump (or pin) the p4d version.Follow-up: shellcheck severity
This codebase has never been linted before, so a normal-severity shellcheck run would fail immediately on pre-existing style findings that aren't real bugs. The gate is set to
-S error(fails only on genuine breakage) so the workflow lands green. Tightening it to-S warning(or default) once the existing findings are cleaned up is a follow-up, not done here. See verification output below - both severities were run locally and-S erroris clean while default severity has pre-existing findings.Verification (run locally, real output)
Both workflow files parse:
Discovered scripts (7, matches everything under
dev/,prod/,shared/,examples/; none extensionless today):bash -non all 7: all passed, no output (silence = success forbash -n).shellcheck -S erroron all 7 (Homebrew shellcheck 0.11.0, close to what Ubuntu runners ship): exit 0, no findings. I also ran default-severity shellcheck against the same files as a sanity check (not part of this PR's gate) and it does surface pre-existing style findings, confirming the note above about a real future follow-up rather than a hypothetical one.docker compose config -qon both compose files (Docker Compose v5.1.2):Without a
P4PASSWDvalue, prod correctly fails config withrequired variable P4PASSWD is missing a value: P4PASSWD is required for production- confirming the placeholder is necessary and that this isn't accidentally masking the "no default password in prod" behavior.Image build, verified locally (my host is arm64, so amd64 needed emulation):
A fresh, uncached build genuinely succeeds and installs
helix-p4d 2026.1-2972966~noble_amd64. I also ranp4d -V/p4 -Vinside the built image under emulation and got real version output back (Version of OpenSSL Libraries: OpenSSL 3.5.7), confirming the binaries actually execute, not just that the apt step exited 0.I did not and cannot run the actual GitHub Actions workflows from here (no way to trigger Actions or push to ghcr.io outside CI), and did not push any image myself - everything above is the exact logic each workflow step runs, executed directly against the same Dockerfile, compose additional-context wiring, and upstream apt repo the workflow will use.
Scope notes
main, not off the openfix/protections-only-seeded-on-first-initPR branch (fix(prod): only seed default protections on first init #3). Nothing inprod/entrypoint.shwas touched.For a human after merge
perforce-dockerpackage underButterStack's GHCR namespace. Like the connector's package, a brand-new GHCR package typically lands private by default and needs a manual visibility flip to public in the package settings (Package settings -> Danger Zone -> Change visibility) beforedocker pullworks for anyone outside the org.helix-p4dbuild, re-run the verification above before addinglinux/arm64back to theplatforms:list and re-adding adocker/setup-qemu-actionstep.