Skip to content

Commit 3540337

Browse files
committed
ci(image): add multi-arch GHCR container image workflow
Add .github/workflows/image.yml to build and publish the hypercache-server Docker image for linux/amd64 and linux/arm64 via buildx + QEMU. Trigger behaviour: - pull_request: build-only (no push) to catch Dockerfile regressions - push to main: publish :main and :sha-<short> - semver tag push (v*.*.*): publish :v1.2.3, :1.2.3, :1.2, :1, :latest :latest is intentionally restricted to semver tag pushes so production deployments pinning :latest always resolve to a stable release rather than an in-flight main commit. GHA layer caching keeps re-builds fast when only Go source has changed. Also replace stdlib encoding/json with github.com/goccy/go-json in dist_memory.go and integration tests, update CHANGELOG.md, and add buildx to the cspell allow-list.
1 parent 1ddde9e commit 3540337

9 files changed

Lines changed: 122 additions & 7 deletions

File tree

.github/workflows/image.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
name: image
3+
4+
# Build (and on the right refs, publish) the hypercache-server
5+
# container image. Three trigger shapes:
6+
# * pull_request — build only, never push (catches Dockerfile
7+
# regressions without polluting the registry).
8+
# * push to main — build + push as `:main` and `:sha-<short>`
9+
# so consumers can pin to either.
10+
# * tag push (v*.*.*) — build + push semver-flavored tags
11+
# (`:v1.2.3`, `:1.2.3`, `:1.2`, `:1`, `:latest`) for stable
12+
# pinning.
13+
# Multi-arch linux/amd64 + linux/arm64 via buildx + qemu so
14+
# operators on Apple Silicon (or k8s nodes on Graviton) get a
15+
# native binary without emulation.
16+
17+
on:
18+
pull_request:
19+
push:
20+
branches: [ main ]
21+
tags: [ "v*.*.*" ]
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
packages: write
27+
28+
env:
29+
REGISTRY: ghcr.io
30+
IMAGE_NAME: ${{ github.repository }}/hypercache-server
31+
32+
jobs:
33+
build:
34+
name: build${{ github.event_name == 'pull_request' && ' (no push)' || ' + push'
35+
}}
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 20
38+
39+
steps:
40+
- uses: actions/checkout@v6
41+
42+
- name: Set up QEMU
43+
uses: docker/setup-qemu-action@v4
44+
45+
- name: Set up Docker Buildx
46+
uses: docker/setup-buildx-action@v4
47+
48+
# Login is gated on non-PR events. Forks running PR workflows
49+
# don't have access to GITHUB_TOKEN with packages:write, and
50+
# we never push from a PR anyway — so skipping the login step
51+
# avoids an avoidable failure on those events.
52+
- name: Log in to GHCR
53+
if: github.event_name != 'pull_request'
54+
uses: docker/login-action@v4.1.0
55+
with:
56+
registry: ${{ env.REGISTRY }}
57+
username: ${{ github.actor }}
58+
password: ${{ secrets.GITHUB_TOKEN }}
59+
60+
# docker/metadata-action computes the tag set + OCI labels
61+
# from the triggering ref. The semver patterns only match
62+
# when the ref is a `v*.*.*` tag; on branch/PR pushes they
63+
# produce no tags and the type=ref/type=sha entries take over.
64+
# `:latest` is restricted to semver tag pushes — production
65+
# operators pinning to `:latest` get the highest stable
66+
# release, never an in-flight main commit. The `latest=false`
67+
# flavor disables the metadata-action default behavior that
68+
# would otherwise tag `:latest` on every default-branch push.
69+
- name: Compute tags and labels
70+
id: meta
71+
uses: docker/metadata-action@v6
72+
with:
73+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
74+
tags: |
75+
type=ref,event=branch
76+
type=ref,event=pr
77+
type=sha,format=short
78+
type=semver,pattern={{version}}
79+
type=semver,pattern={{major}}.{{minor}}
80+
type=semver,pattern={{major}}
81+
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') }}
82+
flavor: |
83+
latest=false
84+
85+
- name: Build${{ github.event_name == 'pull_request' && '' || ' + push' }}
86+
uses: docker/build-push-action@v7.1.0
87+
with:
88+
context: .
89+
file: cmd/hypercache-server/Dockerfile
90+
platforms: linux/amd64,linux/arm64
91+
push: ${{ github.event_name != 'pull_request' }}
92+
tags: ${{ steps.meta.outputs.tags }}
93+
labels: ${{ steps.meta.outputs.labels }}
94+
# GHA cache speeds re-builds when only Go source changed
95+
# (the dependency-download layer stays warm).
96+
cache-from: type=gha
97+
cache-to: type=gha,mode=max

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3939
(replica path), `json.RawMessage` (non-owner-GET path), and the
4040
base64-heuristic length floors. Runs without docker for tight
4141
feedback during development.
42+
- **Multi-arch container image workflow**
43+
[.github/workflows/image.yml](.github/workflows/image.yml) builds
44+
the `hypercache-server` Docker image for `linux/amd64` and
45+
`linux/arm64` via buildx + QEMU, publishing to GHCR
46+
(`ghcr.io/<owner>/<repo>/hypercache-server`). PR triggers
47+
build-only (no registry pollution), `main` pushes publish
48+
`:main` and `:sha-<short>`, semver tag pushes (`v*.*.*`)
49+
publish `:v1.2.3`, `:1.2.3`, `:1.2`, `:1`, and `:latest`.
50+
`:latest` is **deliberately restricted to semver tag pushes**
51+
production deployments pinning `:latest` always get a stable
52+
release, never an in-flight `main` commit. GHA cache speeds
53+
re-builds when only Go source has changed.
4254

4355
### Fixed
4456

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ words:
4646
- bitnami
4747
- bodyclose
4848
- bufbuild
49+
- buildx
4950
- cacheerrors
5051
- cachev
5152
- calledback

pkg/backend/dist_memory.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"crypto/rand"
66
"crypto/sha256"
7-
"encoding/json"
87
"errors"
98
"hash"
109
"hash/fnv"
@@ -17,6 +16,7 @@ import (
1716
"sync/atomic"
1817
"time"
1918

19+
"github.com/goccy/go-json"
2020
"github.com/hyp3rd/ewrap"
2121
"go.opentelemetry.io/otel/attribute"
2222
"go.opentelemetry.io/otel/codes"
@@ -1443,7 +1443,8 @@ func (dm *DistMemory) Drain(_ context.Context) error {
14431443

14441444
dm.metrics.drains.Add(1)
14451445

1446-
dm.logger.Info("dist node draining",
1446+
dm.logger.Info(
1447+
"dist node draining",
14471448
slog.String("addr", dm.nodeAddr),
14481449
)
14491450

tests/dist_drain_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ func newDrainNode(t *testing.T) *backend.DistMemory {
2121

2222
addr := AllocatePort(t)
2323

24-
bi, err := backend.NewDistMemory(context.Background(),
24+
bi, err := backend.NewDistMemory(
25+
context.Background(),
2526
backend.WithDistNode("drain-A", addr),
2627
backend.WithDistReplication(1),
2728
)

tests/dist_http_compression_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func valueAsBytes(v any) ([]byte, bool) {
2424

2525
case string:
2626
// Try base64 first — that's how []byte serializes through
27-
// encoding/json. Fall back to the raw string bytes for
27+
// github.com/goccy/go-json. Fall back to the raw string bytes for
2828
// values that were always-string.
2929
decoded, err := base64.StdEncoding.DecodeString(x)
3030
if err == nil {

tests/dist_keys_cursor_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ func newKeysCursorNode(t *testing.T) *backend.DistMemory {
8080

8181
addr := AllocatePort(t)
8282

83-
bi, err := backend.NewDistMemory(context.Background(),
83+
bi, err := backend.NewDistMemory(
84+
context.Background(),
8485
backend.WithDistNode("keys-A", addr),
8586
backend.WithDistReplication(1),
8687
)

tests/integration/dist_phase1_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package integration
33
import (
44
"context"
55
"encoding/base64"
6-
"encoding/json"
76
"fmt"
87
"net"
98
"testing"
109
"time"
1110

11+
"github.com/goccy/go-json"
12+
1213
"github.com/hyp3rd/hypercache/pkg/backend"
1314
cache "github.com/hyp3rd/hypercache/pkg/cache/v2"
1415
)

tests/integration/dist_seed_spec_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ func buildSeedSpecCluster(t *testing.T) *seedSpecCluster {
5151
}
5252

5353
mkNode := func(id, addr string) *backend.DistMemory {
54-
bm, err := backend.NewDistMemory(ctx,
54+
bm, err := backend.NewDistMemory(
55+
ctx,
5556
backend.WithDistNode(id, addr),
5657
backend.WithDistSeeds(seedsFor(id)),
5758
backend.WithDistReplication(3),

0 commit comments

Comments
 (0)