Skip to content

feat: implement vendor update command with version management - #1531

Closed
Erik Osterman (Cloud Posse) (osterman) wants to merge 1 commit into
mainfrom
vendor-updates
Closed

Erik Osterman (Cloud Posse) (osterman) wants to merge 1 commit into
mainfrom
vendor-updates

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Sep 26, 2025 •

Copy link
Copy Markdown
Member

what

  • Add new atmos vendor update command to check and update component versions in vendor configurations
  • Support Git repository version checking (tags and commits)
  • Preserve YAML structure including anchors, comments, and formatting when updating files
  • Handle vendor config imports recursively with proper file tracking
  • Add --check flag for dry-run mode to preview available updates
  • Add --pull flag to update versions and pull components in one operation
  • Deprecate vendor diff command in favor of vendor update --check

why

  • Manual version management of vendored components is error-prone and time-consuming
  • Users need a way to check for and apply updates while preserving their YAML configuration structure
  • The existing vendor diff command name was confusing - it actually checks for updates, not differences
  • Automating version updates reduces maintenance burden and ensures components stay current

references

Testing

  • Created comprehensive unit tests with mock interfaces
  • Tests cover version checking, YAML preservation, and filtering logic
  • All tests can run without network access or external dependencies

Documentation

  • Added complete command documentation at website/docs/cli/commands/vendor/vendor-update.mdx
  • Created PRD at docs/prd/vendor-update.md outlining requirements and future enhancements
  • Includes migration guide from deprecated vendor diff command

Key Features

Supported Upstream Sources

  • ✅ Git repositories (GitHub, GitLab, Bitbucket) - version checking via tags and commits
  • ⏳ OCI registries - logs debug message, support coming in future
  • ⏳ S3/GCS - logs debug message, support coming in future
  • ❌ Direct HTTP/HTTPS files - not applicable for version checking
  • ❌ Local files - not applicable for version checking

YAML Structure Preservation

The command preserves:

  • YAML anchors and references
  • Comments (including inline comments)
  • Indentation and formatting
  • Quote styles (single, double, or no quotes)

Command Examples

# Check for available updates without making changes
atmos vendor update --check

# Update version references in vendor configuration files
atmos vendor update

# Update versions and pull components in one operation
atmos vendor update --pull

# Update specific component
atmos vendor update --component vpc

# Update components with specific tags
atmos vendor update --tags terraform,networking

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Introduced atmos vendor update command with --check, --pull, --component/-c, --tags, and --type flags. Checks for newer vendor versions, updates config files while preserving YAML structure (comments, anchors, formatting), and can optionally pull updates.
    • Added new config option: templates.settings.import.process_without_context.
  • Deprecations

    • vendor diff is deprecated with clear warnings.
  • Documentation

    • Added vendor update CLI docs and usage examples.
    • Published a product requirements document for vendor update.
  • Chores

    • Added .go-version (1.24.6).
    • Expanded .gitignore for test artifacts.

@mergify mergify Bot added the stacked Stacked label Sep 26, 2025
@mergify

mergify Bot commented Sep 26, 2025

Copy link
Copy Markdown
Contributor

💥 This pull request now has conflicts. Could you fix it Erik Osterman (Cloud Posse) (@osterman)? 🙏

@mergify mergify Bot added conflict This PR has conflicts triage Needs triage and removed conflict This PR has conflicts labels Sep 26, 2025
@mergify

mergify Bot commented Sep 26, 2025

Copy link
Copy Markdown
Contributor

Important

Cloud Posse Engineering Team Review Required

This pull request modifies files that require Cloud Posse's review. Please be patient, and a core maintainer will review your changes.

To expedite this process, reach out to us on Slack in the #pr-reviews channel.

@mergify mergify Bot added the needs-cloudposse Needs Cloud Posse assistance label Sep 26, 2025
@osterman
Erik Osterman (Cloud Posse) (osterman) changed the base branch from devel to main September 26, 2025 23:20
@mergify mergify Bot removed the stacked Stacked label Sep 26, 2025
@github-actions

Copy link
Copy Markdown

Warning

This PR is blocked from merging because a required semver label is missing.

major, minor, patch, no-release

You'll need to add one before this PR can be merged.

@github-actions github-actions Bot added the size/xl Extra large size PR label Sep 26, 2025
@mergify

mergify Bot commented Sep 26, 2025

Copy link
Copy Markdown
Contributor

Warning

This PR exceeds the recommended limit of 1,000 lines.

Large PRs are difficult to review and may be rejected due to their size.

Please verify that this PR does not address multiple issues.
Consider refactoring it into smaller, more focused PRs to facilitate a smoother review process.

Comment thread internal/exec/vendor.go Fixed
Comment thread internal/exec/vendor.go Fixed
Comment thread internal/exec/vendor.go Fixed
Comment thread internal/exec/vendor.go Fixed
Comment thread internal/exec/vendor_model.go Fixed
Comment thread internal/exec/vendor_update.go Fixed
Comment thread internal/exec/vendor_update.go Fixed
Comment thread internal/exec/vendor_update_test.go Fixed
Comment thread internal/exec/vendor_yaml_updater.go Fixed
Comment thread internal/exec/vendor_yaml_updater.go Fixed
@coderabbitai

coderabbitai Bot commented Sep 27, 2025 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

Erik Osterman (Cloud Posse) (@osterman) has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 91106aa and 624f2d0.

📒 Files selected for processing (21)
  • .gitignore (1 hunks)
  • .go-version (1 hunks)
  • cmd/markdown/atmos_vendor_update_usage.md (1 hunks)
  • cmd/vendor_diff.go (2 hunks)
  • cmd/vendor_update.go (1 hunks)
  • docs/prd/vendor-update.md (1 hunks)
  • errors/errors.go (3 hunks)
  • internal/exec/terraform_clean.go (3 hunks)
  • internal/exec/vendor.go (4 hunks)
  • internal/exec/vendor_constants.go (1 hunks)
  • internal/exec/vendor_diff.go (1 hunks)
  • internal/exec/vendor_filter.go (1 hunks)
  • internal/exec/vendor_interfaces.go (1 hunks)
  • internal/exec/vendor_model.go (8 hunks)
  • internal/exec/vendor_model_helpers.go (1 hunks)
  • internal/exec/vendor_update.go (1 hunks)
  • internal/exec/vendor_update_test.go (1 hunks)
  • internal/exec/vendor_utils.go (9 hunks)
  • internal/exec/vendor_utils_test.go (1 hunks)
  • internal/exec/vendor_yaml_updater.go (1 hunks)
  • internal/exec/vendor_yaml_updater_simple.go (1 hunks)
📝 Walkthrough

Walkthrough

Adds a new vendor update feature and deprecates vendor diff: new CLI command, execution pipeline, filtering, version checking, YAML version updaters, interfaces, constants, and extensive tests/docs. Also updates schema for templates import settings, adjusts dependencies, and minor housekeeping (.gitignore, .go-version, comments).

Changes

Cohort / File(s) Summary
Repo meta
./.gitignore, ./.go-version, go.mod
Ignore pattern added; Go toolchain version file added; semver dependency moved to direct.
CLI commands and docs
cmd/vendor_update.go, cmd/vendor_diff.go, cmd/markdown/atmos_vendor_update_usage.md, cmd/root.go, website/docs/cli/commands/vendor/vendor-update.mdx, docs/prd/vendor-update.md
New vendor update command with flags and embedded examples; vendor diff marked deprecated/hidden and flags expanded; minor comment tweak; new docs and PRD.
Errors
errors/errors.go
New vendor-related error variables for config, version checking, and update scenarios.
Vendor execution core
internal/exec/vendor_update.go, internal/exec/vendor.go, internal/exec/vendor_diff.go, internal/exec/vendor_filter.go, internal/exec/vendor_model.go, internal/exec/vendor_model_helpers.go, internal/exec/vendor_constants.go, internal/exec/vendor_interfaces.go, internal/exec/vendor_utils.go, internal/exec/vendor_yaml_updater.go, internal/exec/vendor_yaml_updater_simple.go
Implements vendor update/diff pipelines: config init, imports processing with file tracking, source filtering, update checks, diff display, optional pull, YAML version updaters (AST-based and simple), new interfaces, constants, helpers, and model extensions for diff flows.
Tests and snapshots
internal/exec/vendor_update_test.go, internal/exec/vendor_yaml_updater_test.go, internal/exec/vendor_utils_test.go, tests/snapshots/*, test-failures-summary.md, test-fixes-completed.md
Comprehensive tests for update logic, YAML updater behavior, filtering, flag validation, pull scenarios, and updated goldens; auxiliary markdown summaries.
Misc
internal/exec/terraform_clean.go, pkg/schema/schema.go
Import formatting tweak; schema adds TemplatesSettingsImport and TemplatesSettings.Import; snapshot updated accordingly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as atmos vendor update
  participant Cfg as initVendorUpdateConfig
  participant Src as ProcessImports(+file map)
  participant Flt as filterSources
  participant Ver as checkForVendorUpdates
  participant Yml as updateVendorConfigFile
  participant Pull as ExecuteVendorPullCmd

  User->>CLI: run with flags (--check/--pull/-c/--tags/--type)
  CLI->>Cfg: load Atmos config
  Cfg-->>CLI: AtmosConfiguration
  CLI->>Src: read vendor configs + imports
  Src-->>CLI: sources, source->file map
  CLI->>Flt: apply component/tags filters
  Flt-->>CLI: filtered sources
  loop each source
    CLI->>Ver: determine latest version (git/http/oci/local)
    Ver-->>CLI: update candidate or up-to-date
  end
  alt --check (dry-run)
    CLI-->>User: report available updates
  else updates present
    CLI->>Yml: write versions to originating files
    Yml-->>CLI: success/error
    opt --pull
      CLI->>Pull: vendor pull (preserve filters)
      Pull-->>CLI: completion
    end
    CLI-->>User: summary
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Diff as atmos vendor diff (deprecated)
  participant Exec as ExecuteVendorDiffCmd
  participant Comp as compareAndDisplayVendorDiffs

  User->>Diff: run vendor diff
  Diff-->>User: deprecation warning
  Diff->>Exec: init config + flags
  Exec->>Comp: prepare diff packages, check updates
  Comp-->>User: diff results (optionally update/pull)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

minor

Suggested reviewers

  • aknysh
  • kevcube
  • nitrocode

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the primary feature introduced by the pull request, namely the implementation of a new vendor update command with integrated version management functionality.

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 and usage tips.

@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

Caution

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

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

296-316: Document TemplateSettingsImport.

Exported type without a GoDoc tripwire trips golangci-lint in this repo. Please add a brief comment.

As per coding guidelines.

 type TemplatesSettings struct {
 	Enabled     bool                      `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
 	Sprig       TemplatesSettingsSprig    `yaml:"sprig" json:"sprig" mapstructure:"sprig"`
 	Gomplate    TemplatesSettingsGomplate `yaml:"gomplate" json:"gomplate" mapstructure:"gomplate"`
 	Delimiters  []string                  `yaml:"delimiters,omitempty" json:"delimiters,omitempty" mapstructure:"delimiters"`
 	Evaluations int                       `yaml:"evaluations,omitempty" json:"evaluations,omitempty" mapstructure:"evaluations"`
 	Env         map[string]string         `yaml:"env,omitempty" json:"env,omitempty" mapstructure:"env"`
 	Import      TemplateSettingsImport    `yaml:"import,omitempty" json:"import,omitempty" mapstructure:"import"`
 }
 
+// TemplateSettingsImport configures import template processing behavior.
 type TemplateSettingsImport struct {
 	ProcessWithoutContext bool `yaml:"process_without_context" json:"process_without_context" mapstructure:"process_without_context"`
 }
🧹 Nitpick comments (2)
cmd/markdown/atmos_vendor_update_usage.md (1)

1-21: Align command examples with established formatting.

House style wants $ atmos … in fenced blocks (with the leading space) so the CLI docs look consistent.

Based on learnings.

-# Check for available version updates without making changes
-atmos vendor update --check
+# Check for available version updates without making changes
+```console
+ $ atmos vendor update --check
+```
 
-# Update version references in vendor configuration files
-atmos vendor update
+# Update version references in vendor configuration files
+```console
+ $ atmos vendor update
+```
 
-# Update version references AND pull the new component versions
-atmos vendor update --pull
+# Update version references AND pull the new component versions
+```console
+ $ atmos vendor update --pull
+```
 
-# Update version for a specific component
-atmos vendor update --component vpc
-atmos vendor update -c vpc-flow-logs-bucket
+# Update version for a specific component
+```console
+ $ atmos vendor update --component vpc
+ $ atmos vendor update -c vpc-flow-logs-bucket
+```
 
-# Update versions for components with specific tags
-atmos vendor update --tags terraform
-atmos vendor update --tags networking,storage
+# Update versions for components with specific tags
+```console
+ $ atmos vendor update --tags terraform
+ $ atmos vendor update --tags networking,storage
+```
 
-# Combine flags for specific workflows
-atmos vendor update --component vpc --pull
-atmos vendor update --tags terraform --check
+# Combine flags for specific workflows
+```console
+ $ atmos vendor update --component vpc --pull
+ $ atmos vendor update --tags terraform --check
+```
docs/prd/vendor-update.md (1)

81-94: Specify language for the terminal snippet.

Markdown lint is flagging this fence because it lacks a language hint. Tag it as text (or console) so tooling and syntax highlighters behave.

Apply this diff:

-```
+```text
 Checking for vendor updates...
 
 [=========>] 9/10 Checking terraform-aws-ecs...
 
 ✓ terraform-aws-vpc (1.323.0 → 1.372.0)
 ✓ terraform-aws-s3-bucket (4.1.0 → 4.2.0)
 ✓ terraform-aws-eks (2.1.0 - up to date)
 ⚠ custom-module (skipped - templated version {{.Version}})
 ⚠ terraform-aws-ecs (skipped - OCI registry not yet supported)
 ✓ terraform-aws-rds (5.0.0 → 5.1.0)
 
 Found 3 updates available. Use 'atmos vendor update' to update the configuration files.

Based on static analysis hints.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d586060 and 517a732.

📒 Files selected for processing (28)
  • .gitignore (1 hunks)
  • .go-version (1 hunks)
  • cmd/markdown/atmos_vendor_update_usage.md (1 hunks)
  • cmd/vendor_diff.go (2 hunks)
  • cmd/vendor_update.go (1 hunks)
  • docs/prd/vendor-update.md (1 hunks)
  • errors/errors.go (1 hunks)
  • go.mod (1 hunks)
  • internal/exec/stack_processor_utils.go (1 hunks)
  • internal/exec/terraform_clean.go (1 hunks)
  • internal/exec/vendor.go (4 hunks)
  • internal/exec/vendor_constants.go (1 hunks)
  • internal/exec/vendor_diff.go (1 hunks)
  • internal/exec/vendor_filter.go (1 hunks)
  • internal/exec/vendor_interfaces.go (1 hunks)
  • internal/exec/vendor_model.go (7 hunks)
  • internal/exec/vendor_model_helpers.go (1 hunks)
  • internal/exec/vendor_update.go (1 hunks)
  • internal/exec/vendor_update_test.go (1 hunks)
  • internal/exec/vendor_utils.go (2 hunks)
  • internal/exec/vendor_utils_test.go (1 hunks)
  • internal/exec/vendor_yaml_updater.go (1 hunks)
  • internal/exec/vendor_yaml_updater_test.go (1 hunks)
  • pkg/schema/schema.go (2 hunks)
  • tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden (1 hunks)
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stderr.golden (1 hunks)
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden (1 hunks)
  • website/docs/cli/commands/vendor/vendor-update.mdx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (14)
pkg/**/*.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

Place business logic in pkg rather than in cmd

Target >80% coverage for packages, focusing on pkg/ and internal/exec/.

Files:

  • pkg/schema/schema.go
**/*.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

**/*.go: All code must pass golangci-lint checks
Follow Go error handling idioms and use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider custom error types for domain-specific errors
Follow standard Go coding style; run gofmt and goimports
Use snake_case for environment variables
Document complex logic with inline comments

**/*.go: All comments must end with periods.
Wrap all returned errors with static errors from errors/errors.go; never return dynamic errors directly.
Use fmt.Errorf with %w to wrap the static error first, then add context details.
Always bind environment variables via viper.BindEnv, providing an ATMOS_ alternative for each external var.
All new configurations must support Go templating using the shared FuncMap(); test template rendering with various contexts.
Prefer SDKs over shelling out to binaries; use standard library for cross-platform behavior (filepath.Join, os.PathSeparator, runtime.GOOS).
For non-standard execution paths, capture telemetry via telemetry.CaptureCmd or telemetry.CaptureCmdString without user data.

Files:

  • pkg/schema/schema.go
  • internal/exec/vendor_utils.go
  • internal/exec/vendor_constants.go
  • internal/exec/stack_processor_utils.go
  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_diff.go
  • cmd/vendor_update.go
  • errors/errors.go
  • internal/exec/vendor_filter.go
  • cmd/vendor_diff.go
  • internal/exec/terraform_clean.go
  • internal/exec/vendor.go
  • internal/exec/vendor_model_helpers.go
  • internal/exec/vendor_model.go
  • internal/exec/vendor_update.go
  • internal/exec/vendor_utils_test.go
  • internal/exec/vendor_interfaces.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
**/!(*_test).go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

Document all exported functions, types, and methods with Go doc comments

Files:

  • pkg/schema/schema.go
  • internal/exec/vendor_utils.go
  • internal/exec/vendor_constants.go
  • internal/exec/stack_processor_utils.go
  • internal/exec/vendor_diff.go
  • cmd/vendor_update.go
  • errors/errors.go
  • internal/exec/vendor_filter.go
  • cmd/vendor_diff.go
  • internal/exec/terraform_clean.go
  • internal/exec/vendor.go
  • internal/exec/vendor_model_helpers.go
  • internal/exec/vendor_model.go
  • internal/exec/vendor_update.go
  • internal/exec/vendor_interfaces.go
  • internal/exec/vendor_yaml_updater.go
cmd/markdown/atmos_*_*_usage.md

📄 CodeRabbit inference engine (CLAUDE.md)

Example files must be named atmos___usage.md and live under cmd/markdown/.

Files:

  • cmd/markdown/atmos_vendor_update_usage.md
cmd/markdown/*_usage.md

📄 CodeRabbit inference engine (CLAUDE.md)

Examples auto-load from cmd/markdown/*_usage.md via //go:embed; register them in cmd/markdown_help.go examples map.

Files:

  • cmd/markdown/atmos_vendor_update_usage.md
internal/exec/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain >80% coverage for core orchestration in internal/exec/.

Files:

  • internal/exec/vendor_utils.go
  • internal/exec/vendor_constants.go
  • internal/exec/stack_processor_utils.go
  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_diff.go
  • internal/exec/vendor_filter.go
  • internal/exec/terraform_clean.go
  • internal/exec/vendor.go
  • internal/exec/vendor_model_helpers.go
  • internal/exec/vendor_model.go
  • internal/exec/vendor_update.go
  • internal/exec/vendor_utils_test.go
  • internal/exec/vendor_interfaces.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
internal/exec/stack_processor_utils.go

📄 CodeRabbit inference engine (CLAUDE.md)

Update and verify stack processing utilities in internal/exec/stack_processor_utils.go when changing stack processing.

Files:

  • internal/exec/stack_processor_utils.go
**/*_test.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

**/*_test.go: Every new feature must include comprehensive unit tests
Test both happy paths and error conditions
Use table-driven tests for multiple scenarios

**/*_test.go: Use table-driven tests for unit tests where applicable.
Always use t.Skipf() with a reason; do not use t.Skip() or t.Skipf() without explanation.
TestMain must call os.Exit(m.Run()) to propagate test exit code for CLI tests.

Files:

  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_utils_test.go
  • internal/exec/vendor_update_test.go
go.{mod,sum}

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

go.{mod,sum}: Manage dependencies with Go modules
Keep dependencies up to date

Files:

  • go.mod
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place integration tests and shared test utilities under tests/ with fixtures in tests/test-cases/.

Files:

  • tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stderr.golden
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
cmd/**/*.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

cmd/**/*.go: Use Cobra's recommended command structure with a root command and subcommands
Implement each CLI command in a separate file under cmd/
Use Viper for managing configuration, environment variables, and flags in the CLI
Keep separation of concerns between CLI interface (cmd) and business logic
Use kebab-case for command-line flags
Provide comprehensive help text for all commands and flags
Include examples in Cobra command help
Use Viper for configuration management; support files, env vars, and flags with precedence flags > env > config > defaults
Follow single responsibility; separate command interface from business logic
Provide meaningful user feedback and include progress indicators for long-running operations
Provide clear error messages to users and troubleshooting hints where appropriate

cmd/**/*.go: One Cobra command per file in cmd/; follow embedded markdown example pattern with //go:embed and utils.PrintfMarkdown.
Use markdown formatting in Cobra Short/Long descriptions for commands.
Distinguish UI output (stderr) from structured logging; never use logging for UI elements.
Most text UI must go to stderr; only data/results go to stdout. Prefer utils.PrintfMessageToTUI for UI messages.

Files:

  • cmd/vendor_update.go
  • cmd/vendor_diff.go
errors/errors.go

📄 CodeRabbit inference engine (CLAUDE.md)

Define static errors in errors/errors.go (e.g., ErrInvalidComponent, ErrInvalidStack, ErrInvalidConfig).

Files:

  • errors/errors.go
website/**

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

website/**: Update website documentation in website/ when adding features
Ensure consistency between CLI help text and website documentation
Follow the website's documentation structure and style
Keep website code in website/ and follow its architecture/style; test changes locally
Keep CLI and website documentation in sync; document new features with examples and use cases

Files:

  • website/docs/cli/commands/vendor/vendor-update.mdx
website/docs/cli/commands/**/**/*.mdx

📄 CodeRabbit inference engine (CLAUDE.md)

website/docs/cli/commands/**/**/*.mdx: All new commands/flags/parameters must have Docusaurus documentation at website/docs/cli/commands//.mdx using prescribed frontmatter and sections.
Use definition lists

instead of tables for arguments and flags in command docs.

Files:

  • website/docs/cli/commands/vendor/vendor-update.mdx
🧠 Learnings (34)
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to cmd/markdown/atmos_*_*_usage.md : Example files must be named atmos_<command>_<subcommand>_usage.md and live under cmd/markdown/.

Applied to files:

  • cmd/markdown/atmos_vendor_update_usage.md
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-09-13T16:39:20.007Z
Learnt from: samtholiya
PR: cloudposse/atmos#1466
File: cmd/markdown/atmos_toolchain_aliases.md:2-4
Timestamp: 2025-09-13T16:39:20.007Z
Learning: In the cloudposse/atmos repository, CLI documentation files in cmd/markdown/ follow a specific format that uses " $ atmos command" (with leading space and dollar sign prompt) in code blocks. This is the established project convention and should not be changed to comply with standard markdownlint rules MD040 and MD014.

Applied to files:

  • cmd/markdown/atmos_vendor_update_usage.md
  • website/docs/cli/commands/vendor/vendor-update.mdx
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stderr.golden
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to {pkg/datafetcher/schema/config/global/1.0.json,pkg/datafetcher/schema/atmos/manifest/1.0.json,pkg/datafetcher/schema/stacks/stack-config/1.0.json,pkg/datafetcher/schema/vendor/package/1.0.json} : Update all listed JSON schema files when adding Atmos configuration options and validate changes.

Applied to files:

  • cmd/markdown/atmos_vendor_update_usage.md
📚 Learning: 2024-10-22T23:00:20.627Z
Learnt from: Cerebrovinny
PR: cloudposse/atmos#737
File: internal/exec/vendor_utils.go:131-141
Timestamp: 2024-10-22T23:00:20.627Z
Learning: In the `ReadAndProcessVendorConfigFile` function in `internal/exec/vendor_utils.go`, the existence of the vendor config file is already checked, so additional file existence checks may be unnecessary.

Applied to files:

  • internal/exec/vendor_utils.go
  • internal/exec/vendor_constants.go
  • internal/exec/vendor_update.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to internal/exec/stack_processor_utils.go : Update and verify stack processing utilities in internal/exec/stack_processor_utils.go when changing stack processing.

Applied to files:

  • internal/exec/stack_processor_utils.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to **/*.go : All new configurations must support Go templating using the shared FuncMap(); test template rendering with various contexts.

Applied to files:

  • internal/exec/stack_processor_utils.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to pkg/stack/**/*.go : Core stack processing changes should be implemented under pkg/stack/ with tests for inheritance scenarios.

Applied to files:

  • internal/exec/stack_processor_utils.go
📚 Learning: 2024-12-07T16:16:13.038Z
Learnt from: Listener430
PR: cloudposse/atmos#825
File: internal/exec/helmfile_generate_varfile.go:28-31
Timestamp: 2024-12-07T16:16:13.038Z
Learning: In `internal/exec/helmfile_generate_varfile.go`, the `--help` command (`./atmos helmfile generate varfile --help`) works correctly without requiring stack configurations, and the only change needed was to make `ProcessCommandLineArgs` exportable by capitalizing its name.

Applied to files:

  • internal/exec/stack_processor_utils.go
  • internal/exec/vendor.go
📚 Learning: 2024-11-19T23:00:45.899Z
Learnt from: osterman
PR: cloudposse/atmos#795
File: internal/exec/stack_processor_utils.go:378-386
Timestamp: 2024-11-19T23:00:45.899Z
Learning: In the `ProcessYAMLConfigFile` function within `internal/exec/stack_processor_utils.go`, directory traversal in stack imports is acceptable and should not be restricted.

Applied to files:

  • internal/exec/stack_processor_utils.go
📚 Learning: 2025-09-23T02:30:42.362Z
Learnt from: CR
PR: cloudposse/atmos#0
File: .cursor/rules/atmos-rules.mdc:0-0
Timestamp: 2025-09-23T02:30:42.362Z
Learning: Applies to **/*_test.go : Every new feature must include comprehensive unit tests

Applied to files:

  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_update_test.go
📚 Learning: 2025-01-17T00:21:32.987Z
Learnt from: aknysh
PR: cloudposse/atmos#944
File: go.mod:3-3
Timestamp: 2025-01-17T00:21:32.987Z
Learning: Go version 1.23.0 was deliberately introduced by the maintainer (aknysh) in January 2025. While this might be a pre-release or development version of Go, it has been approved for use in this project.

Applied to files:

  • .go-version
  • go.mod
📚 Learning: 2025-01-17T00:21:32.987Z
Learnt from: aknysh
PR: cloudposse/atmos#944
File: go.mod:3-3
Timestamp: 2025-01-17T00:21:32.987Z
Learning: The project uses Go version 1.23.0 which has been confirmed by the maintainer to be working in production for months. Do not flag this as an invalid Go version.

Applied to files:

  • .go-version
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to cmd/new_command.go : When adding a new CLI command, create cmd/new_command.go with Cobra command, embed examples, register markdown help, and place business logic in pkg/ or internal/exec/.

Applied to files:

  • cmd/vendor_update.go
📚 Learning: 2024-12-13T15:28:13.630Z
Learnt from: Listener430
PR: cloudposse/atmos#844
File: cmd/version.go:34-44
Timestamp: 2024-12-13T15:28:13.630Z
Learning: In `cmd/version.go`, when handling the `--check` flag in the `versionCmd`, avoid using `CheckForAtmosUpdateAndPrintMessage(cliConfig)` as it updates the cache timestamp, which may not be desired in this context.

Applied to files:

  • cmd/vendor_update.go
  • internal/exec/vendor.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to cmd/**/*.go : One Cobra command per file in cmd/; follow embedded markdown example pattern with //go:embed and utils.PrintfMarkdown.

Applied to files:

  • cmd/vendor_update.go
📚 Learning: 2025-09-23T02:30:42.362Z
Learnt from: CR
PR: cloudposse/atmos#0
File: .cursor/rules/atmos-rules.mdc:0-0
Timestamp: 2025-09-23T02:30:42.362Z
Learning: Applies to cmd/**/*.go : Use Cobra's recommended command structure with a root command and subcommands

Applied to files:

  • cmd/vendor_update.go
📚 Learning: 2025-01-30T19:30:59.120Z
Learnt from: samtholiya
PR: cloudposse/atmos#959
File: cmd/workflow.go:74-74
Timestamp: 2025-01-30T19:30:59.120Z
Learning: Error handling for `cmd.Usage()` is not required in the Atmos CLI codebase, as confirmed by the maintainer.

Applied to files:

  • cmd/vendor_update.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to errors/errors.go : Define static errors in errors/errors.go (e.g., ErrInvalidComponent, ErrInvalidStack, ErrInvalidConfig).

Applied to files:

  • errors/errors.go
📚 Learning: 2024-10-31T19:25:41.298Z
Learnt from: osterman
PR: cloudposse/atmos#727
File: internal/exec/terraform_clean.go:233-235
Timestamp: 2024-10-31T19:25:41.298Z
Learning: When specifying color values in functions like `confirmDeleteTerraformLocal` in `internal/exec/terraform_clean.go`, avoid hardcoding color values. Instead, use predefined color constants or allow customization through configuration settings to improve accessibility and user experience across different terminals and themes.

Applied to files:

  • internal/exec/terraform_clean.go
📚 Learning: 2024-10-27T04:41:49.199Z
Learnt from: haitham911
PR: cloudposse/atmos#727
File: internal/exec/terraform_clean.go:215-223
Timestamp: 2024-10-27T04:41:49.199Z
Learning: In `internal/exec/terraform_clean.go`, the function `determineCleanPath` is necessary and should not be removed.

Applied to files:

  • internal/exec/terraform_clean.go
📚 Learning: 2024-11-02T15:35:09.958Z
Learnt from: aknysh
PR: cloudposse/atmos#759
File: internal/exec/terraform.go:366-368
Timestamp: 2024-11-02T15:35:09.958Z
Learning: In `internal/exec/terraform.go`, the workspace cleaning code under both the general execution path and within the `case "init":` block is intentionally duplicated because the code execution paths are different. The `.terraform/environment` file should be deleted before executing `terraform init` in both scenarios to ensure a clean state.

Applied to files:

  • internal/exec/terraform_clean.go
📚 Learning: 2024-11-12T03:16:02.910Z
Learnt from: aknysh
PR: cloudposse/atmos#775
File: internal/exec/template_funcs_component.go:157-159
Timestamp: 2024-11-12T03:16:02.910Z
Learning: In the Go code for `componentFunc` in `internal/exec/template_funcs_component.go`, the function `cleanTerraformWorkspace` does not return errors, and it's acceptable if the file does not exist. Therefore, error handling for `cleanTerraformWorkspace` is not needed.

Applied to files:

  • internal/exec/terraform_clean.go
📚 Learning: 2025-09-13T18:06:07.674Z
Learnt from: samtholiya
PR: cloudposse/atmos#1466
File: toolchain/list.go:39-42
Timestamp: 2025-09-13T18:06:07.674Z
Learning: In the cloudposse/atmos repository, for UI messages in the toolchain package, use utils.PrintfMessageToTUI instead of log.Error or fmt.Fprintln(os.Stderr, ...). Import pkg/utils with alias "u" to follow the established pattern.

Applied to files:

  • internal/exec/terraform_clean.go
  • internal/exec/vendor_model.go
📚 Learning: 2025-07-05T20:59:02.914Z
Learnt from: aknysh
PR: cloudposse/atmos#1363
File: internal/exec/template_utils.go:18-18
Timestamp: 2025-07-05T20:59:02.914Z
Learning: In the Atmos project, gomplate v4 is imported with a blank import (`_ "github.com/hairyhenderson/gomplate/v4"`) alongside v3 imports to resolve AWS SDK version conflicts. V3 uses older AWS SDK versions that conflict with newer AWS modules used by Atmos. A full migration to v4 requires extensive refactoring due to API changes and should be handled in a separate PR.

Applied to files:

  • internal/exec/terraform_clean.go
📚 Learning: 2024-11-12T13:06:56.194Z
Learnt from: osterman
PR: cloudposse/atmos#768
File: website/docs/cheatsheets/vendoring.mdx:70-70
Timestamp: 2024-11-12T13:06:56.194Z
Learning: In `atmos vendor pull --everything`, the `--everything` flag uses the TTY for TUI but is not interactive.

Applied to files:

  • website/docs/cli/commands/vendor/vendor-update.mdx
📚 Learning: 2025-03-18T12:26:25.329Z
Learnt from: Listener430
PR: cloudposse/atmos#1149
File: tests/snapshots/TestCLICommands_atmos_vendor_pull_ssh.stderr.golden:7-7
Timestamp: 2025-03-18T12:26:25.329Z
Learning: In the Atmos project, typos or inconsistencies in test snapshot files (such as "terrafrom" instead of "terraform") may be intentional as they capture the exact output of commands and should not be flagged as issues requiring correction.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stderr.golden
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-02-14T23:12:38.030Z
Learnt from: Listener430
PR: cloudposse/atmos#1061
File: tests/snapshots/TestCLICommands_atmos_vendor_pull_ssh.stderr.golden:8-8
Timestamp: 2025-02-14T23:12:38.030Z
Learning: Test snapshots in the Atmos project, particularly for dry run scenarios, may be updated during the development process, and temporary inconsistencies in their content should not be flagged as issues.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stderr.golden
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2024-10-23T21:36:40.262Z
Learnt from: osterman
PR: cloudposse/atmos#740
File: cmd/cmd_utils.go:340-359
Timestamp: 2024-10-23T21:36:40.262Z
Learning: In the Go codebase for Atmos, when reviewing functions like `checkAtmosConfig` in `cmd/cmd_utils.go`, avoid suggesting refactoring to return errors instead of calling `os.Exit` if such changes would significantly increase the scope due to the need to update multiple call sites.

Applied to files:

  • internal/exec/vendor.go
📚 Learning: 2024-10-19T17:42:29.152Z
Learnt from: Cerebrovinny
PR: cloudposse/atmos#729
File: internal/exec/terraform.go:258-264
Timestamp: 2024-10-19T17:42:29.152Z
Learning: Prefer not to declare variables within 'if' statements in Go code.

Applied to files:

  • internal/exec/vendor.go
📚 Learning: 2025-04-11T22:06:46.999Z
Learnt from: samtholiya
PR: cloudposse/atmos#1147
File: internal/exec/validate_schema.go:42-57
Timestamp: 2025-04-11T22:06:46.999Z
Learning: The "ExecuteAtmosValidateSchemaCmd" function in internal/exec/validate_schema.go has been reviewed and confirmed to have acceptable cognitive complexity despite static analysis warnings. The function uses a clean structure with only three if statements for error handling and delegates complex operations to helper methods.

Applied to files:

  • internal/exec/vendor.go
  • internal/exec/vendor_update.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to internal/exec/**/*.go : Maintain >80% coverage for core orchestration in internal/exec/.

Applied to files:

  • .gitignore
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to pkg/**/*.go : Target >80% coverage for packages, focusing on pkg/ and internal/exec/.

Applied to files:

  • .gitignore
📚 Learning: 2025-01-19T15:49:15.593Z
Learnt from: samtholiya
PR: cloudposse/atmos#955
File: tests/snapshots/TestCLICommands_atmos_validate_editorconfig_--help.stdout.golden:0-0
Timestamp: 2025-01-19T15:49:15.593Z
Learning: In future commits, the help text for Atmos CLI commands should be limited to only show component and stack parameters for commands that actually use them. This applies to the example usage section in command help text.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2024-11-25T17:17:15.703Z
Learnt from: RoseSecurity
PR: cloudposse/atmos#797
File: pkg/list/atmos.yaml:213-214
Timestamp: 2024-11-25T17:17:15.703Z
Learning: The file `pkg/list/atmos.yaml` is primarily intended for testing purposes.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
🧬 Code graph analysis (13)
internal/exec/vendor_utils.go (1)
internal/exec/vendor_yaml_updater.go (1)
  • NewYAMLVersionUpdater (17-19)
internal/exec/stack_processor_utils.go (1)
pkg/schema/schema.go (2)
  • Templates (285-287)
  • Settings (682-686)
internal/exec/vendor_yaml_updater_test.go (1)
internal/exec/vendor_yaml_updater.go (1)
  • NewYAMLVersionUpdater (17-19)
internal/exec/vendor_diff.go (2)
pkg/schema/schema.go (2)
  • AtmosVendorSource (708-717)
  • AtmosConfiguration (26-61)
internal/exec/vendor.go (1)
  • ExecuteVendorPullCmd (38-40)
cmd/vendor_update.go (2)
pkg/utils/log_utils.go (1)
  • PrintfMessageToTUI (29-31)
internal/exec/vendor_update.go (1)
  • ExecuteVendorUpdateCmd (17-37)
internal/exec/vendor_filter.go (1)
pkg/schema/schema.go (1)
  • AtmosVendorSource (708-717)
cmd/vendor_diff.go (2)
pkg/logger/log.go (1)
  • Warn (44-46)
cmd/cmd_utils.go (1)
  • AddStackCompletion (718-723)
internal/exec/vendor.go (8)
internal/exec/cli_utils.go (1)
  • ProcessCommandLineArgs (62-144)
pkg/config/config.go (1)
  • InitCliConfig (25-62)
pkg/schema/schema.go (3)
  • AtmosConfiguration (26-61)
  • Vendor (745-750)
  • AtmosVendorSource (708-717)
internal/exec/vendor_utils.go (1)
  • ReadAndProcessVendorConfigFile (65-93)
errors/errors.go (9)
  • ErrVendorConfigFileNotFound (138-138)
  • ErrVersionCheckingNotSupported (139-139)
  • ErrOCIVersionCheckingNotImplemented (147-147)
  • ErrLocalSourceDoesNotExist (144-144)
  • ErrNoTagsFound (141-141)
  • ErrNoStableReleaseTags (142-142)
  • ErrNoCommitsFound (143-143)
  • ErrInvalidGitLsRemoteOutput (146-146)
  • ErrNoValidCommitsFound (140-140)
internal/exec/template_utils.go (1)
  • ProcessTmpl (28-65)
pkg/utils/go_getter_utils.go (1)
  • ValidateURI (23-50)
pkg/utils/file_utils.go (1)
  • FileExists (26-32)
internal/exec/vendor_model_helpers.go (4)
pkg/schema/schema.go (1)
  • AtmosConfiguration (26-61)
pkg/downloader/custom_git_detector.go (1)
  • NewCustomGitDetector (25-30)
pkg/utils/file_utils.go (2)
  • FileExists (26-32)
  • IsDirectory (17-23)
errors/errors.go (2)
  • ErrValidPackage (27-27)
  • ErrCheckingForUpdates (148-148)
internal/exec/vendor_model.go (3)
pkg/schema/schema.go (1)
  • AtmosVendorSource (708-717)
internal/exec/vendor_model_helpers.go (1)
  • ExecuteInstall (119-140)
pkg/logger/log.go (2)
  • Printf (159-161)
  • Info (34-36)
internal/exec/vendor_update.go (8)
pkg/schema/schema.go (3)
  • AtmosConfiguration (26-61)
  • Vendor (745-750)
  • AtmosVendorSource (708-717)
internal/exec/cli_utils.go (1)
  • ProcessCommandLineArgs (62-144)
pkg/config/config.go (1)
  • InitCliConfig (25-62)
internal/exec/vendor.go (2)
  • VendorFlags (103-112)
  • ExecuteVendorPullCommand (43-71)
pkg/config/const.go (1)
  • AtmosVendorConfigFileName (53-53)
internal/exec/vendor_utils.go (1)
  • ReadAndProcessVendorConfigFile (65-93)
errors/errors.go (1)
  • ErrVendorConfigFileNotFound (138-138)
internal/exec/vendor_component_utils.go (1)
  • ReadAndProcessComponentVendorConfigFile (59-106)
internal/exec/vendor_interfaces.go (3)
pkg/utils/file_utils.go (1)
  • FileExists (26-32)
pkg/schema/schema.go (2)
  • AtmosConfiguration (26-61)
  • AtmosVendorSource (708-717)
pkg/schema/vendor_component.go (1)
  • VendorComponentConfig (30-35)
internal/exec/vendor_update_test.go (4)
pkg/schema/schema.go (1)
  • AtmosVendorSource (708-717)
cmd/cmd_utils.go (1)
  • Contains (750-757)
internal/exec/vendor_yaml_updater.go (1)
  • NewYAMLVersionUpdater (17-19)
internal/exec/vendor.go (1)
  • VendorFlags (103-112)
🪛 markdownlint-cli2 (0.18.1)
docs/prd/vendor-update.md

81-81: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Build (windows-latest, windows)
  • GitHub Check: Run pre-commit hooks
  • GitHub Check: website-deploy-preview
  • GitHub Check: Analyze (go)
  • GitHub Check: Lint (golangci)
  • GitHub Check: Summary
🔇 Additional comments (4)
.go-version (1)

1-1: Confirm the Go 1.24.6 toolchain is actually available.

We were previously pinned to the (already unconventional) Go 1.23.0 toolchain per maintainer guidance, so bumping straight to 1.24.6 will break local tooling if that release hasn’t shipped yet or isn’t mirrored wherever CI pulls Go toolchains. Please double-check availability (and align go.mod/toolchain directives if needed).

Based on learnings.

tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden (1)

2-24: Snapshot reflects new onboarding guidance.

Output matches the new telemetry notice and onboarding copy, so the test now covers the expected messaging.

internal/exec/vendor_yaml_updater.go (1)

40-58: Reduce UpdateVersionsInContent complexity to silence lint.

golangci-lint is currently complaining about cyclomatic complexity (12 > 10) on this function. We need to break the logic into smaller helpers or reorganize the conditionals so the linter passes. This will otherwise keep CI red. (See the existing static analysis alert.)

internal/exec/vendor_update.go (1)

142-194: Split executeVendorUpdate to satisfy length/complexity lint.

golangci-lint is flagging this function for both excessive length (85 lines > 60) and cyclomatic complexity (>10). We need to carve the logic into smaller helpers—e.g., isolate config resolution, import processing, and update orchestration—to get back under the configured thresholds so CI will pass. (Echoing the static analysis warning.)

Comment thread cmd/vendor_diff.go
Comment on lines +21 to 23
// Print deprecation notice
log.Warn("'atmos vendor diff' is deprecated. Please use 'atmos vendor update --check' instead.")

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.

🛠️ Refactor suggestion | 🟠 Major

Route the deprecation notice through the TUI helper.

Per the CLI guidelines we should treat this as user-facing output and send it via utils.PrintfMessageToTUI (stderr), not the structured logger.

Apply this diff:

-import (
-	log "github.com/charmbracelet/log"
+import (
 	"github.com/spf13/cobra"
 
 	e "github.com/cloudposse/atmos/internal/exec"
+	u "github.com/cloudposse/atmos/pkg/utils"
 )
@@
-		// Print deprecation notice
-		log.Warn("'atmos vendor diff' is deprecated. Please use 'atmos vendor update --check' instead.")
+		// Print deprecation notice
+		u.PrintfMessageToTUI("⚠️  'atmos vendor diff' is deprecated. Please use 'atmos vendor update --check' instead.\n")

As per coding guidelines.

🤖 Prompt for AI Agents
In cmd/vendor_diff.go around lines 21 to 23, the deprecation notice is currently
sent via the structured logger (log.Warn); replace that call with the TUI helper
by routing the message through utils.PrintfMessageToTUI to stderr (e.g.
utils.PrintfMessageToTUI(os.Stderr, "'atmos vendor diff' is deprecated. Please
use 'atmos vendor update --check' instead.\n")), remove the log.Warn invocation,
and add an import for os if it's not already present.

Comment on lines +28 to +137
if err := executeVendorModel(diffPackages, true, &schema.AtmosConfiguration{}); err != nil {
return fmt.Errorf("vendor diff failed: %w", err)
}

// Handle update request if specified
if updateVendorFile {
return helper.handleUpdateRequest(sources, vendorConfigFile)
}

return nil
}

// printHeader prints the operation header.
func (h *vendorDiffHelper) printHeader() {
fmt.Println("Checking for vendor updates...")
fmt.Println()
}

// prepareDiffPackages converts sources to diff packages for the TUI.
func (h *vendorDiffHelper) prepareDiffPackages(sources []schema.AtmosVendorSource, outdatedOnly bool) []pkgVendorDiff {
diffPackages := make([]pkgVendorDiff, 0, len(sources))

for i := range sources {
source := &sources[i]
pkg := h.createDiffPackage(source, outdatedOnly)
diffPackages = append(diffPackages, pkg)
}

return diffPackages
}

// createDiffPackage creates a diff package from a vendor source.
func (h *vendorDiffHelper) createDiffPackage(source *schema.AtmosVendorSource, outdatedOnly bool) pkgVendorDiff {
componentName := h.extractComponentName(source)
currentVersion := h.extractCurrentVersion(source)

return pkgVendorDiff{
name: componentName,
currentVersion: currentVersion,
source: *source,
outdatedOnly: outdatedOnly,
}
}

// extractComponentName extracts the component name from a source.
func (h *vendorDiffHelper) extractComponentName(source *schema.AtmosVendorSource) string {
if source.Component != "" {
return source.Component
}
return extractComponentNameFromSource(source.Source)
}

// extractCurrentVersion extracts the current version from a source.
func (h *vendorDiffHelper) extractCurrentVersion(source *schema.AtmosVendorSource) string {
if source.Version != "" {
return source.Version
}
return "latest"
}

// handleUpdateRequest handles the vendor file update request.
func (h *vendorDiffHelper) handleUpdateRequest(sources []schema.AtmosVendorSource, vendorConfigFile string) error {
// Collect updates
updatedVersions := h.collectUpdates(sources)

if len(updatedVersions) == 0 {
fmt.Println("\nNo updates available.")
return nil
}

// Apply updates to vendor config file
return h.applyUpdates(updatedVersions, vendorConfigFile)
}

// collectUpdates collects available version updates.
func (h *vendorDiffHelper) collectUpdates(sources []schema.AtmosVendorSource) map[string]string {
updatedVersions := make(map[string]string)

fmt.Println("\nCollecting update information...")

for i := range sources {
source := &sources[i]
componentName := h.extractComponentName(source)

// Check for updates using the existing logic
updateAvailable, latestInfo, err := checkForVendorUpdates(&sources[i], true)
if err != nil {
continue // Skip components with errors
}

if updateAvailable && latestInfo != "" {
updatedVersions[componentName] = latestInfo
}
}

return updatedVersions
}

// applyUpdates applies the collected updates to the vendor config file.
func (h *vendorDiffHelper) applyUpdates(updatedVersions map[string]string, vendorConfigFile string) error {
fmt.Printf("\nUpdating %d components in vendor config...\n", len(updatedVersions))

if err := updateVendorConfigFile(updatedVersions, vendorConfigFile); err != nil {
return fmt.Errorf("failed to update vendor config file %s: %w", vendorConfigFile, err)
}

// Pull the updated components
fmt.Println("\nPulling updated components...")
if err := ExecuteVendorPullCmd(nil, []string{}); err != nil {
return fmt.Errorf("version references updated but pull failed: %w", err)

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.

⚠️ Potential issue | 🟠 Major

Wrap returned errors with static error constants.
Line [28], Line [131], and Line [136] build dynamic fmt.Errorf strings. The repo guidelines require wrapping outbound errors with the static sentinels from errors/errors.go before adding context. Please wrap these with the appropriate errUtils constant (add one if none fits yet) using fmt.Errorf("%w: …", errUtils.SomeError, err) so callers can reliably detect them.

As per coding guidelines.

🤖 Prompt for AI Agents
internal/exec/vendor_diff.go around lines 28, 131 and 136: the code currently
returns fmt.Errorf(...) with dynamic messages; change these to wrap the
underlying errors with the project’s static error sentinel from errors/errors.go
(errUtils) before adding context — e.g. pick or add an appropriate errUtils
constant (create one if none fits, e.g., ErrVendorDiff or ErrVendorUpdate) and
return fmt.Errorf("%w: failed to ...: %v", errUtils.ErrVendorDiff, err) so
callers can detect the sentinel; ensure you import the errUtils package if not
already imported and replace the three fmt.Errorf return sites accordingly,
preserving the original context text after the sentinel.

Comment on lines +29 to +60
// Check component filter
if !f.matchesComponent(source, component) {
return false
}

// Check tags filter
if !f.matchesTags(source, tags) {
return false
}

return true
}

// matchesComponent checks if source matches the component filter.
func (f *vendorSourceFilter) matchesComponent(source *schema.AtmosVendorSource, component string) bool {
// If no component filter specified, include all
if component == "" {
return true
}
// Check if source component matches filter
return source.Component == component
}

// matchesTags checks if source has at least one matching tag.
func (f *vendorSourceFilter) matchesTags(source *schema.AtmosVendorSource, tags []string) bool {
// If no tags filter specified, include all
if len(tags) == 0 {
return true
}

// Check if source has any matching tag
return f.hasAnyMatchingTag(source.Tags, tags)

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.

🛠️ Refactor suggestion | 🟠 Major

Ensure inline comments end with periods.

Coding guidelines ask every comment to terminate with a period. The inline comments in this block are missing it. Please add the trailing punctuation so we stay consistent.

@@
-	// Check component filter
+	// Check component filter.
@@
-	// Check tags filter
+	// Check tags filter.
@@
-	// If no component filter specified, include all
+	// If no component filter specified, include all.
@@
-	// Check if source component matches filter
+	// Check if source component matches filter.
@@
-	// If no tags filter specified, include all
+	// If no tags filter specified, include all.
@@
-	// Check if source has any matching tag
+	// Check if source has any matching tag.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check component filter
if !f.matchesComponent(source, component) {
return false
}
// Check tags filter
if !f.matchesTags(source, tags) {
return false
}
return true
}
// matchesComponent checks if source matches the component filter.
func (f *vendorSourceFilter) matchesComponent(source *schema.AtmosVendorSource, component string) bool {
// If no component filter specified, include all
if component == "" {
return true
}
// Check if source component matches filter
return source.Component == component
}
// matchesTags checks if source has at least one matching tag.
func (f *vendorSourceFilter) matchesTags(source *schema.AtmosVendorSource, tags []string) bool {
// If no tags filter specified, include all
if len(tags) == 0 {
return true
}
// Check if source has any matching tag
return f.hasAnyMatchingTag(source.Tags, tags)
// Check component filter.
if !f.matchesComponent(source, component) {
return false
}
// Check tags filter.
if !f.matchesTags(source, tags) {
return false
}
return true
}
// matchesComponent checks if source matches the component filter.
func (f *vendorSourceFilter) matchesComponent(source *schema.AtmosVendorSource, component string) bool {
// If no component filter specified, include all.
if component == "" {
return true
}
// Check if source component matches filter.
return source.Component == component
}
// matchesTags checks if source has at least one matching tag.
func (f *vendorSourceFilter) matchesTags(source *schema.AtmosVendorSource, tags []string) bool {
// If no tags filter specified, include all.
if len(tags) == 0 {
return true
}
// Check if source has any matching tag.
return f.hasAnyMatchingTag(source.Tags, tags)
}
🤖 Prompt for AI Agents
internal/exec/vendor_filter.go lines 29-60: several inline comment lines lack
terminal periods; update each comment to end with a period (e.g., "If no
component filter specified, include all" -> "If no component filter specified,
include all."). Ensure every comment in this block ends with a period while
preserving original wording and spacing.

Comment on lines +285 to +314
if pkg.diffPackage != nil {
return m, tea.Sequence(
tea.Printf("%s", msg.name),
tea.Quit,
)
}

version := grayColor.Render(version)
return m, tea.Sequence(
tea.Printf("%s %s %s %s", mark, pkg.name, version, errMsg),
tea.Quit,
)
}

if !m.isTTY {
log.Info(mark, "package", pkg.name, "version", version)
}

m.index++
// Update progress bar
progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)))

// For diff packages, use the exact message
if pkg.diffPackage != nil {
return m, tea.Batch(
progressCmd,
tea.Printf("%s", msg.name),
ExecuteInstall(m.packages[m.index], m.dryRun, m.atmosConfig),
)
}

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.

⚠️ Potential issue | 🟠 Major

Preserve diff error output.
When the diff branch on Line [285] or Line [308] receives an error (msg.err != nil), msg.name is just the component name, so the CLI never surfaces errMsg. Users lose the failure reason. Please push the formatted error (e.g., prefix with mark and reuse errMsg) into the tea.Printf call before returning.

@@
-	if pkg.diffPackage != nil {
-		return m, tea.Sequence(
-			tea.Printf("%s", msg.name),
-			tea.Quit,
-		)
-	}
+	if pkg.diffPackage != nil {
+		output := msg.name
+		if msg.err != nil {
+			output = fmt.Sprintf("%s %s", mark, errMsg)
+		}
+		return m, tea.Sequence(
+			tea.Printf("%s", output),
+			tea.Quit,
+		)
+	}
@@
-	if pkg.diffPackage != nil {
-		return m, tea.Batch(
-			progressCmd,
-			tea.Printf("%s", msg.name),
-			ExecuteInstall(m.packages[m.index], m.dryRun, m.atmosConfig),
-		)
-	}
+	if pkg.diffPackage != nil {
+		output := msg.name
+		if msg.err != nil {
+			output = fmt.Sprintf("%s %s", mark, errMsg)
+		}
+		return m, tea.Batch(
+			progressCmd,
+			tea.Printf("%s", output),
+			ExecuteInstall(m.packages[m.index], m.dryRun, m.atmosConfig),
+		)
+	}
🤖 Prompt for AI Agents
In internal/exec/vendor_model.go around lines 285 to 314, the diff branch
returns only msg.name when msg.err != nil, which hides the formatted error;
update both return sites for diff packages (the early return that uses
tea.Sequence and the later return inside tea.Batch) to print the full formatted
message (include mark, pkg.name/version context if appropriate, and errMsg)
instead of only msg.name so the CLI surfaces the actual error; keep the existing
flow (Sequence vs Batch) but replace the tea.Printf format/arguments to include
mark and errMsg (matching the non-diff printf pattern).

Comment on lines +17 to +224
func ExecuteVendorUpdateCmd(cmd *cobra.Command, args []string) error {
// Initialize configuration
atmosConfig, err := initVendorUpdateConfig(cmd, args)
if err != nil {
return err
}

// Parse and validate flags
vendorFlags, err := parseVendorUpdateFlags(cmd)
if err != nil {
return err
}

// Execute the vendor update
if err := executeVendorUpdate(&atmosConfig, vendorFlags); err != nil {
return err
}

// Execute vendor pull if requested
return executeVendorPullIfRequested(cmd, args, vendorFlags)
}

// initVendorUpdateConfig initializes the configuration for vendor update.
func initVendorUpdateConfig(cmd *cobra.Command, args []string) (schema.AtmosConfiguration, error) {
info, err := ProcessCommandLineArgs("terraform", cmd, args, nil)
if err != nil {
return schema.AtmosConfiguration{}, err
}

// vendor update doesn't use stack flag
processStacks := false

atmosConfig, err := cfg.InitCliConfig(info, processStacks)
if err != nil {
return schema.AtmosConfiguration{}, fmt.Errorf("failed to initialize CLI config: %w", err)
}

return atmosConfig, nil
}

// parseVendorUpdateFlags parses and validates vendor update flags.
func parseVendorUpdateFlags(cmd *cobra.Command) (*VendorFlags, error) {
flags := cmd.Flags()

checkOnly, err := flags.GetBool("check")
if err != nil {
return nil, err
}

component, err := flags.GetString("component")
if err != nil {
return nil, err
}

tagsCsv, err := flags.GetString("tags")
if err != nil {
return nil, err
}

var tags []string
if tagsCsv != "" {
tags = strings.Split(tagsCsv, ",")
}

componentType, err := flags.GetString("type")
if err != nil {
return nil, err
}

vendorFlags := &VendorFlags{
Component: component,
Tags: tags,
ComponentType: componentType,
DryRun: checkOnly, // --check flag means dry-run
Update: !checkOnly, // Update unless --check is set
}

// Validate flags
if err := validateVendorUpdateFlags(vendorFlags); err != nil {
return nil, err
}

return vendorFlags, nil
}

// executeVendorPullIfRequested executes vendor pull if the --pull flag is set.
func executeVendorPullIfRequested(cmd *cobra.Command, args []string, vendorFlags *VendorFlags) error {
flags := cmd.Flags()

pullAfterUpdate, err := flags.GetBool("pull")
if err != nil {
return err
}

if !pullAfterUpdate || vendorFlags.DryRun {
return nil
}

log.Info("Executing vendor pull for updated components...")

// Get original flag values for passing to pull command
tagsCsv, _ := flags.GetString("tags")

// Create a new command for vendor pull with same filters
pullCmd := &cobra.Command{}
pullCmd.Flags().StringP("component", "c", vendorFlags.Component, "")
pullCmd.Flags().String("tags", tagsCsv, "")
pullCmd.Flags().StringP("type", "t", vendorFlags.ComponentType, "")

if err := ExecuteVendorPullCommand(pullCmd, args); err != nil {
return fmt.Errorf("version references updated but pull failed: %w", err)
}

log.Info("Successfully updated version references and pulled new component versions")
return nil
}

// validateVendorUpdateFlags validates the vendor update command flags.
func validateVendorUpdateFlags(flg *VendorFlags) error {
// Component and tags can be used together for vendor update
// No additional validation needed beyond basic checks
return nil
}

// executeVendorUpdate performs the actual vendor update logic.
func executeVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
// Determine the vendor config file path
vendorConfigFileName := cfg.AtmosVendorConfigFileName
if atmosConfig.Vendor.BasePath != "" {
vendorConfigFileName = atmosConfig.Vendor.BasePath
}

// Read the main vendor config
vendorConfig, vendorConfigExists, foundVendorConfigFile, err := ReadAndProcessVendorConfigFile(
atmosConfig,
vendorConfigFileName,
true,
)
if err != nil {
return err
}

if !vendorConfigExists {
// Try component vendor config if no main vendor config
if flg.Component != "" {
return executeComponentVendorUpdate(atmosConfig, flg)
}
return fmt.Errorf("%w: %s", errUtils.ErrVendorConfigFileNotFound, vendorConfigFileName)
}

// Process all imports and get sources with file mappings
sources, importedFiles, err := processVendorImportsWithFileTracking(
atmosConfig,
foundVendorConfigFile,
vendorConfig.Spec.Imports,
vendorConfig.Spec.Sources,
[]string{foundVendorConfigFile},
)
if err != nil {
return err
}

if len(sources) == 0 {
fmt.Println("No vendor sources configured")
return nil
}

// Filter sources based on component and tags
filteredSources := filterSources(sources, flg.Component, flg.Tags)

if len(filteredSources) == 0 {
reportNoSourcesFound(flg)
return nil
}

// Check for updates and display results
return checkAndUpdateVendorVersions(filteredSources, importedFiles, flg.DryRun, foundVendorConfigFile)
}

// executeComponentVendorUpdate handles vendor update for component.yaml files.
func executeComponentVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
componentType := flg.ComponentType
if componentType == "" {
componentType = "terraform"
}

config, path, err := ReadAndProcessComponentVendorConfigFile(
atmosConfig,
flg.Component,
componentType,
)
if err != nil {
return err
}

// Create a source from the component config
source := schema.AtmosVendorSource{
Component: flg.Component,
Source: config.Spec.Source.Uri,
Version: config.Spec.Source.Version,
}

// Check for updates
sources := []schema.AtmosVendorSource{source}
fileMap := map[string]string{flg.Component: path + "/component.yaml"}

return checkAndUpdateVendorVersions(sources, fileMap, flg.DryRun, path+"/component.yaml")
}

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.

⚠️ Potential issue | 🟠 Major

Wrap every returned error with the project’s static sentinels.

Throughout this file we return bare errors from helpers or build dynamic fmt.Errorf strings (e.g., Lines 21, 27, 32, 50, 63, 68, 83, 104, 127, 155, 176, 209, 223, 246, 393, 439). The coding guidelines require that each exit path wraps a static error from errors/errors.go using fmt.Errorf("%w: …", errSentinel, err) and avoids returning raw values. Please define/ reuse the appropriate sentinel errors (init failure, flag parse failure, pull failure, import processing failure, etc.) and make sure every return err/fmt.Errorf("…%w…") here conforms before merging. Based on coding guidelines.

Comment on lines +47 to +450
processStacks := false

atmosConfig, err := cfg.InitCliConfig(info, processStacks)
if err != nil {
return schema.AtmosConfiguration{}, fmt.Errorf("failed to initialize CLI config: %w", err)
}

return atmosConfig, nil
}

// parseVendorUpdateFlags parses and validates vendor update flags.
func parseVendorUpdateFlags(cmd *cobra.Command) (*VendorFlags, error) {
flags := cmd.Flags()

checkOnly, err := flags.GetBool("check")
if err != nil {
return nil, err
}

component, err := flags.GetString("component")
if err != nil {
return nil, err
}

tagsCsv, err := flags.GetString("tags")
if err != nil {
return nil, err
}

var tags []string
if tagsCsv != "" {
tags = strings.Split(tagsCsv, ",")
}

componentType, err := flags.GetString("type")
if err != nil {
return nil, err
}

vendorFlags := &VendorFlags{
Component: component,
Tags: tags,
ComponentType: componentType,
DryRun: checkOnly, // --check flag means dry-run
Update: !checkOnly, // Update unless --check is set
}

// Validate flags
if err := validateVendorUpdateFlags(vendorFlags); err != nil {
return nil, err
}

return vendorFlags, nil
}

// executeVendorPullIfRequested executes vendor pull if the --pull flag is set.
func executeVendorPullIfRequested(cmd *cobra.Command, args []string, vendorFlags *VendorFlags) error {
flags := cmd.Flags()

pullAfterUpdate, err := flags.GetBool("pull")
if err != nil {
return err
}

if !pullAfterUpdate || vendorFlags.DryRun {
return nil
}

log.Info("Executing vendor pull for updated components...")

// Get original flag values for passing to pull command
tagsCsv, _ := flags.GetString("tags")

// Create a new command for vendor pull with same filters
pullCmd := &cobra.Command{}
pullCmd.Flags().StringP("component", "c", vendorFlags.Component, "")
pullCmd.Flags().String("tags", tagsCsv, "")
pullCmd.Flags().StringP("type", "t", vendorFlags.ComponentType, "")

if err := ExecuteVendorPullCommand(pullCmd, args); err != nil {
return fmt.Errorf("version references updated but pull failed: %w", err)
}

log.Info("Successfully updated version references and pulled new component versions")
return nil
}

// validateVendorUpdateFlags validates the vendor update command flags.
func validateVendorUpdateFlags(flg *VendorFlags) error {
// Component and tags can be used together for vendor update
// No additional validation needed beyond basic checks
return nil
}

// executeVendorUpdate performs the actual vendor update logic.
func executeVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
// Determine the vendor config file path
vendorConfigFileName := cfg.AtmosVendorConfigFileName
if atmosConfig.Vendor.BasePath != "" {
vendorConfigFileName = atmosConfig.Vendor.BasePath
}

// Read the main vendor config
vendorConfig, vendorConfigExists, foundVendorConfigFile, err := ReadAndProcessVendorConfigFile(
atmosConfig,
vendorConfigFileName,
true,
)
if err != nil {
return err
}

if !vendorConfigExists {
// Try component vendor config if no main vendor config
if flg.Component != "" {
return executeComponentVendorUpdate(atmosConfig, flg)
}
return fmt.Errorf("%w: %s", errUtils.ErrVendorConfigFileNotFound, vendorConfigFileName)
}

// Process all imports and get sources with file mappings
sources, importedFiles, err := processVendorImportsWithFileTracking(
atmosConfig,
foundVendorConfigFile,
vendorConfig.Spec.Imports,
vendorConfig.Spec.Sources,
[]string{foundVendorConfigFile},
)
if err != nil {
return err
}

if len(sources) == 0 {
fmt.Println("No vendor sources configured")
return nil
}

// Filter sources based on component and tags
filteredSources := filterSources(sources, flg.Component, flg.Tags)

if len(filteredSources) == 0 {
reportNoSourcesFound(flg)
return nil
}

// Check for updates and display results
return checkAndUpdateVendorVersions(filteredSources, importedFiles, flg.DryRun, foundVendorConfigFile)
}

// executeComponentVendorUpdate handles vendor update for component.yaml files.
func executeComponentVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
componentType := flg.ComponentType
if componentType == "" {
componentType = "terraform"
}

config, path, err := ReadAndProcessComponentVendorConfigFile(
atmosConfig,
flg.Component,
componentType,
)
if err != nil {
return err
}

// Create a source from the component config
source := schema.AtmosVendorSource{
Component: flg.Component,
Source: config.Spec.Source.Uri,
Version: config.Spec.Source.Version,
}

// Check for updates
sources := []schema.AtmosVendorSource{source}
fileMap := map[string]string{flg.Component: path + "/component.yaml"}

return checkAndUpdateVendorVersions(sources, fileMap, flg.DryRun, path+"/component.yaml")
}

// processVendorImportsWithFileTracking processes imports and tracks which file each source comes from.
func processVendorImportsWithFileTracking(
atmosConfig *schema.AtmosConfiguration,
vendorConfigFile string,
imports []string,
sources []schema.AtmosVendorSource,
allImports []string,
) ([]schema.AtmosVendorSource, map[string]string, error) {
// This will map component names to their source files
fileMap := make(map[string]string)

// Process imports recursively
allSources, _, err := processVendorImports(
atmosConfig,
vendorConfigFile,
imports,
sources,
allImports,
)
if err != nil {
return nil, nil, err
}

// Build the file map from the sources
for i := range allSources {
source := &allSources[i]
componentName := source.Component
if componentName == "" {
componentName = extractComponentNameFromSource(source.Source)
}

// Use the File field that was set during import processing
if source.File != "" {
fileMap[componentName] = source.File
} else {
fileMap[componentName] = vendorConfigFile
}
}

return allSources, fileMap, nil
}

// vendorUpdateHelper contains helper methods for vendor update operations.
type vendorUpdateHelper struct{}

// newVendorUpdateHelper creates a new vendor update helper.
func newVendorUpdateHelper() *vendorUpdateHelper {
return &vendorUpdateHelper{}
}

// prepareDiffPackages converts vendor sources to diff packages for the TUI.
func (h *vendorUpdateHelper) prepareDiffPackages(sources []schema.AtmosVendorSource) []pkgVendorDiff {
diffPackages := make([]pkgVendorDiff, 0, len(sources))

for i := range sources {
componentName := h.extractComponentName(&sources[i])
currentVersion := h.extractCurrentVersion(&sources[i])

// Skip templated versions
if h.isTemplatedVersion(currentVersion) {
// Skip silently - logging happens elsewhere
continue
}

diffPackages = append(diffPackages, pkgVendorDiff{
name: componentName,
currentVersion: currentVersion,
source: sources[i],
outdatedOnly: false,
})
}

return diffPackages
}

// extractComponentName extracts the component name from a vendor source.
func (h *vendorUpdateHelper) extractComponentName(source *schema.AtmosVendorSource) string {
if source.Component != "" {
return source.Component
}
return extractComponentNameFromSource(source.Source)
}

// extractCurrentVersion extracts the current version from a vendor source.
func (h *vendorUpdateHelper) extractCurrentVersion(source *schema.AtmosVendorSource) string {
if source.Version != "" {
return source.Version
}
return defaultVersionLatest
}

// isTemplatedVersion checks if a version contains template markers.
func (h *vendorUpdateHelper) isTemplatedVersion(version string) bool {
return strings.Contains(version, templateStartMarker)
}

// groupUpdatesByFile organizes version updates by configuration file.
func (h *vendorUpdateHelper) groupUpdatesByFile(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
mainConfigFile string,
) map[string]map[string]string {
updatesByFile := make(map[string]map[string]string)

fmt.Println("\nChecking for version updates...")

for i := range sources {
componentName := h.extractComponentName(&sources[i])

// Skip templated versions
if h.isTemplatedVersion(sources[i].Version) {
continue
}

// Check for updates
updateAvailable, latestVersion, err := checkForVendorUpdates(&sources[i], true)
if err != nil {
// Skip silently - error will be logged elsewhere
continue
}

if updateAvailable && latestVersion != "" {
configFile := h.determineConfigFile(componentName, fileMap, mainConfigFile)
h.addUpdateToFile(updatesByFile, configFile, componentName, latestVersion)
}
}

return updatesByFile
}

// determineConfigFile determines which configuration file to update.
func (h *vendorUpdateHelper) determineConfigFile(
componentName string,
fileMap map[string]string,
mainConfigFile string,
) string {
if configFile, exists := fileMap[componentName]; exists {
return configFile
}
return mainConfigFile
}

// addUpdateToFile adds an update to the appropriate file's update map.
func (h *vendorUpdateHelper) addUpdateToFile(
updatesByFile map[string]map[string]string,
configFile string,
componentName string,
latestVersion string,
) {
if updatesByFile[configFile] == nil {
updatesByFile[configFile] = make(map[string]string)
}
updatesByFile[configFile][componentName] = latestVersion
}

// applyUpdatesToFiles applies version updates to configuration files.
func (h *vendorUpdateHelper) applyUpdatesToFiles(updatesByFile map[string]map[string]string) error {
totalUpdates := 0

for configFile, updates := range updatesByFile {
if len(updates) == 0 {
continue
}

fmt.Printf("\nUpdating %d components in %s...\n", len(updates), configFile)

if err := updateVendorConfigFile(updates, configFile); err != nil {
return fmt.Errorf("error updating %s: %w", configFile, err)
}

totalUpdates += len(updates)
}

h.printUpdateSummary(totalUpdates, len(updatesByFile))
return nil
}

// printUpdateSummary prints a summary of the updates performed.
func (h *vendorUpdateHelper) printUpdateSummary(totalUpdates int, fileCount int) {
if totalUpdates > 0 {
fmt.Printf("\nSuccessfully updated %d components across %d files\n", totalUpdates, fileCount)
} else {
fmt.Println("\nAll vendor dependencies are up to date!")
}
}

// printOperationHeader prints the appropriate header for the operation.
func (h *vendorUpdateHelper) printOperationHeader(dryRun bool) {
if dryRun {
fmt.Println("Checking for vendor updates...")
} else {
fmt.Println("Updating vendor configurations...")
}
fmt.Println()
}

// checkAndUpdateVendorVersions checks for version updates and optionally updates the files.
func checkAndUpdateVendorVersions(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
dryRun bool,
mainConfigFile string,
) error {
helper := newVendorUpdateHelper()

// Print operation header
helper.printOperationHeader(dryRun)

// Prepare diff packages for version checking
diffPackages := helper.prepareDiffPackages(sources)

// Execute version check using the TUI model
if err := executeVendorModel(diffPackages, true, &schema.AtmosConfiguration{}); err != nil {
return fmt.Errorf("failed to check vendor updates: %w", err)
}

// If dry-run, we're done
if dryRun {
return nil
}

// Group updates by configuration file
updatesByFile := helper.groupUpdatesByFile(sources, fileMap, mainConfigFile)

// Apply updates to files

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.

🛠️ Refactor suggestion | 🟠 Major

Add missing periods to inline comments.

Same comment-style rule applies here—several inline comments end without a period. Touch them up for consistency.

@@
-	// vendor update doesn't use stack flag
+	// vendor update doesn't use stack flag.
@@
-	// Get original flag values for passing to pull command
+	// Get original flag values for passing to pull command.
@@
-	// Create a new command for vendor pull with same filters
+	// Create a new command for vendor pull with same filters.
@@
-	// Determine the vendor config file path
+	// Determine the vendor config file path.
@@
-	// Read the main vendor config
+	// Read the main vendor config.
@@
-	// Try component vendor config if no main vendor config
+	// Try component vendor config if no main vendor config.
@@
-	// Process all imports and get sources with file mappings
+	// Process all imports and get sources with file mappings.
@@
-	// Filter sources based on component and tags
+	// Filter sources based on component and tags.
@@
-	// Check for updates and display results
+	// Check for updates and display results.
@@
-	// Create a source from the component config
+	// Create a source from the component config.
@@
-	// Check for updates
+	// Check for updates.
@@
-	// This will map component names to their source files
+	// This will map component names to their source files.
@@
-	// Process imports recursively
+	// Process imports recursively.
@@
-	// Build the file map from the sources
+	// Build the file map from the sources.
@@
-		// Use the File field that was set during import processing
+		// Use the File field that was set during import processing.
@@
-	// Skip templated versions
+	// Skip templated versions.
@@
-			// Skip silently - logging happens elsewhere
+			// Skip silently - logging happens elsewhere.
@@
-		// Skip templated versions
+		// Skip templated versions.
@@
-		// Check for updates
+		// Check for updates.
@@
-			// Skip silently - error will be logged elsewhere
+			// Skip silently - error will be logged elsewhere.
@@
-	// Print operation header
+	// Print operation header.
@@
-	// Prepare diff packages for version checking
+	// Prepare diff packages for version checking.
@@
-	// Execute version check using the TUI model
+	// Execute version check using the TUI model.
@@
-	// If dry-run, we're done
+	// If dry-run, we're done.
@@
-	// Group updates by configuration file
+	// Group updates by configuration file.
@@
-	// Apply updates to files
+	// Apply updates to files.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
processStacks := false
atmosConfig, err := cfg.InitCliConfig(info, processStacks)
if err != nil {
return schema.AtmosConfiguration{}, fmt.Errorf("failed to initialize CLI config: %w", err)
}
return atmosConfig, nil
}
// parseVendorUpdateFlags parses and validates vendor update flags.
func parseVendorUpdateFlags(cmd *cobra.Command) (*VendorFlags, error) {
flags := cmd.Flags()
checkOnly, err := flags.GetBool("check")
if err != nil {
return nil, err
}
component, err := flags.GetString("component")
if err != nil {
return nil, err
}
tagsCsv, err := flags.GetString("tags")
if err != nil {
return nil, err
}
var tags []string
if tagsCsv != "" {
tags = strings.Split(tagsCsv, ",")
}
componentType, err := flags.GetString("type")
if err != nil {
return nil, err
}
vendorFlags := &VendorFlags{
Component: component,
Tags: tags,
ComponentType: componentType,
DryRun: checkOnly, // --check flag means dry-run
Update: !checkOnly, // Update unless --check is set
}
// Validate flags
if err := validateVendorUpdateFlags(vendorFlags); err != nil {
return nil, err
}
return vendorFlags, nil
}
// executeVendorPullIfRequested executes vendor pull if the --pull flag is set.
func executeVendorPullIfRequested(cmd *cobra.Command, args []string, vendorFlags *VendorFlags) error {
flags := cmd.Flags()
pullAfterUpdate, err := flags.GetBool("pull")
if err != nil {
return err
}
if !pullAfterUpdate || vendorFlags.DryRun {
return nil
}
log.Info("Executing vendor pull for updated components...")
// Get original flag values for passing to pull command
tagsCsv, _ := flags.GetString("tags")
// Create a new command for vendor pull with same filters
pullCmd := &cobra.Command{}
pullCmd.Flags().StringP("component", "c", vendorFlags.Component, "")
pullCmd.Flags().String("tags", tagsCsv, "")
pullCmd.Flags().StringP("type", "t", vendorFlags.ComponentType, "")
if err := ExecuteVendorPullCommand(pullCmd, args); err != nil {
return fmt.Errorf("version references updated but pull failed: %w", err)
}
log.Info("Successfully updated version references and pulled new component versions")
return nil
}
// validateVendorUpdateFlags validates the vendor update command flags.
func validateVendorUpdateFlags(flg *VendorFlags) error {
// Component and tags can be used together for vendor update
// No additional validation needed beyond basic checks
return nil
}
// executeVendorUpdate performs the actual vendor update logic.
func executeVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
// Determine the vendor config file path
vendorConfigFileName := cfg.AtmosVendorConfigFileName
if atmosConfig.Vendor.BasePath != "" {
vendorConfigFileName = atmosConfig.Vendor.BasePath
}
// Read the main vendor config
vendorConfig, vendorConfigExists, foundVendorConfigFile, err := ReadAndProcessVendorConfigFile(
atmosConfig,
vendorConfigFileName,
true,
)
if err != nil {
return err
}
if !vendorConfigExists {
// Try component vendor config if no main vendor config
if flg.Component != "" {
return executeComponentVendorUpdate(atmosConfig, flg)
}
return fmt.Errorf("%w: %s", errUtils.ErrVendorConfigFileNotFound, vendorConfigFileName)
}
// Process all imports and get sources with file mappings
sources, importedFiles, err := processVendorImportsWithFileTracking(
atmosConfig,
foundVendorConfigFile,
vendorConfig.Spec.Imports,
vendorConfig.Spec.Sources,
[]string{foundVendorConfigFile},
)
if err != nil {
return err
}
if len(sources) == 0 {
fmt.Println("No vendor sources configured")
return nil
}
// Filter sources based on component and tags
filteredSources := filterSources(sources, flg.Component, flg.Tags)
if len(filteredSources) == 0 {
reportNoSourcesFound(flg)
return nil
}
// Check for updates and display results
return checkAndUpdateVendorVersions(filteredSources, importedFiles, flg.DryRun, foundVendorConfigFile)
}
// executeComponentVendorUpdate handles vendor update for component.yaml files.
func executeComponentVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
componentType := flg.ComponentType
if componentType == "" {
componentType = "terraform"
}
config, path, err := ReadAndProcessComponentVendorConfigFile(
atmosConfig,
flg.Component,
componentType,
)
if err != nil {
return err
}
// Create a source from the component config
source := schema.AtmosVendorSource{
Component: flg.Component,
Source: config.Spec.Source.Uri,
Version: config.Spec.Source.Version,
}
// Check for updates
sources := []schema.AtmosVendorSource{source}
fileMap := map[string]string{flg.Component: path + "/component.yaml"}
return checkAndUpdateVendorVersions(sources, fileMap, flg.DryRun, path+"/component.yaml")
}
// processVendorImportsWithFileTracking processes imports and tracks which file each source comes from.
func processVendorImportsWithFileTracking(
atmosConfig *schema.AtmosConfiguration,
vendorConfigFile string,
imports []string,
sources []schema.AtmosVendorSource,
allImports []string,
) ([]schema.AtmosVendorSource, map[string]string, error) {
// This will map component names to their source files
fileMap := make(map[string]string)
// Process imports recursively
allSources, _, err := processVendorImports(
atmosConfig,
vendorConfigFile,
imports,
sources,
allImports,
)
if err != nil {
return nil, nil, err
}
// Build the file map from the sources
for i := range allSources {
source := &allSources[i]
componentName := source.Component
if componentName == "" {
componentName = extractComponentNameFromSource(source.Source)
}
// Use the File field that was set during import processing
if source.File != "" {
fileMap[componentName] = source.File
} else {
fileMap[componentName] = vendorConfigFile
}
}
return allSources, fileMap, nil
}
// vendorUpdateHelper contains helper methods for vendor update operations.
type vendorUpdateHelper struct{}
// newVendorUpdateHelper creates a new vendor update helper.
func newVendorUpdateHelper() *vendorUpdateHelper {
return &vendorUpdateHelper{}
}
// prepareDiffPackages converts vendor sources to diff packages for the TUI.
func (h *vendorUpdateHelper) prepareDiffPackages(sources []schema.AtmosVendorSource) []pkgVendorDiff {
diffPackages := make([]pkgVendorDiff, 0, len(sources))
for i := range sources {
componentName := h.extractComponentName(&sources[i])
currentVersion := h.extractCurrentVersion(&sources[i])
// Skip templated versions
if h.isTemplatedVersion(currentVersion) {
// Skip silently - logging happens elsewhere
continue
}
diffPackages = append(diffPackages, pkgVendorDiff{
name: componentName,
currentVersion: currentVersion,
source: sources[i],
outdatedOnly: false,
})
}
return diffPackages
}
// extractComponentName extracts the component name from a vendor source.
func (h *vendorUpdateHelper) extractComponentName(source *schema.AtmosVendorSource) string {
if source.Component != "" {
return source.Component
}
return extractComponentNameFromSource(source.Source)
}
// extractCurrentVersion extracts the current version from a vendor source.
func (h *vendorUpdateHelper) extractCurrentVersion(source *schema.AtmosVendorSource) string {
if source.Version != "" {
return source.Version
}
return defaultVersionLatest
}
// isTemplatedVersion checks if a version contains template markers.
func (h *vendorUpdateHelper) isTemplatedVersion(version string) bool {
return strings.Contains(version, templateStartMarker)
}
// groupUpdatesByFile organizes version updates by configuration file.
func (h *vendorUpdateHelper) groupUpdatesByFile(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
mainConfigFile string,
) map[string]map[string]string {
updatesByFile := make(map[string]map[string]string)
fmt.Println("\nChecking for version updates...")
for i := range sources {
componentName := h.extractComponentName(&sources[i])
// Skip templated versions
if h.isTemplatedVersion(sources[i].Version) {
continue
}
// Check for updates
updateAvailable, latestVersion, err := checkForVendorUpdates(&sources[i], true)
if err != nil {
// Skip silently - error will be logged elsewhere
continue
}
if updateAvailable && latestVersion != "" {
configFile := h.determineConfigFile(componentName, fileMap, mainConfigFile)
h.addUpdateToFile(updatesByFile, configFile, componentName, latestVersion)
}
}
return updatesByFile
}
// determineConfigFile determines which configuration file to update.
func (h *vendorUpdateHelper) determineConfigFile(
componentName string,
fileMap map[string]string,
mainConfigFile string,
) string {
if configFile, exists := fileMap[componentName]; exists {
return configFile
}
return mainConfigFile
}
// addUpdateToFile adds an update to the appropriate file's update map.
func (h *vendorUpdateHelper) addUpdateToFile(
updatesByFile map[string]map[string]string,
configFile string,
componentName string,
latestVersion string,
) {
if updatesByFile[configFile] == nil {
updatesByFile[configFile] = make(map[string]string)
}
updatesByFile[configFile][componentName] = latestVersion
}
// applyUpdatesToFiles applies version updates to configuration files.
func (h *vendorUpdateHelper) applyUpdatesToFiles(updatesByFile map[string]map[string]string) error {
totalUpdates := 0
for configFile, updates := range updatesByFile {
if len(updates) == 0 {
continue
}
fmt.Printf("\nUpdating %d components in %s...\n", len(updates), configFile)
if err := updateVendorConfigFile(updates, configFile); err != nil {
return fmt.Errorf("error updating %s: %w", configFile, err)
}
totalUpdates += len(updates)
}
h.printUpdateSummary(totalUpdates, len(updatesByFile))
return nil
}
// printUpdateSummary prints a summary of the updates performed.
func (h *vendorUpdateHelper) printUpdateSummary(totalUpdates int, fileCount int) {
if totalUpdates > 0 {
fmt.Printf("\nSuccessfully updated %d components across %d files\n", totalUpdates, fileCount)
} else {
fmt.Println("\nAll vendor dependencies are up to date!")
}
}
// printOperationHeader prints the appropriate header for the operation.
func (h *vendorUpdateHelper) printOperationHeader(dryRun bool) {
if dryRun {
fmt.Println("Checking for vendor updates...")
} else {
fmt.Println("Updating vendor configurations...")
}
fmt.Println()
}
// checkAndUpdateVendorVersions checks for version updates and optionally updates the files.
func checkAndUpdateVendorVersions(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
dryRun bool,
mainConfigFile string,
) error {
helper := newVendorUpdateHelper()
// Print operation header
helper.printOperationHeader(dryRun)
// Prepare diff packages for version checking
diffPackages := helper.prepareDiffPackages(sources)
// Execute version check using the TUI model
if err := executeVendorModel(diffPackages, true, &schema.AtmosConfiguration{}); err != nil {
return fmt.Errorf("failed to check vendor updates: %w", err)
}
// If dry-run, we're done
if dryRun {
return nil
}
// Group updates by configuration file
updatesByFile := helper.groupUpdatesByFile(sources, fileMap, mainConfigFile)
// Apply updates to files
// vendor update doesn't use stack flag.
processStacks := false
atmosConfig, err := cfg.InitCliConfig(info, processStacks)
if err != nil {
return schema.AtmosConfiguration{}, fmt.Errorf("failed to initialize CLI config: %w", err)
}
return atmosConfig, nil
}
// parseVendorUpdateFlags parses and validates vendor update flags.
func parseVendorUpdateFlags(cmd *cobra.Command) (*VendorFlags, error) {
flags := cmd.Flags()
checkOnly, err := flags.GetBool("check")
if err != nil {
return nil, err
}
component, err := flags.GetString("component")
if err != nil {
return nil, err
}
tagsCsv, err := flags.GetString("tags")
if err != nil {
return nil, err
}
var tags []string
if tagsCsv != "" {
tags = strings.Split(tagsCsv, ",")
}
componentType, err := flags.GetString("type")
if err != nil {
return nil, err
}
vendorFlags := &VendorFlags{
Component: component,
Tags: tags,
ComponentType: componentType,
DryRun: checkOnly, // --check flag means dry-run
Update: !checkOnly, // Update unless --check is set
}
// Validate flags.
if err := validateVendorUpdateFlags(vendorFlags); err != nil {
return nil, err
}
return vendorFlags, nil
}
// executeVendorPullIfRequested executes vendor pull if the --pull flag is set.
func executeVendorPullIfRequested(cmd *cobra.Command, args []string, vendorFlags *VendorFlags) error {
flags := cmd.Flags()
pullAfterUpdate, err := flags.GetBool("pull")
if err != nil {
return err
}
if !pullAfterUpdate || vendorFlags.DryRun {
return nil
}
log.Info("Executing vendor pull for updated components...")
// Get original flag values for passing to pull command.
tagsCsv, _ := flags.GetString("tags")
// Create a new command for vendor pull with same filters.
pullCmd := &cobra.Command{}
pullCmd.Flags().StringP("component", "c", vendorFlags.Component, "")
pullCmd.Flags().String("tags", tagsCsv, "")
pullCmd.Flags().StringP("type", "t", vendorFlags.ComponentType, "")
if err := ExecuteVendorPullCommand(pullCmd, args); err != nil {
return fmt.Errorf("version references updated but pull failed: %w", err)
}
log.Info("Successfully updated version references and pulled new component versions")
return nil
}
// validateVendorUpdateFlags validates the vendor update command flags.
func validateVendorUpdateFlags(flg *VendorFlags) error {
// Component and tags can be used together for vendor update.
// No additional validation needed beyond basic checks.
return nil
}
// executeVendorUpdate performs the actual vendor update logic.
func executeVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
// Determine the vendor config file path.
vendorConfigFileName := cfg.AtmosVendorConfigFileName
if atmosConfig.Vendor.BasePath != "" {
vendorConfigFileName = atmosConfig.Vendor.BasePath
}
// Read the main vendor config.
vendorConfig, vendorConfigExists, foundVendorConfigFile, err := ReadAndProcessVendorConfigFile(
atmosConfig,
vendorConfigFileName,
true,
)
if err != nil {
return err
}
if !vendorConfigExists {
// Try component vendor config if no main vendor config.
if flg.Component != "" {
return executeComponentVendorUpdate(atmosConfig, flg)
}
return fmt.Errorf("%w: %s", errUtils.ErrVendorConfigFileNotFound, vendorConfigFileName)
}
// Process all imports and get sources with file mappings.
sources, importedFiles, err := processVendorImportsWithFileTracking(
atmosConfig,
foundVendorConfigFile,
vendorConfig.Spec.Imports,
vendorConfig.Spec.Sources,
[]string{foundVendorConfigFile},
)
if err != nil {
return err
}
if len(sources) == 0 {
fmt.Println("No vendor sources configured")
return nil
}
// Filter sources based on component and tags.
filteredSources := filterSources(sources, flg.Component, flg.Tags)
if len(filteredSources) == 0 {
reportNoSourcesFound(flg)
return nil
}
// Check for updates and display results.
return checkAndUpdateVendorVersions(filteredSources, importedFiles, flg.DryRun, foundVendorConfigFile)
}
// executeComponentVendorUpdate handles vendor update for component.yaml files.
func executeComponentVendorUpdate(atmosConfig *schema.AtmosConfiguration, flg *VendorFlags) error {
componentType := flg.ComponentType
if componentType == "" {
componentType = "terraform"
}
config, path, err := ReadAndProcessComponentVendorConfigFile(
atmosConfig,
flg.Component,
componentType,
)
if err != nil {
return err
}
// Create a source from the component config.
source := schema.AtmosVendorSource{
Component: flg.Component,
Source: config.Spec.Source.Uri,
Version: config.Spec.Source.Version,
}
// Check for updates.
sources := []schema.AtmosVendorSource{source}
fileMap := map[string]string{flg.Component: path + "/component.yaml"}
return checkAndUpdateVendorVersions(sources, fileMap, flg.DryRun, path+"/component.yaml")
}
// processVendorImportsWithFileTracking processes imports and tracks which file each source comes from.
func processVendorImportsWithFileTracking(
atmosConfig *schema.AtmosConfiguration,
vendorConfigFile string,
imports []string,
sources []schema.AtmosVendorSource,
allImports []string,
) ([]schema.AtmosVendorSource, map[string]string, error) {
// This will map component names to their source files.
fileMap := make(map[string]string)
// Process imports recursively.
allSources, _, err := processVendorImports(
atmosConfig,
vendorConfigFile,
imports,
sources,
allImports,
)
if err != nil {
return nil, nil, err
}
// Build the file map from the sources.
for i := range allSources {
source := &allSources[i]
componentName := source.Component
if componentName == "" {
componentName = extractComponentNameFromSource(source.Source)
}
// Use the File field that was set during import processing.
if source.File != "" {
fileMap[componentName] = source.File
} else {
fileMap[componentName] = vendorConfigFile
}
}
return allSources, fileMap, nil
}
// vendorUpdateHelper contains helper methods for vendor update operations.
type vendorUpdateHelper struct{}
// newVendorUpdateHelper creates a new vendor update helper.
func newVendorUpdateHelper() *vendorUpdateHelper {
return &vendorUpdateHelper{}
}
// prepareDiffPackages converts vendor sources to diff packages for the TUI.
func (h *vendorUpdateHelper) prepareDiffPackages(sources []schema.AtmosVendorSource) []pkgVendorDiff {
diffPackages := make([]pkgVendorDiff, 0, len(sources))
for i := range sources {
componentName := h.extractComponentName(&sources[i])
currentVersion := h.extractCurrentVersion(&sources[i])
// Skip templated versions.
if h.isTemplatedVersion(currentVersion) {
// Skip silently - logging happens elsewhere.
continue
}
diffPackages = append(diffPackages, pkgVendorDiff{
name: componentName,
currentVersion: currentVersion,
source: sources[i],
outdatedOnly: false,
})
}
return diffPackages
}
// extractComponentName extracts the component name from a vendor source.
func (h *vendorUpdateHelper) extractComponentName(source *schema.AtmosVendorSource) string {
if source.Component != "" {
return source.Component
}
return extractComponentNameFromSource(source.Source)
}
// extractCurrentVersion extracts the current version from a vendor source.
func (h *vendorUpdateHelper) extractCurrentVersion(source *schema.AtmosVendorSource) string {
if source.Version != "" {
return source.Version
}
return defaultVersionLatest
}
// isTemplatedVersion checks if a version contains template markers.
func (h *vendorUpdateHelper) isTemplatedVersion(version string) bool {
return strings.Contains(version, templateStartMarker)
}
// groupUpdatesByFile organizes version updates by configuration file.
func (h *vendorUpdateHelper) groupUpdatesByFile(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
mainConfigFile string,
) map[string]map[string]string {
updatesByFile := make(map[string]map[string]string)
fmt.Println("\nChecking for version updates...")
for i := range sources {
componentName := h.extractComponentName(&sources[i])
// Skip templated versions.
if h.isTemplatedVersion(sources[i].Version) {
continue
}
// Check for updates.
updateAvailable, latestVersion, err := checkForVendorUpdates(&sources[i], true)
if err != nil {
// Skip silently - error will be logged elsewhere.
continue
}
if updateAvailable && latestVersion != "" {
configFile := h.determineConfigFile(componentName, fileMap, mainConfigFile)
h.addUpdateToFile(updatesByFile, configFile, componentName, latestVersion)
}
}
return updatesByFile
}
// determineConfigFile determines which configuration file to update.
func (h *vendorUpdateHelper) determineConfigFile(
componentName string,
fileMap map[string]string,
mainConfigFile string,
) string {
if configFile, exists := fileMap[componentName]; exists {
return configFile
}
return mainConfigFile
}
// addUpdateToFile adds an update to the appropriate file's update map.
func (h *vendorUpdateHelper) addUpdateToFile(
updatesByFile map[string]map[string]string,
configFile string,
componentName string,
latestVersion string,
) {
if updatesByFile[configFile] == nil {
updatesByFile[configFile] = make(map[string]string)
}
updatesByFile[configFile][componentName] = latestVersion
}
// applyUpdatesToFiles applies version updates to configuration files.
func (h *vendorUpdateHelper) applyUpdatesToFiles(updatesByFile map[string]map[string]string) error {
totalUpdates := 0
for configFile, updates := range updatesByFile {
if len(updates) == 0 {
continue
}
fmt.Printf("\nUpdating %d components in %s...\n", len(updates), configFile)
if err := updateVendorConfigFile(updates, configFile); err != nil {
return fmt.Errorf("error updating %s: %w", configFile, err)
}
totalUpdates += len(updates)
}
h.printUpdateSummary(totalUpdates, len(updatesByFile))
return nil
}
// printUpdateSummary prints a summary of the updates performed.
func (h *vendorUpdateHelper) printUpdateSummary(totalUpdates int, fileCount int) {
if totalUpdates > 0 {
fmt.Printf("\nSuccessfully updated %d components across %d files\n", totalUpdates, fileCount)
} else {
fmt.Println("\nAll vendor dependencies are up to date!")
}
}
// printOperationHeader prints the appropriate header for the operation.
func (h *vendorUpdateHelper) printOperationHeader(dryRun bool) {
if dryRun {
fmt.Println("Checking for vendor updates...")
} else {
fmt.Println("Updating vendor configurations...")
}
fmt.Println()
}
// checkAndUpdateVendorVersions checks for version updates and optionally updates the files.
func checkAndUpdateVendorVersions(
sources []schema.AtmosVendorSource,
fileMap map[string]string,
dryRun bool,
mainConfigFile string,
) error {
helper := newVendorUpdateHelper()
// Print operation header.
helper.printOperationHeader(dryRun)
// Prepare diff packages for version checking.
diffPackages := helper.prepareDiffPackages(sources)
// Execute version check using the TUI model.
if err := executeVendorModel(diffPackages, true, &schema.AtmosConfiguration{}); err != nil {
return fmt.Errorf("failed to check vendor updates: %w", err)
}
// If dry-run, we're done.
if dryRun {
return nil
}
// Group updates by configuration file.
updatesByFile := helper.groupUpdatesByFile(sources, fileMap, mainConfigFile)
// Apply updates to files.
🤖 Prompt for AI Agents
internal/exec/vendor_update.go lines 47-450: several inline comment sentences in
this range end without a period; update each inline comment to end with a period
to match project style (for example comments like "// This will map component
names to their source files" should become "// This will map component names to
their source files."). Make a pass through the file and add a trailing period to
all inline comment sentences, then run the repo linter/formatter to ensure no
other style issues remain.

Comment on lines +23 to +179
// Read the file
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}

// Update versions in content
updatedContent, err := u.UpdateVersionsInContent(content, updates)
if err != nil {
return fmt.Errorf("failed to update versions: %w", err)
}

// Write the file back
return os.WriteFile(filePath, updatedContent, vendorDefaultFilePermissions)
}

// UpdateVersionsInContent updates component versions in YAML content while preserving structure.
func (u *YAMLVersionUpdater) UpdateVersionsInContent(content []byte, updates map[string]string) ([]byte, error) {
if len(updates) == 0 {
return content, nil
}

// Parse YAML to AST to preserve structure, comments, and anchors
file, err := parser.ParseBytes(content, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}

// Check if we have documents to process
if !u.hasValidDocument(file) {
return content, nil
}

// Apply updates and encode back to YAML
return u.applyUpdatesAndEncode(file, updates)
}

// hasValidDocument checks if the file has a valid document to process.
func (u *YAMLVersionUpdater) hasValidDocument(file *ast.File) bool {
return len(file.Docs) > 0 && file.Docs[0] != nil && file.Docs[0].Body != nil
}

// applyUpdatesAndEncode applies updates to the AST and encodes back to YAML.
func (u *YAMLVersionUpdater) applyUpdatesAndEncode(file *ast.File, updates map[string]string) ([]byte, error) {
// Track components and their anchors for proper updates
componentNodes := u.findComponentNodes(file.Docs[0].Body)

// Apply updates to the AST
u.applyUpdatesToNodes(componentNodes, updates)

// Convert AST back to YAML bytes
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)

if err := encoder.Encode(file.Docs[0].Body); err != nil {
return nil, fmt.Errorf("failed to encode YAML: %w", err)
}

return buf.Bytes(), nil
}

// applyUpdatesToNodes applies version updates to the component nodes.
func (u *YAMLVersionUpdater) applyUpdatesToNodes(componentNodes map[string][]*ast.MappingNode, updates map[string]string) {
for componentName, newVersion := range updates {
if nodes, exists := componentNodes[componentName]; exists {
for _, node := range nodes {
u.updateVersionInNode(node, newVersion)
}
}
}
}

// findComponentNodes finds all nodes that define components in the YAML AST.
func (u *YAMLVersionUpdater) findComponentNodes(node ast.Node) map[string][]*ast.MappingNode {
components := make(map[string][]*ast.MappingNode)
u.walkAST(node, func(n ast.Node) bool {
if mapping, ok := n.(*ast.MappingNode); ok {
componentName := u.extractComponentName(mapping)
if componentName != "" {
components[componentName] = append(components[componentName], mapping)
}
}
return true
})
return components
}

// extractComponentName extracts the component name from a mapping node.
func (u *YAMLVersionUpdater) extractComponentName(mapping *ast.MappingNode) string {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "component" {
if valueNode, ok := value.Value.(*ast.StringNode); ok {
return valueNode.Value
}
}
}
return ""
}

// updateVersionInNode updates the version field in a mapping node.
func (u *YAMLVersionUpdater) updateVersionInNode(mapping *ast.MappingNode, newVersion string) {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "version" {
// Update the version value
if valueNode, ok := value.Value.(*ast.StringNode); ok {
valueNode.Value = newVersion
} else {
// Create new string node if version wasn't a string
value.Value = &ast.StringNode{
Value: newVersion,
}
}
return
}
}

// If no version field exists, add one
u.addVersionField(mapping, newVersion)
}

// addVersionField adds a version field to a mapping node.
func (u *YAMLVersionUpdater) addVersionField(mapping *ast.MappingNode, version string) {
versionKey := &ast.StringNode{Value: "version"}
versionValue := &ast.StringNode{Value: version}

mapping.Values = append(mapping.Values, &ast.MappingValueNode{
Key: versionKey,
Value: versionValue,
})
}

// walkAST walks the AST and calls the visitor function for each node.
func (u *YAMLVersionUpdater) walkAST(node ast.Node, visitor func(ast.Node) bool) {
if node == nil || !visitor(node) {
return
}

u.walkNodeChildren(node, visitor)
}

// walkNodeChildren walks the children of a node based on its type.
func (u *YAMLVersionUpdater) walkNodeChildren(node ast.Node, visitor func(ast.Node) bool) {
switch n := node.(type) {
case *ast.DocumentNode:
u.walkAST(n.Body, visitor)
case *ast.MappingNode:
u.walkMappingNode(n, visitor)
case *ast.SequenceNode:
u.walkSequenceNode(n, visitor)
case *ast.MappingValueNode:
u.walkAST(n.Key, visitor)
u.walkAST(n.Value, visitor)
case *ast.AnchorNode:
u.walkAST(n.Value, visitor)
case *ast.AliasNode:
// Aliases are references, don't walk into them
}

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.

🛠️ Refactor suggestion | 🟠 Major

Bring every comment in line with the period requirement.

Several comments here still end without a period, which violates the repository guideline. Touch them up while you’re in this file.

@@
-	// Read the file
+	// Read the file.
@@
-	// Update versions in content
+	// Update versions in content.
@@
-	// Write the file back
+	// Write the file back.
@@
-	// Parse YAML to AST to preserve structure, comments, and anchors
+	// Parse YAML to AST to preserve structure, comments, and anchors.
@@
-	// Check if we have documents to process
+	// Check if we have documents to process.
@@
-	// Apply updates and encode back to YAML
+	// Apply updates and encode back to YAML.
@@
-	// Track components and their anchors for proper updates
+	// Track components and their anchors for proper updates.
@@
-	// Apply updates to the AST
+	// Apply updates to the AST.
@@
-	// Convert AST back to YAML bytes
+	// Convert AST back to YAML bytes.
@@
-			// Update the version value
+			// Update the version value.
@@
-				// Create new string node if version wasn't a string
+				// Create new string node if version wasn't a string.
@@
-	// If no version field exists, add one
+	// If no version field exists, add one.
@@
-		// Aliases are references, don't walk into them
+		// Aliases are references, don't walk into them.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Read the file
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}
// Update versions in content
updatedContent, err := u.UpdateVersionsInContent(content, updates)
if err != nil {
return fmt.Errorf("failed to update versions: %w", err)
}
// Write the file back
return os.WriteFile(filePath, updatedContent, vendorDefaultFilePermissions)
}
// UpdateVersionsInContent updates component versions in YAML content while preserving structure.
func (u *YAMLVersionUpdater) UpdateVersionsInContent(content []byte, updates map[string]string) ([]byte, error) {
if len(updates) == 0 {
return content, nil
}
// Parse YAML to AST to preserve structure, comments, and anchors
file, err := parser.ParseBytes(content, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
// Check if we have documents to process
if !u.hasValidDocument(file) {
return content, nil
}
// Apply updates and encode back to YAML
return u.applyUpdatesAndEncode(file, updates)
}
// hasValidDocument checks if the file has a valid document to process.
func (u *YAMLVersionUpdater) hasValidDocument(file *ast.File) bool {
return len(file.Docs) > 0 && file.Docs[0] != nil && file.Docs[0].Body != nil
}
// applyUpdatesAndEncode applies updates to the AST and encodes back to YAML.
func (u *YAMLVersionUpdater) applyUpdatesAndEncode(file *ast.File, updates map[string]string) ([]byte, error) {
// Track components and their anchors for proper updates
componentNodes := u.findComponentNodes(file.Docs[0].Body)
// Apply updates to the AST
u.applyUpdatesToNodes(componentNodes, updates)
// Convert AST back to YAML bytes
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
if err := encoder.Encode(file.Docs[0].Body); err != nil {
return nil, fmt.Errorf("failed to encode YAML: %w", err)
}
return buf.Bytes(), nil
}
// applyUpdatesToNodes applies version updates to the component nodes.
func (u *YAMLVersionUpdater) applyUpdatesToNodes(componentNodes map[string][]*ast.MappingNode, updates map[string]string) {
for componentName, newVersion := range updates {
if nodes, exists := componentNodes[componentName]; exists {
for _, node := range nodes {
u.updateVersionInNode(node, newVersion)
}
}
}
}
// findComponentNodes finds all nodes that define components in the YAML AST.
func (u *YAMLVersionUpdater) findComponentNodes(node ast.Node) map[string][]*ast.MappingNode {
components := make(map[string][]*ast.MappingNode)
u.walkAST(node, func(n ast.Node) bool {
if mapping, ok := n.(*ast.MappingNode); ok {
componentName := u.extractComponentName(mapping)
if componentName != "" {
components[componentName] = append(components[componentName], mapping)
}
}
return true
})
return components
}
// extractComponentName extracts the component name from a mapping node.
func (u *YAMLVersionUpdater) extractComponentName(mapping *ast.MappingNode) string {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "component" {
if valueNode, ok := value.Value.(*ast.StringNode); ok {
return valueNode.Value
}
}
}
return ""
}
// updateVersionInNode updates the version field in a mapping node.
func (u *YAMLVersionUpdater) updateVersionInNode(mapping *ast.MappingNode, newVersion string) {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "version" {
// Update the version value
if valueNode, ok := value.Value.(*ast.StringNode); ok {
valueNode.Value = newVersion
} else {
// Create new string node if version wasn't a string
value.Value = &ast.StringNode{
Value: newVersion,
}
}
return
}
}
// If no version field exists, add one
u.addVersionField(mapping, newVersion)
}
// addVersionField adds a version field to a mapping node.
func (u *YAMLVersionUpdater) addVersionField(mapping *ast.MappingNode, version string) {
versionKey := &ast.StringNode{Value: "version"}
versionValue := &ast.StringNode{Value: version}
mapping.Values = append(mapping.Values, &ast.MappingValueNode{
Key: versionKey,
Value: versionValue,
})
}
// walkAST walks the AST and calls the visitor function for each node.
func (u *YAMLVersionUpdater) walkAST(node ast.Node, visitor func(ast.Node) bool) {
if node == nil || !visitor(node) {
return
}
u.walkNodeChildren(node, visitor)
}
// walkNodeChildren walks the children of a node based on its type.
func (u *YAMLVersionUpdater) walkNodeChildren(node ast.Node, visitor func(ast.Node) bool) {
switch n := node.(type) {
case *ast.DocumentNode:
u.walkAST(n.Body, visitor)
case *ast.MappingNode:
u.walkMappingNode(n, visitor)
case *ast.SequenceNode:
u.walkSequenceNode(n, visitor)
case *ast.MappingValueNode:
u.walkAST(n.Key, visitor)
u.walkAST(n.Value, visitor)
case *ast.AnchorNode:
u.walkAST(n.Value, visitor)
case *ast.AliasNode:
// Aliases are references, don't walk into them
}
// Read the file.
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}
// Update versions in content.
updatedContent, err := u.UpdateVersionsInContent(content, updates)
if err != nil {
return fmt.Errorf("failed to update versions: %w", err)
}
// Write the file back.
return os.WriteFile(filePath, updatedContent, vendorDefaultFilePermissions)
}
// UpdateVersionsInContent updates component versions in YAML content while preserving structure.
func (u *YAMLVersionUpdater) UpdateVersionsInContent(content []byte, updates map[string]string) ([]byte, error) {
if len(updates) == 0 {
return content, nil
}
// Parse YAML to AST to preserve structure, comments, and anchors.
file, err := parser.ParseBytes(content, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
// Check if we have documents to process.
if !u.hasValidDocument(file) {
return content, nil
}
// Apply updates and encode back to YAML.
return u.applyUpdatesAndEncode(file, updates)
}
// hasValidDocument checks if the file has a valid document to process.
func (u *YAMLVersionUpdater) hasValidDocument(file *ast.File) bool {
return len(file.Docs) > 0 && file.Docs[0] != nil && file.Docs[0].Body != nil
}
// applyUpdatesAndEncode applies updates to the AST and encodes back to YAML.
func (u *YAMLVersionUpdater) applyUpdatesAndEncode(file *ast.File, updates map[string]string) ([]byte, error) {
// Track components and their anchors for proper updates.
componentNodes := u.findComponentNodes(file.Docs[0].Body)
// Apply updates to the AST.
u.applyUpdatesToNodes(componentNodes, updates)
// Convert AST back to YAML bytes.
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
if err := encoder.Encode(file.Docs[0].Body); err != nil {
return nil, fmt.Errorf("failed to encode YAML: %w", err)
}
return buf.Bytes(), nil
}
// applyUpdatesToNodes applies version updates to the component nodes.
func (u *YAMLVersionUpdater) applyUpdatesToNodes(componentNodes map[string][]*ast.MappingNode, updates map[string]string) {
for componentName, newVersion := range updates {
if nodes, exists := componentNodes[componentName]; exists {
for _, node := range nodes {
u.updateVersionInNode(node, newVersion)
}
}
}
}
// findComponentNodes finds all nodes that define components in the YAML AST.
func (u *YAMLVersionUpdater) findComponentNodes(node ast.Node) map[string][]*ast.MappingNode {
components := make(map[string][]*ast.MappingNode)
u.walkAST(node, func(n ast.Node) bool {
if mapping, ok := n.(*ast.MappingNode); ok {
componentName := u.extractComponentName(mapping)
if componentName != "" {
components[componentName] = append(components[componentName], mapping)
}
}
return true
})
return components
}
// extractComponentName extracts the component name from a mapping node.
func (u *YAMLVersionUpdater) extractComponentName(mapping *ast.MappingNode) string {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "component" {
if valueNode, ok := value.Value.(*ast.StringNode); ok {
return valueNode.Value
}
}
}
return ""
}
// updateVersionInNode updates the version field in a mapping node.
func (u *YAMLVersionUpdater) updateVersionInNode(mapping *ast.MappingNode, newVersion string) {
for _, value := range mapping.Values {
if keyNode, ok := value.Key.(*ast.StringNode); ok && keyNode.Value == "version" {
// Update the version value.
if valueNode, ok := value.Value.(*ast.StringNode); ok {
valueNode.Value = newVersion
} else {
// Create new string node if version wasn't a string.
value.Value = &ast.StringNode{
Value: newVersion,
}
}
return
}
}
// If no version field exists, add one.
u.addVersionField(mapping, newVersion)
}
// addVersionField adds a version field to a mapping node.
func (u *YAMLVersionUpdater) addVersionField(mapping *ast.MappingNode, version string) {
versionKey := &ast.StringNode{Value: "version"}
versionValue := &ast.StringNode{Value: version}
mapping.Values = append(mapping.Values, &ast.MappingValueNode{
Key: versionKey,
Value: versionValue,
})
}
// walkAST walks the AST and calls the visitor function for each node.
func (u *YAMLVersionUpdater) walkAST(node ast.Node, visitor func(ast.Node) bool) {
if node == nil || !visitor(node) {
return
}
u.walkNodeChildren(node, visitor)
}
// walkNodeChildren walks the children of a node based on its type.
func (u *YAMLVersionUpdater) walkNodeChildren(node ast.Node, visitor func(ast.Node) bool) {
switch n := node.(type) {
case *ast.DocumentNode:
u.walkAST(n.Body, visitor)
case *ast.MappingNode:
u.walkMappingNode(n, visitor)
case *ast.SequenceNode:
u.walkSequenceNode(n, visitor)
case *ast.MappingValueNode:
u.walkAST(n.Key, visitor)
u.walkAST(n.Value, visitor)
case *ast.AnchorNode:
u.walkAST(n.Value, visitor)
case *ast.AliasNode:
// Aliases are references, don't walk into them.
}
}
🤖 Prompt for AI Agents
internal/exec/vendor_yaml_updater.go lines 23-179: several inline comments
(e.g., "// Read the file", "// Update versions in content", "// Write the file
back", "// Check if we have documents to process", "// Apply updates and encode
back to YAML", "// Track components and their anchors for proper updates", "//
Apply updates to the AST", "// Convert AST back to YAML bytes", "// Apply
version updates to the component nodes.", "// Find all nodes that define
components in the YAML AST.", "// Extract the component name from a mapping
node.", "// Update the version field in a mapping node.", "// If no version
field exists, add one", "// Add a version field to a mapping node.", "// Walk
the AST and call the visitor function for each node.", and "// Walk the children
of a node based on its type." are missing trailing periods; update each comment
to end with a period so they conform to the repository guideline (ensure every
single-line comment in this range ends with a period, preserving original
wording and capitalization).

Comment on lines +24 to +82
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}

// Update versions in content
updatedContent, err := u.UpdateVersionsInContent(content, updates)
if err != nil {
return fmt.Errorf("failed to update versions: %w", err)
}

// Write the file back
return os.WriteFile(filePath, updatedContent, vendorDefaultFilePermissions)
}

// UpdateVersionsInContent updates component versions in YAML content while preserving structure.
func (u *YAMLVersionUpdater) UpdateVersionsInContent(content []byte, updates map[string]string) ([]byte, error) {
if len(updates) == 0 {
return content, nil
}

// Parse YAML to AST to preserve structure, comments, and anchors
file, err := parser.ParseBytes(content, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}

// Check if we have documents to process
if !u.hasValidDocument(file) {
return content, nil
}

// Apply updates and encode back to YAML
return u.applyUpdatesAndEncode(file, updates)
}

// hasValidDocument checks if the file has a valid document to process.
func (u *YAMLVersionUpdater) hasValidDocument(file *ast.File) bool {
return len(file.Docs) > 0 && file.Docs[0] != nil && file.Docs[0].Body != nil
}

// applyUpdatesAndEncode applies updates to the AST and encodes back to YAML.
func (u *YAMLVersionUpdater) applyUpdatesAndEncode(file *ast.File, updates map[string]string) ([]byte, error) {
// Track components and their anchors for proper updates
componentNodes := u.findComponentNodes(file.Docs[0].Body)

// Apply updates to the AST
u.applyUpdatesToNodes(componentNodes, updates)

// Convert AST back to YAML bytes
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)

if err := encoder.Encode(file.Docs[0].Body); err != nil {
return nil, fmt.Errorf("failed to encode YAML: %w", err)
}

return buf.Bytes(), 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.

⚠️ Potential issue | 🟠 Major

Wrap returned errors with static error constants.

Per the coding guidelines we must wrap every returned error with the appropriate static sentinel from errors/errors.go using fmt.Errorf("%w: …", errSentinel, err). The new UpdateVersionsInFile/UpdateVersionsInContent/applyUpdatesAndEncode paths currently return dynamically formatted errors (fmt.Errorf("failed to …"), raw err) without the sentinel, which breaks the contract and makes upstream error handling inconsistent. Please introduce/ reuse the relevant static error(s) in errors/errors.go (e.g., dedicated read/parse/encode failures) and wrap each return from these helpers accordingly. This applies to the read, parse, encode, and write error branches in this file. Based on coding guidelines.

Comment thread internal/exec/vendor.go
Comment on lines +429 to +437
if !u.FileExists(uri) {
return false, currentVersion, fmt.Errorf("%w: %s", errUtils.ErrLocalSourceDoesNotExist, uri)
}

// Get file/directory info using the same patterns
info, err := os.Stat(uri)
if err != nil {
return false, currentVersion, fmt.Errorf("failed to stat local source: %w", err)
}

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.

⚠️ Potential issue | 🔴 Critical

Local directory sources always fail version checks.

u.FileExists only returns true for regular files, so every local directory source (the dominant case for vendoring local components) immediately trips ErrLocalSourceDoesNotExist and aborts the diff/update. Please rely on os.Stat instead and translate os.IsNotExist to the static error so directories are handled correctly.

-	if !u.FileExists(uri) {
-		return false, currentVersion, fmt.Errorf("%w: %s", errUtils.ErrLocalSourceDoesNotExist, uri)
-	}
-
-	// Get file/directory info using the same patterns
-	info, err := os.Stat(uri)
-	if err != nil {
-		return false, currentVersion, fmt.Errorf("failed to stat local source: %w", err)
-	}
+	info, err := os.Stat(uri)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, currentVersion, fmt.Errorf(errUtils.ErrStringWrappingFormat, errUtils.ErrLocalSourceDoesNotExist, uri)
+		}
+		return false, currentVersion, fmt.Errorf(errUtils.ErrWrappingFormat, errUtils.ErrCheckingForUpdates, err)
+	}
🤖 Prompt for AI Agents
In internal/exec/vendor.go around lines 429 to 437, the current check uses
u.FileExists which only returns true for regular files and therefore incorrectly
treats local directories as missing; replace that pre-check with a call to
os.Stat(uri) and if os.Stat returns an error, map os.IsNotExist(err) to return
(false, currentVersion, fmt.Errorf("%w: %s",
errUtils.ErrLocalSourceDoesNotExist, uri)) and for other errors return the
existing "failed to stat local source" wrapped error; this ensures directories
are detected correctly while preserving existing error handling.

@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: 2

🧹 Nitpick comments (2)
internal/exec/vendor_utils.go (1)

566-570: Skip the rewrite when there’s nothing to update. Short-circuiting on an empty map avoids needless disk churn (and potential timestamp noise) when no versions changed.

 func updateVendorConfigFile(updatedVersions map[string]string, vendorConfigFile string) error {
+	if len(updatedVersions) == 0 {
+		return nil
+	}
 	// Use the simple YAML updater that preserves formatting and comments
 	updater := NewSimpleYAMLVersionUpdater()
 	return updater.UpdateVersionsInFile(vendorConfigFile, updatedVersions)
 }
internal/exec/vendor_yaml_updater_test.go (1)

173-175: Let’s re-enable the anchor case. The new simple updater no longer hits the goccy panic, so we can drop the skip and let the test guarantee anchors stay intact.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 517a732 and 91106aa.

📒 Files selected for processing (10)
  • cmd/root.go (1 hunks)
  • internal/exec/vendor_update_test.go (1 hunks)
  • internal/exec/vendor_utils.go (2 hunks)
  • internal/exec/vendor_yaml_updater.go (1 hunks)
  • internal/exec/vendor_yaml_updater_simple.go (1 hunks)
  • internal/exec/vendor_yaml_updater_test.go (1 hunks)
  • pkg/schema/schema.go (2 hunks)
  • test-failures-summary.md (1 hunks)
  • test-fixes-completed.md (1 hunks)
  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • cmd/root.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/schema/schema.go
🧰 Additional context used
📓 Path-based instructions (5)
**/*.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

**/*.go: All code must pass golangci-lint checks
Follow Go error handling idioms and use meaningful error messages
Wrap errors with context using fmt.Errorf("context: %w", err)
Consider custom error types for domain-specific errors
Follow standard Go coding style; run gofmt and goimports
Use snake_case for environment variables
Document complex logic with inline comments

**/*.go: All comments must end with periods.
Wrap all returned errors with static errors from errors/errors.go; never return dynamic errors directly.
Use fmt.Errorf with %w to wrap the static error first, then add context details.
Always bind environment variables via viper.BindEnv, providing an ATMOS_ alternative for each external var.
All new configurations must support Go templating using the shared FuncMap(); test template rendering with various contexts.
Prefer SDKs over shelling out to binaries; use standard library for cross-platform behavior (filepath.Join, os.PathSeparator, runtime.GOOS).
For non-standard execution paths, capture telemetry via telemetry.CaptureCmd or telemetry.CaptureCmdString without user data.

Files:

  • internal/exec/vendor_yaml_updater_simple.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_utils.go
**/!(*_test).go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

Document all exported functions, types, and methods with Go doc comments

Files:

  • internal/exec/vendor_yaml_updater_simple.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_utils.go
internal/exec/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain >80% coverage for core orchestration in internal/exec/.

Files:

  • internal/exec/vendor_yaml_updater_simple.go
  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
  • internal/exec/vendor_yaml_updater_test.go
  • internal/exec/vendor_utils.go
**/*_test.go

📄 CodeRabbit inference engine (.cursor/rules/atmos-rules.mdc)

**/*_test.go: Every new feature must include comprehensive unit tests
Test both happy paths and error conditions
Use table-driven tests for multiple scenarios

**/*_test.go: Use table-driven tests for unit tests where applicable.
Always use t.Skipf() with a reason; do not use t.Skip() or t.Skipf() without explanation.
TestMain must call os.Exit(m.Run()) to propagate test exit code for CLI tests.

Files:

  • internal/exec/vendor_update_test.go
  • internal/exec/vendor_yaml_updater_test.go
tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place integration tests and shared test utilities under tests/ with fixtures in tests/test-cases/.

Files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
🧠 Learnings (11)
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to internal/exec/stack_processor_utils.go : Update and verify stack processing utilities in internal/exec/stack_processor_utils.go when changing stack processing.

Applied to files:

  • internal/exec/vendor_yaml_updater.go
  • internal/exec/vendor_update_test.go
📚 Learning: 2025-09-23T02:30:42.362Z
Learnt from: CR
PR: cloudposse/atmos#0
File: .cursor/rules/atmos-rules.mdc:0-0
Timestamp: 2025-09-23T02:30:42.362Z
Learning: Applies to **/*.go : Document complex logic with inline comments

Applied to files:

  • internal/exec/vendor_yaml_updater.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to **/*.go : Wrap all returned errors with static errors from errors/errors.go; never return dynamic errors directly.

Applied to files:

  • internal/exec/vendor_yaml_updater.go
📚 Learning: 2025-09-26T05:52:03.918Z
Learnt from: CR
PR: cloudposse/atmos#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-26T05:52:03.918Z
Learning: Applies to **/*.go : Use fmt.Errorf with %w to wrap the static error first, then add context details.

Applied to files:

  • internal/exec/vendor_yaml_updater.go
📚 Learning: 2025-09-23T02:30:42.362Z
Learnt from: CR
PR: cloudposse/atmos#0
File: .cursor/rules/atmos-rules.mdc:0-0
Timestamp: 2025-09-23T02:30:42.362Z
Learning: Applies to **/*.go : Wrap errors with context using fmt.Errorf("context: %w", err)

Applied to files:

  • internal/exec/vendor_yaml_updater.go
📚 Learning: 2025-09-23T02:30:42.362Z
Learnt from: CR
PR: cloudposse/atmos#0
File: .cursor/rules/atmos-rules.mdc:0-0
Timestamp: 2025-09-23T02:30:42.362Z
Learning: Applies to **/*_test.go : Every new feature must include comprehensive unit tests

Applied to files:

  • internal/exec/vendor_update_test.go
📚 Learning: 2024-10-22T23:00:20.627Z
Learnt from: Cerebrovinny
PR: cloudposse/atmos#737
File: internal/exec/vendor_utils.go:131-141
Timestamp: 2024-10-22T23:00:20.627Z
Learning: In the `ReadAndProcessVendorConfigFile` function in `internal/exec/vendor_utils.go`, the existence of the vendor config file is already checked, so additional file existence checks may be unnecessary.

Applied to files:

  • internal/exec/vendor_update_test.go
  • internal/exec/vendor_utils.go
📚 Learning: 2025-03-18T12:26:25.329Z
Learnt from: Listener430
PR: cloudposse/atmos#1149
File: tests/snapshots/TestCLICommands_atmos_vendor_pull_ssh.stderr.golden:7-7
Timestamp: 2025-03-18T12:26:25.329Z
Learning: In the Atmos project, typos or inconsistencies in test snapshot files (such as "terrafrom" instead of "terraform") may be intentional as they capture the exact output of commands and should not be flagged as issues requiring correction.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-02-14T23:12:38.030Z
Learnt from: Listener430
PR: cloudposse/atmos#1061
File: tests/snapshots/TestCLICommands_atmos_vendor_pull_ssh.stderr.golden:8-8
Timestamp: 2025-02-14T23:12:38.030Z
Learning: Test snapshots in the Atmos project, particularly for dry run scenarios, may be updated during the development process, and temporary inconsistencies in their content should not be flagged as issues.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-09-10T21:17:55.273Z
Learnt from: samtholiya
PR: cloudposse/atmos#1466
File: toolchain/http_client_test.go:3-10
Timestamp: 2025-09-10T21:17:55.273Z
Learning: In the cloudposse/atmos repository, imports should never be changed as per samtholiya's coding guidelines.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
📚 Learning: 2025-09-13T16:39:20.007Z
Learnt from: samtholiya
PR: cloudposse/atmos#1466
File: cmd/markdown/atmos_toolchain_aliases.md:2-4
Timestamp: 2025-09-13T16:39:20.007Z
Learning: In the cloudposse/atmos repository, CLI documentation files in cmd/markdown/ follow a specific format that uses " $ atmos command" (with leading space and dollar sign prompt) in code blocks. This is the established project convention and should not be changed to comply with standard markdownlint rules MD040 and MD014.

Applied to files:

  • tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden
🧬 Code graph analysis (3)
internal/exec/vendor_update_test.go (4)
pkg/schema/schema.go (1)
  • AtmosVendorSource (708-717)
cmd/cmd_utils.go (1)
  • Contains (750-757)
internal/exec/vendor_yaml_updater_simple.go (1)
  • NewSimpleYAMLVersionUpdater (17-19)
internal/exec/vendor.go (1)
  • VendorFlags (103-112)
internal/exec/vendor_yaml_updater_test.go (1)
internal/exec/vendor_yaml_updater_simple.go (1)
  • NewSimpleYAMLVersionUpdater (17-19)
internal/exec/vendor_utils.go (1)
internal/exec/vendor_yaml_updater_simple.go (1)
  • NewSimpleYAMLVersionUpdater (17-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Build (windows-latest, windows)
  • GitHub Check: Lint (golangci)
  • GitHub Check: Analyze (go)
  • GitHub Check: website-deploy-preview
  • GitHub Check: Run pre-commit hooks
  • GitHub Check: Summary
🔇 Additional comments (4)
tests/snapshots/TestCLICommands_atmos_doesnt_warn_if_not_in_git_repo_with_atmos_config.stdout.golden (1)

10-10: Snapshot aligns with stack list. Appreciate the refresh so the fixture mirrors the current CLI output.

test-fixes-completed.md (1)

19-86: Thanks for the crisp retrospective. The step-by-step notes on the fixes and skipped cases will save future triage cycles.

test-failures-summary.md (1)

11-62: Handy breadcrumb trail. Keeping the failure snapshot alongside root causes makes it easy to validate that the fixes covered every regression.

internal/exec/vendor_yaml_updater.go (1)

21-35: Still returning dynamic errors.

This path continues to return raw fmt.Errorf("failed …") values instead of wrapping a static sentinel from errors/errors.go, which violates our error contract. Please switch to fmt.Errorf("%w: …", sentinel, err) (or add a new sentinel) before release. As per coding guidelines.

Comment on lines +24 to +37
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}

// Update versions in content
updatedContent, err := u.UpdateVersionsInContent(content, updates)
if err != nil {
return fmt.Errorf("failed to update versions: %w", err)
}

// Write the file back
return os.WriteFile(filePath, updatedContent, vendorDefaultFilePermissions)
}

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.

⚠️ Potential issue | 🟠 Major

Follow the static error wrapping contract.

Per our error-handling rules, every returned error must wrap the appropriate sentinel from errors/errors.go via fmt.Errorf("%w: …", sentinel, err). The read/update/write branches here still bubble up ad-hoc fmt.Errorf values, which breaks upstream matching. Please swap these for the sanctioned sentinels (introduce new ones if needed) before merging. As per coding guidelines.

🤖 Prompt for AI Agents
internal/exec/vendor_yaml_updater_simple.go lines 24-37: the function returns
ad-hoc fmt.Errorf values on read, update and write failures which violates the
project's static error-wrapping contract; replace those returns so each error
wraps the proper sentinel from errors/errors.go (create and export new sentinels
there if none exist, e.g., ErrReadVendorFile, ErrUpdateVendorFile,
ErrWriteVendorFile) and format the returned error as fmt.Errorf("%w: <context>:
%v", sentinel, err) so upstream callers can match against the sentinels.

Comment on lines +48 to +56
updates: updates,
componentRegex: regexp.MustCompile(`^\s*-?\s*component:\s*["']?(\w+)["']?\s*(?:#.*)?$`),
versionRegex: regexp.MustCompile(`^(\s*version:\s*)["']?([^"'\s]+)["']?(\s*(?:#.*)?)$`),
}

for scanner.Scan() {
line := scanner.Text()
processedLine := processor.processLine(line)
result.WriteString(processedLine)

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.

⚠️ Potential issue | 🔴 Critical

Regex drops canonical component names.

The componentRegex only matches \w+, so it ignores components containing hyphens or other valid chars (for example, vpc-flow-logs-bucket straight from the docs). Those entries will never set currentComponent, so their versions will never update. Please widen the character class (e.g., allow [-./A-Za-z0-9]) so we don’t strand real-world vendor configs.(atmos.tools)

@mergify

mergify Bot commented Sep 30, 2025

Copy link
Copy Markdown
Contributor

💥 This pull request now has conflicts. Could you fix it Erik Osterman (Cloud Posse) (@osterman)? 🙏

…updating

This commit adds comprehensive vendor update functionality to Atmos:

- New 'atmos vendor update' command for checking and updating vendor dependencies
- Automatic version checking for Git repositories (tags and commits)
- YAML-preserving version updater that maintains structure, comments, and anchors
- Interactive progress UI showing update status
- Filtering by component name and tags
- --check flag to only show updates without modifying files
- --outdated flag to show only components with available updates
- --update flag to automatically update vendor.yaml with latest versions

Implementation details:
- checkForVendorUpdates() uses git ls-remote for version detection
- SimpleYAMLVersionUpdater for line-by-line YAML updates
- YAMLVersionUpdater using goccy/go-yaml AST for complex structures
- Progress tracking with Bubble Tea TUI
- Comprehensive test coverage including version comparison and YAML updates

Technical notes:
- Supports semantic versioning with Masterminds/semver
- Handles SSH to HTTPS fallback for Git operations
- Pre-release tag filtering
- Commit hash validation (7-40 characters)
- Template processing for dynamic source URLs

Documentation:
- Complete MDX documentation for the command
- PRD describing architecture and design decisions
- Usage examples and integration tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

This branch was successfully deployed

No deployments
preview — 624f2d06 Deployed Oct 21, 2025 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl Extra large size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants