Both Claude reviewers caught this independently, and they are right. The test
job I added wrote a PUSH-CAPABLE REGISTRY_PASSWORD into a plaintext
~/.gitconfig and then ran `go build`/`go vet`/`go test` — repository code — on
pull_request events. This repo is public, so a fork PR could ship a test whose
only job is to print that file. The image build had already answered this
question correctly: its credentials are BuildKit secrets scoped to the
module-download RUN and are never present while code executes. I bolted on a
job that skipped the boundary its neighbour maintains.
Dependencies are now fetched in their own step which deletes ~/.gitconfig
before anything else runs, and asserts the scrub — against the whole home
directory, not against the file it just removed, because the credential can
also land in ~/.netrc or ~/.config/go/env. Verified the assertion is not
vacuous: planting the secret in ~/.netrc trips it. Later steps run with
GOPROXY=off, so any attempt to reach the network fails loudly rather than
quietly hunting for the credential that is now gone.
Also from round 4: TestEndpointProviderNamesAreAllAccepted pinned only
endpointProvider, while the constant is the error text for BOTH resolution
paths — it now asserts each advertised name resolves either way (break-checked
by dropping the gemini alias from resolveModel alone). preflight.sh documents
that ollama-cloud is checked on OLLAMA_API_KEY but hinted as
OLLAMA_CLOUD_API_KEY because run.sh copies one to the other first, an ordering
dependency that was invisible from the file.
And the comments that narrated this PR's own edit history ("the first version
of this change...") are rewritten as invariants. That history stops being true
the moment this merges, and the repo's doc policy says as much.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
183 lines
7.4 KiB
YAML
183 lines
7.4 KiB
YAML
name: Build & push image
|
|
|
|
# Builds the Gadfly reviewer container and pushes it to the Gitea container
|
|
# registry. Mirrors mort-ci.yml's build-and-push (BuildKit secrets for private
|
|
# module access + the LAN --add-host so the builder can reach the registry).
|
|
#
|
|
# push to main -> :latest + :sha-<short>
|
|
# push tag v* -> :<tag> + :latest
|
|
# other branch push -> :branch-<safe> + :sha-<short>
|
|
# pull_request -> build only (no push), as a sanity check
|
|
#
|
|
# Required repo secrets:
|
|
# REGISTRY_USER / REGISTRY_PASSWORD Gitea creds with registry push + read
|
|
# access to the private majordomo module.
|
|
# Optional:
|
|
# DISCORD_WEBHOOK_URL build notifications (unset => silent).
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ["v*"]
|
|
# Docs/example-only changes don't change the image — skip the rebuild.
|
|
# (Path filters are not applied to tag pushes, so `v*` releases always build.)
|
|
paths-ignore:
|
|
- "**.md"
|
|
- "examples/**"
|
|
- "LICENSE"
|
|
- ".gitignore"
|
|
- ".dockerignore"
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
paths-ignore:
|
|
- "**.md"
|
|
- "examples/**"
|
|
- "LICENSE"
|
|
- ".gitignore"
|
|
- ".dockerignore"
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: gadfly-image-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
IMAGE_NAME: gitea.stevedudenhoeffer.com/steve/gadfly
|
|
|
|
jobs:
|
|
# Runs alongside the image build rather than gating it: a red test should be
|
|
# loud on the PR without standing between Steve and a rebuild. Added because
|
|
# this repo had NO test job at all — `go test` and scripts/preflight_test.sh
|
|
# both existed and neither was ever executed by CI, which is worse than
|
|
# having no tests, since it reads as coverage.
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-go@v5
|
|
with:
|
|
go-version-file: go.mod
|
|
# Fetch dependencies, then DESTROY the credential before any step that
|
|
# executes repository code. REGISTRY_PASSWORD is push-capable, this repo
|
|
# is public so pull_request runs can carry attacker-authored code, and
|
|
# `go test` runs that code — a plaintext ~/.gitconfig left in place is a
|
|
# credential any test could print. The image build faces the same
|
|
# question and answers it the same way: its creds are BuildKit secrets
|
|
# scoped to the module-download RUN, never present while code runs.
|
|
- name: Fetch private modules
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
run: |
|
|
go env -w GOPRIVATE=gitea.stevedudenhoeffer.com/*
|
|
git config --global url."https://${REGISTRY_USER}:${REGISTRY_PASSWORD}@gitea.stevedudenhoeffer.com/".insteadOf "https://gitea.stevedudenhoeffer.com/"
|
|
go mod download
|
|
rm -f "$HOME/.gitconfig"
|
|
# Prove the scrub worked, and prove it against the whole home dir —
|
|
# checking only the file just deleted would pass no matter what, and
|
|
# the credential can also reach ~/.netrc or ~/.config/go/env.
|
|
test ! -e "$HOME/.gitconfig"
|
|
if grep -rqF "${REGISTRY_PASSWORD}" "$HOME" 2>/dev/null; then
|
|
echo "::error::registry credential still present under \$HOME after scrub"
|
|
exit 1
|
|
fi
|
|
|
|
# GOPROXY=off from here on: the module cache is already warm, so any
|
|
# attempt to reach the network is a bug — and it fails loudly instead of
|
|
# quietly looking for the credential that is now gone.
|
|
- name: go build
|
|
env: { GOPROXY: "off" }
|
|
run: go build ./...
|
|
- name: go vet
|
|
env: { GOPROXY: "off" }
|
|
run: go vet ./...
|
|
- name: gofmt
|
|
run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
|
|
- name: go test
|
|
env: { GOPROXY: "off" }
|
|
run: go test -count=1 ./...
|
|
- name: pre-flight credential table
|
|
run: bash scripts/preflight_test.sh
|
|
|
|
build-and-push:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
run: docker buildx create --use --name gadfly-builder --driver docker-container 2>/dev/null || docker buildx use gadfly-builder
|
|
|
|
- name: Log in to the registry
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
run: echo "${REGISTRY_PASSWORD}" | docker login gitea.stevedudenhoeffer.com -u "${REGISTRY_USER}" --password-stdin
|
|
|
|
- name: Compute tags
|
|
id: meta
|
|
run: |
|
|
SHA_SHORT=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
|
PUSH=true
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
# Build-only sanity check; nothing published.
|
|
TAGS="${IMAGE_NAME}:pr-${{ github.event.pull_request.number }}"
|
|
PUSH=false
|
|
elif [ "${{ github.ref_type }}" = "tag" ]; then
|
|
TAGS="${IMAGE_NAME}:${GITHUB_REF_NAME},${IMAGE_NAME}:latest"
|
|
elif [ "${GITHUB_REF_NAME}" = "main" ]; then
|
|
TAGS="${IMAGE_NAME}:latest,${IMAGE_NAME}:sha-${SHA_SHORT}"
|
|
else
|
|
BRANCH_SAFE=$(echo "${GITHUB_REF_NAME}" | sed 's/[^a-zA-Z0-9._-]/-/g; s/--*/-/g; s/^-//; s/-$//')
|
|
TAGS="${IMAGE_NAME}:branch-${BRANCH_SAFE},${IMAGE_NAME}:sha-${SHA_SHORT}"
|
|
fi
|
|
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
|
|
echo "push=${PUSH}" >> "$GITHUB_OUTPUT"
|
|
echo "Tags: ${TAGS} (push=${PUSH})"
|
|
|
|
- name: Notify Discord (started)
|
|
if: github.event_name != 'pull_request'
|
|
env:
|
|
WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
|
run: |
|
|
[ -z "$WEBHOOK_URL" ] && exit 0
|
|
MSG="🪰 Gadfly image build #${{ github.run_number }} started on \`${{ github.ref_name }}\` (${{ github.sha }})."
|
|
curl -sS -H 'Content-Type: application/json' -d "{\"content\": \"$MSG\"}" "$WEBHOOK_URL" || true
|
|
|
|
- name: Build and push
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
run: |
|
|
TAG_FLAGS=""
|
|
IFS=',' read -ra TAG_ARRAY <<< "${{ steps.meta.outputs.tags }}"
|
|
for tag in "${TAG_ARRAY[@]}"; do TAG_FLAGS="$TAG_FLAGS --tag $tag"; done
|
|
|
|
PUSH_FLAG="--push"
|
|
[ "${{ steps.meta.outputs.push }}" = "false" ] && PUSH_FLAG="--output=type=cacheonly"
|
|
|
|
docker buildx build \
|
|
$PUSH_FLAG \
|
|
--platform linux/amd64 \
|
|
$TAG_FLAGS \
|
|
--add-host gitea.stevedudenhoeffer.com:192.168.0.134 \
|
|
--secret id=REGISTRY_USER,env=REGISTRY_USER \
|
|
--secret id=REGISTRY_PASSWORD,env=REGISTRY_PASSWORD \
|
|
--file ./Dockerfile \
|
|
.
|
|
|
|
- name: Notify Discord (result)
|
|
if: always() && github.event_name != 'pull_request'
|
|
env:
|
|
WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
|
run: |
|
|
[ -z "$WEBHOOK_URL" ] && exit 0
|
|
if [ "${{ job.status }}" = "success" ]; then
|
|
MSG="✅ Gadfly image build #${{ github.run_number }} succeeded. Tags: \`${{ steps.meta.outputs.tags }}\`."
|
|
else
|
|
MSG="❌ Gadfly image build #${{ github.run_number }} failed. Check Actions logs."
|
|
fi
|
|
curl -sS -H 'Content-Type: application/json' -d "{\"content\": \"$MSG\"}" "$WEBHOOK_URL" || true
|