From 77f1722140ee7546209e1b028baa00abe2064f38 Mon Sep 17 00:00:00 2001 From: Ryan L'Italien Date: Wed, 9 Sep 2026 17:13:17 -0400 Subject: [PATCH 1/2] ci: add CI and tag-driven release workflows 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. --- .github/workflows/ci.yml | 67 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 32 +++++++++++++++++ README.md | 13 +++++++ 3 files changed, 112 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2598a4a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + # Lets release.yml call this workflow so a tag can never cut a release + # from checks that differ from what a normal PR has to pass. + workflow_call: + +jobs: + lint: + name: Lint scripts and validate compose + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Discover shell scripts + run: | + set -euo pipefail + { + find . -not -path './.git/*' -type f -name '*.sh' + find . -not -path './.git/*' -type f ! -name '*.*' \ + -exec sh -c 'head -n1 "$1" | grep -qE "^#!.*(bash|sh)" && echo "$1"' _ {} \; + } | sort -u > shell_scripts.txt + echo "Discovered $(wc -l < shell_scripts.txt) script(s):" + cat shell_scripts.txt + + - name: Syntax check (bash -n) + run: | + set -euo pipefail + while IFS= read -r f; do + echo "bash -n $f" + bash -n "$f" + done < shell_scripts.txt + + - name: Ensure shellcheck is available + run: | + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y shellcheck + fi + shellcheck --version + + - name: shellcheck (error severity) + run: | + set -euo pipefail + # This codebase has never been linted, so a strict run fails on + # pre-existing style findings. Gate on -S error (genuine breakage) + # for now; tightening to `warning` once the backlog is cleaned up + # is a tracked follow-up, not done in this workflow. + xargs shellcheck -S error < shell_scripts.txt + + - name: Validate compose files + run: | + set -euo pipefail + find . -not -path './.git/*' -iname 'docker-compose*.yml' | while IFS= read -r f; do + dir=$(dirname "$f") + base=$(basename "$f") + echo "Validating $f" + # prod/docker-compose.yml requires P4PASSWD with no default (by + # design, prod has no baked-in credentials); a placeholder is + # enough to let config parse without starting anything. + (cd "$dir" && P4PASSWD=ci-placeholder-password docker compose -f "$base" config -q) + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..78a3d72 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +# Cut a release by pushing a tag: `git tag v0.1.0 && git push origin v0.1.0`. +# The tag run re-runs the same script and compose validation as CI (so a tag +# can never cut a release from a broken tree), then produces a GitHub Release +# on the Releases page with auto-generated notes from the commit history. +# Merging to main releases nothing - only a `v*` tag push triggers this. +# This does not build or publish a container image; see the repo README and +# the PR that introduced this workflow for that open question. +name: release +on: + push: + tags: ["v*"] +permissions: + contents: write +jobs: + ci: + uses: ./.github/workflows/ci.yml + + release: + needs: ci + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + --title "${GITHUB_REF_NAME}" \ + --generate-notes \ + --verify-tag diff --git a/README.md b/README.md index 72dab80..d6a87a3 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,19 @@ Create a dedicated `service` type user for MCP (no password expiry, revocable ti See [AGENTS.md](AGENTS.md) for the full setup guide - creating the service account, generating tickets, Docker Compose networking, and an LLM-friendly command reference. +## Releases + +Every push to `main` runs CI (script syntax/lint checks, compose file validation) but publishes nothing. + +A release is cut by pushing a version tag: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +The tag push re-runs the same validation as CI, then creates a GitHub Release with notes generated from the commit history. There's no prebuilt image today - clone this repo and build with `docker compose up` as shown above. + ## License MIT. See [LICENSE](LICENSE). From 89f683a814dba707ac9b7bc258b7952a74e7540c Mon Sep 17 00:00:00 2001 From: Ryan L'Italien Date: Wed, 9 Sep 2026 17:31:45 -0400 Subject: [PATCH 2/2] ci: publish the production image to GHCR on tagged releases 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. --- .github/workflows/release.yml | 51 ++++++++++++++++++++++++++++++++--- README.md | 21 ++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78a3d72..da94d18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,17 @@ # Cut a release by pushing a tag: `git tag v0.1.0 && git push origin v0.1.0`. # The tag run re-runs the same script and compose validation as CI (so a tag # can never cut a release from a broken tree), then produces a GitHub Release -# on the Releases page with auto-generated notes from the commit history. +# on the Releases page with auto-generated notes from the commit history, and +# publishes the production image to ghcr.io/butterstack/perforce-docker +# tagged with the version and `latest`. # Merging to main releases nothing - only a `v*` tag push triggers this. -# This does not build or publish a container image; see the repo README and -# the PR that introduced this workflow for that open question. name: release on: push: tags: ["v*"] permissions: contents: write + packages: write jobs: ci: uses: ./.github/workflows/ci.yml @@ -30,3 +31,47 @@ jobs: --title "${GITHUB_REF_NAME}" \ --generate-notes \ --verify-tag + + image: + needs: ci + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # No docker/setup-qemu-action step here (unlike the connector image): + # QEMU exists to let one amd64 runner cross-build a second + # architecture, and this image only targets linux/amd64 - see why + # below. Add it back if linux/arm64 ever becomes buildable. + # + # helix-p4d has no arm64 build in Perforce's own apt repo (the repo's + # dists/noble/Release lists "Architectures: amd64 i386" only, and its + # binary-arm64/Packages path 404s), confirmed by an actual local arm64 + # build attempt failing with "Unable to locate package helix-p4d". So, + # unlike the connector image, this publishes linux/amd64 only. Do not + # add linux/arm64 to the platforms list below without first confirming + # Perforce has shipped an arm64 helix-p4d package - adding it back + # blind reproduces the connector's v0.1.0 issue #5 (buildx silently + # emitting an amd64-only manifest under a multi-arch tag). + # + # There is no multi-stage build here to target: prod/Dockerfile is a + # single FROM ubuntu:24.04 stage, so there is no "runtime"-style stage + # name to pin. + - uses: docker/build-push-action@v7 + with: + context: prod + file: prod/Dockerfile + build-contexts: | + shared=shared + platforms: linux/amd64 + push: true + provenance: true + sbom: true + tags: | + ghcr.io/butterstack/perforce-docker:${{ github.ref_name }} + ghcr.io/butterstack/perforce-docker:latest diff --git a/README.md b/README.md index d6a87a3..6c41eb8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,19 @@ cd prod P4PASSWD=YourSecurePassword123% docker compose up -d ``` +`docker compose up` builds the image locally from `prod/Dockerfile`. To use +the prebuilt image from GitHub Container Registry instead (see +[Releases](#releases) below), pull it and run it directly: + +```bash +docker pull ghcr.io/butterstack/perforce-docker:v0.1.0 +docker run -d --name perforce \ + -p 1666:1666 -p 8090:8090 \ + -e P4PASSWD=YourSecurePassword123% \ + -v perforce_data:/data \ + ghcr.io/butterstack/perforce-docker:v0.1.0 +``` + Connect: ``` Server: ssl:localhost:1666 @@ -294,7 +307,13 @@ git tag v0.1.0 git push origin v0.1.0 ``` -The tag push re-runs the same validation as CI, then creates a GitHub Release with notes generated from the commit history. There's no prebuilt image today - clone this repo and build with `docker compose up` as shown above. +The tag push re-runs the same validation as CI, creates a GitHub Release with notes generated from the commit history, and publishes the production image (`prod/Dockerfile`) to GitHub Container Registry, tagged with both the version and `latest`: + +```bash +docker pull ghcr.io/butterstack/perforce-docker:v0.1.0 +``` + +**Architecture: `linux/amd64` only.** Perforce's own apt repository does not ship an `arm64` build of `helix-p4d` (its `noble` release lists `amd64` and `i386` only), so this image cannot be built for `arm64` today. Running it on Apple Silicon or another arm64 host requires emulation (e.g. Docker Desktop/OrbStack's `linux/amd64` emulation), which works but is slower than a native image. If you don't want to rely on an image built by us, or need `arm64`, clone the repo and build it yourself with `docker compose up` as shown above - the compose build honors your host's native architecture. ## License