Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ on:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Superseded PR pushes are worth cancelling; main is not. A cancelled main run takes
# publish-canary down with it (run-level cancellation ignores job-level concurrency),
# and that commit's immutable sha- tag would then never exist.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

# Least privilege by default: only publish-canary needs a write scope, and it asks for it itself.
permissions:
contents: read

env:
CARGO_TERM_COLOR: always
Expand Down Expand Up @@ -81,3 +88,82 @@ jobs:
- name: Build the SPA and the SDK
run: just web-build
- run: just e2e

# Canary image publish. Lives in this workflow, not a separate one, so `needs` gates it on the
# same green run that just tested the commit: a red main never produces a canary tag, and there
# is no workflow_run indirection re-deriving the SHA from an event payload.
#
# ghcr.io/<owner>/walgit:canary moving tag, always the newest green main
# ghcr.io/<owner>/walgit:sha-<40> immutable, what a rollback or a bug report pins to
#
# Re-running a run on the same commit is safe, and it does NOT touch an existing sha- tag: the
# build is not bit-reproducible, so republishing that tag would repoint what a rollback pinned to
# at a digest nobody chose. A re-run moves `canary` only; the sha- tag is written once, ever.
publish-canary:
name: publish canary image
needs: [build-test, e2e]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
# The only write scope this workflow needs; GITHUB_TOKEN carries nothing else.
packages: write
steps:
# This job holds a token that can write packages, so its actions are pinned to commit SHAs:
# a moving tag would let an upstream compromise reach the registry. The other jobs read only.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Nothing here runs git again; leaving the token in .git/config only widens the blast radius.
persist-credentials: false
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# GHCR rejects an uppercase path, and github.repository carries the owner's real casing.
- name: Resolve the image name
id: image
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

# "Immutable" has to mean it: if this commit already has a sha- tag, leave it at the digest
# it already points to and let this run move `canary` alone.
- name: Decide whether the rollback tag is still unwritten
id: rollback
run: |
if docker buildx imagetools inspect "${{ steps.image.outputs.name }}:sha-${GITHUB_SHA}" >/dev/null 2>&1; then
echo "unwritten=false" >> "$GITHUB_OUTPUT"
else
echo "unwritten=true" >> "$GITHUB_OUTPUT"
fi

# format=long, not short: a seven-character prefix is a namespace that collides, and the
# collision would silently move an older commit's immutable tag onto a newer image. Both tags
# and the OCI source/revision labels come from one place rather than hand-built strings.
- name: Derive tags and labels
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ steps.image.outputs.name }}
tags: |
type=raw,value=canary
type=sha,format=long,prefix=sha-,enable=${{ steps.rollback.outputs.unwritten }}

- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Containerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
labels: ${{ steps.meta.outputs.labels }}
# The build stage never copies .git, so this ARG is the binary's only build identity:
# without it walgit-server/build.rs falls back to "dev" and every image reports the same
# version on /healthz. Full 40 chars: build.rs takes the value verbatim.
build-args: |
WALGIT_BUILD_SHA=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ open https://walgit.localhost:8080/

* `walgit.standalone.toml` — the one-machine shape (self-signed TLS, rustfs, every role). Start here.
* `walgit.example.toml` — every key with its default and a comment.
* `Containerfile`, `flake.nix` — an OCI image and a Nix package/devshell.
* `Containerfile`, `flake.nix` — an OCI image and a Nix package/devshell. Every green push to `main` publishes
that image to `ghcr.io/<owner>/walgit` as `canary` (moving) and `sha-<40>` (immutable: what a rollback pins to).
* `deploy/nginx.conf.example` — an optional nginx in front: public TLS, one `auth_request` per credential, and
**byte offload**: walgit answers bundle/LFS downloads with `X-Accel-Redirect` and nginx streams + caches the
object from the bucket itself (S3 presigned or GCS with walgit's bearer). The file documents the contract.
Expand Down
Loading