Skip to content

ci: add CI and tag-driven release workflows - #4

Merged
ryanlitalien merged 2 commits into
mainfrom
ci/add-ci-and-tag-driven-releases
Sep 9, 2026
Merged

ryanlitalien merged 2 commits into
mainfrom
ci/add-ci-and-tag-driven-releases

Conversation

@ryanlitalien

@ryanlitalien ryanlitalien commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

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 that butterstack-connector already 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 to main. One job, lint, with a 10 minute timeout:

  • Discovers every shell script in the repo (*.sh, plus any extensionless file whose first line matches a bash/sh shebang) rather than hardcoding a list.
  • Syntax-checks each discovered script with bash -n.
  • Lints each discovered script with shellcheck -S error.
  • Validates both dev/docker-compose.yml and prod/docker-compose.yml with docker compose config -q. The prod file requires P4PASSWD with 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.
  • Also carries a workflow_call trigger so release.yml can invoke it directly instead of duplicating the steps.

.github/workflows/release.yml - triggers only on a v* tag push (merging to main publishes nothing). Calls ci.yml as 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:

gh release create "${GITHUB_REF_NAME}" --title "${GITHUB_REF_NAME}" --generate-notes --verify-tag

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 image job to release.yml that builds and pushes to ghcr.io/butterstack/perforce-docker on every tag push, following the house pattern from butterstack-connector's release.yml (docker/setup-buildx-action, docker/login-action against ghcr.io with github.actor/GITHUB_TOKEN, then docker/build-push-action with push: true, provenance: true, sbom: true, and both a version tag and latest). 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:

  1. Which Dockerfile: prod/Dockerfile only, published as ghcr.io/butterstack/perforce-docker. The repo has separate dev/ (fast, no SSL, default creds - meant to stay local) and prod/ (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.

  2. Build context and target: context: prod, file: prod/Dockerfile, plus a named build context shared=shared, no target. Read prod/docker-compose.yml, which builds with context: . (i.e. the prod/ directory) and additional_contexts: { shared: ../shared } - that's how COPY --from=shared typemaps /shared/typemaps and COPY --from=shared setup-typemap.sh ... in the Dockerfile resolve. docker/build-push-action@v7's build-contexts input is the equivalent of buildx's --build-context flag, 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 no target: because prod/Dockerfile is a single FROM ubuntu:24.04 stage - no multi-stage build to name a stage in, so nothing for a Dockerfile reorder to silently swap.

  3. Architectures: linux/amd64 only, 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/Release shows Architectures: amd64 i386 - no arm64 at all - and dists/noble/release/binary-arm64/Packages 404s. I then actually ran docker buildx build --platform linux/arm64 -f prod/Dockerfile --build-context shared=shared prod locally: it fails at the apt-get install helix-p4d step with E: Unable to locate package helix-p4d. So arm64 is genuinely unbuildable today, not just untested. The workflow publishes platforms: linux/amd64 only, carries no docker/setup-qemu-action step (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.

  4. Build args: P4D_VERSION left untouched. The Dockerfile's ARG P4D_VERSION=2026.1 default is not wired to github.ref_name or 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 error is clean while default severity has pre-existing findings.

Verification (run locally, real output)

Both workflow files parse:

$ python3 -c "import yaml;yaml.safe_load(open('.github/workflows/ci.yml'))" && echo OK
OK
$ python3 -c "import yaml;yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK
OK

Discovered scripts (7, matches everything under dev/, prod/, shared/, examples/; none extensionless today):

./dev/entrypoint.sh
./examples/ci-triggers/github-actions-trigger.sh
./examples/ci-triggers/jenkins-trigger.sh
./examples/ci-triggers/webhook-trigger.sh
./examples/streams/setup-stream-depot.sh
./prod/entrypoint.sh
./shared/setup-typemap.sh

bash -n on all 7: all passed, no output (silence = success for bash -n).

shellcheck -S error on 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 -q on both compose files (Docker Compose v5.1.2):

$ (cd dev && docker compose -f docker-compose.yml config -q) && echo OK
OK
$ (cd prod && P4PASSWD=ci-placeholder-password docker compose -f docker-compose.yml config -q) && echo OK
OK

Without a P4PASSWD value, prod correctly fails config with required 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):

$ docker buildx build --no-cache --platform linux/amd64 -f prod/Dockerfile \
    --build-context shared=shared -t perforce-docker-test:amd64 prod --load
...
Setting up helix-p4d (2026.1-2972966~noble) ...
#17 writing image sha256:6a457af... done
real  1m33.113s

A fresh, uncached build genuinely succeeds and installs helix-p4d 2026.1-2972966~noble_amd64. I also ran p4d -V / p4 -V inside 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.

$ docker buildx build --platform linux/arm64 -f prod/Dockerfile \
    --build-context shared=shared -t perforce-docker-test:arm64 prod --load
...
E: Unable to locate package helix-p4d
ERROR: failed to build: ... exit code: 100

$ curl -fsSL http://package.perforce.com/apt/ubuntu/dists/noble/Release | grep Architectures
Architectures: amd64 i386
$ curl -s -o /dev/null -w "%{http_code}\n" http://package.perforce.com/apt/ubuntu/dists/noble/release/binary-arm64/Packages
404

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

  • Branched fresh off main, not off the open fix/protections-only-seeded-on-first-init PR branch (fix(prod): only seed default protections on first init #3). Nothing in prod/entrypoint.sh was touched.
  • No script or Dockerfile changes; documentation changes are limited to the Releases section and the Production quick-start's pull-vs-build note.

For a human after merge

  • The first tag push will create the perforce-docker package under ButterStack'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) before docker pull works for anyone outside the org.
  • If Perforce ever ships an arm64 helix-p4d build, re-run the verification above before adding linux/arm64 back to the platforms: list and re-adding a docker/setup-qemu-action step.

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.
@ryanlitalien
ryanlitalien merged commit d6ba4f6 into main Sep 9, 2026
1 check passed
@ryanlitalien
ryanlitalien deleted the ci/add-ci-and-tag-driven-releases branch September 9, 2026 21:37
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