diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f401f89..41b4d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,26 +3,53 @@ name: CI on: push: branches: [main] + 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 + +# 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 + 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 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 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" @@ -31,10 +58,53 @@ 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 ./... - 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 + timeout-minutes: 30 + permissions: + contents: read + 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 + 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 + # 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' || '' }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 0300bb2..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@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - 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 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3195e45..aa1b686 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,17 @@ 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" + # 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" . 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)