Port Atlassian shared config (cfl+jtk) to cli-common statedir resolver [MON-5370]#374
Conversation
Brings shared and tools/cfl onto the same cli-common pseudo-version (e67b2fc) that tools/jtk already uses (MON-5369), converging the workspace onto one cli-common (statedir + cache + WriteEnvelope + underscore-safe locator). Prereq for the statedir-backed shared config resolver. [MON-5370]
…tatedir Resolve the shared store path through cli-common statedir (os.UserConfigDir()/atlassian-cli) instead of the hand-rolled $XDG_CONFIG_HOME|~/.config branch. A relative/unresolvable $XDG_CONFIG_HOME now returns an error (the working-with-state.md §1.1 intentional tightening) rather than the prior silent cwd-relative ./.atlassian-cli fallback. All call sites updated to the (string,error) signature: - cfl/jtk init: resolver error is a hard error (cannot place config) - cfl/jtk runtime config + keyring clear/migrate existence checks: resolver error is treated as "no shared file" (fall back / skip) Linux layout is unchanged (statedir honors $XDG_CONFIG_HOME/~/.config); macOS moves to ~/Library/Application Support, Windows to %APPDATA%. The dependent pre-existing-test migration onto the 7-var hermetic statedirtest harness lands in the hermetic-adoption commit. [MON-5370]
…d apply The pre-statedir hand-rolled shared location ($XDG_CONFIG_HOME|~/.config /atlassian-cli/config.yml) becomes an ADDITIVE pre-token legacy source so the macOS/Windows resolver move (MON-5370 commit 2) does not strand a user's config — or a plaintext api_token — at the old path. Pure detect/enumerate (no mutation): - DetectSharedRelocation: skip on relative-$XDG / unresolvable home; path-identity short-circuit (Linux: old==new, deduped); old absent → no-op; malformed old/new → ErrCorruptStore (fail loud, new never overwritten); old-only → CopyNeeded; both present → compared on a dedicated relocation projection (canonical tool defaults + legacy per-tool conn/token, migration-skew-tolerant token compare) → identical no-op, divergent → ErrRelocationConflict naming both paths. - OldSharedConnCandidates: old-shared joins the per-tool connection-divergence candidate set so a copy is gated on that check (no copy while a divergence is pending). - OldSharedProjection: old-shared joins the keyring token-migration source set; a stale plaintext token there is consolidated/scrubbed before token resolution. clear.go (both sites) + ClearPlan list and remove the old path under `config clear --all` (path-identity deduped). Gated apply (only after relocation AND per-tool divergence pass): - ApplySharedRelocation: atomic copy-leave-old (temp+rename, 0700/0600). - cfl/jtk init reconcile run the pure gate first (before per-tool divergence, before any mutation), then copy + reload so the remainder reconciles the materialized file as a returning user. Includes focused pure-logic tests (relocate_test.go, hermetic via cli-common/statedirtest). The dependent pre-existing-test migration onto the 7-var hermetic harness + the dual cfl+jtk §3.2 matrices land in the hermetic-adoption commit. [MON-5370]
cfl's legacy ~/.config/cfl config Save() did a plain WriteFile (a crash mid-write could truncate the config) and created the dir 0750. Make the write atomic (temp+rename, stale .tmp cleaned on error) and tighten the dir to 0700 per the §3 on-disk-state standard. File mode 0600 was already correct. The legacy path itself is unchanged (it is an exempt legacy source). [MON-5370]
jtk's legacy jira-ticket-cli config Save() did a plain WriteFile, so a crash mid-write could truncate the config. Make it atomic (temp+rename, stale .tmp cleaned on error). Dir/file modes were already 0700/0600. The legacy path itself is unchanged (exempt legacy source). [MON-5370]
The DefaultPath resolver change (MON-5370 commit 2) made HOME/XDG-only test isolation a macOS/Windows real-dir leak: the prior helpers hand-built "<root>/atlassian-cli/config.yml", which only matched the resolver on Linux. - credtest.Hermetic now delegates to the canonical cli-common statedirtest 7-var harness (HOME/USERPROFILE/AppData/LocalAppData/ XDG_*) so os.UserConfigDir/os.UserCacheDir never resolve to a real directory on ANY OS, and adds SharedConfigPath(t) so tests derive the shared path from the production resolver instead of a layout. - keyring composes statedirtest directly (cannot import credtest — import cycle) via a local sharedConfigPath(t); jtk config's setupTestConfig delegates to credtest.Hermetic and returns the resolved path. Every hand-built shared path is migrated. - New pure §3.2 dedup tests assert the Linux invariant that old-shared (where oldSharedPath ≡ DefaultPath) is NOT double-enumerated as a second keyring token / clear source. This removes the real-dir-leak failures (incl. one that read the developer's real bearer config) and makes the whole suite green on macOS and Linux. Pre-existing, unrelated: 2 govet `reflect.Ptr` nits in shared/testutil/assert.go (untouched by this branch). [MON-5370]
…only Codex PR review (blocker): DetectSharedRelocation/ApplySharedRelocation ran only from init reconcile, so a normal cfl/jtk command saw an old-only shared config (prior macOS/Windows path) as absent until the user ran init, and an old↔new divergence was not surfaced on a normal load — breaking the §3.2 matrix for the runtime/token-first path the plan promised. - Add credstore.LoadSharedRuntime (mutation-free): canonical store, transparent read-fallback to the prior location when only it exists, ErrRelocationConflict (alongside the usable canonical store) on an old↔new divergence so commands keep working while the conflict is surfaced once. The copy stays init-only and gated behind the per-tool divergence check. - cfl LoadWithEnv + jtk loadShared use it instead of bare Load(DefaultPath()). - Split out loadSharedRuntime(newPath) (mirrors DetectSharedRelocation's injectable newPath) so the old≠new branches are hermetically testable on Linux (resolver else collapses old≡new). - Entry-point tests (Codex major): cfl/jtk init old-only→copied and per-tool-divergence-pending→no-copy; runtime old-only read-fallback / divergent / equal / path-identity. Relocation-equality now pins the durable tool-default divergence rows (Codex minor). [MON-5370]
|
Blocker
Major
Minor
Token-skew tolerance itself looks right: one-sided plaintext tokens are migration skew, while two distinct non-empty tokens still fail loud. |
|
TDD coverage assessment (independent) Scope: test coverage only. Ran Well covered
Gaps (specific)
Most important gap: #1 — a stale plaintext |
Addresses the TDD review gap on the MON-5370 commit-4/5 atomic-write hardening: pin cfl Save → 0600 file / 0700 dir / no leftover .tmp, and jtk Save → no leftover .tmp on success (file mode already pinned by TestConfig_FilePermissions). [MON-5370]
|
Response to TDD coverage assessment Thanks — actioned the cheap, high-value gap and justifying the rest with the architectural constraint behind them. Gap #4 (atomic Save perms/atomicity) — FIXED. Added Gaps #1–#3 (old-shared keyring scrub e2e, The old-shared behavior is therefore verified where it is hermetically constructible: the pure layer takes an injectable |
Codex re-review (minor): ErrRelocationConflict is a readable, canonical-store-in-use condition, not an unreadable/fallback one. Split warnCorruptSharedOnce so a relocation divergence says prior and current shared config diverge, the current config is in use, and init will reconcile — instead of the misleading "unreadable / falling back to per-tool config" text. [MON-5370]
|
Minor
Everything else is architecturally aligned. The runtime path is now mutation-free but old-aware, init remains the gated copy point, per-tool divergence still blocks copy, token-skew tolerance is correct, and the Linux old==new limitation is reasonably covered at the pure injectable layer without adding production-only test seams. |
CI surfaced two issues from the relocation work: - tools/cfl now imports cli-common directly (statedirtest in reconcile_test.go), so `go mod tidy` promotes it from indirect to a direct require — commit the tidied go.mod. - ineffassign: the CopyNeeded reload reassigned `proj`, but the divergence candidates were already built/checked above so `proj` is never read again. Reload only the canonical store (which the subsequent fold + preserveDefaults operate on) in both cfl and jtk. [MON-5370]
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 0d46e2a
Summary
| Reviewer | Findings |
|---|---|
| harness-engineering:harness-architecture-reviewer | 6 |
| harness-engineering:harness-enforcement-reviewer | 2 |
| harness-engineering:harness-knowledge-reviewer | 2 |
| harness-engineering:harness-self-documenting-code-reviewer | 3 |
harness-engineering:harness-architecture-reviewer (6 findings)
shared/credstore/relocate.go:193
loadSharedRuntime re-implements ~40 lines of detection logic already present in DetectSharedRelocation (old-path resolution, path-identity dedup, LoadSharedLegacyProjection probes, relocationEqual comparison, ErrRelocationConflict wrapping). Future bug fixes to DetectSharedRelocation will not propagate to loadSharedRuntime. The runtime read-only contract should be a thin wrapper around DetectSharedRelocation that skips the copy gate and returns the canonical store alongside any conflict error. Note: the open PR discussion thread from rianjs identifies this as a blocker — runtime config loading in cfl LoadWithEnv and jtk loadShared does not consult the old-shared path at all, so an old-only shared config at the prior macOS/Windows path is silently ignored until the user runs init.
💡 Suggestion - shared/credstore/relocate.go:93
Load(oldPath) is called unconditionally before the newProj == nil guard. In the common first-run case where the new path is absent (CopyNeeded=true branch), oldStore is loaded and then discarded without use — relocationEqual, the only consumer of oldStore, is never reached in that branch. Moving Load(oldPath) to after the newProj == nil check eliminates a redundant disk read in the most common relocation scenario.
💡 Suggestion - shared/credtest/credtest.go:134
credtest.Hermetic resets keyring.ResetCorruptWarnOnce and keyring.ResetMigrationNotice, but does not reset the package-level corruptSharedWarnOnce sync.Once in tools/cfl/internal/config/config.go or tools/jtk/internal/config/config.go. Tests that trigger warnCorruptSharedOnce (including the new ErrRelocationConflict branch) will leave the once latched, silently suppressing the warning and any assertions on it in subsequent tests within the same package run. Expose a reset function in each config package and call it from credtest.Hermetic.
💡 Suggestion - shared/keyring/clear.go:166
ClearFiles removes OldSharedConfigPath unconditionally (relying on os.Remove + IsNotExist suppression), while PlanClear checks fileExists before setting OldSharedConfigPath. The behavioral difference is harmless today, but the inconsistency makes the invariant 'only remove files we know exist' harder to verify and could mislead a future reader into thinking the guard was intentionally omitted.
💡 Suggestion - tools/cfl/internal/cmd/init/reconcile_test.go:24
oldSharedFixture builds the old-shared path by reading $XDG_CONFIG_HOME from the live environment (set implicitly by statedirtest.Hermetic) rather than using an explicit override to a known distinct base. If statedirtest.Hermetic ever stops setting XDG_CONFIG_HOME on a given platform, oldSharedFixture would write to whatever XDG_CONFIG_HOME happened to be, and oldSharedPath() would resolve to a different location, silently voiding the test. Use the explicit-override style from relocate_test.go's oldBase.
💡 Suggestion - tools/jtk/internal/cmd/initcmd/reconcile_test.go:16
Same oldSharedFixture fragility as tools/cfl/internal/cmd/init/reconcile_test.go:24 — old-shared path derived from a live env var rather than an explicit XDG_CONFIG_HOME override, making the test depend on statedirtest.Hermetic's platform-specific behavior. Use the explicit-override style from relocate_test.go's oldBase.
harness-engineering:harness-enforcement-reviewer (2 findings)
shared/credstore/relocate.go:88
Normalization mismatch between relocationEqual and OldSharedConnCandidates: relocationEqual normalizes URLs via NormalizeBaseURL and auth methods via canonAuthMethod before comparing, but OldSharedConnCandidates contributes raw OldProj field values as ConnCandidates. If DetectConnDivergence does not apply the same normalization, configs that relocationEqual deems equal (e.g. 'http://acme.atlassian.net' vs 'https://acme.atlassian.net') could still raise a false ConnConflict error via the candidate set, blocking init with a spurious conflict message.
💡 Suggestion - shared/keyring/clear.go:158
ClearFiles() silently discards a credstore.DefaultPath() resolution error (e.g. relative $XDG_CONFIG_HOME): neither the current nor the prior shared config is removed, and no error is returned to the caller. The --all clear succeeds superficially while leaving plaintext tokens on disk with no user-visible indication. The error should be appended to errs so callers can surface it.
harness-engineering:harness-knowledge-reviewer (2 findings)
💡 Suggestion - tools/cfl/internal/cmd/init/reconcile.go:119
After ApplySharedRelocation copies old→new, store and proj are reloaded but candidates and chosen are not recomputed. The affectsSibling guard downstream evaluates against the post-copy store rather than the pre-init empty state. Functionally correct (chosen and reloaded store are consistent since they came from the same source), but a user doing a pure relocation on macOS/Windows could see an unexpected 'this will also affect jtk' prompt even though no config values changed.
💡 Suggestion - tools/jtk/internal/cmd/initcmd/reconcile.go:109
Same issue as tools/cfl/internal/cmd/init/reconcile.go:119 — after ApplySharedRelocation reloads store and proj, chosen is not recomputed. The affectsSibling guard operates on the post-copy store rather than the pre-init empty state, which can produce a false-positive sibling-impact warning during a pure path relocation on macOS/Windows.
harness-engineering:harness-self-documenting-code-reviewer (3 findings)
💡 Suggestion - shared/credstore/relocate.go:234
OldSharedConnCandidates does not guard on r.CopyNeeded. When both old and new shared files exist and are equal (CopyNeeded=false), the function still returns candidates from the old path. These are appended to the reconcile candidate pool in cfl and jtk reconcile.go, adding redundant equal-connection entries. When a conflict with a per-tool legacy file IS detected, the error message will redundantly cite both the old-shared path and the canonical path as sources of the same connection, which is confusing. Guard should be: if r == nil || r.OldProj == nil || r.OldPath == "" || !r.CopyNeeded { return nil }.
💡 Suggestion - shared/credstore/relocate.go:24
oldSharedPath() declares a (string, error) return type but never returns a non-nil error — all error conditions return ("", nil). Every caller correctly discards the error with _, but the signature implies callers should handle an error. This misleading contract will propagate via copy-paste. Either drop the error return or document that nil error is always expected.
💡 Suggestion - shared/keyring/migrate.go:64
When sperr != nil (DefaultPath resolver failure), proj is set to an empty &SharedLegacyProjection{} with Path == "", and sharedPath is also "". The subsequent add() calls embed the empty path in source label strings (e.g. "shared config default ()"). Although all token fields are empty so add() is not invoked today, if a future path adds a non-empty field to the empty fallback proj, migration conflict messages would reference an empty untraceable path. Consider using a nil proj and skipping add() calls with an explicit nil check.
7 info-level observations excluded. Run with --verbose to include.
4 PR discussion threads considered.
Completed in 7m 03s | $1.95 | sonnet | daemon 0.2.120 | Glorfindel
| Field | Value |
|---|---|
| Model | sonnet |
| Reviewers | hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor |
| Engine | claude · sonnet |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 7m 03s wall · 17m 16s compute (Reviewers: 4m 49s · Synthesis: 2m 10s) |
| Cost | $1.95 |
| Tokens | 255.4k in / 65.6k out |
| Turns | 6 |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost |
|---|---|---|---|---|---|---|
| hybrid-synthesis | sonnet | 35.9k | 8.6k | 0 | 35.9k (1h) | $0.27 |
| harness-engineering:harness-architecture-reviewer | sonnet | 44.8k | 10.7k | 2.1k | 42.7k (1h) | $0.36 |
| harness-engineering:harness-enforcement-reviewer | sonnet | 44.8k | 15.4k | 2.1k | 42.7k (1h) | $0.43 |
| harness-engineering:harness-knowledge-reviewer | sonnet | 44.8k | 11.6k | 2.1k | 42.7k (1h) | $0.37 |
| harness-engineering:harness-self-documenting-code-reviewer | sonnet | 42.4k | 15.4k | 2.1k | 40.3k (1h) | $0.42 |
| security:security-code-auditor | haiku | 42.7k | 3.9k | 0 | 42.7k (1h) | $0.11 |
Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.
Daemon PR review follow-ups: - DRY: extract classifyRelocation — one pure detection core shared by DetectSharedRelocation (init) and loadSharedRuntime (runtime); the divergent case is a kind so the loud-vs-soft policy lives in callers. Future fixes land once. Load(oldPath) is deferred to the both-present branch (the common old-only first run no longer reads the old store). - oldSharedPath() drops its structurally-always-nil error return (skip is a single "" sentinel) — self-documenting. - OldSharedConnCandidates now contributes only when a copy is pending (old-only); both-equal is already covered by the canonical file. - clear.go: `clear --all` surfaces an unresolvable shared-path error instead of silently succeeding with possible plaintext left behind; old-path removal gains the same fileExists guard as PlanClear. - corrupt old/new via the relocation gate keeps the existing "unreadable / refusing to overwrite" UX (cfl + jtk). - oldSharedFixture sets $XDG_CONFIG_HOME explicitly (mirrors relocate_test oldBase) so the test can't silently void. [MON-5370]
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 0031d0b | Previous: 0d46e2a (incremental)
Approved with 4 non-blocking suggestions below. Address at your discretion.
Summary
| Reviewer | Findings |
|---|---|
| harness-engineering:harness-architecture-reviewer | 1 |
| harness-engineering:harness-enforcement-reviewer | 2 |
| harness-engineering:harness-knowledge-reviewer | 1 |
harness-engineering:harness-architecture-reviewer (1 findings)
💡 Suggestion - shared/credstore/relocate.go:118
classifyRelocation reads the old-shared file twice in the both-present branch: once via LoadSharedLegacyProjection(oldPath) and again via Load(oldPath). relocationEqual merges fields from both reads. Between the two reads there is a narrow TOCTOU window where a concurrent write could cause an inconsistent (proj, store) pair, making the equality decision non-atomic. For a local CLI config file this is unlikely to cause problems in practice, but the fix is straightforward: extend SharedLegacyProjection to carry the ToolSection defaults so the file is parsed only once.
harness-engineering:harness-enforcement-reviewer (2 findings)
💡 Suggestion - shared/credstore/relocate.go:148
classifyRelocation calls Load(newPath) unconditionally at the top of every invocation, including the runtime path (LoadSharedRuntime). On a fully-migrated installation where no old path exists, this is an additional file open per command compared to the old credstore.Load(DefaultPath()) call — the old-path stat that follows is a new overhead on every CLI invocation. The impact is small for a CLI tool, but the Load(newPath) call could be moved into the both-paths branch to match the old single-read contract on the happy path.
💡 Suggestion - tools/cfl/internal/cmd/init/reconcile.go:100
reconcile.go calls credstore.Load(sharedPath) immediately after DetectSharedRelocation(sharedPath), which already loaded the same file internally via classifyRelocation. In the relocation case a third load follows ApplySharedRelocation. The three reads are not incorrect, but the SharedRelocation struct already carries newStore — returning it from DetectSharedRelocation and consuming it in reconcile.go would eliminate the redundant reads and prevent the double-read pattern from reappearing. Same issue exists in tools/jtk/internal/cmd/initcmd/reconcile.go.
harness-engineering:harness-knowledge-reviewer (1 findings)
💡 Suggestion - shared/credstore/relocate.go:225
ApplySharedRelocation uses a deterministic temp path (r.NewPath + ".tmp"). If cfl init and jtk init run concurrently against the same shared path they will clobber each other's temp file, potentially leaving a truncated or zeroed destination after rename. Use os.CreateTemp in the same directory instead of a fixed ".tmp" suffix.
6 info-level observations excluded. Run with --verbose to include.
17 PR discussion threads considered.
Completed in 7m 34s | $1.93 | sonnet | daemon 0.2.120 | Glorfindel
| Field | Value |
|---|---|
| Model | sonnet |
| Mode | Re-review · Cycle 2 · Session resumed |
| Reviewers | hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor |
| Engine | claude · sonnet |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 7m 34s wall · 16m 49s compute (Reviewers: 3m 54s · Synthesis: 2m 49s) |
| Cost | $1.93 |
| Tokens | 301.7k in / 61.1k out |
| Turns | 6 |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost |
|---|---|---|---|---|---|---|
| hybrid-synthesis | sonnet | 52.3k | 9.9k | 13.6k | 38.7k (1h) | $0.30 |
| harness-engineering:harness-architecture-reviewer | sonnet | 45.9k | 12.3k | 2.1k | 43.8k (1h) | $0.38 |
| harness-engineering:harness-enforcement-reviewer | sonnet | 45.9k | 11.4k | 2.1k | 43.8k (1h) | $0.37 |
| harness-engineering:harness-knowledge-reviewer | sonnet | 45.9k | 8.0k | 2.1k | 43.8k (1h) | $0.32 |
| harness-engineering:harness-self-documenting-code-reviewer | sonnet | 43.4k | 11.9k | 2.1k | 41.3k (1h) | $0.37 |
| security:security-code-auditor | haiku | 43.7k | 6.1k | 0 | 43.7k (1h) | $0.12 |
| discussion-summarizer | — | 24.4k | 1.4k | 13.6k | 10.8k (1h) | $0.07 |
Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.
| // kind. Load(oldPath) is deferred to the both-present branch — the | ||
| // common old-only first run never reads the old store here. | ||
| func classifyRelocation(newPath string) (*relocState, error) { | ||
| newStore, err := Load(newPath) |
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-architecture-reviewer): classifyRelocation reads the old-shared file twice in the both-present branch: once via LoadSharedLegacyProjection(oldPath) and again via Load(oldPath). relocationEqual merges fields from both reads. Between the two reads there is a narrow TOCTOU window where a concurrent write could cause an inconsistent (proj, store) pair, making the equality decision non-atomic. For a local CLI config file this is unlikely to cause problems in practice, but the fix is straightforward: extend SharedLegacyProjection to carry the ToolSection defaults so the file is parsed only once.
Reply to this thread when addressed.
| _ = os.Remove(tmp) | ||
| return fmt.Errorf("relocating shared config: writing %s: %w", tmp, err) | ||
| } | ||
| if err := os.Rename(tmp, r.NewPath); err != nil { |
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-knowledge-reviewer): ApplySharedRelocation uses a deterministic temp path (r.NewPath + ".tmp"). If cfl init and jtk init run concurrently against the same shared path they will clobber each other's temp file, potentially leaving a truncated or zeroed destination after rename. Use os.CreateTemp in the same directory instead of a fixed ".tmp" suffix.
Reply to this thread when addressed.
| oldStore, err := Load(oldPath) | ||
| if err != nil { | ||
| return nil, err | ||
| } |
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-enforcement-reviewer): classifyRelocation calls Load(newPath) unconditionally at the top of every invocation, including the runtime path (LoadSharedRuntime). On a fully-migrated installation where no old path exists, this is an additional file open per command compared to the old credstore.Load(DefaultPath()) call — the old-path stat that follows is a new overhead on every CLI invocation. The impact is small for a CLI tool, but the Load(newPath) call could be moved into the both-paths branch to match the old single-read contract on the happy path.
Reply to this thread when addressed.
| @@ -81,11 +100,35 @@ func detectAndReconcile( | |||
| // Build the full named connection candidate set and detect | |||
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-enforcement-reviewer): reconcile.go calls credstore.Load(sharedPath) immediately after DetectSharedRelocation(sharedPath), which already loaded the same file internally via classifyRelocation. In the relocation case a third load follows ApplySharedRelocation. The three reads are not incorrect, but the SharedRelocation struct already carries newStore — returning it from DetectSharedRelocation and consuming it in reconcile.go would eliminate the redundant reads and prevent the double-read pattern from reappearing. Same issue exists in tools/jtk/internal/cmd/initcmd/reconcile.go.
Reply to this thread when addressed.
|
Findings None. The final refactor preserves the agreed architecture: one pure relocation classifier, init-only fail-loud policy, runtime soft-conflict policy using canonical config, old-only runtime read fallback without copy, and gated copy only after relocation and per-tool divergence checks pass. The token skew tolerance still looks right: one-sided plaintext token is migration skew, while two differing non-empty tokens remain a conflict. Clear/keyring handling is aligned with the durable-data rule, and surfacing |
Summary
working-with-state.md §6.4 unit 2 — adopt the cli-common
statedirresolver for the shared Atlassian credential-scope config
(
atlassian-cli/config.yml), the combined cfl+jtk unit. Durable config⇒ fail-loud semantics (not cache "miss").
6 decomposed commits:
shared+tools/cflcli-common →e67b2fc(jtk already there from MON-5369; workspace converges on one cli-common).
DefaultPath()→(string,error)viastatedir.Scope{Name:"atlassian-cli"}.ConfigDir(). A relative/unresolvable
$XDG_CONFIG_HOMEnow errors (§1.1 tightening) —no silent cwd-relative
./.atlassian-clifallback. All 7 call sitesupdated (init = hard error; runtime/keyring existence checks treat a
resolver error as "no shared file"). Linux layout unchanged; macOS →
~/Library/Application Support, Windows →%APPDATA%.additive pre-token legacy source — strict pure detect/enumerate
(no mutation; composes with the existing fail-loud/mutate-nothing
per-tool divergence machinery) vs gated apply/copy (only after
relocation AND per-tool conflict checks pass). Dedicated
relocation-equality projection (canonical tool defaults + legacy
per-tool conn/token, migration-skew-tolerant token compare).
copy-leave-old; path-identity short-circuit + dedup; old-shared wired
into the keyring token-migration +
clear --allsource sets so astale plaintext
api_tokenat the old path is scrubbed/listed beforetoken resolution.
statedirtest7-var adoption (the resolver changemade HOME/XDG-only isolation a macOS/Windows real-dir leak — one test
was reading the developer's real bearer config) + pure §3.2 dedup
coverage.
CI note
This unit touches
shared/**, so the path filter triggers both cfland jtk build/test/lint — expected.
Test plan
make testgreen (3237 passed, 61 packages) on macOS devmake build+ module-qualifiedgo build ./shared/... ./tools/cfl/... ./tools/jtk/...make lint— only 2 pre-existinggovetreflect.Ptrnits inshared/testutil/assert.go(untouched by this branch; unrelated)both-equal/divergent/malformed-old/malformed-new/token-skew/
not-double-enumerated — hermetic
Closes #372