From d76c14c1abaeb398695b7c9b2cf3b282db48b805 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:47:49 +0200 Subject: [PATCH 01/10] make the determinism test catch the uninitialized-buffer bug --- convert_test.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/convert_test.go b/convert_test.go index 70fed69..68c0bb5 100644 --- a/convert_test.go +++ b/convert_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "os" "reflect" + "sync" "testing" ) @@ -83,10 +84,31 @@ func TestEncodeDeterministic(t *testing.T) { if err != nil { t.Fatal(err) } - for i := 0; i < 8; i++ { - next, err := encodeWOFF2(sfnt) - if err != nil { - t.Fatal(err) + // A different font to dirty the C heap between encodes. The v0.2.2 bug + // (uninitialized encode buffer) only showed up in batches: re-encoding the + // same font reuses a buffer holding the same bytes, which hides it. + other, err := subsetSFNT(sfnt, subsetOptions{unicodes: []unicodeRange{{0x20, 0x7E}}}) + if err != nil { + t.Fatal(err) + } + // Concurrent encodes, like a batch run, each after encoding the other font. + outs := make([][]byte, 8) + errs := make([]error, len(outs)) + var wg sync.WaitGroup + for i := range outs { + wg.Add(1) + go func() { + defer wg.Done() + if _, errs[i] = encodeWOFF2(other); errs[i] != nil { + return + } + outs[i], errs[i] = encodeWOFF2(sfnt) + }() + } + wg.Wait() + for i, next := range outs { + if errs[i] != nil { + t.Fatal(errs[i]) } if !bytes.Equal(first, next) { t.Fatalf("encodeWOFF2 is not deterministic (run %d differs)", i) From 6f39725cecc2ae7b662fbd89c64816f15dd16573 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:49:28 +0200 Subject: [PATCH 02/10] move actions to their node 24 majors --- .github/workflows/ci.yml | 4 ++-- .github/workflows/docker.yml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f401f89..0c111b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,8 @@ jobs: matrix: go: ['1.26.4', 'stable'] # exercise the go.mod minimum and the latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go }} - name: Install C dependencies diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0300bb2..b839fac 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,21 +12,21 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ghcr.io/gheop/woffify tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=tag - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . push: true From 0af24684764e39ce1a6dcc392ddae34a07a2bba1 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:49:57 +0200 Subject: [PATCH 03/10] publish the image only after the tests pass --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ .github/workflows/docker.yml | 36 ------------------------------------ 2 files changed, 34 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c111b3..24dbc5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: CI on: push: branches: [main] + tags: ['v*'] pull_request: jobs: @@ -38,3 +39,36 @@ jobs: - run: go vet ./... - run: go test ./... - run: go build -o woffify . + + # Publish only what passed the tests. Pull requests build the image without + # pushing it, so a broken Dockerfile shows up before the merge. + image: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - if: github.event_name == 'push' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/gheop/woffify + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index b839fac..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Docker - -on: - push: - branches: [main] - tags: ['v*'] - -jobs: - image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - with: - images: ghcr.io/gheop/woffify - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=tag - - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max From f8b5d28065c72a5fc63bb8a9f92e814636b89bcf Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:52:29 +0200 Subject: [PATCH 04/10] set explicit permissions, timeouts and non-persisted checkout credentials --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24dbc5b..a5275c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,20 @@ on: tags: ['v*'] pull_request: +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: matrix: go: ['1.26.4', 'stable'] # exercise the go.mod minimum and the latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go }} @@ -45,11 +51,14 @@ jobs: image: needs: test runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read - packages: write + packages: write # push to ghcr.io steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - if: github.event_name == 'push' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 From 96b47e7b876e879a99cf45320724db19c29f845b Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:53:15 +0200 Subject: [PATCH 05/10] cache the woff2 encoder build in CI --- .github/workflows/ci.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5275c7..66dd018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,9 @@ jobs: test: runs-on: ubuntu-latest timeout-minutes: 15 + env: + WOFF2_TAG: v1.0.2 + WOFF2_SHA: 1bccf208bca986e53a647dfe4811322adb06ecf8 strategy: matrix: go: ['1.26.4', 'stable'] # exercise the go.mod minimum and the latest @@ -24,12 +27,21 @@ jobs: with: go-version: ${{ matrix.go }} - name: Install C dependencies - env: - WOFF2_TAG: v1.0.2 - WOFF2_SHA: 1bccf208bca986e53a647dfe4811322adb06ecf8 run: | sudo apt-get update sudo apt-get install -y libharfbuzz-dev libbrotli-dev pkg-config g++ + # The woff2 encoder is built from a pinned source, so its build is cached; + # the key also follows the runner image and compiler. + - id: toolchain + run: echo "key=$ImageOS-$(g++ -dumpfullversion)" >> "$GITHUB_OUTPUT" + - id: woff2-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/woff2-build + key: woff2-${{ env.WOFF2_SHA }}-O2-cstdint-${{ steps.toolchain.outputs.key }} + - name: Build the woff2 encoder + if: steps.woff2-cache.outputs.cache-hit != 'true' + run: | # Ubuntu has no woff2 -dev package; build the encoder from a pinned source. git clone --depth 1 --branch "$WOFF2_TAG" https://github.com/google/woff2 /tmp/woff2 test "$(git -C /tmp/woff2 rev-parse HEAD)" = "$WOFF2_SHA" @@ -38,8 +50,13 @@ jobs: src/woff2_enc.cc src/font.cc src/glyph.cc src/normalize.cc \ src/transform.cc src/table_tags.cc src/variable_length.cc \ src/woff2_common.cc src/woff2_out.cc - sudo ar rcs /usr/lib/libwoff2enc.a *.o - sudo cp -r include/woff2 /usr/include/ + mkdir -p ~/woff2-build/include + ar rcs ~/woff2-build/libwoff2enc.a ./*.o + cp -r include/woff2 ~/woff2-build/include/ + - name: Install the woff2 encoder + run: | + sudo cp ~/woff2-build/libwoff2enc.a /usr/lib/ + sudo cp -r ~/woff2-build/include/woff2 /usr/include/ printf 'Name: woff2enc\nDescription: woff2 encoder\nVersion: 1.0.2\nLibs: -lwoff2enc -lbrotlienc -lbrotlicommon\nCflags:\n' \ | sudo tee /usr/lib/pkgconfig/libwoff2enc.pc > /dev/null - run: go vet ./... From bd583a62d1e95b27639afd113f3c682bb55b237a Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 14:57:48 +0200 Subject: [PATCH 06/10] keep the image layer cache read-only on pull requests --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66dd018..5d1522d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,4 +97,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max + # Only main and tags refresh the layer cache; exporting it from every + # pull request run doubles the build step. + cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max' || '' }} From c09680333ea452d0fb6aa8f27d0f2f1a60546dc8 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 15:00:14 +0200 Subject: [PATCH 07/10] skip CI on documentation-only changes --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d1522d..05915d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,10 @@ name: CI on: push: branches: [main] - tags: ['v*'] + tags: ['v*'] # path filters do not apply to tag pushes: releases always build + paths-ignore: ['**.md', 'LICENSE'] pull_request: + paths-ignore: ['**.md', 'LICENSE'] permissions: contents: read From 6156da5e504cecc314187ac6b67eeea9d4bc5c83 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 15:00:14 +0200 Subject: [PATCH 08/10] cancel superseded pull request runs --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05915d5..41b4d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,12 @@ on: permissions: contents: read +# A new push to a pull request cancels its previous run; main and tags always +# run to the end, since they publish the image. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: test: runs-on: ubuntu-latest From 869795039cef51e8fe06644bb68c305e71a224ad Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 15:00:44 +0200 Subject: [PATCH 09/10] publish the GitLab image on release tags too --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3195e45..f5453b3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,11 @@ build-image: - docker:27-dind rules: - if: '$CI_COMMIT_BRANCH == "main"' + - if: '$CI_COMMIT_TAG =~ /^v/' + variables: + IMAGE_TAG: latest script: + - if [ -n "$CI_COMMIT_TAG" ]; then IMAGE_TAG="$CI_COMMIT_TAG"; fi - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - docker build -t "$CI_REGISTRY_IMAGE:latest" . - - docker push "$CI_REGISTRY_IMAGE:latest" + - docker build -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . + - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" From 7d5bda48d5a9e687319f029fa2907908a6ddca36 Mon Sep 17 00:00:00 2001 From: sib Date: Fri, 25 Sep 2026 15:00:57 +0200 Subject: [PATCH 10/10] cache the GitLab image build in the registry --- .gitlab-ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f5453b3..aa1b686 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,5 +41,11 @@ build-image: script: - if [ -n "$CI_COMMIT_TAG" ]; then IMAGE_TAG="$CI_COMMIT_TAG"; fi - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - docker build -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . - - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" + # Layer cache kept in the registry: HarfBuzz is only rebuilt when the + # Dockerfile changes, not on every push. + - docker buildx create --use --driver docker-container + - > + docker buildx build --push + --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE:buildcache" + --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max,image-manifest=true,oci-mediatypes=true" + -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .