Skip to content

feat(toolchain): add update command, fix version-pinning bugs - #2894

Open
Erik Osterman (Cloud Posse) (osterman) wants to merge 13 commits into
mainfrom
osterman/toolchain-update-pinning-field-test
Open

feat(toolchain): add update command, fix version-pinning bugs#2894
Erik Osterman (Cloud Posse) (osterman) wants to merge 13 commits into
mainfrom
osterman/toolchain-update-pinning-field-test

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Aug 6, 2026

Copy link
Copy Markdown
Member

what

  • Adds atmos toolchain update [tool...] to move a pinned tool to its newest available version and reinstall it, with --dry-run and bounded --max-concurrency. Tools pinned to pr:/sha:/ref: are skipped with an explanation instead of silently left alone.
  • Fixes which/exec resolving the wrong version (last token instead of the default first token) on a multi-version .tool-versions line, which caused false "not installed" errors.
  • Fixes set appending instead of replacing the default version, contradicting its documented behavior.
  • Fixes add/install silently accepting SemVer range syntax (^1.2.0, ~>1.0.0) and only failing later with a raw HTTP 404; now rejected immediately with a hint toward dependencies.tools/atmos version track.
  • Fixes atmos version track add/set corrupting any value containing <, >, or & (a json.Marshal HTML-escaping bug), which broke the exact ~>/>= constraint syntax the toolchain docs recommend.
  • Fixes atmos toolchain versions --help silently rendering the wrong command's help and exiting 0 instead of erroring; fixed globally in root help routing (atmos <cmd> <bogus-subcommand> --help now errors for every command tree). Removes the stale toolchain-versions and toolchain-aliases docs/casts for commands that were never implemented.
  • Implements six previously documented-but-missing flags: list --format/--installed-only/--pending-only, clean --dry-run/--cache-only/--force, exec --dry-run.
  • Fixes updateToolVersionsFile writing to the hardcoded default .tool-versions path instead of the configured one.
  • Adds a changelog post and roadmap entry for the new update command.

why

  • A field test of atmos toolchain surfaced that there was no way to update a pinned tool to a newer version, and no clear signal for why range/constraint syntax (^1.2.0, ~>1.0.0) didn't work when the docs implied it should.
  • Live-testing the closest existing workaround (add <tool>@latest + install --reinstall) reproduced a real crash in which/exec, which led to finding the rest of the bugs above along the way — a documented set behavior that didn't match reality, a JSON-escaping bug corrupting exactly the constraint syntax the toolchain skill doc recommends, and a help-routing bug that let atmos toolchain versions --help silently succeed for a command that doesn't exist (which is also why its docs page and cast looked legitimate despite documenting nothing real).
  • Together these close the gap between what atmos toolchain's docs promised and what the CLI actually did, and give users a real, safe way to move a pinned tool forward.

references

  • Field test and fix session: this branch (osterman/toolchain-update-pinning-field-test)

Summary by CodeRabbit

  • New Features

    • Added toolchain update for selected or all tools, with dry-run and concurrency controls.
    • Added toolchain lock to verify artifacts and create lock entries without installation.
    • Added dry-run, filtering, format, cache-only, force, and default-version options across toolchain commands.
    • Added JSON, plain-text, and table output for tool listings.
    • Added toolchain uninstall --all to remove all installed tools.
  • Bug Fixes

    • Improved help validation for unknown subcommands.
    • Prevented tool references from rendering as email links.
    • Rejects unsupported version ranges before modifying configuration.
  • Documentation

    • Updated toolchain command guidance and added update and lock documentation.

Adds `atmos toolchain update` to move a pinned tool to its newest
available version safely, and fixes several bugs found while field
testing the update/pinning workflow:

- which/exec resolved the wrong version on a multi-version
  .tool-versions line (last token instead of the default first token),
  causing false "not installed" errors.
- set appended instead of replacing the default version, contradicting
  its own documented behavior.
- add/install silently accepted SemVer range syntax (^1.2.0, ~>1.0.0)
  and only failed later with a raw HTTP 404; now rejected immediately
  with a hint toward dependencies.tools/atmos version track.
- atmos version track add/set corrupted any value containing <, >, or
  & (a json.Marshal HTML-escaping bug), breaking exactly the ~>/>=
  constraint syntax the toolchain docs recommend.
- atmos toolchain versions --help silently rendered the wrong command's
  help and exited 0 instead of erroring; fixed globally in root help
  routing. Removed the stale toolchain-versions and toolchain-aliases
  docs for commands that were never implemented.
- Implemented six previously documented-but-missing flags: list
  --format/--installed-only/--pending-only, clean
  --dry-run/--cache-only/--force, exec --dry-run.
- updateToolVersionsFile wrote to the hardcoded default .tool-versions
  path instead of the configured one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dmap

Adds the changelog post for the new atmos toolchain update command
and links it from the toolchain milestone list in the roadmap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@atmos-pro

atmos-pro Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@osterman Erik Osterman (Cloud Posse) (osterman) added the minor New features that do not break anything label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds toolchain update and toolchain lock, expands toolchain flags and output modes, validates version specifications, fixes help routing and Markdown linkification, and updates tests, documentation, snapshots, and terminal captures.

Changes

Toolchain modernization

Layer / File(s) Summary
Command workflows and orchestration
cmd/toolchain/*, pkg/toolchain/{update,lock,clean,exec,list}.go, pkg/toolchain/installer/*
Adds update and lock workflows, parser-backed command flags, dry-run and cleanup controls, structured list output, artifact verification, and ordered concurrent reporting.
Version validation and resolution
pkg/toolchain/{add,set,version_spec,which}.go, pkg/toolchain/install*.go
Rejects unsupported version ranges, supports default replacement, uses configured tool-version paths, and selects the configured default version.
CLI, rendering, and serialization fixes
cmd/root.go, pkg/ui/**, pkg/version/manager/crud.go
Rejects unknown help subcommands, preserves literal JSON operators, and prevents tool references from becoming mailto: links.
Documentation and generated references
website/docs/**, website/blog/**, agent-skills/**, website/static/casts/**, tests/snapshots/**
Documents new commands and options, removes obsolete command references, and refreshes help recordings and snapshots.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: aknysh

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: adding the toolchain update command and fixing version-pinning behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch osterman/toolchain-update-pinning-field-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/toolchain/install_validation_helpers.go (1)

49-61: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the validation comment.

ValidateVersionSpec accepts literal vendor tags such as jq-1.7.1 that ParseVersionSpec does not recognize. Describe ValidateVersionSpec in this comment.

Proposed fix
-// version is a format ParseVersionSpec recognizes, before any network call is made.
+// version is accepted by ValidateVersionSpec before any network call is made.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/install_validation_helpers.go` around lines 49 - 61, Update the
comment above validateToolAndVersion to state that the version format is
validated by ValidateVersionSpec, replacing the inaccurate reference to
ParseVersionSpec while preserving the existing description of non-empty tool and
version validation before network calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/toolchain/add.go`:
- Around line 46-48: Wrap the errors returned by addParser.BindFlagsToViper in
cmd/toolchain/add.go:46-48 and cleanParser.BindFlagsToViper in
cmd/toolchain/clean.go:29-33 with their applicable static errors from
errors/errors.go, preserving each original error using %w before returning it.

In `@cmd/toolchain/update.go`:
- Around line 35-40: Before reading v.GetBool("dry-run") in the update and exec
command flows, use StandardParser.IsBoolFlagExplicitlySet to determine whether
the command’s own CLI flag or registered environment variable was explicitly
provided. Apply this change in cmd/toolchain/update.go lines 35-40 and
cmd/toolchain/exec.go lines 35-40, ensuring each command does not consume the
other command’s dry-run environment variable while preserving the existing
dry-run behavior.

In `@errors/errors.go`:
- Around line 353-376: Add the static ErrToolchainCleanConfirmation sentinel in
errors/errors.go, wrap the confirmation prompt failure in pkg/toolchain/clean.go
using that sentinel and %w, and wrap the tool-versions load failure in
pkg/toolchain/update.go with ErrToolVersionsFileOperation and %w. Apply these
changes at errors/errors.go lines 353-376, pkg/toolchain/clean.go lines 138-143,
and pkg/toolchain/update.go lines 55-59 respectively, preserving the underlying
errors for errors.Is checks.

In `@pkg/toolchain/exec_test.go`:
- Around line 205-210: Update the test around RunExecCommandWithOptions to cover
a missing-binary dry-run case: do not pre-install terraform, invoke the command
with dry-run enabled, and assert the tool installation occurs while the injected
execFunc is never called. Prefer adding this as a behavior-focused table-driven
case using the existing fake tool setup.

In `@pkg/toolchain/list.go`:
- Around line 231-242: The empty-configuration path in RunListWithOptions must
emit valid JSON when format is "json". Before returning after the existing UI
message, write ListToolsOutput with an empty Tools slice via data.WriteJSON for
JSON output, while preserving the current table-output message; add a test
covering empty .tool-versions JSON output.

In `@pkg/toolchain/set.go`:
- Around line 343-347: In SetToolVersion, validate the selected version with
ValidateVersionSpec after interactive selection and before calling
AddToolToVersionsAsDefault. Prevent persistence when validation fails,
preserving the existing default-version replacement behavior for valid versions.

In `@pkg/toolchain/update_test.go`:
- Around line 160-171: The TestRunUpdate_ConcurrencyPreservesOrder test
currently verifies only the error result; capture RunUpdate’s reported output
and assert owner/a appears before owner/b and owner/c, with owner/b before
owner/c. Use the existing test I/O setup and keep the test focused on output
order under MaxConcurrency: 4.

In `@pkg/toolchain/update.go`:
- Around line 237-249: Reorder the update flow around
installSingleToolWithInstaller and AddToolToVersionsAsDefault so newest is
installed successfully before .tool-versions is changed. Preserve the existing
failure result for installation errors, and add a failure-path test verifying
the previous default remains configured when installation fails.

In `@pkg/version/manager/crud.go`:
- Around line 182-183: Update marshalJSONNoEscape to wrap enc.Encode failures
with the existing ErrEncode sentinel before returning them, so SetEntryFields
and AddEntry expose errors.Is-compatible encoding errors. Add a negative test
using an unsupported field value and assert that the returned error matches
ErrEncode via errors.Is.

In `@website/docs/cli/commands/toolchain/toolchain-info.mdx`:
- Around line 212-215: Update the Tool Aliases link in the toolchain-info
documentation to target the new toolchain aliases page, matching the route
defined by that page rather than the legacy aliases route. Remove the duplicate
legacy aliases page if it is no longer referenced or needed.

In `@website/docs/cli/commands/toolchain/toolchain-update.mdx`:
- Around line 36-38: Update the documentation bullet describing pr:, sha:, and
ref: pins to remove the claim that they are immutable and clarify that updates
skip all three to preserve the explicitly selected source; retain the existing
guidance to use atmos toolchain add to change them.
- Around line 44-47: Update the dependencies.tools link in the toolchain update
documentation to point to /stacks/dependencies, matching the configuration
reference used by toolchain usage documentation; leave the surrounding
version-tracking guidance unchanged.

In `@website/static/casts/screengrabs/atmos-toolchain-exec--help.cast`:
- Line 8: Fix the toolchain exec help rendering so examples contain only the
executable version reference, not an injected mailto link, after terraform@1.5.0
and kubectl@1.28.0. Update the source/renderer responsible for the exec
examples, then regenerate atmos-toolchain-exec--help.cast and verify copied
commands contain no mailto arguments.

In `@website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast`:
- Line 8: Update the uninstall command help text to mention tools installed via
component, workflow, and command dependencies, matching the existing dependency
wording. Then regenerate the atmos-toolchain-uninstall help cast so it reflects
the updated output.

---

Outside diff comments:
In `@pkg/toolchain/install_validation_helpers.go`:
- Around line 49-61: Update the comment above validateToolAndVersion to state
that the version format is validated by ValidateVersionSpec, replacing the
inaccurate reference to ParseVersionSpec while preserving the existing
description of non-empty tool and version validation before network calls.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 243991f8-cabb-465b-97ec-cf333d4b27b1

📥 Commits

Reviewing files that changed from the base of the PR and between 3ce4349 and b3f52aa.

📒 Files selected for processing (74)
  • agent-skills/skills/atmos-toolchain/SKILL.md
  • cmd/root.go
  • cmd/root_help_routing_test.go
  • cmd/toolchain/add.go
  • cmd/toolchain/add_test.go
  • cmd/toolchain/clean.go
  • cmd/toolchain/clean_test.go
  • cmd/toolchain/command_provider_test.go
  • cmd/toolchain/exec.go
  • cmd/toolchain/exec_test.go
  • cmd/toolchain/list.go
  • cmd/toolchain/list_test.go
  • cmd/toolchain/provider_test.go
  • cmd/toolchain/toolchain.go
  • cmd/toolchain/update.go
  • cmd/toolchain/update_test.go
  • demo/casts/atmos.d/screengrabs/cli.yaml
  • errors/errors.go
  • pkg/ai/tools/atmos/toolchain_add.go
  • pkg/toolchain/add.go
  • pkg/toolchain/add_test.go
  • pkg/toolchain/clean.go
  • pkg/toolchain/clean_test.go
  • pkg/toolchain/exec.go
  • pkg/toolchain/exec_test.go
  • pkg/toolchain/install.go
  • pkg/toolchain/install_validation_helpers.go
  • pkg/toolchain/list.go
  • pkg/toolchain/list_test.go
  • pkg/toolchain/lockfile/lockfile.go
  • pkg/toolchain/set.go
  • pkg/toolchain/set_test.go
  • pkg/toolchain/update.go
  • pkg/toolchain/update_test.go
  • pkg/toolchain/version_spec.go
  • pkg/toolchain/version_spec_test.go
  • pkg/toolchain/which.go
  • pkg/toolchain/which_test.go
  • pkg/version/manager/crud.go
  • pkg/version/manager/crud_test.go
  • tests/snapshots/TestCLICommands_atmos_toolchain_--help.stdout.golden
  • website/blog/2026-08-06-toolchain-update-command.mdx
  • website/docs/cli/commands/toolchain/toolchain-aliases.mdx
  • website/docs/cli/commands/toolchain/toolchain-get.mdx
  • website/docs/cli/commands/toolchain/toolchain-info.mdx
  • website/docs/cli/commands/toolchain/toolchain-remove.mdx
  • website/docs/cli/commands/toolchain/toolchain-set.mdx
  • website/docs/cli/commands/toolchain/toolchain-uninstall.mdx
  • website/docs/cli/commands/toolchain/toolchain-update.mdx
  • website/docs/cli/commands/toolchain/toolchain-versions.mdx
  • website/docs/cli/commands/toolchain/usage.mdx
  • website/src/data/roadmap.js
  • website/static/casts/screengrabs/atmos-toolchain--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-add--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-aliases--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-clean--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-du--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-env--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-exec--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-get--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-info--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-install--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-path--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-search--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-remove--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-search--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-set--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-update--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-versions--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-which--help.cast
💤 Files with no reviewable changes (5)
  • website/static/casts/screengrabs/atmos-toolchain-versions--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-aliases--help.cast
  • website/docs/cli/commands/toolchain/toolchain-get.mdx
  • website/docs/cli/commands/toolchain/toolchain-aliases.mdx
  • website/docs/cli/commands/toolchain/toolchain-versions.mdx

Comment thread cmd/toolchain/add.go
Comment thread cmd/toolchain/update.go
Comment thread errors/errors.go
Comment thread pkg/toolchain/exec_test.go
Comment thread pkg/toolchain/list.go
Comment on lines +231 to +242
// RunListWithOptions prints tools from .tool-versions in the requested format, optionally
// filtered to only installed or only pending (not-yet-installed) tools.
func RunListWithOptions(format string, installedOnly, pendingOnly bool) error {
defer perf.Track(nil, "toolchain.RunListWithOptions")()

if !slices.Contains(supportedListFormats, format) {
return fmt.Errorf("%w: %q (supported: %v)", errUtils.ErrInvalidFlagValue, format, supportedListFormats)
}
if installedOnly && pendingOnly {
return errUtils.ErrMutuallyExclusiveFlags
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Return an empty JSON result when no tools are configured.

When format == "json" and .tool-versions is empty, this function returns after writing a UI message. JSON consumers receive no JSON document and cannot deserialize the result.

Write ListToolsOutput{Tools: []ListToolEntry{}} through data.WriteJSON for this case. Keep the current message for table output. Add an empty-config JSON test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/list.go` around lines 231 - 242, The empty-configuration path
in RunListWithOptions must emit valid JSON when format is "json". Before
returning after the existing UI message, write ListToolsOutput with an empty
Tools slice via data.WriteJSON for JSON output, while preserving the current
table-output message; add a test covering empty .tool-versions JSON output.

Comment on lines +212 to +215
- [`atmos toolchain get`](/cli/commands/toolchain/get) - Browse available versions with `--all --limit`
- [`atmos toolchain list`](/cli/commands/toolchain/list) - View installed tools
- [`atmos toolchain registry search`](/cli/commands/toolchain/registry/registry-search) - Search for tools
- [`atmos toolchain aliases`](/cli/commands/toolchain/aliases) - View configured aliases
- [Tool Aliases](/cli/configuration/toolchain/aliases) - Configure convenient tool name aliases

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

mapfile -t files < <(fd -i -t f 'toolchain-(get|registry-search)|aliases' website/docs)

if ((${`#files`[@]} == 0)); then
  echo "No target documentation files found." >&2
  exit 1
fi

for file in "${files[@]}"; do
  printf '== %s ==\n' "$file"
  sed -n '1,35p' "$file" | rg -n '^(id|slug|title):' || true
done

Repository: cloudposse/atmos

Length of output: 206


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== tracked file candidates =="
git ls-files website/docs | rg -i 'toolchain-(get|registry-search)|aliases|registry-search|toolchain.*get|aliases|aliases\.mdx' || true

echo "== locate reference locations in info file =="
file="website/docs/cli/commands/toolchain/toolchain-info.mdx"
if [ -f "$file" ]; then
  wc -l "$file"
  sed -n '200,220p' "$file" | cat -n -v
else
  echo "missing $file"
fi

echo "== frontmatter id/slug/title for candidate files =="
mapfile -t files < <(git ls-files website/docs | rg -i 'toolchain-(get|registry-search)|aliases|registry-search|toolchain.*get')
for f in "${files[@]}"; do
  printf '\n== %s ==\n' "$f"
  sed -n '1,40p' "$f" | rg -n '^(id|slug|title):' || rg -n '^(id|slug|title):' "$f" || true
done

echo "== site route config (Docusaurus) =="
find website -maxdepth 3 -type f \( -name "docusaurus.config.*" -o -name "sidebar.*" -o -name "sidebars.*" \) -print | sort
for f in $(find website -maxdepth 3 -type f \( -name "docusaurus.config.*" -o -name "sidebar.*" -o -name "sidebars.*" \)); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: cloudposse/atmos

Length of output: 1654


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== frontmatter id/slug/title for candidate files =="
for f in \
  website/docs/cli/commands/toolchain/toolchain-get.mdx \
  website/docs/cli/configuration/aliases.mdx \
  website/docs/cli/configuration/toolchain/aliases.mdx
do
  echo "== $f =="
  sed -n '1,45p' "$f" | rg -n '^(id|slug|title):' | sed -n '1,20p' || true
done

echo "== tracked docs with alias references =="
git ls-files | rg 'alias|Tool Aliases|registry-search|toolchain.*registry' | sed -n '1,120p'

echo "== site config and route docs =="
for f in $(find website -maxdepth 4 -type f \( -name "docusaurus.config.*" -o -name "sidebars.*" -o -name "sidebar.*" \) 2>/dev/null | sort); do
  echo "--- $f ---"
  sed -n '1,260p' "$f" | sed -n '1,260p' | rg -n 'cli|toolchain|id:|routeBasePath|docs|sidebars|registry|alias|slug|toolchain-get|aliases' || true
done

Repository: cloudposse/atmos

Length of output: 8611


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== frontend docusaurus/plugin/redirects =="
find website -maxdepth 4 -type f \( -name "docusaurus.config.*" -o -name "plugin*.js" -o -name "*redirect*" \) -print 2>/dev/null | sort
for f in $(find website -maxdepth 4 -type f \( -name "docusaurus.config.*" -o -name "plugin*.js" -o -name "*redirect*" \) 2>/dev/null | sort); do
  echo "--- $f ---"
  sed -n '1,320p' "$f" | rg -n 'plugin-client-redirects|redirects|cli/commands/toolchain|toolchain-get|toolchain registry|registry/search|aliases|toolchain/aliases' || true
done

echo "== cli sidebar exact section =="
node - <<'JS'
const sidebar = require('./website/sidebars.js')
for (let depth=0; depth<3; depth++) { if (sidebar.length<depth+1) continue; const group=sidebar[depth]; console.log(JSON.stringify(group, null, 2)) }
JS

echo "== markdown headings for aliases at target file =="
for f in \
  website/docs/cli/configuration/aliases.mdx \
  website/docs/cli/configuration/toolchain/aliases.mdx
do
  echo "== $f =="
  rg -n '^#+ .*((Toolchain|Toolchain Aliases|Aliases)|id: ' "$f"
done

Repository: cloudposse/atmos

Length of output: 696


🌐 Web query:

Docusaurus plugin-client-redirects from option slug path without leading slash route

💡 Result:

In Docusaurus, the @docusaurus/plugin-client-redirects plugin expects path values (both from and to) to be absolute-like pathnames starting with a leading slash (e.g., /my-page) [1][2][3]. Unlike document front matter slug settings, which can interpret paths without a leading slash as folder-relative [4][5], the plugin-client-redirects configuration validates paths to ensure they are standard, site-root-relative pathnames [2][3]. Providing a path without a leading slash in the plugin configuration is generally considered invalid, as the plugin relies on these paths to correctly identify and match existing routes within the site's generated structure [1][6]. If you are experiencing issues with routes containing or missing slashes, note the following: 1. Validation: The plugin configuration schema requires paths to start with a slash [2][3]. Providing a path without one may result in configuration errors [2]. 2. Trailing Slashes: The plugin's behavior regarding trailing slashes is influenced by your site's overall trailingSlash configuration in docusaurus.config.js [6][7]. The plugin automatically normalizes to paths to match your site's trailing slash preference (e.g., ensuring a redirect target correctly points to the trailing-slashed or non-slashed version of a page) [6]. 3. Relative vs. Absolute: While document slug front matter supports relative paths (e.g., slug: my-page becomes /docs/my-page) [4], the plugin-client-redirects expects the to field to be the full, absolute pathname of the target route as it exists in the final built site [1][3]. To resolve issues with redirects, ensure all from and to paths in your plugin configuration begin with a leading slash [1][3]. If a redirect is failing to match, verify the exact route path generated by your build (e.g., check the output files in your build directory) and use that exact string, including the leading slash [6][5].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== aliases file content =="
for f in \
  website/docs/cli/configuration/aliases.mdx \
  website/docs/cli/configuration/toolchain/aliases.mdx
do
  echo "== $f =="
  sed -n '1,180p' "$f"
done

echo "== redirects block =="
sed -n '54,125p' website/docusaurus.config.js | cat -n

echo "== toolchain registry search frontmatter =="
for f in \
  website/docs/cli/commands/toolchain/registry/search.mdx \
  website/docs/cli/commands/toolchain/registry/list.mdx \
  website/docs/cli/commands/toolchain/registry/usage.mdx
do
  echo "== $f =="
  sed -n '1,50p' "$f" | rg -n '^(---|id:|slug:|title:|---)' || true
done

echo "== git tracked toolchain registry search file exists =="
git ls-files website/docs/cli/commands/toolchain/registry/search.mdx || true

Repository: cloudposse/atmos

Length of output: 12294


🌐 Web query:

Docusaurus docs id file nested folder route example _category_ id slug file

💡 Result:

In Docusaurus, URL paths are primarily determined by the document's file path and can be customized using the slug front matter [1][2]. Document ID and URL Every document has a unique id, which defaults to the file path relative to the root docs directory (excluding the file extension) [1][3]. By default, the URL is derived from this ID, appended to the plugin's routeBasePath (defaulting to /docs) [1][3]. Customizing URLs To override the default URL, use the slug front matter in your markdown file [1][3]: --- id: custom-id slug: /my-custom-url --- This allows you to change the URL without changing the file location [1][3]. You can use relative slugs (e.g., slug:./another-path) or absolute slugs (e.g., slug: /root-level-url) [1][3]. Nested Folders and category.json When using autogenerated sidebars, the folder structure determines the sidebar structure [1][3]. If you have a nested folder, you can control the index page of that category using a category.json or category.yml file [4][5]. To set a custom slug for a category index page (which defaults to /category/[categoryName]), you can specify a link in the category.json file [5][6]: { "label": "My Category", "link": { "type": "generated-index", "slug": "/my-custom-category-slug" } } Summary of Best Practices 1. File Organization: Keep your file system structure mirroring your desired sidebar structure to simplify autogeneration [1][3]. 2. URL Stability: To prevent broken links when renaming files, use explicit slug front matter [1][3]. 3. Category Customization: Use category.json for category metadata, position, and to define a custom slug for the category index page if needed [4][5].

Citations:


🌐 Web query:

Docusaurus autogenerated sidebars docs folder file id route documentation

💡 Result:

In Docusaurus, autogenerated sidebars automatically map your filesystem structure within the docs folder to your site's navigation sidebar [1][2]. When you use the autogenerated type, Docusaurus creates a sidebar slice where each folder becomes a category and each file becomes a document link [1][3]. Key Concepts and Mappings: 1. Document ID: Every document has a unique ID, which by default is the document's path relative to the root docs directory (excluding the file extension) [4][5]. For example, a file at docs/api/setup.md has the ID api/setup [4]. You can override this ID by setting it manually in the file's front matter [4]. 2. Routes: By default, a document's URL (route) is derived from its ID [4]. For example, the ID api/setup corresponds to the URL /docs/api/setup [6]. If you need to decouple the URL from the filesystem structure, you can use the slug front matter to customize the document's route [4][6]. 3. Sidebar Generation: When defining an autogenerated sidebar in sidebars.js, you specify a dirName, which is the source folder relative to the docs directory [1][2]. - Example configuration: js export default { mySidebar: [ { type: 'autogenerated', dirName: 'api', // Generates sidebar from docs/api }, ], }; - Only the items inside the specified directory become part of the sidebar slice [1][7]. - You can control category behavior (like ordering or labeling) by adding a category.json or category.yml file within the corresponding folder [7][8]. 4. Advanced Customization: If the default generation logic does not meet your needs, you can provide a custom sidebarItemsGenerator function in your docs plugin configuration to programmatically control how the sidebar is built [5][7].

Citations:


Fix the Tool Aliases documentation target.

/cli/configuration/toolchain/aliases is the wrong page: website/docs/cli/configuration/aliases.mdx uses that exact route, while cli/commands/toolchain/toolchain-info.mdx also already links to a toolchain alias reference. Update this to the new toolchain aliases page and remove the duplicate legacy page if it is no longer needed.

🧰 Tools
🪛 LanguageTool

[typographical] ~212-~212: To join two clauses or introduce examples, consider using an em dash.
Context: ...chain get](/cli/commands/toolchain/get) - Browse available versions with --all --...

(DASH_RULE)


[typographical] ~213-~213: To join two clauses or introduce examples, consider using an em dash.
Context: ...ain list](/cli/commands/toolchain/list) - View installed tools - [atmos toolchain...

(DASH_RULE)


[typographical] ~215-~215: To join two clauses or introduce examples, consider using an em dash.
Context: ...s](/cli/configuration/toolchain/aliases) - Configure convenient tool name aliases ...

(DASH_RULE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/docs/cli/commands/toolchain/toolchain-info.mdx` around lines 212 -
215, Update the Tool Aliases link in the toolchain-info documentation to target
the new toolchain aliases page, matching the route defined by that page rather
than the legacy aliases route. Remove the duplicate legacy aliases page if it is
no longer referenced or needed.

Source: Learnings

Comment thread website/docs/cli/commands/toolchain/toolchain-update.mdx Outdated
Comment thread website/docs/cli/commands/toolchain/toolchain-update.mdx
Comment thread website/static/casts/screengrabs/atmos-toolchain-exec--help.cast Outdated
Comment thread website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast Outdated
Addresses 13 of 14 findings from CodeRabbit's review (the 14th, a doc
link, was already correct):

- Wrap addParser/cleanParser BindFlagsToViper errors with a new
  ErrFlagBinding sentinel instead of returning them raw.
- Fix update/exec --dry-run flags reading the wrong command's env var
  (both register a Viper key literally named "dry-run", and BindEnv on
  a shared key lets the second parser's registration silently override
  the first's env var binding). Use IsBoolFlagExplicitlySet instead of
  v.GetBool so each command only reads its own flag/env var.
- Add ErrToolchainCleanConfirmation sentinel for the clean confirmation
  prompt failure path; wrap update's .tool-versions load failure with
  ErrToolVersionsFileOperation.
- Add a dry-run test that starts with no binary installed, confirming
  exec auto-installs before reporting instead of always assuming the
  tool is already present.
- Fix `list --format=json` on an empty .tool-versions to emit a valid
  empty JSON document instead of only a human-readable message.
- Fix `set` never validating the version before persisting it -- it
  could write invalid range syntax into .tool-versions the same way
  `add`/`install` could before this PR's earlier fix.
- Fix update's exact-version path writing the new version into
  .tool-versions as the default *before* installing it; if install
  then failed, the configured default pointed at an uninstalled
  version. Install first, then persist. Add a failure-path test.
- Rewrite the concurrency-order test to actually assert output order,
  and fix a real bug it caught: `atmos toolchain update` with no
  arguments iterated a Go map (randomized order) instead of a sorted
  one, so tools were reported in a different order on every run.
- Wrap marshalJSONNoEscape's encode failures with the existing
  ErrEncode sentinel.
- Fix an unrelated pre-existing bug the exec cast surfaced: command
  help/usage text rendered through pkg/ui/formatter.go's bare glamour
  renderer had no strict-linkify protection, so package/tool
  references like foo/bar@1.0.0 rendered with a stray mailto: link
  auto-attached. Export ApplyStrictLinkify for that renderer to use,
  and fix a latent ast.String Pos() issue in the linkify extension
  that could reorder or drop the replacement text once wired in.
- Correct the toolchain-update.mdx doc's claim that ref: pins are
  immutable (a named ref can move; it's skipped to preserve the
  user's explicit source selection, not because it can't change), fix
  its dependencies.tools link to point at /stacks/dependencies, and
  widen uninstall's --all help text to match its actual dependency
  scope. Regenerate the affected casts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/toolchain/update_test.go`:
- Around line 114-149: Make
TestUpdateOneTool_ExactPin_InstallFailureLeavesToolVersionsUnchanged
deterministic by mocking or injecting the installation step to fail locally
instead of relying on the real installer and unavailable upstream release.
Retain the mocked GitHub release list, exercise updateOneTool with the injected
failure, and keep asserting that the original tool version remains unchanged.

In `@pkg/ui/markdown/extensions/linkify.go`:
- Around line 86-125: Replace the global searchFrom-based lookup in
replacementTextNode and its caller with the current ast.AutoLink’s source
position, deriving the ast.Text segment directly from that node’s label/source
span so repeated labels bind to the correct occurrence. Update the affected
linkify logic in pkg/ui/markdown/extensions/linkify.go (lines 86-125) and adjust
the corresponding coverage in pkg/ui/markdown/extensions/extensions_test.go
(lines 627-650) to validate repeated identical links preserve their source
order.
- Around line 144-153: Update stringNodeRenderer.renderString to capture and
return the error from w.Write(str.Value) instead of discarding it, while
preserving the existing walk status and early-return behavior for non-entering
nodes and non-string AST nodes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03f95944-a417-42a3-81ad-09e8b4fc7740

📥 Commits

Reviewing files that changed from the base of the PR and between b3f52aa and 82e3136.

📒 Files selected for processing (25)
  • cmd/toolchain/add.go
  • cmd/toolchain/clean.go
  • cmd/toolchain/exec.go
  • cmd/toolchain/uninstall.go
  • cmd/toolchain/update.go
  • errors/errors.go
  • pkg/toolchain/clean.go
  • pkg/toolchain/exec_test.go
  • pkg/toolchain/list.go
  • pkg/toolchain/list_test.go
  • pkg/toolchain/set.go
  • pkg/toolchain/set_test.go
  • pkg/toolchain/update.go
  • pkg/toolchain/update_test.go
  • pkg/ui/formatter.go
  • pkg/ui/formatter_test.go
  • pkg/ui/markdown/custom_renderer.go
  • pkg/ui/markdown/extensions/extensions_test.go
  • pkg/ui/markdown/extensions/linkify.go
  • pkg/version/manager/crud.go
  • pkg/version/manager/crud_test.go
  • website/docs/cli/commands/toolchain/toolchain-uninstall.mdx
  • website/docs/cli/commands/toolchain/toolchain-update.mdx
  • website/static/casts/screengrabs/atmos-toolchain-exec--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast
🚧 Files skipped from review as they are similar to previous changes (15)
  • website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast
  • errors/errors.go
  • website/static/casts/screengrabs/atmos-toolchain-exec--help.cast
  • cmd/toolchain/clean.go
  • cmd/toolchain/update.go
  • pkg/toolchain/set.go
  • website/docs/cli/commands/toolchain/toolchain-update.mdx
  • website/docs/cli/commands/toolchain/toolchain-uninstall.mdx
  • cmd/toolchain/add.go
  • pkg/toolchain/update.go
  • pkg/version/manager/crud.go
  • cmd/toolchain/exec.go
  • pkg/toolchain/clean.go
  • pkg/toolchain/list_test.go
  • pkg/toolchain/list.go

Comment on lines +114 to +149
// TestUpdateOneTool_ExactPin_InstallFailureLeavesToolVersionsUnchanged reproduces a bug where
// the exact-pin update path wrote the new "newest" version into .tool-versions as the default
// BEFORE installing it. If install then failed, .tool-versions pointed at a version that was
// never actually installed. This asserts install is attempted first, and a failed install
// leaves the previously-configured (and actually-installed) default version untouched.
func TestUpdateOneTool_ExactPin_InstallFailureLeavesToolVersionsUnchanged(t *testing.T) {
setupTestIO(t)

filePath := createTempToolVersionsFile(t, "hashicorp/terraform 1.9.8\n")

// InstallPath MUST be isolated to a per-test temp dir: a failed install still touches
// the real, shared, XDG toolchain cache directory otherwise (see install_test.go for the
// same isolation requirement and rationale).
prevConfig := atmosConfig
t.Cleanup(func() { SetAtmosConfig(prevConfig) })
SetAtmosConfig(&schema.AtmosConfiguration{Toolchain: schema.Toolchain{
VersionsFile: filePath,
InstallPath: filepath.Join(t.TempDir(), ".tools"),
}})

// "99.99.99" is not a real hashicorp/terraform release, so the real (unmocked) install
// step must fail -- this is the "newest" version fetchAllGitHubVersions reports via the
// mocked GitHub API, but the mock only fakes the release listing, not the download.
mock := NewMockGitHubAPI()
mock.SetReleases("hashicorp", "terraform", []string{"1.9.8", "99.99.99"})
SetGitHubAPI(mock)
t.Cleanup(ResetGitHubAPI)

outcome := updateOneTool("hashicorp/terraform", UpdateOptions{MaxConcurrency: 1})
assert.Equal(t, updateResultFailed, outcome.result)

toolVersions, err := LoadToolVersions(filePath)
require.NoError(t, err)
assert.Equal(t, []string{"1.9.8"}, toolVersions.Tools["hashicorp/terraform"],
"a failed install must not overwrite .tool-versions with the un-installed version")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make the installation failure deterministic.

Lines 134-142 deliberately call the real install path against an unavailable upstream release. This unit test depends on outbound network access and remote failure behavior. It can fail from rate limits, network errors, or installer changes unrelated to the persistence contract.

Inject a local installation failure and retain the mocked release list.

As per coding guidelines, “Prefer behavior-focused, table-driven unit tests with mocks; avoid tautological, stub, always-skipped, or coverage-only tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/update_test.go` around lines 114 - 149, Make
TestUpdateOneTool_ExactPin_InstallFailureLeavesToolVersionsUnchanged
deterministic by mocking or injecting the installation step to fail locally
instead of relying on the real installer and unavailable upstream release.
Retain the mocked GitHub release list, exercise updateOneTool with the injected
failure, and keep asserting that the original tool version remains unchanged.

Source: Coding guidelines

Comment on lines +86 to +125
// Replace each auto-link with plain text, walked (and thus replaced) in document
// order, so searchFrom always advances forward through source and lands on the
// correct occurrence even when the same label repeats later in the document.
searchFrom := 0
for _, autoLink := range nodesToReplace {
parent := autoLink.Parent()
if parent == nil {
continue
}

// Create a text node with the original content.
// AutoLink stores its text in a single child segment.
textNode := ast.NewString(autoLink.Label(source))
parent.ReplaceChild(parent, autoLink, textNode)
label := autoLink.Label(source)
parent.ReplaceChild(parent, autoLink, replacementTextNode(source, label, &searchFrom))
}
}

// replacementTextNode builds the plain-text replacement for an unlinked auto-link.
//
// It prefers an ast.Text node anchored to the label's real position in source
// (found via a forward-only search from *searchFrom, advanced past each match so
// repeated labels resolve to their next occurrence in document order), because
// ast.Text's Pos() reflects that real source offset, which glamour's ANSI
// renderer relies on to place inline content in the correct order. In contrast,
// ast.String nodes always report Pos() -1 ("not associated with a source text"),
// which previously caused glamour to render the replacement text out of order
// relative to its surrounding words.
//
// If the label can't be located (should not normally happen -- AutoLink labels are
// verbatim source bytes), this falls back to an ast.String node so the content is
// still rendered, just without an order guarantee; NewStrictLinkifyExtension also
// registers a renderer for ast.KindString to keep that fallback from silently
// disappearing under glamour's ANSI renderer, which has no built-in handling for it.
func replacementTextNode(source, label []byte, searchFrom *int) ast.Node {
if idx := bytes.Index(source[*searchFrom:], label); idx >= 0 {
start := *searchFrom + idx
stop := start + len(label)
*searchFrom = stop
return ast.NewTextSegment(text.NewSegment(start, stop))
}
return ast.NewString(label)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)pkg/ui/markdown/extensions/(linkify|extensions)_test\.go$|go\.mod$' || true

echo "== linkify outline =="
ast-grep outline pkg/ui/markdown/extensions/linkify.go --view expanded || true

echo "== relevant linkify source =="
cat -n pkg/ui/markdown/extensions/linkify.go | sed -n '1,180p'

echo "== relevant test source =="
cat -n pkg/ui/markdown/extensions/extensions_test.go | sed -n '600,670p'

echo "== goldmark / glamour deps =="
rg 'github.com/yuin/goldmark|github.com/charmbracelet/glamour' go.mod go.sum || true

Repository: cloudposse/atmos

Length of output: 13509


🌐 Web query:

goldmark ast Text Segment text reader source bytes Index

💡 Result:

In the goldmark Go library, AST nodes do not store text content directly to maintain memory efficiency [1][2]. Instead, nodes store references to the original source document via the text.Segment structure, which acts as a pointer into the source byte slice [2][3]. The text.Segment structure consists of three primary attributes: 1. Start: The starting byte offset in the original source [2]. 2. End (often referred to as Stop): The ending byte offset in the original source (exclusive) [2][4]. 3. Padding: An integer representing the number of virtual spaces to prepend to the segment, commonly used for handling indentation without modifying the source data [2]. To access the actual text associated with a node, you must use the original document source (the []byte slice) in conjunction with these segments [2]. The text.Reader interface manages the source bytes during parsing, and methods such as reader.Value(segment) are used to retrieve the underlying []byte content for a given segment [5][6]. When working with collections of text, the text.Segments type provides a container (a slice of Segment structs) to manage multiple segments [4]. Nodes that hold block-level text (such as paragraphs) typically use this to track their lines [7][2]. Access to individual segments within such a collection is provided by index via methods like At(index int) [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod module cache availability =="
go env GOMODCACHE || true

echo "== goldmark ast.AutoLink API if module cache present =="
if [ "${GOMODCACHE:-}" != "" ]; then
  grep -R "type AutoLink struct" "${GOMODCACHE}/github.com/yuin/goldmark@" 2>/dev/null | head -5 || true
  grep -R "func .*AutoLink.*Label|func .*AutoLink.*URL|func .*AutoLink.*Segment" "${GOMODCACHE}/github.com/yuin/goldmark@" 2>/dev/null | head -20 || true
fi

echo "== goldmark source files in module cache =="
find "${GOMODCACHE}/github.com/yuin/goldmark@v1.8.2" -type f -name '*.go' 2>/dev/null | head -50 || true

echo "== package reference transformer priority vs auto-link priority candidates =="
rg -n "AutoLink|Linkify|PackageRef|TransformerPriority|AutoLinkEmail|AutoLinkURL" "${GOMODCACHE}/github.com/yuin/goldmark@v1.8.2" 2>/dev/null | head -80 || true

Repository: cloudposse/atmos

Length of output: 397


🌐 Web query:

github.com/yuin/goldmark v1.8.2 source ast.Autolink Label method

💡 Result:

In the yuin/goldmark library (including version v1.8.2), the ast.AutoLink struct provides a Label method [1][2]. The method is defined as: func (n *AutoLink) Label(source []byte) []byte It returns the label (the text content) of the AutoLink node [1][2]. When called, it accepts the source document bytes as an argument to retrieve the corresponding text segment for that node [1][2]. This method is part of the Node interface implementation for the AutoLink AST node type [2].

Citations:


🌐 Web query:

github.com/yuin/goldmark github.com/yuin/goldmark/text Reader Segment source Label AutoLink

💡 Result:

In the goldmark library, the components you referenced relate to how the parser manages document source content and represents it within the Abstract Syntax Tree (AST). text.Reader The text.Reader is the interface used by goldmark to read and traverse the Markdown source document [1][2]. It provides methods such as PeekLine and Advance to navigate the source text during the parsing process [3][4]. text.Segment Because goldmark is designed to be memory-efficient, AST nodes do not store the actual text content of the Markdown document [1][2]. Instead, they use the text.Segment structure to store references to specific parts of the source [2]. A text.Segment contains three attributes [1][2]: - Start: The starting byte offset in the source document [2]. - End: The ending byte offset in the source document (exclusive) [2]. - Padding: The number of spaces to virtually prepend to the segment [2]. AutoLink and Source Mapping The AutoLink node is an AST element representing an autolink [4][5]. A technical challenge arises because some AutoLink implementations internally store their text content in a way that may not be directly accessible as a standard child node, which can complicate retrieving the exact source position (the text.Segment) of the link [6]. When an AutoLink is created by a parser (such as the default AutoLink parser or the Linkify extension), it is typically initialized with a text.Segment that maps to the original source [4][5]. For example, the parser identifies the boundaries of a URL or email in the source using the text.Reader, creates a new text.Segment based on those offsets, and then associates that segment with the resulting ast.AutoLink node [4][5]. If you are working with these nodes and need their source position, you are essentially retrieving the text.Segment that was assigned during the node's creation [4][5].

Citations:


Bind replacement segments to the current auto-link source location.

The global searchFrom scan can match an earlier identical byte span before the current ast.AutoLink. If that happens, the replacement ast.Text segment points at the wrong source offset and renders in the wrong inline order. Derive the replacement segment from the current auto-link’s label/source span instead of searching from the document start.

📍 Affects 2 files
  • pkg/ui/markdown/extensions/linkify.go#L86-L125 (this comment)
  • pkg/ui/markdown/extensions/extensions_test.go#L627-L650
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ui/markdown/extensions/linkify.go` around lines 86 - 125, Replace the
global searchFrom-based lookup in replacementTextNode and its caller with the
current ast.AutoLink’s source position, deriving the ast.Text segment directly
from that node’s label/source span so repeated labels bind to the correct
occurrence. Update the affected linkify logic in
pkg/ui/markdown/extensions/linkify.go (lines 86-125) and adjust the
corresponding coverage in pkg/ui/markdown/extensions/extensions_test.go (lines
627-650) to validate repeated identical links preserve their source order.

Comment on lines +144 to +153
func (r *stringNodeRenderer) renderString(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
str, ok := n.(*ast.String)
if !ok {
return ast.WalkContinue, nil
}
_, _ = w.Write(str.Value)
return ast.WalkContinue, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant renderer and the fallback/write handling path.
printf '--- files ---\n'
git ls-files | grep -F 'pkg/ui/markdown/extensions/linkify.go' || true

printf '\n--- linkify.go outline ---\n'
ast-grep outline pkg/ui/markdown/extensions/linkify.go || true

printf '\n--- relevant linkify.go sections ---\n'
sed -n '1,240p' pkg/ui/markdown/extensions/linkify.go

printf '\n--- usages of renderMarkdown/renderString/linkify ---\n'
rg -n "renderMarkdown|renderString|linkify|StringNodeRenderer|stringNodeRenderer" pkg/ui/markdown/extensions pkg/ui/markdown -S || true

Repository: cloudposse/atmos

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files ---'
git ls-files | grep -F 'pkg/ui/markdown/extensions/linkify.go' || true

printf '%b\n' '\n--- linkify.go outline ---'
ast-grep outline pkg/ui/markdown/extensions/linkify.go || true

printf '%s\n' '--- relevant linkify.go sections ---'
sed -n '1,240p' pkg/ui/markdown/extensions/linkify.go

printf '%b\n' '\n--- usages of renderMarkdown/renderString/linkify ---'
rg -n "renderMarkdown|renderString|linkify|StringNodeRenderer|stringNodeRenderer" pkg/ui/markdown/extensions pkg/ui/markdown -S || true

Repository: cloudposse/atmos

Length of output: 11343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- markdown custom renderer sections ---'
sed -n '1,260p' pkg/ui/markdown/custom_renderer.go

printf '%s\n' '--- markdown formatter sections ---'
sed -n '1,260p' pkg/ui/formatter.go

printf '%s\n' '--- renderer fallback/write handling search ---'
rg -n "renderMarkdown|renderMarkdownCore|renderString|customRenderer|renderer|walk|Write|render\(" pkg/ui/markdown pkg/ui -S || true

Repository: cloudposse/atmos

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- formatter render markdown sections ---'
sed -n '1090,1155p' pkg/ui/formatter.go
sed -n '846,872p' pkg/ui/formatter.go
sed -n '260,290p' pkg/ui/formatter.go

printf '%s\n' '--- go.mod glamour and goldmark versions ---'
rg -n "charmbracelet/glamour|yuin/goldmark" go.mod go.sum || true

printf '%s\n' '--- goldmark/render/BufWriter source in vendored/modules cache if available ---'
cache="$(go env GOMODCACHE 2>/dev/null || true)"
if [ -n "$cache" ]; then
  find "$cache" -path '*github.com/yuin/goldmark*' -type d 2>/dev/null | head -20
  find "$cache" -path '*glamour/ansi*' -type d 2>/dev/null | head -20
  match="$cache"/github.com/yuin/goldmark*/renderer/renderer.go
  if [ -f "$match" ]; then
    sed -n '1,220p' "$match"
  fi
fi

printf '%s\n' '--- standalone goldmark renderer NodeRendererFunc source ---'
python3 - <<'PY'
import os, subprocess, tempfile, textwrap
try:
    subprocess.run(["go", "get", "-d", "github.com/yuin/goldmark@latest"], check=True, capture_output=True, text=True)
except Exception as e:
    print("go get failed:", e)
    raise SystemExit(0)
match = subprocess.check_output(["go", "list", "-f", "{{.Dir}}/renderer/renderer.go", "github.com/yuin/goldmark/renderer"], text=True).strip()
print(match)
with open(match) as fh:
    for i,line in enumerate(fh,1):
        print(f"{i}: {line}", end="")
PY

printf '%s\n' '--- deterministic Go error propagation probe ---'
python3 - <<'PY'
import subprocess, tempfile, os
src = textwrap.dedent('''
package main
import (
	"fmt"
)
type BufWriter interface{ Write([]byte) (int, error) }
type bad int
func (bad) Write(p []byte) (int, error) { return len(p)/2, fmt.Errorf("write failed") }
type Node interface{}
type stringNodeRenderer struct{}
func (r stringNodeRenderer) renderString(w BufWriter, node Node) error {
	_, _ = w.Write([]byte("x"))
	return nil
}
type renderer func(BufWriter, Node) error
func render(w BufWriter, node Node, fn renderer) error {
	status, err := fn(w, node)
	if err != nil {
		return fmt.Errorf("writer: %w: status=%d", err, status)
	}
	return nil
}
func main() {
	err := render(bad(0), nil, func(w BufWriter, node Node) (int, error) {
		r := stringNodeRenderer{}
		return 0, r.renderString(w, node)
	})
	fmt.Println(err)
}
''')
with tempfile.NamedTemporaryFile(suffix=".go", delete=False) as f:
    f.write(src.encode())
    name=f.name
try:
    subprocess.run(["go","build","-o","/tmp/probe_bin",name], check=True)
    out=subprocess.check_output(["/tmp/probe_bin"], text=True)
    print(out)
finally:
    os.unlink(name)
PY

Repository: cloudposse/atmos

Length of output: 5153


🌐 Web query:

github.com yuin goldmark 1.8.2 renderer renderer.go WalkFunc WalkStatus error Write

💡 Result:

In the goldmark library, specifically within the renderer package (renderer/renderer.go), the rendering process uses a tree traversal mechanism to generate output [1][2]. The core type involved in this traversal is the NodeRendererFunc, which is defined as follows: type NodeRendererFunc func(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) [2] This function signature controls the rendering of an AST node [3]. It returns an ast.WalkStatus and an error to guide the traversal: 1. ast.WalkStatus: This return value allows the renderer to control the flow of the walk, such as continuing to the next node (ast.WalkContinue) or skipping the children of the current node (ast.WalkSkipChildren) [3]. 2. error: If an error is returned during the rendering process, the traversal stops, and the error is propagated up to the caller [1][2]. During the Render execution, the renderer performs an ast.Walk on the AST node. For each node encountered, it retrieves the corresponding NodeRendererFunc from its registry and executes it [1][2]. The writer used is an abstraction (util.BufWriter) that allows for efficient buffered output [1][2]. It is important to distinguish this from the standard library's filepath.Walk, which serves an entirely different purpose (file system traversal) and uses a different WalkFunc signature [4]. Similarly, other markdown libraries for Go, such as gomarkdown, have their own distinct implementations of AST walking [5][6][7].

Citations:


Propagate the renderer write error.

Return the w.Write(str.Value) error from renderString; goldmark treats renderer errors as writer failures, and this path is used by renderMarkdown, which falls back to plain content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ui/markdown/extensions/linkify.go` around lines 144 - 153, Update
stringNodeRenderer.renderString to capture and return the error from
w.Write(str.Value) instead of discarding it, while preserving the existing walk
status and early-return behavior for non-entering nodes and non-string AST
nodes.

Source: Coding guidelines

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/xl Extra large size PR label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

  • website/pnpm-lock.yaml

Removed content duplicated in references/commands-reference.md and the
Custom Registries/Configuration sections; the CI "Validate agent skills
structure and size" job flagged the file at 502 lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Resource Changes Found for bucket in test

Atmos CI

create

Plan: 4 to add, 0 to change, 0 to destroy.
To reproduce this locally, run:

atmos terraform plan bucket -s test

Create

+ aws_s3_bucket.checkov_target
+ aws_s3_bucket.this
+ aws_s3_bucket.trivy_target
+ aws_s3_bucket_public_access_block.trivy_target
Terraform Plan Summary
  # aws_s3_bucket.checkov_target will be created
  + resource "aws_s3_bucket" "checkov_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-checkov-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.this will be created
  + resource "aws_s3_bucket" "this" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + tags_all                    = {
          + "AtmosFixture" = "native-ci-e2e"
          + "Stage"        = "test"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket.trivy_target will be created
  + resource "aws_s3_bucket" "trivy_target" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "atmos-native-ci-e2e-trivy-test"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket_public_access_block.trivy_target will be created
  + resource "aws_s3_bucket_public_access_block" "trivy_target" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + restrict_public_buckets = true
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name = "atmos-native-ci-e2e-test"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent-skills/skills/atmos-toolchain/SKILL.md`:
- Around line 206-216: Update the common command list in the toolchain skill
documentation to include atmos toolchain update [tool...], noting its --dry-run
and --max-concurrency options or linking to the full update reference. Keep the
existing command entries unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a9979ff-fe9b-4adf-8d73-fda963a84b40

📥 Commits

Reviewing files that changed from the base of the PR and between 82e3136 and 6e85498.

📒 Files selected for processing (1)
  • agent-skills/skills/atmos-toolchain/SKILL.md

Comment thread agent-skills/skills/atmos-toolchain/SKILL.md
Addresses CodeRabbit review comment: the toolchain skill's command
list and reference doc omitted the update command introduced in this
PR, including its --dry-run and --max-concurrency flags.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generates or refreshes toolchain.lock.yaml (checksum/provenance
records) without reinstalling tools, for the use_lock_file workflow
where a lockfile is wanted but a full reinstall isn't.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l tests

install_test.go read updated tool-versions state from the hardcoded
DefaultToolVersionsFilePath constant instead of the tempDir-isolated
path the test actually configured. This was masked by a matching bug
in updateToolVersionsFile (already fixed) that made writes land on
the same wrong path; once the write path was corrected, these tests
started reading a stale/shared file and picking up unrelated content
from CI's own toolchain install step, causing intermittent failures.

Also regenerate the install --help golden snapshot, stale since the
mailto-linkify fix landed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
pkg/toolchain/installer/lock_tool_test.go (2)

73-113: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Verify the full no-op contract when lock files are disabled.

Installer.LockTool still builds the asset URL, downloads the asset, and verifies it before updateLockFile in pkg/toolchain/installer/installer.go Lines 451-481. This test only checks that no lock file exists, so a regression that performs network I/O while useLockFile is false would still pass. Count requests and assert zero, or rename the test and comment to promise only that no lock file is written.

As per coding guidelines, behavior-focused tests must assert the behavior named by the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/installer/lock_tool_test.go` around lines 73 - 113, Strengthen
TestLockTool_NoOpsWhenLockFileDisabled by counting HTTP requests handled by the
test server and asserting the count remains zero after LockTool returns. Keep
the existing no-error and no-lock-file assertions, ensuring the test verifies
the complete no-op behavior when useLockFile is false.

Source: Coding guidelines


39-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the existing-binary path.

The test starts with an empty binDir, so assert.Empty only proves that LockTool did not create a new entry. It does not prove that an existing binary remains unchanged. Seed binDir with sentinel contents and assert that the contents remain unchanged after locking.

As per coding guidelines, new features need comprehensive behavior-focused tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/installer/lock_tool_test.go` around lines 39 - 70, Update the
LockTool test around the existing binDir setup to create a sentinel binary file
with known contents before calling installer.LockTool. After locking, read that
same file and assert its contents are unchanged, replacing the
empty-directory-only assertion while preserving the lock-file verification.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/toolchain/lock.go`:
- Around line 63-84: Add GoDoc comments to the exported methods on
LockCommandProvider: GetCommand, GetName, GetGroup, GetFlagsBuilder,
GetPositionalArgsBuilder, and GetCompatibilityFlags. Start each comment with the
corresponding method name and briefly describe its returned command, name,
group, flags builder, positional-argument builder, or compatibility flags.
- Around line 55-57: Update cmd/toolchain/lock.go lines 55-57 by introducing an
injectable lock-runner interface or dependency and have runLock invoke it while
preserving argument and MaxConcurrency forwarding. In cmd/toolchain/lock_test.go
lines 19-48, add table-driven mock-runner tests covering default and explicit
concurrency, invalid concurrency values, and multiple tool arguments; both sites
require changes.
- Around line 25-27: Replace the inline Example text in the toolchain lock
command with a dedicated cmd/markdown/*_usage.md usage file, embed that file
using //go:embed, and render the embedded content through
utils.PrintfMarkdown().
- Around line 46-47: Update the error return in lockCmd after
lockParser.BindFlagsToViper fails to wrap the underlying error with fmt.Errorf
using errors.ErrWrapFormat and errors.ErrFlagBinding, preserving the original
error while adding command-context classification.

In `@pkg/toolchain/install_test.go`:
- Line 334: Update the comment near the tool registration test to end with a
period after “resolution,” preserving the existing wording and meaning.

In `@pkg/toolchain/lock_test.go`:
- Around line 73-96: Add a success-path test for RunLock using a valid tool
fixture and LockOptions with UseLockFile: false. Assert the configured lock file
is written with the expected contents and verify that no binary installation
occurs, covering the command’s force-write behavior without relying only on
helper or failure-path tests.

In `@pkg/toolchain/lock.go`:
- Around line 52-55: Update the error returned by the tool-versions loading flow
around GetToolVersionsFilePath and LoadToolVersions to report the configured
filePath value instead of hardcoding “.tool-versions”, while preserving the
existing wrapped errors and operation context.
- Around line 120-123: Update the error return after installer.ParseToolSpec in
the tool resolution flow to wrap errUtils.ErrInvalidToolSpec alongside the
underlying error, preserving the existing tool name context and enabling
errors.Is classification for malformed specifications.

In
`@tests/snapshots/TestCLICommands_atmos_toolchain_install_--help.stdout.golden`:
- Around line 10-11: Separate the two install examples in the source help text
for the toolchain install command so they render as distinct commands, then
regenerate the affected golden snapshot using -regenerate-snapshots; do not edit
the snapshot directly.

---

Nitpick comments:
In `@pkg/toolchain/installer/lock_tool_test.go`:
- Around line 73-113: Strengthen TestLockTool_NoOpsWhenLockFileDisabled by
counting HTTP requests handled by the test server and asserting the count
remains zero after LockTool returns. Keep the existing no-error and no-lock-file
assertions, ensuring the test verifies the complete no-op behavior when
useLockFile is false.
- Around line 39-70: Update the LockTool test around the existing binDir setup
to create a sentinel binary file with known contents before calling
installer.LockTool. After locking, read that same file and assert its contents
are unchanged, replacing the empty-directory-only assertion while preserving the
lock-file verification.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de714dcd-b404-42ce-aa1c-2c0338605a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 6e85498 and 0ada0e3.

📒 Files selected for processing (36)
  • agent-skills/skills/atmos-toolchain/SKILL.md
  • agent-skills/skills/atmos-toolchain/references/commands-reference.md
  • cmd/toolchain/lock.go
  • cmd/toolchain/lock_test.go
  • cmd/toolchain/provider_test.go
  • cmd/toolchain/toolchain.go
  • demo/casts/atmos.d/screengrabs/cli.yaml
  • pkg/toolchain/install_test.go
  • pkg/toolchain/installer/installer.go
  • pkg/toolchain/installer/lock_tool_test.go
  • pkg/toolchain/lock.go
  • pkg/toolchain/lock_test.go
  • pkg/toolchain/types.go
  • tests/snapshots/TestCLICommands_atmos_toolchain_--help.stdout.golden
  • tests/snapshots/TestCLICommands_atmos_toolchain_install_--help.stdout.golden
  • website/docs/cli/commands/toolchain/toolchain-lock.mdx
  • website/static/casts/screengrabs/atmos-toolchain--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-add--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-clean--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-du--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-env--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-exec--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-get--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-install--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-lock--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-path--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-search--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-remove--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-search--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-set--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-update--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-which--help.cast
🚧 Files skipped from review as they are similar to previous changes (23)
  • website/static/casts/screengrabs/atmos-toolchain-which--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-env--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-search--help.cast
  • cmd/toolchain/provider_test.go
  • tests/snapshots/TestCLICommands_atmos_toolchain_--help.stdout.golden
  • website/static/casts/screengrabs/atmos-toolchain-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-set--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry-list--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-add--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-du--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-get--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-update--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-uninstall--help.cast
  • demo/casts/atmos.d/screengrabs/cli.yaml
  • website/static/casts/screengrabs/atmos-toolchain--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-registry--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-install--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-clean--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-path--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-remove--help.cast
  • agent-skills/skills/atmos-toolchain/SKILL.md
  • website/static/casts/screengrabs/atmos-toolchain-exec--help.cast
  • website/static/casts/screengrabs/atmos-toolchain-search--help.cast

Comment thread cmd/toolchain/lock.go
Comment on lines +25 to +27
Example: ` atmos toolchain lock
atmos toolchain lock terraform
atmos toolchain lock terraform kubectl --max-concurrency 2`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the required embedded Markdown help.

Replace the inline Example value with a cmd/markdown/*_usage.md file. Embed it with //go:embed and render it with utils.PrintfMarkdown().

As per coding guidelines, “Embed command usage examples from cmd/markdown/*_usage.md with //go:embed and render them with utils.PrintfMarkdown().”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/toolchain/lock.go` around lines 25 - 27, Replace the inline Example text
in the toolchain lock command with a dedicated cmd/markdown/*_usage.md usage
file, embed that file using //go:embed, and render the embedded content through
utils.PrintfMarkdown().

Source: Coding guidelines

Comment thread cmd/toolchain/lock.go Outdated
Comment thread cmd/toolchain/lock.go
Comment on lines +55 to +57
return toolchain.RunLock(args, toolchain.LockOptions{
MaxConcurrency: maxConcurrency,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Make the lock runner testable and test command behavior.

runLock directly calls toolchain.RunLock. The tests only inspect metadata. This leaves argument forwarding and --max-concurrency behavior untested.

  • cmd/toolchain/lock.go#L55-L57: inject a lock-runner dependency that the command can call.
  • cmd/toolchain/lock_test.go#L19-L48: add table-driven tests with a mock runner for default concurrency, explicit concurrency, invalid values, and multiple tool arguments.

As per coding guidelines, “Use interfaces and dependency injection for major functionality” and “Every new feature must include comprehensive unit tests.”

📍 Affects 2 files
  • cmd/toolchain/lock.go#L55-L57 (this comment)
  • cmd/toolchain/lock_test.go#L19-L48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/toolchain/lock.go` around lines 55 - 57, Update cmd/toolchain/lock.go
lines 55-57 by introducing an injectable lock-runner interface or dependency and
have runLock invoke it while preserving argument and MaxConcurrency forwarding.
In cmd/toolchain/lock_test.go lines 19-48, add table-driven mock-runner tests
covering default and explicit concurrency, invalid concurrency values, and
multiple tool arguments; both sites require changes.

Source: Coding guidelines

Comment thread cmd/toolchain/lock.go
Comment on lines +63 to +84
func (l *LockCommandProvider) GetCommand() *cobra.Command {
return lockCmd
}

func (l *LockCommandProvider) GetName() string {
return "lock"
}

func (l *LockCommandProvider) GetGroup() string {
return "Toolchain Commands"
}

func (l *LockCommandProvider) GetFlagsBuilder() flags.Builder {
return lockParser
}

func (l *LockCommandProvider) GetPositionalArgsBuilder() *flags.PositionalArgsBuilder {
return nil
}

func (l *LockCommandProvider) GetCompatibilityFlags() map[string]compat.CompatibilityFlag {
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document the exported provider methods.

Add GoDoc comments for GetCommand, GetName, GetGroup, GetFlagsBuilder, GetPositionalArgsBuilder, and GetCompatibilityFlags.

As per coding guidelines, “Document all exported functions, types, and methods following Go's documentation conventions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/toolchain/lock.go` around lines 63 - 84, Add GoDoc comments to the
exported methods on LockCommandProvider: GetCommand, GetName, GetGroup,
GetFlagsBuilder, GetPositionalArgsBuilder, and GetCompatibilityFlags. Start each
comment with the corresponding method name and briefly describe its returned
command, name, group, flags builder, positional-argument builder, or
compatibility flags.

Source: Coding guidelines

Comment thread pkg/toolchain/install_test.go Outdated
Comment thread pkg/toolchain/lock_test.go
Comment thread pkg/toolchain/lock.go Outdated
Comment thread pkg/toolchain/lock.go
Comment on lines +10 to +11
Examples: atmos toolchain install hashicorp/terraform@1.5.0 atmos toolchain
install opentofu@1.6.0 tflint@0.50.0 kubectl@1.29.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Separate the two install examples in the source help text.

The snapshot renders atmos toolchain install hashicorp/terraform@1.5.0 atmos toolchain install ... as one command. Copying it passes atmos, toolchain, and install as extra tool arguments. Add a newline or another clear command separator in the source help text, then regenerate this snapshot. Do not edit the golden file manually.

As per coding guidelines, regenerate golden snapshots with -regenerate-snapshots instead of editing them manually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/snapshots/TestCLICommands_atmos_toolchain_install_--help.stdout.golden`
around lines 10 - 11, Separate the two install examples in the source help text
for the toolchain install command so they render as distinct commands, then
regenerate the affected golden snapshot using -regenerate-snapshots; do not edit
the snapshot directly.

Source: Coding guidelines

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.66990% with 161 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.77%. Comparing base (3ce4349) to head (e409ea8).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pkg/toolchain/update.go 75.00% 22 Missing and 10 partials ⚠️
cmd/toolchain/update.go 41.17% 19 Missing and 1 partial ⚠️
pkg/toolchain/clean.go 74.68% 18 Missing and 2 partials ⚠️
cmd/toolchain/lock.go 40.00% 17 Missing and 1 partial ⚠️
cmd/toolchain/add.go 67.85% 5 Missing and 4 partials ⚠️
pkg/toolchain/installer/installer.go 64.00% 5 Missing and 4 partials ⚠️
pkg/ui/markdown/extensions/linkify.go 64.00% 9 Missing ⚠️
cmd/toolchain/list.go 74.19% 5 Missing and 3 partials ⚠️
pkg/toolchain/lock.go 90.36% 4 Missing and 4 partials ⚠️
pkg/toolchain/list.go 90.38% 3 Missing and 2 partials ⚠️
... and 10 more

❌ Your patch check has failed because the patch coverage (77.66%) is below the target coverage (85.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main    #2894    +/-   ##
========================================
  Coverage   82.76%   82.77%            
========================================
  Files        1861     1865     +4     
  Lines      180478   181124   +646     
========================================
+ Hits       149380   149926   +546     
- Misses      23311    23376    +65     
- Partials     7787     7822    +35     
Flag Coverage Δ
unittests 82.77% <77.66%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/toolchain/install.go 55.93% <ø> (ø)
cmd/toolchain/toolchain.go 65.00% <100.00%> (+0.89%) ⬆️
cmd/toolchain/uninstall.go 62.06% <ø> (ø)
errors/errors.go 100.00% <ø> (ø)
pkg/sbom/sbom.go 93.62% <100.00%> (+0.03%) ⬆️
pkg/toolchain/exec.go 73.17% <100.00%> (+4.59%) ⬆️
pkg/toolchain/filemanager/lockfile.go 93.33% <100.00%> (+0.78%) ⬆️
pkg/toolchain/set.go 87.26% <100.00%> (+0.09%) ⬆️
pkg/toolchain/tool_versions.go 91.01% <100.00%> (+0.40%) ⬆️
pkg/toolchain/types.go 94.54% <100.00%> (+0.31%) ⬆️
... and 24 more

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Fixes from PR #2894 review: wrap lock command's flag-binding error with the
static ErrFlagBinding sentinel, interpolate the actual configured
tool-versions path (instead of a hardcoded ".tool-versions" literal) into
RunLock's load-failure message, classify ParseToolSpec failures in
resolveLockTargets under ErrInvalidToolSpec so callers can errors.Is()
them, add a period to a dangling comment, and split `toolchain install`'s
Long description away from its Example field so the two usage lines no
longer render concatenated on one line in --help output. Also adds
TestRunLock_ForceWritesLockFileWithoutInstalling, covering the force-write
contract `atmos toolchain lock` exists to provide (lock file written and no
binary installed even with toolchain.use_lock_file: false).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/toolchain/lock_test.go (1)

114-126: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Keep the default unit-test path offline.

Lines 114-118 state that this test performs a real registry lookup and download. This makes the unit-test result depend on network availability, registry behavior, and remote artifacts.

Use a deterministic package-level hook or mock for this test. Put live registry coverage in an opt-in integration test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/toolchain/lock_test.go` around lines 114 - 126, Update the test setup
around RunLock to replace the real NewInstaller registry lookup and download
with a deterministic package-level hook or mock, ensuring the default unit-test
path remains offline while preserving the existing temporary InstallPath
isolation. Move live registry/download coverage into a separately opt-in
integration test.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/toolchain/lock_test.go`:
- Around line 131-135: Strengthen the lock-file assertions in the test around
lockFilePath by decoding lockData with the existing lock-file model, then locate
the hashicorp/terraform@1.11.4 entry and assert its tool, version,
checksum_algorithm, and non-empty checksum value instead of checking only raw
text containment.

---

Nitpick comments:
In `@pkg/toolchain/lock_test.go`:
- Around line 114-126: Update the test setup around RunLock to replace the real
NewInstaller registry lookup and download with a deterministic package-level
hook or mock, ensuring the default unit-test path remains offline while
preserving the existing temporary InstallPath isolation. Move live
registry/download coverage into a separately opt-in integration test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 387ea6ab-baa8-4705-84a9-930ed0c5b092

📥 Commits

Reviewing files that changed from the base of the PR and between 0ada0e3 and b8dc1a6.

📒 Files selected for processing (7)
  • cmd/toolchain/install.go
  • cmd/toolchain/lock.go
  • pkg/toolchain/install_test.go
  • pkg/toolchain/lock.go
  • pkg/toolchain/lock_test.go
  • tests/snapshots/TestCLICommands_atmos_toolchain_install_--help.stdout.golden
  • website/static/casts/screengrabs/atmos-toolchain-info--help.cast
💤 Files with no reviewable changes (1)
  • tests/snapshots/TestCLICommands_atmos_toolchain_install_--help.stdout.golden
🚧 Files skipped from review as they are similar to previous changes (4)
  • website/static/casts/screengrabs/atmos-toolchain-info--help.cast
  • pkg/toolchain/install_test.go
  • pkg/toolchain/lock.go
  • cmd/toolchain/lock.go

Comment thread pkg/toolchain/lock_test.go Outdated
Bumps js-yaml (3.15.0->3.15.1, 4.3.0->4.3.1) and mermaid (11.16.0->11.16.1)
pnpm overrides to patched versions. All are patch-level bumps within the
allowed (non-major) range per .github/dependabot.yml's ignore policy.

Fixes GHSA-5p4m-2wfm-xmqj (js-yaml quadratic CPU in !!omap resolution,
high severity, alerts #268/#269) and GHSA-rhh3-jpg6-66xh/GHSA-c4c3-pg64-4m4v/
GHSA-6x64-9x62-f2gx/GHSA-3rrr-jr9j-h3q3/GHSA-2v8p-3f2j-5mp7 (mermaid,
medium/low severity, alerts #263-#267).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ainment

TestRunLock_ForceWritesLockFileWithoutInstalling only checked that
"checksum_algorithm" and "terraform" appeared somewhere in the raw
lock file bytes, which would still pass if the requested
hashicorp/terraform@1.11.4 entry itself had no checksum. Decode with
lockfile.Load and assert the specific entry's version, checksum
algorithm, and non-empty checksum for the current platform.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ulti-version bugs

Found via a field-test pass of `atmos toolchain update`/`atmos toolchain
lock`: AddVersionToTool(asDefault=true) only prepended a new version
instead of replacing the line, so `set`, `add --default`, and `update` all
left stale versions permanently pinned alongside the new one (existing
tests encoded this as correct behavior, checking versions[0] but never the
full slice). Fixed to fully replace the line, matching asdf's own `set`
convention (its docs describe `asdf set <tool> <version>` as equivalent to
`echo "<tool> <version>" > .tool-versions`).

toolchain.lock.yaml's schema keyed tools by owner/repo with a single flat
Version field, so locking a second version of an already-locked tool
silently discarded the first's checksum -- a real risk since
.tool-versions can legitimately pin multiple versions of one tool (e.g.
examples/toolchain's "yq 4.45.1 4.50.1"). Restructured Tool to hold a
nested Versions map so each locked version's data survives independently,
and bumped lock_file_version to 2.

`install` never verified a freshly downloaded checksum against what was
already recorded in the lock file, despite `lock`'s own warning implying
it would once toolchain.use_lock_file is enabled -- a tampered or
corrupted lock entry was silently overwritten instead of failing. Added a
verifyAgainstLock installer field (true for config-driven installs, false
for `lock`'s own force-write/refresh path) and a checksum-mismatch check
before any lock entry is overwritten.

Also: resolveLockFilePath's default fallback now matches
GetInstallPath()'s XDG-cache-first chain instead of a hardcoded relative
".tools" (they previously resolved to different directories by default);
`add`'s error wrapping used a double-%w fmt.Errorf that silently discarded
cockroachdb/errors hints attached via errUtils.Build, now uses WithCause
so the range/constraint-syntax rejection hint actually reaches users; and
`update`'s --help text no longer claims ref:-pinned tools are "immutable
by design" (a named ref can move; it's skipped by choice).

Nine regression tests were written and confirmed failing before each fix
landed. Fixing the shared replace-semantics helper and the lock schema
surfaced two more affected consumers not caught by the original field
test: pkg/toolchain/filemanager's ToolVersionsFileManager/LockFileManager
(unwired, but still compiled against and tested), whose own tests also
encoded the same bugs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor New features that do not break anything size/xl Extra large size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant