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..da94d18 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +# 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, 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. +name: release +on: + push: + tags: ["v*"] +permissions: + contents: write + packages: 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 + + 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 72dab80..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 @@ -283,6 +296,25 @@ 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, 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 MIT. See [LICENSE](LICENSE).