From 03e5ec31f15c62bdcd5413f034e5f34fffccdb17 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Tue, 5 May 2026 08:40:50 +0100 Subject: [PATCH] ci: shard test job + cheap argon2 in test mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test job was wall-clocked by `internal/server/http` (~156s on the self-hosted runner under -race). Two changes here cut that: 1. Matrix-shard the test job by package group: server-http, store, and "rest" (everything else, computed via `go list | grep -v`). Each shard runs on its own runner so the heavy package isn't CPU-starved by siblings. 2. `auth.HashPassword` drops to cheap argon2id params (8 KiB / 1 iter / 1 lane) when `testing.Testing()` returns true. Production params are unchanged. VerifyPassword reads params from the encoded hash so cheap-params hashes verify identically — no test call sites need to change. --- .gitea/workflows/ci.yml | 31 +++++++++++++++++++++++++++++-- internal/auth/passwords.go | 21 +++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a3f4c9b..a69d2c1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -53,8 +53,24 @@ env: jobs: test: - name: Test (linux/amd64) + # Sharded by package group. server/http and store are the two + # heavy packages (~156s and ~75s in CI respectively under + # `-race`); pulling them onto their own runners lets each shard + # have all CPUs to itself instead of CPU-starving each other on + # one runner. The third shard ("rest") covers everything else. + name: Test (${{ matrix.name }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: server-http + packages: ./internal/server/http/... + - name: store + packages: ./internal/store/... + - name: rest + # Computed at runtime — see the "go test" step below. + packages: "" steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 @@ -64,7 +80,18 @@ jobs: - name: go vet run: go vet ./... - name: go test - run: go test -race -coverprofile=coverage.out ./... + run: | + set -euo pipefail + if [ -n "${{ matrix.packages }}" ]; then + pkgs="${{ matrix.packages }}" + else + # "rest" shard: everything except the dedicated shards. + pkgs=$(go list ./... \ + | grep -v '/internal/server/http$' \ + | grep -v '/internal/store$') + fi + # shellcheck disable=SC2086 + go test -race -coverprofile=coverage.out $pkgs - name: coverage summary run: go tool cover -func=coverage.out | tail -1 diff --git a/internal/auth/passwords.go b/internal/auth/passwords.go index 6e35321..ae40fca 100644 --- a/internal/auth/passwords.go +++ b/internal/auth/passwords.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "strings" + "testing" "golang.org/x/crypto/argon2" ) @@ -27,22 +28,38 @@ const ( defaultKeyLen = 32 ) +// Cheap params used only when the binary is a `go test` binary +// (testing.Testing() == true). Argon2id at production params costs +// 300–500 ms per hash and dominates wall time on CI runners under +// `-race`. Tests don't need real KDF strength — VerifyPassword reads +// params from the encoded hash, so verifying a cheap-params hash +// works the same way. +const ( + testMemoryKiB = 8 + testIterations = 1 + testParallel = 1 +) + // HashPassword returns an argon2id-encoded string of the form // // $argon2id$v=19$m=...,t=...,p=...$$ // // safe to store in a TEXT column. The salt is freshly random per call. func HashPassword(password string) (string, error) { + mem, iter, par := uint32(defaultMemoryKiB), uint32(defaultIterations), uint8(defaultParallel) + if testing.Testing() { + mem, iter, par = testMemoryKiB, testIterations, testParallel + } salt := make([]byte, defaultSaltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("auth: read salt: %w", err) } hash := argon2.IDKey([]byte(password), salt, - defaultIterations, defaultMemoryKiB, defaultParallel, defaultKeyLen) + iter, mem, par, defaultKeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, - defaultMemoryKiB, defaultIterations, defaultParallel, + mem, iter, par, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash), ), nil