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
132 changes: 132 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: CI

on:
push:
branches: [main]
pull_request:

# A newer push to the same branch makes an in-flight run irrelevant.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
# Unit tests need no GTK4 headers, so they run on plain ubuntu and report fast.
# PURE_PKGS is derived by the Makefile, so a newly added package is picked up
# here without touching this file.
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
# goreleaser runs `go mod tidy` as a before-hook, so an untidy tree means
# every release build silently rewrites go.sum after goreleaser's git-dirty
# check has already passed — the released binary's inputs then differ from
# the tagged tree. Nothing else here would notice: stale go.sum entries are
# ignored by the build. v1.1.6 hashes survived yesterday's api bump this way.
- name: go.mod and go.sum are tidy
run: go mod tidy -diff
- name: Unit tests
run: make test
- name: Unit tests (race detector)
run: make race
- name: Coverage summary
run: make cover

# Compiling needs the GTK4 and layer-shell headers, so this uses the same Arch
# container as the release job. Kept separate from `test` so a logic failure is
# not hidden behind a toolchain problem.
build:
name: Build and lint
runs-on: ubuntu-latest
container: archlinux:latest
steps:
- name: Install dependencies
run: pacman -Syu --noconfirm git base-devel go gtk4 gtk4-layer-shell gobject-introspection
- uses: actions/checkout@v7
- name: Fix git ownership
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
# cache: false because this job manages the Go caches explicitly below.
# Leaving setup-go's implicit cache on would have two mechanisms writing the
# same directories, and its key has no fallback — any go.sum change is a
# total miss, which is exactly the case that hurts most here.
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false

# Ask the toolchain where its caches live rather than hardcoding paths.
# Inside a container HOME is /github/home, not the runner's ~, so a guessed
# path silently caches nothing — the failure mode is a green run that is
# still slow, which is easy to miss.
- name: Locate Go caches
id: gocache
run: |
echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"

# This is the whole cost of the job. A cold CGO build of the gotk4 bindings
# took 707s on the last run, against ~40s for every other step combined.
# Those bindings are tens of thousands of generated cgo wrappers that change
# only when the dependency does, so they cache almost perfectly.
#
# The key pins the exact dependency set; the fallbacks widen from there, so a
# dependency bump still restores the previous cache and recompiles only what
# actually changed instead of starting from nothing.
#
# Caches are branch-scoped, with only the default branch's visible everywhere,
# so the first run after this merges still pays full price — the saving shows
# from the run after that.
# Restore and save are split rather than using the combined action, so the
# cache is written even when a later step fails. That is not hypothetical:
# the previous run spent 707s building successfully and then failed on lint,
# and a combined cache would have discarded the expensive part and paid for
# it again next time.
- name: Restore the CGO build cache
id: cgocache
uses: actions/cache/restore@v6
with:
path: |
${{ steps.gocache.outputs.build }}
${{ steps.gocache.outputs.mod }}
key: cgo-${{ runner.os }}-go${{ hashFiles('go.mod') }}-${{ hashFiles('go.sum') }}
restore-keys: |
cgo-${{ runner.os }}-go${{ hashFiles('go.mod') }}-
cgo-${{ runner.os }}-

- name: Build
run: make build
# Same target `make lint` depends on, so a formatting slip fails locally
# before it can fail here. Previously this step carried its own copy of the
# command and nothing in the Makefile checked formatting at all.
- name: gofmt
run: make fmt-check
# Pinned, not `latest`. `latest` is what broke this: action v6 only knows the
# golangci-lint v1 line, whose final release (1.64.8) was built with Go 1.24
# and so refuses a go.mod targeting 1.25 — a failure no change to this repo
# could have caused or fixed. A pin also makes CI run the identical linter to
# `make lint`, so "passes locally" means something. Bump deliberately, in step
# with the Go version in go.mod.
- uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2

# Last, and always: lint reuses the warm build cache above and adds its own
# analysis output to it, so saving after lint captures more than saving after
# build would. `always()` is what makes a failing lint still leave a usable
# cache behind; the cache-hit guard avoids the warning from rewriting a key
# that already exists.
- name: Save the CGO build cache
if: always() && steps.cgocache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: |
${{ steps.gocache.outputs.build }}
${{ steps.gocache.outputs.mod }}
key: ${{ steps.cgocache.outputs.cache-primary-key }}
53 changes: 43 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,22 @@ jobs:
steps:
- name: Install dependencies
run: pacman -Syu --noconfirm git base-devel go gtk4 gtk4-layer-shell gobject-introspection
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Fix git ownership
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- uses: actions/setup-go@v5
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: goreleaser/goreleaser-action@v6
# Constrained to the v2 line rather than `latest`, so patch and minor fixes
# still arrive but a major cannot land unannounced in the middle of a
# release. `.goreleaser.yml` pins the config schema at version 2, so the two
# stay in step. Local `make snapshot` runs 2.14.0.
- uses: goreleaser/goreleaser-action@v7
with:
version: latest
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -61,20 +65,49 @@ jobs:
(github.event_name == 'workflow_dispatch' && inputs.aur && (needs.release.result == 'success' || needs.release.result == 'skipped'))
)
steps:
- uses: actions/checkout@v4
# Check out the tag being released, not the default branch. The release job
# above is explicit about this and this one was not, so a workflow_dispatch
# for an older tag would package that tag's binary with the PKGBUILD from
# main. Harmless on a tag push, where the default ref is already the tag.
- uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Get version, sha256, and pkgrel
id: meta
run: |
# pipefail is the point of this line. The default shell is `bash -e`
# without it, so a pipeline's status is its *last* command: a failed
# download still exits 0, and `sha256sum` of the resulting empty stream
# returns e3b0c442… — a perfectly well-formed checksum that is wrong for
# every file. The AUR package would then publish and fail its integrity
# check for every user, with this workflow green.
set -euo pipefail

TAG="${{ github.event.inputs.tag || github.ref_name }}"
VERSION="${TAG#v}"
URL="https://github.com/dahui/z13gui/releases/download/${TAG}/z13gui_${VERSION}_linux_amd64.tar.gz"
SHA256=$(curl -fsSL "$URL" | sha256sum | cut -d' ' -f1)

# Compute pkgrel: increment if same version already on AUR, else 1
# Belt and braces, and self-documenting: name the failure rather than
# leaving a future reader to recognise the empty-stream hash.
EMPTY_SHA=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
if [ "$SHA256" = "$EMPTY_SHA" ]; then
echo "::error::downloaded an empty stream from $URL — refusing to publish a bad checksum"
exit 1
fi
case "$SHA256" in
[0-9a-f][0-9a-f]*) [ "${#SHA256}" -eq 64 ] || { echo "::error::malformed sha256: $SHA256"; exit 1; } ;;
*) echo "::error::malformed sha256: $SHA256"; exit 1 ;;
esac

# Compute pkgrel: increment if same version already on AUR, else 1.
# This lookup is advisory — it only picks pkgrel — so unlike the download
# above it is deliberately tolerant of a blip in the AUR API, which must
# not fail a release. `|| true` keeps that behaviour now pipefail is on.
PKGREL=1
AUR_VER=$(curl -fsSL "https://aur.archlinux.org/rpc/v5/info?arg[]=z13gui-bin" \
| jq -r '.results[0].Version // empty')
| jq -r '.results[0].Version // empty' || true)
if [ -n "$AUR_VER" ]; then
AUR_PKGVER="${AUR_VER%-*}"
AUR_PKGREL="${AUR_VER##*-}"
Expand Down Expand Up @@ -151,13 +184,13 @@ jobs:
(github.event_name == 'workflow_dispatch' && inputs.docs && (needs.release.result == 'success' || needs.release.result == 'skipped'))
)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: 3.x

- uses: actions/cache@v4
- uses: actions/cache@v6
with:
key: mkdocs-material-${{ hashFiles('requirements.txt') }}
path: .cache
Expand Down
11 changes: 11 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ archives:
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE
- NOTICE
- TRADEMARK.md
# Inter is embedded into the binary with //go:embed, so the OFL notice has
# to travel with every artifact, not just sit in the source tree.
- internal/gui/fonts/LICENSE-Inter.txt
- contrib/z13gui.service
- contrib/z13gui.desktop
- contrib/99-z13gui-gamepad.rules
Expand Down Expand Up @@ -55,6 +60,12 @@ nfpms:
contents:
- src: LICENSE
dst: /usr/share/licenses/z13gui/LICENSE
- src: NOTICE
dst: /usr/share/licenses/z13gui/NOTICE
# The embedded typeface ships inside the binary, so its licence belongs
# alongside the package's own.
- src: internal/gui/fonts/LICENSE-Inter.txt
dst: /usr/share/licenses/z13gui/LICENSE-Inter.txt
- src: contrib/z13gui.service
dst: /usr/lib/systemd/user/z13gui.service
- src: contrib/z13gui.desktop
Expand Down
Loading