Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
084f0bd
Initial plan
Copilot Mar 12, 2026
ba4f0a6
feat(vendor): warn when vendoring from an archived GitHub repository
Copilot Mar 12, 2026
07c0aeb
docs: add changelog blog post and roadmap entry for archived repo war…
Copilot Mar 12, 2026
d721176
fix: remove debug logs from archived check; fix .git suffix in ssh://…
Copilot Mar 13, 2026
0f0dcd3
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 13, 2026
89271f7
Merge branch 'main' into copilot/warn-when-vendor-archived-repo
nitrocode Mar 13, 2026
971da59
fix(vendor): address CodeRabbit feedback on archived repo warning fea…
Copilot Mar 19, 2026
b9caace
Merge branch 'main' into copilot/warn-when-vendor-archived-repo
nitrocode Mar 19, 2026
3883c8c
Changes before error encountered
Copilot Mar 19, 2026
2efff88
fix(vendor): address second round of CodeRabbit feedback on archived …
Copilot Mar 20, 2026
a23ad73
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 20, 2026
c595c06
Merge branch 'main' into copilot/warn-when-vendor-archived-repo
nitrocode Mar 22, 2026
e33da0e
fix(vendor): move API error log to trace level to fix snapshot test f…
Copilot Mar 22, 2026
33a52dc
fix(vendor): address CodeRabbit Round 4 audit - all high/medium/low s…
Copilot Mar 22, 2026
39ed392
fix(vendor): address CodeRabbit Round 5 audit - critical/medium/low s…
Copilot Mar 22, 2026
0dd3c77
fix(vendor): address CodeRabbit Round 6 audit - all remaining action …
Copilot Mar 22, 2026
275f95e
fix(vendor): address CodeRabbit Round 7 audit - high/medium/low sever…
Copilot Mar 22, 2026
d1b306f
Merge branch 'main' into copilot/warn-when-vendor-archived-repo
nitrocode Mar 23, 2026
a6708a3
fix(vendor): resolve high/medium/low severity follow-up issues from P…
Copilot Mar 23, 2026
f101f2b
fix(vendor): second round of PR #2175 follow-up fixes
Copilot Mar 23, 2026
899013b
fix(logger): sync charm output writer when nil charmLogger + custom w…
Copilot Mar 23, 2026
d297fa0
Merge branch 'main' into copilot/warn-when-vendor-archived-repo
nitrocode Mar 24, 2026
7f647c5
fix(logger): snapshot isolation tests, thread-safety godoc, CHANGELOG…
Copilot Mar 24, 2026
46ece28
feat: concurrency test, doc fixes, vendor code polish, TODO comments
Copilot Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

## Unreleased

### Breaking Changes

- `pkg/logger.NewAtmosLogger` now requires an explicit `io.Writer` second argument.
Pass `nil` to use the default `os.Stderr` writer.
Migration: `NewAtmosLogger(charmLogger)` → `NewAtmosLogger(charmLogger, nil)`.

### Links

- Blog: [Warn when vendoring from an archived GitHub repository](/blog/warn-vendor-archived-repo)
(`website/blog/2026-03-12-warn-vendor-archived-repo.mdx`)
- Docs: [`atmos vendor pull` — ATMOS_GITHUB_ARCHIVED_CHECK_TIMEOUT](/cli/commands/vendor/pull)
(`website/docs/cli/commands/vendor/vendor-pull.mdx`)
- Docs: [Environment Variables — ATMOS_GITHUB_ARCHIVED_CHECK_TIMEOUT](/cli/environment-variables)
(`website/docs/cli/environment-variables.mdx`)
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,34 @@ ALWAYS use `cmd.NewTestKit(t)` for cmd tests. Auto-cleans RootCmd state (flags,
### Follow-up Tracking (MANDATORY)
When a PR defers work to a follow-up (e.g., migration, cleanup, refactor), **open a GitHub issue and link it by number** in the blog post, roadmap, and/or PR description before merging. Blog posts with "a follow-up issue will..." with no `#number` are incomplete — the work will never be tracked.

### Test-Only Helpers in Production Packages (MANDATORY)
When a test utility (seed/reset/inject) must be accessible from tests in **multiple packages**
(e.g., `pkg/foo` and `internal/exec`), Go's `export_test.go` mechanism cannot help because
`_test.go` files are not visible when another package imports the production package during its
own tests. In these cases:
- Export the helper from the production package with a clear doc comment: `// NOTE: test utility`
- **NEVER** silently export test helpers without documentation
- Prefer suffixes like `ForTest` or prefix with `Test` to signal intent
- Document the limitation in both the function comment and in the audit response

### Context Cancellation in Tests (MANDATORY)
- **NEVER use `context.WithTimeout(ctx, 0)`** for "immediately expired" contexts — a zero
duration creates a deadline of `time.Now()` which may not be past on fast hardware.
- **ALWAYS use `context.WithCancel` + immediate `cancel()`** for reliably cancelled contexts:
```go
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancelled — safe on all hardware
defer cancel()
```
- For a past deadline: `context.WithDeadline(ctx, time.Now().Add(-time.Second))`

### Global State Synchronization (MANDATORY)
For any package-level variable mutated by test helpers and read by production functions:
- Use `sync/atomic` for simple scalars (int64, uint64) — prefer `atomic.Int64` (Go 1.19+)
- Use `sync.RWMutex` for complex types (structs, slices, maps)
- Document the synchronization mechanism in the variable's doc comment
- Test helpers that modify global state must restore it via `t.Cleanup`

### Mock Generation (MANDATORY)
Use `go.uber.org/mock/mockgen` with `//go:generate` directives. Never manual mocks.

Expand Down
2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions internal/exec/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package exec

// ResetWarnedArchivedReposForTest clears the per-run warning-deduplication map so that
// tests in other packages that call warnIfArchivedGitHubRepo indirectly (e.g., integration
// tests that exercise the full vendor pipeline) can reset state between test runs.
//
// NOTE: test utility — exported via export_test.go so it is only compiled during
// `go test ./internal/exec/...` and is not part of the production binary. Tests within
// this package can call the unexported resetWarnedRepos helper directly; this export is
// only needed if integration tests in other packages ever need to reset the map.
func ResetWarnedArchivedReposForTest() {
warnedArchivedRepos.Range(func(k, _ any) bool {
warnedArchivedRepos.Delete(k)
return true
})
}
4 changes: 2 additions & 2 deletions internal/exec/terraform_generate_backends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ components:
// Capture log output
var logBuf bytes.Buffer
originalLogger := log.Default()
testLogger := log.NewAtmosLogger(charm.New(&logBuf))
testLogger := log.NewAtmosLogger(charm.New(&logBuf), &logBuf)
testLogger.SetLevel(log.WarnLevel)
log.SetDefault(testLogger)
defer log.SetDefault(originalLogger)
Expand Down Expand Up @@ -1100,7 +1100,7 @@ components:
// Capture log output
var logBuf bytes.Buffer
originalLogger := log.Default()
testLogger := log.NewAtmosLogger(charm.New(&logBuf))
testLogger := log.NewAtmosLogger(charm.New(&logBuf), &logBuf)
testLogger.SetLevel(log.WarnLevel)
log.SetDefault(testLogger)
defer log.SetDefault(originalLogger)
Expand Down
8 changes: 8 additions & 0 deletions internal/exec/vendor_component_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ func ExecuteComponentVendorInternal(
uri, useLocalFileSystem, sourceIsLocalFile = handleLocalFileScheme(componentPath, uri)
}
pType := determinePackageType(useOciScheme, useLocalFileSystem)

// Warn if the source is an archived GitHub repository.
// TODO: thread context.Context through the vendor pipeline so this check cancels on Ctrl+C.
// See https://github.com/cloudposse/atmos/issues for the tracking issue.
if pType == pkgTypeRemote {
warnIfArchivedGitHubRepo(context.Background(), uri, component)
}

componentPkg := pkgComponentVendor{
uri: uri,
name: component,
Expand Down
66 changes: 66 additions & 0 deletions internal/exec/vendor_github_archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package exec

import (
"context"
"sync"

gh "github.com/cloudposse/atmos/pkg/github"
log "github.com/cloudposse/atmos/pkg/logger"
)

// warnedArchivedRepos tracks repositories for which an archived warning has already
// been emitted during the current run. This prevents duplicate warnings when the same
// owner/repo pair is referenced by both vendor.yaml sources and component.yaml definitions.
// The deduplication key is "owner/repo" (not the full URI), so different URI formats
// pointing to the same repository (e.g., https:// vs github://) are correctly deduplicated.
var warnedArchivedRepos sync.Map

// warnIfArchivedGitHubRepo checks whether the given URI references an archived GitHub
// repository and logs a warning if it does. The check is best-effort: any failure to
// reach the GitHub API is logged at trace level so vendoring is never blocked.
// The component argument is included in the warning when non-empty.
func warnIfArchivedGitHubRepo(ctx context.Context, uri, component string) {
owner, repo, ok := gh.ParseGitHubOwnerRepo(uri)
if !ok {
return
}

archived, err := gh.IsRepoArchived(ctx, owner, repo)
if err != nil {
// Best-effort check: log at trace level and continue so vendoring is never blocked.
// Common causes: network unavailable, rate limit exceeded (set GITHUB_TOKEN),
// or repository not found.
log.Trace("Skipping archived-repo check", "repository", owner+"/"+repo, "error", err)
return
}

if !archived {
return
}

// Deduplicate: emit the warning only once per repo per run, even if the same
// repo appears in both vendor.yaml sources and component.yaml definitions.
repoKey := owner + "/" + repo
if _, loaded := warnedArchivedRepos.LoadOrStore(repoKey, struct{}{}); loaded {
// A warning was already emitted for this repo. Log at trace level so
// engineers can confirm the suppression without polluting normal output.
// Emit unconditionally; conditionally append the component key.
traceArgs := []any{"repository", repoKey}
if component != "" {
traceArgs = append(traceArgs, "component", component)
}
log.Trace("Archived-repo warning already emitted; skipping duplicate", traceArgs...)
return
}

logArgs := []any{
"repository", repoKey,
}
if component != "" {
logArgs = append(logArgs, "component", component)
}
log.Warn("GitHub repository is archived and no longer actively maintained. "+
"Vendoring from an archived repository may include outdated or unsupported code.",
logArgs...,
)
}
Loading
Loading