trigger CI #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI: build, the repo's two test tiers, and — only once those pass — everything | |
| # this repo publishes: the clickd container image and, on a tag, the CLI release. | |
| # `go test ./...` is hermetic (every external service is faked | |
| # in-process), so it runs in the static-checks job; the end-to-end shell suites | |
| # need live Docker Compose stacks, and GitHub's runners ship docker compose, so | |
| # CI runs exactly what a developer runs: make up && make smoke, and | |
| # make up-auth && make smoke-auth. | |
| # | |
| # The image build lives here rather than in a workflow of its own so it can | |
| # depend on those jobs directly (needs:) and build the very commit they tested. | |
| # A separate workflow would have to use workflow_run, which resolves its own | |
| # file from the default branch, checks out the default branch unless told | |
| # otherwise, and never fires at all when CI is skipped — so a tag would silently | |
| # get no image. The CLI release (goreleaser) is gated here for the same reason. | |
| name: ci | |
| on: | |
| # Every push to main runs the full suite, deliberately without a path filter. | |
| # main is what the clickd image is built from below, so "tested" has to mean | |
| # the merge commit itself and not just the pull request that preceded it: a | |
| # squash or rebase can differ from the branch that was reviewed, and a direct | |
| # push to main gets no pull-request run at all. A merged PR therefore runs CI | |
| # twice, which is cheap next to shipping an untested image. | |
| # | |
| # Tags get the same treatment for the same reason: the image a rollout pins | |
| # must come from a commit the suites passed. Note there is deliberately no | |
| # `paths`/`paths-ignore` on push — path filters apply to tag pushes too, so a | |
| # tag whose commit only touched ignored files would silently produce no image. | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| # Docs-only PRs can't affect the binary or the stack, so skip the | |
| # (expensive, multi-arch Docker) smoke suites for them. Scope matters: | |
| # `*.md` is root-only (README/ARCHITECTURE/DESIGN/DEPLOY/AGENTS/CLAUDE — | |
| # pure prose), so | |
| # embedded templates keep running CI — internal/gen/agents.md is go:embed'd | |
| # into the binary and is NOT ignored. docs/** is ignored even though | |
| # docs/site-api.md is embedded there: a content edit still compiles, and a | |
| # rename edits embed.go (a .go file, not ignored), so a build-breaking | |
| # change always still runs CI. | |
| pull_request: | |
| paths-ignore: | |
| - '*.md' | |
| - 'docs/**' | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Arch-agnostic static checks: one runner is enough. | |
| checks: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go build ./... | |
| - run: go vet ./... | |
| # The layer above vet — errcheck, staticcheck, ineffassign, unused and | |
| # modernize, per .golangci.yml (`make lint` runs the same thing locally). | |
| # The version is pinned rather than floating: with `latest`, an upstream | |
| # release that adds a check turns the next unrelated PR red, and the | |
| # bisect points at the wrong commit. | |
| - uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: v2.12.2 | |
| # The Go unit tests are hermetic (every external service is faked | |
| # in-process), so they belong with the static checks rather than with the | |
| # Docker-bound smoke suites. | |
| - run: go test ./... | |
| - name: gofmt | |
| run: | | |
| out=$(gofmt -l ./cmd ./internal) | |
| if [ -n "$out" ]; then | |
| echo "gofmt needed on:" && echo "$out" | |
| exit 1 | |
| fi | |
| # The smoke suites test the containerized platform itself, so they run on | |
| # both architectures — the compose stack has to work on amd64 and arm64 | |
| # (Graviton) nodes alike. | |
| smoke-open: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runner: [ubuntu-latest, ubuntu-24.04-arm] | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: go.mod | |
| # Bringing the stack up pulls six images from Docker Hub, anonymously, | |
| # from a runner IP shared with every other public repo — so it draws | |
| # rate-limit 429s and the occasional 502 that have nothing to do with the | |
| # commit. Retry the bring-up rather than making someone re-run the job. | |
| - name: Start the stack | |
| run: | | |
| for attempt in 1 2 3; do | |
| make up && exit 0 | |
| echo "::warning::stack bring-up failed (attempt $attempt/3), retrying" | |
| docker compose down -v || true | |
| sleep $((attempt * 15)) | |
| done | |
| exit 1 | |
| - run: make smoke | |
| - name: stack logs | |
| if: failure() | |
| run: docker compose logs --tail 200 | |
| smoke-auth: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runner: [ubuntu-latest, ubuntu-24.04-arm] | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: go.mod | |
| # Every curl in the suite maps *.click.localhost to loopback with | |
| # --resolve, but `click login` resolves the OIDC issuer through the system | |
| # resolver — a Go program has no --resolve. macOS maps *.localhost to | |
| # loopback natively and systemd-resolved does the same, so this only bites | |
| # in a bare runner container, where the CLI cannot reach dex at all. | |
| - name: Make the OIDC issuer host resolvable | |
| run: | | |
| if ! grep -q 'dex\.click\.localhost' /etc/hosts; then | |
| echo '127.0.0.1 dex.click.localhost' | tee -a /etc/hosts >/dev/null \ | |
| || echo '127.0.0.1 dex.click.localhost' | sudo tee -a /etc/hosts >/dev/null | |
| fi | |
| getent hosts dex.click.localhost | |
| # Same transient-registry retry as smoke-open above; the auth overlay adds | |
| # three more images (dex, oauth2-proxy, libsql-server) to the pull. | |
| - name: Start the auth stack | |
| run: | | |
| for attempt in 1 2 3; do | |
| make up-auth && exit 0 | |
| echo "::warning::auth stack bring-up failed (attempt $attempt/3), retrying" | |
| docker compose -f docker-compose.yml -f docker-compose.auth.yml down -v || true | |
| sleep $((attempt * 15)) | |
| done | |
| exit 1 | |
| - run: make smoke-auth | |
| - name: stack logs | |
| if: failure() | |
| run: docker compose -f docker-compose.yml -f docker-compose.auth.yml logs --tail 200 | |
| # Build and push the clickd container image to the GitHub Container Registry, | |
| # so a deployment can pin it by tag (see DEPLOY.md / docs/deploy-eks-github.md). | |
| # | |
| # - push to main → clickd:sha-<short-sha> + latest (per-commit image) | |
| # - tag v* → clickd:vX.Y.Z + latest (the tag rollouts pin) | |
| # | |
| # latest always follows the most recent build of either kind. | |
| # | |
| # needs: every test job, so no image ever exists for a commit the suites | |
| # rejected — which matters for any deployment that tracks latest with | |
| # imagePullPolicy: Always, where a pushed image reaches the platform on the | |
| # next pod restart. The event guard keeps pull requests out of the registry | |
| # entirely, and the repository guard keeps forks from trying to push at all. | |
| # | |
| # Multi-arch (amd64 + arm64) so arm64 nodes (e.g. AWS Graviton) can run it. | |
| # The Dockerfile cross-compiles in a native build stage, so no emulated Go — | |
| # QEMU only covers the tiny alpine runtime stage. | |
| image: | |
| needs: [checks, smoke-open, smoke-auth] | |
| if: github.event_name == 'push' && github.repository == 'closeio/click' | |
| runs-on: ubuntu-latest | |
| # GITHUB_TOKEN needs packages:write to push to ghcr.io; contents stays read. | |
| permissions: | |
| contents: read | |
| packages: write | |
| # Job-scoped: a superseded image build is cancelled without disturbing the | |
| # test jobs of the run it belongs to. | |
| concurrency: | |
| group: image-${{ github.ref_name || github.run_id }} | |
| cancel-in-progress: true | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/clickd | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Log in to ghcr.io | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/setup-qemu-action@v4 | |
| - uses: docker/setup-buildx-action@v4 | |
| - name: Compute tags | |
| id: tags | |
| run: | | |
| if [ "${{ github.ref_type }}" = "tag" ]; then | |
| tag="$GITHUB_REF_NAME" | |
| else | |
| tag="sha-$(echo "$GITHUB_SHA" | cut -c1-12)" | |
| fi | |
| echo "tags=$IMAGE:$tag,$IMAGE:latest" >> "$GITHUB_OUTPUT" | |
| - name: Build and push | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.tags.outputs.tags }} | |
| # Keep the index to exactly the two platform manifests: buildx | |
| # attestations add unknown/unknown entries that older containerd | |
| # versions refuse with "no match for platform in manifest". | |
| provenance: false | |
| sbom: false | |
| # Release the click CLI: a v* tag builds the linux amd64/arm64 + macOS arm64 | |
| # binaries (.goreleaser.yaml) and publishes them as a GitHub release. | |
| # | |
| # Gated on the same jobs as the image, *and* on the image itself: gating both | |
| # on the tests alone still lets the registry push fail while goreleaser | |
| # succeeds, which advertises a version nobody can run. Publishing the release | |
| # last means a tag never announces binaries whose image is missing. The | |
| # reverse — image pushed, release failed — is left open deliberately: a | |
| # registry tag nothing references is harmless. | |
| # | |
| # The cost is coupling (a ghcr.io outage blocks an otherwise fine CLI release) | |
| # and about a minute of serialization. Note the image job cancels in progress, | |
| # but its concurrency group is keyed on ref_name, so distinct tags never | |
| # collide and a tag build is never superseded out from under this job. | |
| # | |
| # contents: write is scoped to this job rather than the workflow, so the test | |
| # jobs and the image build keep read-only tokens; it is exactly what the | |
| # release upload needs, no PAT. | |
| release: | |
| needs: [checks, smoke-open, smoke-auth, image] | |
| if: github.ref_type == 'tag' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 # the git-based changelog diffs against the previous tag | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version-file: go.mod | |
| # GoReleaser cross-compiles every target (CGO is off), so one amd64 | |
| # runner builds the linux amd64/arm64 and darwin arm64 binaries alike. | |
| - uses: goreleaser/goreleaser-action@v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |