diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bcf6a5953..30de96985 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "GENEALOGIX Spec", - "image": "mcr.microsoft.com/devcontainers/go:1.26", + "image": "mcr.microsoft.com/devcontainers/go:1.27", "features": { "ghcr.io/devcontainers/features/node:1": { "version": "24" diff --git a/.golangci-lint-version b/.golangci-lint-version index 6f9cc441d..095c2a17d 100644 --- a/.golangci-lint-version +++ b/.golangci-lint-version @@ -1 +1 @@ -v2.12.2 +v2.13.2 diff --git a/.golangci.yml b/.golangci.yml index 9f97d8744..60e6de800 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -235,9 +235,7 @@ linters: nolintlint: require-explanation: true require-specific: true - # allow-unused deferred: the unused-directive check surfaces 15 pre-existing - # redundant //nolint:mnd directives, cleaned up separately in #902. - allow-unused: true + allow-unused: false exclusions: generated: lax diff --git a/CHANGELOG.md b/CHANGELOG.md index 907635b81..7af680514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Changed + +- **Toolchain bump to Go 1.27 and golangci-lint v2.13.2, with `nolintlint` unused-directive enforcement** — `go.mod` and `ci-tools/go.mod` now declare `go 1.27.0` / `toolchain go1.27.1`, the devcontainer image moves to `go:1.27`, and `.golangci-lint-version` pins `v2.13.2`. The 35 `//nolint` directives that no longer suppressed anything (mostly `//nolint:mnd` on numbers already in `ignored-numbers`, plus stale `errcheck`/`nilerr`/`goconst`/`revive`/`gocyclo` directives) were removed, and `nolintlint`'s `allow-unused` is set to `false` so redundant suppressions are caught from now on. Contributors need Go 1.27+, and so do downstream projects importing `github.com/genealogix/glx/go-glx`. (#902) + ### Removed - **Retired the `auto-resolve-conflicts.yml` workflow in favor of git's native `union` merge driver** — The workflow re-merged conflicting PR branches on a runner and then stripped conflict markers from `CHANGELOG.md` with `sed` plus an `awk` header-dedup pass; since it landed in #82 it needed a string of follow-ups (#194, #341, #592, #601, #602, #657, #1054, #1107) to stay correct. `.gitattributes` now maps `CHANGELOG.md merge=union`, so git itself keeps both sides during the merge — no markers to strip, no second workflow, and no `workflow_run` chaining. `union` is a git built-in, so unlike the `merge=glx` driver for `.glx` files it needs no per-clone `git config`; it applies in every clone. It applies to local merges and rebases only: GitHub's server-side merges API (the "Update branch" button and `auto-update-branches.yml`) does not honor `.gitattributes` merge drivers, so a CHANGELOG-only conflict reported on GitHub is resolved by merging `main` locally and pushing. `CONTRIBUTING.md` and the root `CLAUDE.md` document both halves, plus the one case that still needs a human eye — union keeps both sides blindly, so merging a `main` that has just cut a release can land a branch entry inside the released section. `auto-update-branches.yml` is unchanged. Closes #888 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 121d27aa1..4fd97e12e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ What to expect when you pick one up: ### Prerequisites -- **Go 1.26+** ([install](https://golang.org/doc/install)) — the project uses Go 1.26 in go.mod +- **Go 1.27+** ([install](https://golang.org/doc/install)) — the project uses Go 1.27 in go.mod - **Git** ([install](https://git-scm.com/downloads)) - **Node.js** — for website builds (`npm install` in `website/`) and schema validation (`npm ci --prefix specification`) diff --git a/Makefile b/Makefile index 0d4f1e908..a0b577421 100644 --- a/Makefile +++ b/Makefile @@ -88,10 +88,10 @@ lint-codeowners: ## Verify every file is matched by a .github/CODEOWNERS rule fi; \ echo "CODEOWNERS coverage OK" -fix: ## Run Go 1.26 modernizers on codebase +fix: ## Run Go 1.27 modernizers on codebase go fix ./... -fix-diff: ## Preview Go 1.26 modernizer changes without applying +fix-diff: ## Preview Go 1.27 modernizer changes without applying go fix -diff ./... ## Testing diff --git a/ci-tools/go.mod b/ci-tools/go.mod index f38606221..3b45b7e95 100644 --- a/ci-tools/go.mod +++ b/ci-tools/go.mod @@ -1,8 +1,8 @@ module github.com/genealogix/glx/ci-tools -go 1.26.0 +go 1.27.0 -toolchain go1.26.1 +toolchain go1.27.1 tool ( github.com/hmarr/codeowners/cmd/codeowners diff --git a/glx/add_runner.go b/glx/add_runner.go index b8dae4d15..28b156240 100644 --- a/glx/add_runner.go +++ b/glx/add_runner.go @@ -432,7 +432,7 @@ func finalizeAdd(io *IOStreams, opts *addCommonOptions, ctx *addContext, entityT io.Printf("Adding %s %s\n", entityType, entityID) if opts.DryRun { io.Println("(dry run — no files written)") - fmt.Fprintln(io.MachineOut, entityID) //nolint:errcheck // CLI output + fmt.Fprintln(io.MachineOut, entityID) return nil } @@ -441,7 +441,7 @@ func finalizeAdd(io *IOStreams, opts *addCommonOptions, ctx *addContext, entityT return err } - fmt.Fprintln(io.MachineOut, entityID) //nolint:errcheck // CLI output + fmt.Fprintln(io.MachineOut, entityID) return nil } diff --git a/glx/archive_cache.go b/glx/archive_cache.go index be65e26cc..88124dac8 100644 --- a/glx/archive_cache.go +++ b/glx/archive_cache.go @@ -506,7 +506,7 @@ func loadCache(root string) (*glxlib.GLXFile, *CacheHeader, error) { if err != nil { return nil, nil, err } - defer f.Close() //nolint:errcheck // read-only handle + defer f.Close() var archive glxlib.GLXFile if err := dec.Decode(&archive); err != nil { @@ -548,7 +548,7 @@ func tryLoadFreshCache(root string) (*glxlib.GLXFile, []string, bool) { if err != nil { return nil, nil, false } - defer f.Close() //nolint:errcheck // read-only handle + defer f.Close() if !cacheIsFresh(root, header) { return nil, nil, false diff --git a/glx/cache_runner.go b/glx/cache_runner.go index 62e0ae3d6..e12abdcc5 100644 --- a/glx/cache_runner.go +++ b/glx/cache_runner.go @@ -98,7 +98,7 @@ func cleanCache(io *IOStreams, path string) error { if err := os.Remove(cacheDir(root)); err != nil && !os.IsNotExist(err) { // Non-empty or permission issue — leave it; the cache file is gone, which // is what the user asked for. - return nil //nolint:nilerr // directory cleanup is best-effort + return nil } return nil diff --git a/glx/cli_commands.go b/glx/cli_commands.go index 3a57b6508..c8b6be746 100644 --- a/glx/cli_commands.go +++ b/glx/cli_commands.go @@ -1807,7 +1807,7 @@ the driver to fall back to git's text merge — standard <<<<<<< markers land in , and a diagnostic summary listing both sides' values (with assertion confidence and citations when applicable) is written to stderr.`, - Args: cobra.RangeArgs(3, 4), //nolint:mnd // git merge-driver signature is %O %A %B [%P] + Args: cobra.RangeArgs(3, 4), // git merge-driver signature is %O %A %B [%P] RunE: runMergeDriverCmd, } @@ -1817,7 +1817,7 @@ func runMergeDriverCmd(_ *cobra.Command, args []string) error { OursPath: args[1], TheirsPath: args[2], } - if len(args) >= 4 { //nolint:mnd // %P is git's 4th positional arg + if len(args) >= 4 { // %P is git's 4th positional arg in.OrigPath = args[3] } else { in.OrigPath = in.OursPath diff --git a/glx/coverage_state_census.go b/glx/coverage_state_census.go index 88ab7edb4..40ca24d0f 100644 --- a/glx/coverage_state_census.go +++ b/glx/coverage_state_census.go @@ -27,8 +27,6 @@ import ( // Note: some state census years overlap with federal census years (e.g., // Mississippi 1860). The matching logic requires a state-specific signal // to avoid confusing state and federal censuses. -// -//nolint:goconst // state names are data entries, not magic strings var stateCensusYears = map[string][]int{ "Wisconsin": {1855, 1865, 1875, 1885, 1895, 1905}, "New York": {1825, 1835, 1845, 1855, 1865, 1875, 1892, 1905, 1915, 1925}, diff --git a/glx/e2e/harness_test.go b/glx/e2e/harness_test.go index 7c69be984..520332303 100644 --- a/glx/e2e/harness_test.go +++ b/glx/e2e/harness_test.go @@ -61,7 +61,7 @@ func buildAndRun(m *testing.M) (int, error) { if err != nil { return 0, fmt.Errorf("creating temp dir for binary: %w", err) } - defer os.RemoveAll(dir) //nolint:errcheck // best-effort cleanup + defer os.RemoveAll(dir) bin := filepath.Join(dir, "glx") if runtime.GOOS == "windows" { diff --git a/glx/evidence_runner.go b/glx/evidence_runner.go index 30517ec26..d2e76d892 100644 --- a/glx/evidence_runner.go +++ b/glx/evidence_runner.go @@ -448,7 +448,7 @@ func printEvidenceJSON(io *IOStreams, r *EvidenceReport) error { if err != nil { return fmt.Errorf("failed to marshal evidence report: %w", err) } - fmt.Fprintln(io.MachineOut, string(data)) //nolint:errcheck // CLI output + fmt.Fprintln(io.MachineOut, string(data)) return nil } diff --git a/glx/iostreams.go b/glx/iostreams.go index 728a41acf..bbe12cea7 100644 --- a/glx/iostreams.go +++ b/glx/iostreams.go @@ -72,16 +72,16 @@ func TestIOStreams() (*IOStreams, *bytes.Buffer, *bytes.Buffer) { // sequences. MachineOut is not written through this method and is left // byte-faithful. func (s *IOStreams) Printf(format string, args ...any) { - fmt.Fprint(s.Out, sanitizeForTerminal(fmt.Sprintf(format, args...))) //nolint:errcheck // CLI output + fmt.Fprint(s.Out, sanitizeForTerminal(fmt.Sprintf(format, args...))) } // Println writes a line to the standard output stream, sanitized as in Printf. func (s *IOStreams) Println(msg string) { - fmt.Fprintln(s.Out, sanitizeForTerminal(msg)) //nolint:errcheck // CLI output + fmt.Fprintln(s.Out, sanitizeForTerminal(msg)) } // Errorf writes a formatted string to the error output stream, sanitized as // in Printf. func (s *IOStreams) Errorf(format string, args ...any) { - fmt.Fprint(s.ErrOut, sanitizeForTerminal(fmt.Sprintf(format, args...))) //nolint:errcheck // CLI output + fmt.Fprint(s.ErrOut, sanitizeForTerminal(fmt.Sprintf(format, args...))) } diff --git a/glx/migrations_runner.go b/glx/migrations_runner.go index 0f1be6d17..95ec0b7dc 100644 --- a/glx/migrations_runner.go +++ b/glx/migrations_runner.go @@ -542,7 +542,7 @@ func printMigrationsJSON(io *IOStreams, report any) error { if err != nil { return fmt.Errorf("failed to marshal migrations report: %w", err) } - fmt.Fprintln(io.MachineOut, string(data)) //nolint:errcheck // CLI output + fmt.Fprintln(io.MachineOut, string(data)) return nil } diff --git a/glx/proof_runner.go b/glx/proof_runner.go index 4d5b1e458..df01de024 100644 --- a/glx/proof_runner.go +++ b/glx/proof_runner.go @@ -1105,7 +1105,7 @@ func printProofJSON(io *IOStreams, result *proofResult) error { if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } - fmt.Fprintln(io.MachineOut, string(data)) //nolint:errcheck // CLI output + fmt.Fprintln(io.MachineOut, string(data)) return nil } diff --git a/glx/publish_media.go b/glx/publish_media.go index 1725b460b..ee8d7ff97 100644 --- a/glx/publish_media.go +++ b/glx/publish_media.go @@ -282,7 +282,7 @@ func (r *mediaResolver) copyMedia(srcPath, uri string) (string, error) { // refuses symlinks that escape baseDir); either failure is "missing". src, err := openWithin(r.baseDir, srcPath) if err != nil { - return "", nil //nolint:nilerr // missing media is non-fatal + return "", nil } defer func() { _ = src.Close() }() diff --git a/glx/report_runner.go b/glx/report_runner.go index ac6944393..b72d6464f 100644 --- a/glx/report_runner.go +++ b/glx/report_runner.go @@ -74,7 +74,7 @@ type reportData struct { } // assertionSummary is a short description of an assertion for display. -type assertionSummary struct { //nolint:revive // SubjectType field name is part of the JSON wire format +type assertionSummary struct { ID string SubjectType glxlib.EntityType SubjectID string diff --git a/glx/robustio_windows.go b/glx/robustio_windows.go index 1c6700bfa..ecdaf733e 100644 --- a/glx/robustio_windows.go +++ b/glx/robustio_windows.go @@ -26,11 +26,13 @@ import ( // errSharingViolation is syscall.Errno(32), equivalent to // internal/syscall/windows.ERROR_SHARING_VIOLATION which is not importable. -const errSharingViolation syscall.Errno = 32 //nolint:mnd // Windows error code +const errSharingViolation syscall.Errno = 32 -const retryTimeout = 2000 * time.Millisecond //nolint:mnd // matches Go toolchain robustio +// retryTimeout matches the Go toolchain's internal robustio budget. +const retryTimeout = 2000 * time.Millisecond -const maxSleep = 500 * time.Millisecond //nolint:mnd // cap per-retry sleep +// maxSleep caps the per-retry sleep. +const maxSleep = 500 * time.Millisecond // robustRename is like os.Rename but retries on transient Windows errors. // diff --git a/go-glx/constants.go b/go-glx/constants.go index 20823e07f..82fb3961d 100644 --- a/go-glx/constants.go +++ b/go-glx/constants.go @@ -562,9 +562,7 @@ var AllEntityTypes = []EntityType{ // they participate in entity IDs (e.g., `research-log-john-smith`), which are // constrained to `[a-zA-Z0-9-]{1,64}`. This map IS the source of truth — the // values are inherently string literals tied to the entity-type vocabulary, -// so goconst's "extract a constant" suggestion is misplaced here. -// -//nolint:goconst // the literal values are the source of truth for singular names +// not constants to be extracted. var entityTypeSingular = map[EntityType]string{ EntityTypePersons: "person", EntityTypeRelationships: "relationship", diff --git a/go-glx/date_string.go b/go-glx/date_string.go index e41f4ff66..7a447a26e 100644 --- a/go-glx/date_string.go +++ b/go-glx/date_string.go @@ -68,7 +68,7 @@ func (ds DateString) Parse() (glxdate.Date, error) { // range ("TO 1950"), whose end year is the only one present. A BCE year is // negative. func (ds DateString) Year() int { - d, _ := glxdate.Parse(string(ds)) //nolint:errcheck // best-effort year is defined even for non-canonical dates + d, _ := glxdate.Parse(string(ds)) return d.Year() } diff --git a/go-glx/duplicates.go b/go-glx/duplicates.go index 47b5205e6..3c2d6da51 100644 --- a/go-glx/duplicates.go +++ b/go-glx/duplicates.go @@ -147,7 +147,7 @@ func FindCrossArchiveDuplicates(dest, src *GLXFile, opts DuplicateOptions) (*Dup results = append(results, DuplicatePair{ PersonA: pair[0], PersonB: pair[1], - Score: math.Round(score*100) / 100, //nolint:mnd // round to 2 decimal places + Score: math.Round(score*100) / 100, Signals: signals, }) } diff --git a/go-glx/gedcom_blob.go b/go-glx/gedcom_blob.go index 01bd8bc60..70681ead2 100644 --- a/go-glx/gedcom_blob.go +++ b/go-glx/gedcom_blob.go @@ -58,7 +58,7 @@ func DecodeGEDCOMBlob(blobText string) ([]byte, error) { } result := make([]byte, 0, len(cleaned)*3/4) - fullGroups := (len(cleaned) / 4) * 4 //nolint:mnd // 4 chars per group + fullGroups := (len(cleaned) / 4) * 4 // 4 chars per group for i := 0; i < fullGroups; i += 4 { // Validate each character is in valid GEDCOM BLOB range (0x2E '.' to 0x6D 'm') @@ -75,7 +75,7 @@ func DecodeGEDCOMBlob(blobText string) ([]byte, error) { b3 := cleaned[i+2] - '.' b4 := cleaned[i+3] - '.' - //nolint:mnd // well-known base64 bit shifts + // Well-known base64 bit shifts. result = append(result, (b1<<2)|(b2>>4), (b2<<4)|(b3>>2), @@ -96,14 +96,14 @@ func DecodeGEDCOMBlob(blobText string) ([]byte, error) { if trailing == 1 { return nil, fmt.Errorf("%w: %d (trailing single character cannot encode a full byte)", ErrGEDCOMBlobLength, len(cleaned)) } - if trailing >= 2 { //nolint:mnd // trailing group sizes + if trailing >= 2 { // a 2- or 3-char trailing group b1 := cleaned[fullGroups] - '.' b2 := cleaned[fullGroups+1] - '.' - result = append(result, (b1<<2)|(b2>>4)) //nolint:mnd // well-known base64 bit shifts + result = append(result, (b1<<2)|(b2>>4)) // well-known base64 bit shifts - if trailing == 3 { //nolint:mnd // 3-char trailing group + if trailing == 3 { // 3-char trailing group carries a second byte b3 := cleaned[fullGroups+2] - '.' - result = append(result, (b2<<4)|(b3>>2)) //nolint:mnd // well-known base64 bit shifts + result = append(result, (b2<<4)|(b3>>2)) } } diff --git a/go-glx/gedcom_place.go b/go-glx/gedcom_place.go index 73c58ad92..d3632c65c 100644 --- a/go-glx/gedcom_place.go +++ b/go-glx/gedcom_place.go @@ -245,7 +245,7 @@ func inferPlaceType(name string, level int) string { case 2: // Third level - likely state/province return PlaceTypeState - case 3: //nolint:mnd // fourth hierarchy level = country + case 3: // Fourth level - likely country return PlaceTypeCountry default: diff --git a/go-glx/glxdate/parse.go b/go-glx/glxdate/parse.go index 15da58892..33dc6fec9 100644 --- a/go-glx/glxdate/parse.go +++ b/go-glx/glxdate/parse.go @@ -288,7 +288,7 @@ func isPartialMonth(tokens []string) bool { _, ok := MonthNumber(tokens[0]) return ok - case 2: //nolint:mnd // DD MONTH + case 2: // DD MONTH _, ok := MonthNumber(tokens[1]) return ok && dayToken(tokens[0]) > 0 @@ -410,7 +410,7 @@ func parsePoint(cal Calendar, tokens []string) point { // structured body, returning the remaining tokens and the era token as // written ("" when absent). A body that is only an era token is left alone. func splitEra(tokens []string) ([]string, string) { - if len(tokens) < 2 { //nolint:mnd // a date and its era + if len(tokens) < 2 { // an era split needs a date and its era return tokens, "" } last := tokens[len(tokens)-1] @@ -464,11 +464,11 @@ func parseNamedMonth(tokens []string, p *point) bool { var month, year, day int var ok bool switch len(toks) { - case 2: //nolint:mnd // MONTH YYYY + case 2: // MONTH YYYY month, ok = MonthNumber(toks[0]) year = yearToken(toks[1]) p.precision = PrecisionMonth - case 3: //nolint:mnd // DD MONTH YYYY or MONTH DD, YYYY + case 3: // DD MONTH YYYY or MONTH DD, YYYY year = yearToken(toks[2]) if month, ok = MonthNumber(toks[1]); ok { day = dayToken(toks[0]) diff --git a/go-glx/merge3.go b/go-glx/merge3.go index 9e96cc514..278f1f069 100644 --- a/go-glx/merge3.go +++ b/go-glx/merge3.go @@ -205,8 +205,6 @@ func tagConflicts(entityType EntityType, id string, conflicts []Merge3Conflict) // with a null value" — git's merge can land us with a map literal that lists // an ID but no body (e.g. an empty `persons:` entry under YAML serialization // quirks), and we never want to emit `persons: {p1: null}` back out. -// -//nolint:gocyclo // 7-case finite presence enumeration; splitting further hurts readability. func merge3EntityID[V any]( entityType EntityType, id string, base, ours, theirs map[string]*V, diff --git a/go.mod b/go.mod index f5ca5addf..5556901c6 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/genealogix/glx -go 1.26.0 +go 1.27.0 -toolchain go1.26.1 +toolchain go1.27.1 require ( github.com/brianvoe/gofakeit/v7 v7.17.0