From 109711ec0d8c83a7ca9d574e4e703089be9fd423 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Fri, 11 Sep 2026 10:11:02 +0800 Subject: [PATCH 1/2] Upgrade to Go 1.27 and standardize CI badges --- .github/workflows/build-test.yml | 17 +++++++++++++---- README.md | 5 +++-- go.mod | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 49e8de3..107c6d6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,9 +1,14 @@ name: Build & Test on: - workflow_dispatch: + push: + branches: [main] pull_request: branches: [main] + workflow_dispatch: + +permissions: + contents: read jobs: build-and-test: @@ -23,11 +28,12 @@ jobs: env: FMSG_TEST_DATABASE_URL: postgres://postgres@localhost:5432/postgres?sslmode=disable steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-go@v5 + - name: Set up Go + uses: actions/setup-go@v7 with: - go-version-file: go.mod + go-version: "1.27.x" - name: Build run: go build ./... @@ -35,3 +41,6 @@ jobs: - name: Test run: | FMSG_TEST_DD="$(go list -m -f '{{.Dir}}' github.com/markmnl/fmsgd)/dd.sql" go test -race ./... + + - name: Vet + run: go vet ./... diff --git a/README.md b/README.md index 24300d6..ff24a89 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build & Test](https://github.com/markmnl/fmsg-webapi/actions/workflows/build-test.yml/badge.svg)](https://github.com/markmnl/fmsg-webapi/actions/workflows/build-test.yml) +[![Build & Test](https://github.com/markmnl/fmsg-webapi/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/markmnl/fmsg-webapi/actions/workflows/build-test.yml?query=branch%3Amain) +[![Go 1.27+](https://img.shields.io/badge/Go-1.27%2B-00ADD8?logo=go&logoColor=white)](https://go.dev/dl/) # fmsg-webapi @@ -179,7 +180,7 @@ go run ./cmd/fmsg-webapi api-key rotate-delegation \ ## Building -Requires **Go 1.25** or newer. +Requires **Go 1.27** or newer. ```bash go build ./... diff --git a/go.mod b/go.mod index 87d5dbd..2de65f0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/markmnl/fmsg-webapi -go 1.25.0 +go 1.27.0 require ( github.com/MicahParks/keyfunc/v3 v3.8.0 From b389a94389ef5b6ee6b97dc7daa32fc1dbbe5e4e Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Fri, 11 Sep 2026 10:18:10 +0800 Subject: [PATCH 2/2] Apply Go 1.27 modernizations with go fix --- cmd/fmsg-webapi/apikey_cli.go | 2 +- internal/apiauth/token.go | 12 +++++------- internal/emoji/gen/main.go | 10 +++++----- internal/handlers/finalization_integration_test.go | 7 +++---- internal/handlers/hub.go | 12 ++++++------ internal/handlers/ws.go | 7 +++---- internal/middleware/jwt.go | 9 ++++----- internal/middleware/jwt_test.go | 2 +- internal/middleware/util.go | 2 +- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/cmd/fmsg-webapi/apikey_cli.go b/cmd/fmsg-webapi/apikey_cli.go index f675592..c231c9e 100644 --- a/cmd/fmsg-webapi/apikey_cli.go +++ b/cmd/fmsg-webapi/apikey_cli.go @@ -198,7 +198,7 @@ func prepareCLIGrantInputs(owner, agent, cidrsRaw, expiresRaw string) ([]string, } var allowed []string if strings.TrimSpace(cidrsRaw) != "" { - for _, cidr := range strings.Split(cidrsRaw, ",") { + for cidr := range strings.SplitSeq(cidrsRaw, ",") { allowed = append(allowed, strings.TrimSpace(cidr)) } } diff --git a/internal/apiauth/token.go b/internal/apiauth/token.go index 60bbfee..8cafe62 100644 --- a/internal/apiauth/token.go +++ b/internal/apiauth/token.go @@ -65,13 +65,11 @@ func (i *TokenIssuer) Mint(ownerAddr, subAddr, keyID string, now time.Time) (str claims := TokenClaims{ OwnerAddr: ownerAddr, APIKeyID: keyID, - RegisteredClaims: jwt.RegisteredClaims{ - Issuer: i.issuer, - Subject: subAddr, - Audience: jwt.ClaimStrings{i.audience}, - IssuedAt: jwt.NewNumericDate(now), - ExpiresAt: jwt.NewNumericDate(expires), - }, + Issuer: i.issuer, + Subject: subAddr, + Audience: jwt.ClaimStrings{i.audience}, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expires), } tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) signed, err := tok.SignedString(i.privateKey) diff --git a/internal/emoji/gen/main.go b/internal/emoji/gen/main.go index dfd3494..9a0354a 100644 --- a/internal/emoji/gen/main.go +++ b/internal/emoji/gen/main.go @@ -102,8 +102,8 @@ func parseTest(r io.Reader) ([]string, string) { sc := bufio.NewScanner(r) for sc.Scan() { line := sc.Text() - if strings.HasPrefix(line, "# Version:") { - version = strings.TrimSpace(strings.TrimPrefix(line, "# Version:")) + if after, ok := strings.CutPrefix(line, "# Version:"); ok { + version = strings.TrimSpace(after) continue } if line == "" || strings.HasPrefix(line, "#") { @@ -120,7 +120,7 @@ func parseTest(r io.Reader) ([]string, string) { continue } var b strings.Builder - for _, cp := range strings.Fields(line[:semi]) { + for cp := range strings.FieldsSeq(line[:semi]) { n, err := strconv.ParseUint(cp, 16, 32) if err != nil { fmt.Fprintf(os.Stderr, "bad code point %q\n", cp) @@ -153,8 +153,8 @@ func parseData(r io.Reader) map[string][]span { } rng, prop := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) lo, hi := rng, rng - if dots := strings.Index(rng, ".."); dots >= 0 { - lo, hi = rng[:dots], rng[dots+2:] + if before, after, ok := strings.Cut(rng, ".."); ok { + lo, hi = before, after } l, err1 := strconv.ParseUint(lo, 16, 32) h, err2 := strconv.ParseUint(hi, 16, 32) diff --git a/internal/handlers/finalization_integration_test.go b/internal/handlers/finalization_integration_test.go index 5305cdd..30d3d4d 100644 --- a/internal/handlers/finalization_integration_test.go +++ b/internal/handlers/finalization_integration_test.go @@ -271,9 +271,8 @@ func TestFinalizationRollbackAndConcurrentSend(t *testing.T) { id = a.draft(t, alice, "race", nil) var wg sync.WaitGroup codes := make(chan int, 8) - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { defer wg.Done(); codes <- a.request(alice, "POST", "/fmsg/"+id+"/send", nil).Code }() + for range 8 { + wg.Go(func() { codes <- a.request(alice, "POST", "/fmsg/"+id+"/send", nil).Code }) } wg.Wait() close(codes) @@ -290,7 +289,7 @@ func TestFinalizationRollbackAndConcurrentSend(t *testing.T) { } // Race a full draft edit with send. Either order is valid; the committed // digest must describe exactly the content that remains downloadable. - for i := 0; i < 5; i++ { + for range 5 { id = a.draft(t, alice, "before", nil) wg.Add(2) go func(id string) { defer wg.Done(); a.request(alice, "POST", "/fmsg/"+id+"/send", nil) }(id) diff --git a/internal/handlers/hub.go b/internal/handlers/hub.go index aab1f22..ff5b07f 100644 --- a/internal/handlers/hub.go +++ b/internal/handlers/hub.go @@ -24,8 +24,8 @@ const ( // wsEnvelope is the JSON shape of every frame pushed over a WebSocket. The // Type field lets clients route events; Data carries the event-specific body. type wsEnvelope struct { - Type string `json:"type"` - Data interface{} `json:"data"` + Type string `json:"type"` + Data any `json:"data"` } // Hub maintains the set of connected WebSocket clients and fans out database @@ -189,15 +189,15 @@ func (h *Hub) listen(ctx context.Context, onConnected func()) error { // parseNotifyPayload parses a new_msg payload of the form "msgID,addr". func parseNotifyPayload(payload string) (msgID int64, addr string, ok bool) { - comma := strings.IndexByte(payload, ',') - if comma < 0 { + before, after, ok := strings.Cut(payload, ",") + if !ok { return 0, "", false } - id, err := strconv.ParseInt(payload[:comma], 10, 64) + id, err := strconv.ParseInt(before, 10, 64) if err != nil { return 0, "", false } - addr = payload[comma+1:] + addr = after if addr == "" { return 0, "", false } diff --git a/internal/handlers/ws.go b/internal/handlers/ws.go index 2ef4a66..c51c780 100644 --- a/internal/handlers/ws.go +++ b/internal/handlers/ws.go @@ -3,6 +3,7 @@ package handlers import ( "log" "net/http" + "slices" "strings" "sync" "time" @@ -57,10 +58,8 @@ func NewWSHandler(verifier *middleware.Verifier, hub *Hub, allowedOrigins []stri if len(allowedOrigins) == 0 { return true } - for _, o := range allowedOrigins { - if o == origin { - return true - } + if slices.Contains(allowedOrigins, origin) { + return true } log.Printf("ws: rejected upgrade from origin %q", origin) return false diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go index 0a93beb..83b7e86 100644 --- a/internal/middleware/jwt.go +++ b/internal/middleware/jwt.go @@ -97,7 +97,7 @@ func NewVerifier(cfg Config) (*Verifier, error) { if cfg.AddressClaim == "" { return nil, errors.New("middleware: EdDSA mode requires an AddressClaim") } - v.idpKeyFunc = func(t *jwt.Token) (interface{}, error) { + v.idpKeyFunc = func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodEd25519); !ok { return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg()) } @@ -220,7 +220,7 @@ func (v *Verifier) authenticateAPIToken(ctx context.Context, tokenStr, remoteAdd return authResult{}, authError{status: http.StatusForbidden, msg: "act-as is only available with identity-provider authentication"} } claims := &apiauth.TokenClaims{} - _, err := v.apiParser.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { + _, err := v.apiParser.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodEd25519); !ok { return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg()) } @@ -252,8 +252,7 @@ func (e authError) Error() string { } func authFailureFromError(err error) (int, string, bool) { - var ae authError - if errors.As(err, &ae) { + if ae, ok := errors.AsType[authError](err); ok { return ae.status, ae.msg, true } switch { @@ -415,7 +414,7 @@ func CheckFmsgID(idURL, addr string) (int, bool, error) { fmsgIDCache.Delete(addr) } - v, err, _ := fmsgIDGroup.Do(addr, func() (interface{}, error) { + v, err, _ := fmsgIDGroup.Do(addr, func() (any, error) { if v, ok := fmsgIDCache.Load(addr); ok { entry := v.(fmsgIDEntry) if time.Now().Before(entry.expires) { diff --git a/internal/middleware/jwt_test.go b/internal/middleware/jwt_test.go index 5f89e9f..8c98c3b 100644 --- a/internal/middleware/jwt_test.go +++ b/internal/middleware/jwt_test.go @@ -70,7 +70,7 @@ func TestIsValidAddr(t *testing.T) { } func fakeJWKS(kid string, pub ed25519.PublicKey) jwt.Keyfunc { - return func(t *jwt.Token) (interface{}, error) { + return func(t *jwt.Token) (any, error) { k, _ := t.Header["kid"].(string) if k != kid { return nil, jwt.ErrTokenSignatureInvalid diff --git a/internal/middleware/util.go b/internal/middleware/util.go index 160cf5e..c4e6627 100644 --- a/internal/middleware/util.go +++ b/internal/middleware/util.go @@ -5,6 +5,6 @@ import ( "io" ) -func decodeJSON(r io.Reader, v interface{}) error { +func decodeJSON(r io.Reader, v any) error { return json.NewDecoder(r).Decode(v) }