Skip to content

feat: bootstrap the meshStack CLI and move the API client in - #1

Draft
grubmeshi wants to merge 204 commits into
mainfrom
feat/scaffold-cli-and-move-client
Draft

feat: bootstrap the meshStack CLI and move the API client in#1
grubmeshi wants to merge 204 commits into
mainfrom
feat/scaffold-cli-and-move-client

Conversation

@grubmeshi

@grubmeshi grubmeshi commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Stands up this repository and moves the meshStack API client into it from the Terraform provider, so
the provider and the CLI share one client instead of each carrying its own.

ClickUp: 86cb61rzz, milestone
86cb61we4.

Three commits, meant to be read in order:

  1. feat: bootstrap the meshStack CLI repository — Taskfile, Go-only CI, Nix dev shell, and a
    cobra root command that prints help and nothing else yet.
  2. feat: move the meshStack API client in and add pkg/loginclient/ and the shared
    pkg/login.
  3. feat: release with goreleaser and publish a container image — the release path.

Layout

Path Holds
cmd/meshstack/ package main: main() and the root command. The only main package.
cmd/<subcommand>/ One package per subcommand. Package name is the subcommand, file name is the leaf command, so cmd/buildingblock/list.go will hold meshstack buildingblock list.
pkg/ Logic that needs no CLI process, and that the provider can import.
client/ The meshStack API client, on the path it had in the provider.

There is no main.go at the repository root, on purpose: the binary takes its name from its
directory, so go build ./cmd/meshstack and
go install github.com/meshcloud/meshstack-cli/cmd/meshstack@latest both produce meshstack with no
-o flag, while the module stays meshstack-cli. cmd/meshstack is the one directory under cmd/
that is not a subcommand.

Only the root command exists so far. The convention for the rest is written down in AGENTS.md rather
than demonstrated with a stub command that does nothing. cmd/meshstack wires children in explicitly
with AddCommand; nothing registers itself from init().

Dependency policy

One external dependency, cobra, and only cmd/ may use it. Everything else is standard library, with
testify in tests. depguard in .golangci.yml enforces that per directory — read those rules as the
policy. It matters because the provider imports client/ and pkg/login, so anything added here
lands in the provider's dependency tree and in the public checksum database.

cobra brings spf13/pflag and inconshreveable/mousetrap with it as indirect dependencies.

Each depguard rule was checked by compiling a file that violates it, because a rule matching no file
is a silent no-op. That is how a pattern bug surfaced: **/client/**/*.go matches files in
subdirectories of client/ only, never files directly inside it, so most of the client would have
been unguarded. Both patterns are now listed. The same bug was in the provider's config, where it left
51 client files and all 23 clientmock files unchecked — fixed separately and already on the
provider's main.

pkg/login resolves credentials; it does not run the login exchange

The exchange already exists in client/internal/auth.go, where it posts to /api/login, caches the
access token and refreshes it before expiry. Go's internal rule keeps that code inside client/, so
pkg/login reaches it through client.NewApiKeyAuthorization. A second, hand-rolled exchange would
have produced a static token that starts returning 401 once it expires.

credentials := login.FromEnv().Merge(explicitOverrides) // non-empty overrides win
endpoint, err := credentials.EndpointURL()
authorization, err := credentials.Authorization() // ApiToken outranks ApiKey/ApiSecret

Merge exists for the provider, which merges its provider block attributes over FromEnv so an
explicitly configured attribute outranks the environment.

The four MESHSTACK_ names are exported consts, not private ones. The provider's diagnostics
quote the variable names in their message text, so keeping them private would mean the same strings
living in both repositories.

Releasing

A v* tag publishes archives and checksums for linux, darwin and windows on amd64 and arm64, then
builds the container image for the same tag as a separate job. The image workflow is reusable, so a
push to main refreshes :main through the same path — without that, no usable image would exist
until the first release. Pull requests build the image without pushing it.

Everything published carries the repository name — archives, checksums and
ghcr.io/meshcloud/meshstack-cli — while the binary inside is meshstack, so
docker run ghcr.io/meshcloud/meshstack-cli buildingblock list reads like the local invocation.
No GPG signing: the provider signs because the Terraform registry demands it, which does not apply
here.

Known gap, for later

Authorization.Header takes a client/internal.HttpClient, so nothing outside client/ can call it,
and nothing can read the cached token back out. Caching a token in ~/.config/meshstack-cli will
need a new exported seam. pkg/login is the only place that constructs an Authorization, which is
where that seam belongs. The read side is already free: a cached token goes back in through
client.NewApiTokenAuthorization, the same path MESHSTACK_API_TOKEN uses.

Verification

  • All 66 client files build here, and every moved test passes unmodified.
  • pkg/login has 19 subtests across five table-driven tests. The precedence case was
    mutation-checked: it fails on an implementation that prefers key and secret over the token.
  • golangci-lint: clean.
  • goreleaser check passes; goreleaser release --snapshot produces meshstack-cli_*.tar.gz
    containing a meshstack binary that reports the injected version. The image builds and runs, at
    4.6 MB.
  • The provider was rewritten onto this module locally, behind a temporary
    replace github.com/meshcloud/meshstack-cli => ../meshstack-cli, and its 83 unit tests pass with
    no failures
    . That is the gate from the feature plan: nothing was pushed here until the provider
    was green against it. Provider PR: feat!: consume the meshStack API client from meshstack-cli terraform-provider-meshstack#284.
  • Acceptance tests against a live backend have not been run.

Still open

  • The MPL-2.0 LICENSE, matching the license the client code already carries. collie-cli is
    Apache-2.0, so this is a deliberate pick rather than the house default.

🤖 Generated with Claude Code

henryde and others added 30 commits June 20, 2024 13:49
meshStack enforces the Accept header soon, so we have to make sure to always provide it
includes adaptations from PR remarks
tfelix and others added 5 commits July 30, 2026 10:13
…ults

Addresses the PR review on the tenant quota map work.

The provider claimed a requested quota beyond the platform's auto-approval
threshold waits for operator approval. The meshObject API does the opposite:
it refuses such a create outright, deliberately, because it has no quota-request
representation and a pending request would let an apply report success on quotas
that are not in effect. Rewrite the warning and the schema descriptions to
document what actually happens — applied quotas are the landing zone's defaults
overlaid with the request, and a requested key that differs was changed after
creation.

Cover the two cases a real backend can produce: a landing-zone default the
tenant never requests (applied is a strict superset of requested, asserted to
not drift on re-plan) and an above-threshold request being rejected. The tenant
mock now overlays landing-zone default quotas as the backend does, so the former
runs in both unit and acceptance mode.

Also mark the deprecated create-spec quotas field with a godoc marker, document
MeshTenantQuota as the deprecated list-form element, state the real reason
RequestQuotaValue and AppliedQuotaValue stay distinct types, and shorten the
changelog entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…block ref output

Reshape `spec.parent_building_blocks` on `meshstack_building_block` from a set
of `{buildingblock_uuid, definition_uuid}` objects into a plain set of
`{kind, uuid}` building block refs. One ref replaces both fields because
meshStack derives a parent's definition from the referenced building block. The
attribute keeps its name: it is what the meshObject API calls the field, and
naming it `parent_building_block_refs` would have made the provider the only
place with a third spelling.

`meshstack_building_block` also gains the computed `ref` output that feeds
another block's `parent_building_blocks`, so the producing and consuming sides
of a building-block reference land together. A state upgrader rewrites existing
state, so an upgrade does not plan a replacement.

`meshstack_building_blocks` reports parents in the same ref shape. The
deprecated `meshstack_building_block_v2` resource and data source keep their
flat `parent_building_blocks` unchanged: the meshObject API keeps accepting and
returning the deprecated flat fields, so they map to and from the shared client
DTO explicitly.

The version floor stays at 2026.30.0. A parent is written as `kind`, `uuid` and
the deprecated `buildingBlockUuid` twin carrying the identical value, and read
from `uuid` falling back to `buildingBlockUuid` — so this release runs against
today's backend as well as the flattened representation that follows it. That
compatibility lives entirely in `MeshBuildingBlockV2Parent`'s JSON methods.

BREAKING CHANGE: `meshstack_building_block`'s
`spec.parent_building_blocks[*].buildingblock_uuid` and
`spec.parent_building_blocks[*].definition_uuid` are removed; each element is
now a `{kind, uuid}` ref. `meshstack_building_blocks` reports the same shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The meshTenant API deprecated the list-form spec.quotas in favour of the
spec.requested_quotas map, which v0.24.3 added together with the computed
status.applied_quotas. This drops the deprecated field from meshstack_tenant,
its data source and meshstack_tenants, so the provider stops sending and
modelling it.

Only the unsuffixed, ref-based resource is touched. The deprecated
meshstack_tenant_v4 keeps its own spec.quotas: it is removed wholesale once the
meshTenant API goes GA.

Existing state migrates automatically (schema version 1 -> 2): a quota recorded
under spec.quotas is translated into spec.requested_quotas rather than dropped,
so a configuration that restates the same quotas in the map form plans no
change. That matters because a quota change on an existing tenant is rejected --
the meshTenant API cannot update one. The `moved` mover from
meshstack_tenant_v4 translates the same way.

Because this only stops using a field the API still accepts, it needs no newer
meshStack version and the version floor stays at 2026.30.0.

Issue: CU-86c0j0r7q
Send the GA `v4` meshTenant media type instead of `v4-preview` from
meshstack_tenant and meshstack_tenants, and drop the preview disclaimer from
their documentation. This requires a meshStack backend that has promoted
meshTenant v4 to GA; a backend that only serves the preview media type answers
with HTTP 415.

With the unsuffixed resource on the GA API, the deprecated meshstack_tenant_v4
resource and data source go away, together with their client
(client.MeshTenantV4), models, mocks, builder, examples and docs, and their
registration in the provider. The meshstack_tenant MoveState/moveFromV4
migration path goes with them, because the type it migrates from no longer
exists -- so apply that `moved` block on v0.24.x before you upgrade.

client/tenant_v4_test.go becomes client/tenant_v4_deletion_test.go, since the
deletion helpers are all it still covers. The client package keeps the v4 in its
file names, because it is the API version it talks to.

The version floor moves to 2026.34.0, the first release that can still carry the
backend flip: v2026.33.0 was tagged 2026-08-12 while the backend PR was open.
docs/index.md is regenerated from the __MIN_MESHSTACK_VERSION__ placeholder in
templates/index.md.tmpl rather than edited by hand.

Issue: CU-86c0j0r7q
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread Taskfile.yml Outdated
Comment thread Taskfile.yml Outdated
Comment thread .gitignore
Comment thread AGENTS.md Outdated
Comment thread cmd/root/root.go Outdated
Comment thread cmd/root/root.go Outdated
Comment thread pkg/login/apikey_test.go Outdated
@grubmeshi
grubmeshi force-pushed the feat/scaffold-cli-and-move-client branch from 451238f to d39e4c1 Compare August 18, 2026 13:38
Stand up the repository so the meshStack API client has somewhere to move to:
a Taskfile, a Go-only CI workflow, a Nix dev shell and a cobra root command that
prints help and nothing else yet.

The dependency policy is the part worth reading. This repository is allowed one
external dependency, cobra, and only cmd/ may use it; everything else stays on
the standard library, with testify permitted in tests. The reason is that the
Terraform provider will import client/ and pkg/login, so anything added here
lands in the provider's dependency tree and in the public checksum database.
depguard in .golangci.yml enforces that per directory.

Two things about those depguard patterns. A rule that matches no file is a
silent no-op, so each rule was verified by compiling a file that violates it.
That is how the pattern bug surfaced: '**/client/**/*.go' matches files in
subdirectories of client/ only, never files directly inside it, which would have
left most of the client unguarded. Both patterns are therefore listed.

The binary is meshstack while the module is meshstack-cli, and the layout is what
reconciles them: the main package sits in cmd/meshstack, so 'go build' and
'go install' name the binary after that directory. Neither needs -o, and there is
no main.go at the repository root, which would have named the binary after the
module instead. cmd/meshstack holds main() and the root command together and is
the one directory under cmd/ that is not a subcommand.

The root command sets RunE even though it has no work to do, because cobra's help
template skips the usage block entirely while a command is neither runnable nor a
parent of subcommands — without it, 'meshstack --help' printed one line and no
flags at all. Args is NoArgs so an unknown argument fails instead of silently
printing help.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feat/scaffold-cli-and-move-client branch from d39e4c1 to f329c91 Compare August 18, 2026 13:50
JohannesRudolph and others added 14 commits August 18, 2026 21:25
The opt-out was evaluated inside the version-mismatch branch, so it was only
reachable once GET /mesh/info had already succeeded. Setting the flag therefore
never skipped the request — it only suppressed a version mismatch.

/mesh/info is a GET on the retrying client, so an unavailable meshStack made
every provider configure block for the client's full retry budget (~4 minutes)
and then fail, with no way to opt out. Move the check to the top of
checkMeshVersion so the flag short-circuits before the request is built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A building block plan converted spec out of tfsdk.Plan, which fails whenever an
attribute is wired to a resource the same plan creates or replaces. ModifyPlan
now walks the planned spec for unknowns and schedules a run instead, matching
what it already did for an unknown definition version ref.

meshstack_tenant_v4 also lost spec.platform_identifier and
spec.landing_zone_identifier against a meshStack that serves meshTenant v4 in
its ref shape. Both force replacement, so a refresh planned the recreation of a
live tenant. They are recovered from spec.landingZoneRef.name and from
status.tenantName, without relying on the dropped flat identifiers.

Together these unblock destroying a building block composition created before
the backend moved meshTenant v4 to refs.
Resolutions:
- CHANGELOG.md: keep both sections, v0.25.0 above the v0.24.5 patch.
- client/tenant_v4.go and its tests: take main's removal. The meshTenant v4 ref
  read fix only applies to meshstack_tenant_v4, which v0.25.0 drops; it stays on
  the v0.24.5 tag. The unknown-spec fix in ModifyPlan carries forward.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e_flags

Replaces the is_four_eyes_enabled bool with a more general enabled_feature_flags
set-of-strings attribute (currently only four_eyes_role_approval), per PR review.
Brings the meshStack API client in with its history — 197 commits reaching back
to 'refactor: move client to separate package' — rather than as a flat copy, so
authorship and per-change granularity survive the move.

Split from terraform-provider-meshstack's main with
'git subtree split --prefix=client', then added with 'git subtree add', which
means future changes can be carried between the two repositories with
'git subtree pull' and 'git subtree push'.

How to read that history, because the paths shift at this commit. The split
history carries the files at the repository root (client.go, not
client/client.go) and this merge re-roots them under client/, so a path-limited
log from the new path stops here:

    git log -- client/client.go          # only this commit
    git log -- client/client.go client.go # the full 58
    git blame client/client.go            # traverses the merge correctly

'git log --follow' does not help: it resolves renames within one commit's
parents, not across a subtree re-rooting.

The files arrive verbatim, so they still import
github.com/meshcloud/terraform-provider-meshstack/client at this commit and do
not compile in this module yet. The next commit rewrites those import paths.
Adapting them here would have meant rewriting the content of all 197 commits,
which is exactly what not doing this as a copy was meant to avoid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrites the client's own import path in the 38 files that reference it, from
github.com/meshcloud/terraform-provider-meshstack/client to
github.com/meshcloud/meshstack-cli/client. Nothing else changes: the package
keeps the 'client' prefix it had in the provider, so this is the only edit the
move needs and future 'git subtree pull' carries changes across with a conflict
only where a file genuinely diverged.

This is the commit that makes the subtree compile in this module. It is separate
from the subtree merge on purpose — folding it in would have meant rewriting the
content of every imported commit.

go.sum picks up testify, which the client's tests require.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pkg/login is the entry point both the CLI and the Terraform provider use to turn
credentials into a client.Authorization.

It resolves credentials and nothing more. The login exchange stays in
client/internal/auth.go, where it already caches the access token and refreshes
it before expiry; Go's internal rule keeps that code inside client/, and it is
reached through client.NewApiKeyAuthorization. Writing a second exchange here
would have produced a static token that starts returning 401 once it expires.

The four MESHSTACK_ environment variable names are exported consts rather than
private ones, because the provider's diagnostics quote the variable names in
their message text; keeping them private would mean the same strings living in
both repositories.

Credentials.Merge exists for the provider: it merges provider block attributes
over FromEnv so an explicitly configured attribute outranks the environment.

One gap worth recording. Authorization.Header takes a client/internal.HttpClient,
so no package outside client/ can call it, and nothing can read the cached token
back out either. That does not matter yet, but caching a token in
~/.config/meshstack-cli will need a new exported seam. pkg/login is the only
place that constructs an Authorization, which is where that seam belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the release path now, while the repository is still small, rather than
discovering its rough edges at the first tag.

Pushing a v* tag runs goreleaser, which publishes archives and checksums for
linux, darwin and windows on amd64 and arm64, and then builds the container image
for the same tag. The image build is a separate job so a failing image does not
take the archives down with it, and it is a reusable workflow so a push to main
can refresh :main through the same code path. Without that, no usable image would
exist until the first release.

Naming follows what the repository publishes rather than what it builds:
the archives, the checksum file and the image are all meshstack-cli, while the
binary inside them is meshstack. goreleaser's project_name carries the former and
builds[].binary the latter, and the image entrypoint is the meshstack binary, so
'docker run ghcr.io/meshcloud/meshstack-cli buildingblock list' reads like the
local invocation.

The version reaches the binary as an ldflag on main.Version in cmd/meshstack,
which the goreleaser config and the Dockerfile have to keep in agreement. A build
without it reports 'dev'.

Images go to GHCR only. Pull requests build the image without pushing it, so a
broken Dockerfile fails review instead of main. The Dockerfile cross-compiles from
the build platform using buildx's TARGETOS/TARGETARCH rather than emulating the
target, and ships the binary on distroless static, which comes to 4.6 MB.

Tags are computed in a shell step instead of with docker/metadata-action, to keep
the set of SHA-pinned actions small. The action SHAs come from
meshcloud/building-block-runner, whose image workflow this follows.

Verified locally: 'goreleaser check' passes, 'goreleaser release --snapshot'
produces meshstack-cli_*.tar.gz archives containing a meshstack binary that
reports the injected version, and the image builds and runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client came in with 'git subtree add' rather than as a copy, which changes two
things a contributor needs to know and cannot guess from the tree.

Changes travel with 'git subtree pull' and 'git subtree push' against the
Terraform provider. A pull conflicts only where a file genuinely diverged, since
the one local edit the move needed was rewriting the client's own import path.

Reading the pre-import history takes both paths. The split history carries the
files at the repository root and the import merge re-roots them under client/, so
'git log -- client/client.go' stops at the merge while
'git log -- client/client.go client.go' shows all of it. 'git blame' traverses the
merge on its own, and 'git log --follow' does not help, because it resolves
renames within a commit's parents rather than across a subtree re-rooting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feat/scaffold-cli-and-move-client branch from f329c91 to 163b976 Compare August 20, 2026 03:45
@grubmeshi

Copy link
Copy Markdown
Collaborator Author

Redone with git subtree instead of a copy, and force pushed.

git subtree split --prefix=client on the provider's current main, then git subtree add here. The client arrives with 197 commits of its own history, back to refactor: move client to separate package, so authorship and per-change granularity survive the move, and future changes travel with git subtree pull / git subtree push.

Commits

feat: bootstrap the meshStack CLI repository unchanged
Add client/ from terraform-provider-meshstack via git subtree the import merge, 197 commits
refactor: point the moved client at the meshstack-cli module path the one local edit: 38 files
feat: add pkg/login, the shared credential resolution split out of the old commit 2
feat: release with goreleaser and publish a container image unchanged
docs: record that client/ is a git subtree and how to sync it new

Still a clean fast-forward from main (8f6cc2f), so your direct-push plan is unaffected.

Validation

You asked me to prove that before and after differ only by client changes made in the provider meanwhile. Rather than eyeball a diff, I checked both ends by extracting all four trees and normalising the module path:

  • My earlier copy == provider client/ at 8dbef43, the commit I had snapshotted. Identical, so the old copy was faithful.
  • The subtree result == provider client/ at current main. Identical, so the import is faithful.

The observed before/after change set then matches the provider's own 8dbef43..main change set exactly:

Files client/client.go and client/client.go differ
Files client/landingzone.go and client/landingzone.go differ
Only in client: client_test.go
Only in client: mesh_info.go

Whole-tree diff confirms nothing else moved:

 client/client.go      | 34 +++++++++++++-------------
 client/client_test.go | 44 ++++++++++++++++++++++++++++++
 client/landingzone.go | 12 ++++++----
 client/mesh_info.go   | 51 +++++++++++++++++++++++++++++++++++++
 go.mod                |  4 ++--
 go.sum                |  9 ++++-----

So my copy was stale by seven provider commits — the MESHSTACK_SKIP_VERSION_CHECK fix, the landing zone spec.restricted work, and the new meshStack instance data source with mesh_info.go. Those are now in. The go.mod delta is only testify 1.12.0 → 1.12.1 and testify's own transitive yaml module rename; the new client files import stdlib, client/internal and testify in tests, so the stdlib-only depguard rule still holds.

Two things worth knowing

The import merge does not compile on its own. git subtree add brings files verbatim, so at that commit they still import terraform-provider-meshstack/client; the next commit fixes it. Adapting inside the merge would have meant rewriting the content of all 197 commits, which is what avoiding a copy was meant to prevent. CI runs on the branch head, so this is not visible there.

History browsing needs both paths. The split history carries files at the repository root and the merge re-roots them under client/, so git log -- client/client.go stops at the merge while git log -- client/client.go client.go shows all 58. git blame client/client.go traverses it correctly on its own. git log --follow does not help — it resolves renames within a commit's parents, not across a subtree re-rooting. I had claimed --follow worked in the first draft of the merge message; it does not, and the message now documents the forms that do. All of this is recorded in AGENTS.md.

Re-verified after the rebuild: lint clean, all tests pass, goreleaser check passes, and a snapshot cross-compiles all five targets with the version ldflag intact.

@grubmeshi
grubmeshi force-pushed the feat/scaffold-cli-and-move-client branch from 2fe8df9 to 9c96db4 Compare August 20, 2026 03:56
…hanged code

The lint job failed on this branch with a gofmt-class finding on
client/internal/retry_test.go:46 that no local run reproduces.

The cause is which Go built the linter, not which Go is on PATH. golangci-lint's
formatters use the go/format compiled into the binary, so 'version: latest' with
the default binary install downloaded a 2.13.0 built with Go 1.27, while go.mod
pins 1.26. Go 1.27 widens end-of-line comment alignment groups, so it wants that
table's comments aligned to a far wider column — and Go 1.26 then rejects the
result. The two are mutually exclusive: no formatting of that file satisfies both,
which rules out simply reformatting it.

So the linter is now built here from source with go.mod's Go
('install-mode: goinstall') and pinned to v2.13.0. Formatting is then decided by
the Go the code is written against, and a linter upgrade becomes a deliberate edit.
setup-go takes its version from go.mod for the same reason: it is what builds the
linter.

Verified by controlled comparison, because two earlier explanations were wrong.
Same config, same file, pristine checkout, empty cache: golangci-lint 2.13.0 built
with Go 1.26.5 reports a clean tree, the official 2.13.0 binary built with Go
1.27.0 reports the finding, and each rejects the other's preferred formatting.
Neither the patch-filtering mode nor the toolchain on PATH changes that.

Dropping only-new-issues is the second half. It exists so a pull request against a
large legacy codebase is not buried in pre-existing findings; this repository
starts clean and CI keeps it clean from the first commit, so filtering to changed
code cannot help and can only hide a real finding. It hid this one, and the
provider's identical job still hides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feat/scaffold-cli-and-move-client branch from 9c96db4 to 0b77443 Compare August 20, 2026 04:02
@grubmeshi

Copy link
Copy Markdown
Collaborator Author

CI is green. The lint failure after the subtree import took three attempts to diagnose, and the first two explanations were wrong — worth writing down because the cause is not obvious and the same trap sits in the provider's workflow.

Symptom. client/internal/retry_test.go:46: File is not properly formatted, on a file no local run objects to.

What it was not. Not only-new-issues mapping a whole-directory patch (fails identically without it). Not the Go toolchain on PATH (fails identically with CI on go1.26.5, same as local).

What it is. golangci-lint's formatters use the go/format compiled into the linter binary, so the gofmt they enforce comes from the Go release that built the linter, not from the toolchain on PATH. version: latest with the default binary install fetched a 2.13.0 built with Go 1.27, while go.mod pins 1.26. Go 1.27 widens end-of-line comment alignment groups, so it wants that test table's comments at a much wider column.

The two are mutually exclusive — Go 1.26 rejects Go 1.27's formatting and vice versa — so reformatting the file was not an option in either direction.

Controlled comparison, same config and file, pristine checkout, empty cache:

linter built with result
2.13.0 from source Go 1.26.5 clean
2.13.0 official binary Go 1.27.0 reports the finding
2.12.2 system Go 1.26.3 clean

Fix. CI builds the linter from source with go.mod's Go (install-mode: goinstall) and pins v2.13.0. Formatting is then decided by the Go the code is written against, and a linter bump becomes a deliberate edit. setup-go reads go.mod for the same reason — it is what builds the linter. Recorded in AGENTS.md so nobody switches back to the faster binary install without knowing what it costs.

only-new-issues is also gone, and stays gone: this repository starts clean, so filtering to changed code cannot help and can only hide a finding. It hid this one.

Worth a separate look: the provider's lint job has the same version: latest + go-version: stable + only-new-issues: true combination, so the same drift is latent there — currently invisible because the flag filters against pushes that do not touch the file. It will surface the first time someone edits a file with wide aligned comments. Happy to open a small PR for it if you want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.