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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,64 @@ jobs:
- name: Test
run: go test -race -count=1 -tags=integration ./...

dockerfile-lint:
name: Dockerfile lint
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Task
uses: go-task/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Lint the Dockerfile
run: task lint:docker

image:
name: Image (${{ matrix.target }}, ${{ matrix.runner.platform }})
runs-on: ${{ matrix.runner.os }}
strategy:
fail-fast: false
matrix:
target: [binary, debian]
runner:
- os: ubuntu-24.04
platform: linux/amd64
- os: ubuntu-24.04-arm
platform: linux/arm64
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Buildx
uses: docker/setup-buildx-action@v4

- name: Build the runtime image
id: build
uses: specsnl/github-actions/build-image@2.4.3
with:
platform: ${{ matrix.runner.platform }}
image-name: ghcr.io/specsnl/labelsync
dockerfile: Dockerfile
target: ${{ matrix.target }}
load: true
raw-tag: ci-${{ matrix.target }}
build-args: LABELSYNC_VERSION=ci-${{ github.sha }}

- name: Set up bats
id: bats
uses: bats-core/bats-action@4.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Smoke test
env:
BATS_LIB_PATH: ${{ steps.bats.outputs.lib-path }}
IMAGE: ${{ steps.build.outputs.image }}
EXPECTED_VERSION: ci-${{ github.sha }}
TARGET: ${{ matrix.target }}
run: bats test/image.bats

96 changes: 53 additions & 43 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,55 +40,65 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

images:
name: Images (${{ matrix.package }})
runs-on: ubuntu-24.04
image:
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- package: ghcr.io/specsnl/labelsync
target: binary
- package: ghcr.io/specsnl/labelsync/debian
target: debian
steps:
- name: Checkout
uses: actions/checkout@v7
runner:
- os: ubuntu-24.04
platform: linux/amd64
- os: ubuntu-24.04-arm
platform: linux/arm64
uses: specsnl/github-actions/.github/workflows/build-go-cli.yml@2.4.3
with:
runs-on: ${{ matrix.runner.os }}
platform: ${{ matrix.runner.platform }}
image-name: ghcr.io/specsnl/labelsync
target: binary
version-build-arg: LABELSYNC_VERSION

- name: Derive tags and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ matrix.package }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
flavor: latest=auto

- name: Set up Buildx
uses: docker/setup-buildx-action@v4
image-manifest:
needs: image
permissions:
contents: read
packages: write
uses: specsnl/github-actions/.github/workflows/merge-go-cli.yml@2.4.3
with:
runs-on: ubuntu-24.04
image-name: ghcr.io/specsnl/labelsync
target: binary

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
image-debian:
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
runner:
- os: ubuntu-24.04
platform: linux/amd64
- os: ubuntu-24.04-arm
platform: linux/arm64
uses: specsnl/github-actions/.github/workflows/build-go-cli.yml@2.4.3
with:
runs-on: ${{ matrix.runner.os }}
platform: ${{ matrix.runner.platform }}
image-name: ghcr.io/specsnl/labelsync
target: debian
version-build-arg: LABELSYNC_VERSION

- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
target: ${{ matrix.target }}
platforms: linux/amd64,linux/arm64
push: true
provenance: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
build-args: |
LABELSYNC_VERSION=${{ steps.meta.outputs.version }}
image-debian-manifest:
needs: image-debian
permissions:
contents: read
packages: write
uses: specsnl/github-actions/.github/workflows/merge-go-cli.yml@2.4.3
with:
runs-on: ubuntu-24.04
image-name: ghcr.io/specsnl/labelsync
target: debian
variant: debian
6 changes: 6 additions & 0 deletions .hadolint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# https://github.com/hadolint/hadolint#configure
ignored:
# Pinning every apt/apk package on top of an already pinned base image trades a reproducible
# build for one that breaks the moment the distro moves a package version out from under it.
- DL3008 # Pin versions in apt-get install
- DL3018 # Pin versions in apk add
55 changes: 33 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,28 @@ in the output; it is not an error.

Run `task --list` for the full set. The ones used most:

| Command | What it does |
|------------------------|-------------------------------------------------------------------|
| `task checkall` | The full check sequence: `tidy:check`, `lint`, `test`, `md:check` |
| `task tidy:check` | `go mod tidy -diff` — fails if `go.mod`/`go.sum` are untidy |
| `task tidy` | `go mod tidy` |
| `task lint` | `golangci-lint run` |
| `task lint:fix` | `golangci-lint run --fix` |
| `task test` | `go test -race -tags=integration ./...` |
| `task test:update` | Rewrite the golden files from the current output |
| `task md:check` | markdownlint over every Markdown file |
| `task md:fix` | Align Markdown tables, then apply autofixable rules |
| `task build` | Build the binary into the working directory |
| `task docs:serve` | Hugo dev server with live reload on <http://localhost:1313> |
| `task docs:preview` | Build, then serve the static site over nginx on port 8080 |
| `task docs:build` | Build the site into `docs/public/` |
| `task docs:mod:tidy` | Tidy the Hugo module in `docs/` |
| `task release:dry-run` | Local goreleaser snapshot, no publishing |
| `task demo:record:*` | Re-record one demo GIF with VHS: `:labelsync`, `:init` |
| `task dc:shell` | Shell into the `go-builder` service |
| Command | What it does |
|------------------------|----------------------------------------------------------------------------------|
| `task checkall` | The full check sequence: `tidy:check`, `lint`, `lint:docker`, `test`, `md:check` |
| `task tidy:check` | `go mod tidy -diff` — fails if `go.mod`/`go.sum` are untidy |
| `task tidy` | `go mod tidy` |
| `task lint` | `golangci-lint run` |
| `task lint:fix` | `golangci-lint run --fix` |
| `task lint:docker` | `hadolint Dockerfile` |
| `task test` | `go test -race -tags=integration ./...` |
| `task test:update` | Rewrite the golden files from the current output |
| `task md:check` | markdownlint over every Markdown file |
| `task md:fix` | Align Markdown tables, then apply autofixable rules |
| `task build` | Build the binary into the working directory |
| `task image:build` | Load both runtime images locally: `:dev` and `:dev-debian` |
| `task image:smoke` | Build them, then run `test/image.bats` — what CI's image guard does |
| `task docs:serve` | Hugo dev server with live reload on <http://localhost:1313> |
| `task docs:preview` | Build, then serve the static site over nginx on port 8080 |
| `task docs:build` | Build the site into `docs/public/` |
| `task docs:mod:tidy` | Tidy the Hugo module in `docs/` |
| `task release:dry-run` | Local goreleaser snapshot, no publishing |
| `task demo:record:*` | Re-record one demo GIF with VHS: `:labelsync`, `:init` |
| `task dc:shell` | Shell into the `go-builder` service |

### Local check sequence

Expand All @@ -48,13 +51,15 @@ Before opening a pull request, run:
task checkall
```

That is exactly `task tidy:check`, then `task lint`, then `task test`, then `task md:check`, in
that order — run them individually while iterating, and `checkall` before pushing.
That is exactly `task tidy:check`, then `task lint`, then `task lint:docker`, then `task test`, then
`task md:check`, in that order — run them individually while iterating, and `checkall` before
pushing.

Every step of the sequence reports; none of them writes. `tidy:check` runs `go mod tidy -diff`, so
an untidy `go.mod`/`go.sum` fails the check with the diff it would have applied rather than quietly
rewriting the tree mid-check. Run `task tidy` to apply it. CI runs the same check in the `Unit
tests` job.
tests` job, and `lint:docker` — the task itself, so the hadolint version stays pinned once, in
`compose.yml` — in the `Dockerfile lint` job.

`task build` runs `task lint` first, so a green build implies a green lint — but it does not run the
tests or the Markdown checks.
Expand Down Expand Up @@ -120,6 +125,11 @@ tests or the Markdown checks.
or the developer experience, not by reflex. Prefer table-driven tests; use
`net/http/httptest` for the GitHub client and an injected clock for anything time-dependent.

The one exception is [test/image.bats](./test/image.bats), which drives `docker run` against the
published images: the subject is a container, not a package, and bats is what the shared CI action
expects. It runs from `task image:smoke` and from the `Image (...)` jobs in CI, never from
`go test`.

- **Sentinel errors are always wrapped with `%w`.** Every way a run can fail has a sentinel in
[internal/labelsync/errors.go](./internal/labelsync/errors.go). A call site with context to add
never returns a sentinel bare, and never renders one with `%v` or into a freshly constructed
Expand Down Expand Up @@ -148,6 +158,7 @@ tests or the Markdown checks.
```text
labelsync/
├── main.go # XDG init, cmd.Execute()
├── test/ # image.bats — acceptance checks for the published images
└── internal/
├── labelsync/ # configuration.go (XDG paths), errors.go (sentinels + KindOf)
├── cmd/ # one file per Cobra command
Expand Down
41 changes: 40 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ RUN apt-get update \
ca-certificates \
tree \
git \
openssh-client
openssh-client \
&& rm -rf /var/lib/apt/lists/*

FROM base AS builder-download

Expand Down Expand Up @@ -40,6 +41,44 @@ RUN --mount=type=cache,target=/go/pkg/mod \
-tags netgo \
-ldflags "-s -w -X ${GO_MODULE}/internal/cmd.Version=${LABELSYNC_VERSION}" -o ./labelsync

# Latest version: https://hub.docker.com/r/bats/bats/tags
FROM bats/bats:1.14.0 AS bats

ARG TARGETARCH

# Latest version: https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=29.8.0
# Latest version: https://github.com/bats-core/bats-support/releases/latest
ARG BATS_SUPPORT_VERSION=0.3.0
# Latest version: https://github.com/bats-core/bats-assert/releases/latest
ARG BATS_ASSERT_VERSION=2.2.4

# busybox ash, since this stage is Alpine and carries no bash.
SHELL ["/bin/ash", "-o", "pipefail", "-c"]

RUN apk add --no-cache \
curl \
tar

RUN set -eux; \
case "${TARGETARCH}" in \
amd64) altarch=x86_64 ;; \
arm64) altarch=aarch64 ;; \
*) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
curl --fail --silent --show-error --location \
"https://download.docker.com/linux/static/stable/${altarch}/docker-${DOCKER_VERSION}.tgz" \
| tar --extract --gzip --directory /usr/bin --strip-components=1 docker/docker; \
for spec in "support:${BATS_SUPPORT_VERSION}" "assert:${BATS_ASSERT_VERSION}"; do \
name="bats-${spec%%:*}"; \
mkdir -p "/usr/lib/bats/${name}"; \
curl --fail --silent --show-error --location \
"https://github.com/bats-core/${name}/archive/refs/tags/v${spec#*:}.tar.gz" \
| tar --extract --gzip --directory "/usr/lib/bats/${name}" --strip-components=1; \
done

ENV BATS_LIB_PATH=/usr/lib/bats

# Latest version: https://hub.docker.com/_/debian/tags
FROM debian:13.6-slim AS debian

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ Or `go install github.com/specsnl/labelsync@latest`, or download a `tar.gz` for
the [releases page](https://github.com/specsnl/labelsync/releases) — Linux and macOS, amd64 and
arm64.

In a container, `docker run --rm ghcr.io/specsnl/labelsync:0.1 --help` — also published as
`ghcr.io/specsnl/labelsync/debian` for when a step needs a shell.
In a container, `docker run --rm ghcr.io/specsnl/labelsync:0.1 --help` — with a `:0.1-debian` variant
of every tag for when a step needs a shell.

Release candidates are a separate, opt-in cask, so `brew upgrade` never moves a stable install onto
one — while the rc cask itself tracks the leading edge and upgrades onto a stable release once it
Expand Down
Loading
Loading