-
Notifications
You must be signed in to change notification settings - Fork 13
feat(lock): add structural validation and feature-gated lock checks #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,12 @@ distro snapshot time or explicit pin, then records it in locks/<name>.lock. | |
| Subsequent commands (render, build) use the locked commit for deterministic, | ||
| reproducible results. | ||
|
|
||
| Local components are skipped — they have no upstream commit to resolve.`, | ||
| Local components are skipped — they have no upstream commit to resolve. | ||
|
|
||
| When updating all components (-a), orphan lock files (locks for components | ||
| that no longer exist in the project config) are automatically pruned. | ||
| Orphan pruning is skipped when updating individual components to avoid | ||
| accidentally removing lock files for components not included in the filter.`, | ||
| Example: ` # Update all components | ||
| azldev component update -a | ||
|
|
||
|
|
@@ -51,6 +56,9 @@ Local components are skipped — they have no upstream commit to resolve.`, | |
| # Update components in a group | ||
| azldev component update -g core`, | ||
| RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) { | ||
| // Skip lock validation — update is the lock file writer. | ||
| options.ComponentFilter.SkipLockValidation = true | ||
|
|
||
|
dmcilvaney marked this conversation as resolved.
|
||
| options.ComponentFilter.ComponentNamePatterns = append( | ||
| args, options.ComponentFilter.ComponentNamePatterns..., | ||
| ) | ||
|
|
@@ -87,20 +95,25 @@ func UpdateComponents(env *azldev.Env, options *UpdateComponentOptions) ([]Updat | |
| } | ||
|
|
||
| comps := resolved.Components() | ||
| if len(comps) == 0 { | ||
| if len(comps) == 0 && !options.ComponentFilter.IncludeAllComponents { | ||
| return nil, errors.New("no components matched the filter") | ||
| } | ||
|
|
||
| // Resolve upstream commits in parallel. | ||
| // Resolve upstream commits in parallel (no-op for empty list). | ||
| store := env.LockStore() | ||
| if store == nil { | ||
| return nil, errors.New("no project directory configured; cannot update lock files") | ||
| } | ||
|
|
||
| results := resolveUpstreamCommitsParallel(env, comps, store) | ||
|
|
||
| // Check results and bail on errors/cancellation before saving. | ||
| if err := checkUpdateResults(env, results); err != nil { | ||
| // Don't save if the context was cancelled (Ctrl+C). | ||
| if env.Context().Err() != nil { | ||
| return results, errors.New("update cancelled; lock files not updated") | ||
| } | ||
|
|
||
|
dmcilvaney marked this conversation as resolved.
|
||
| // Check results and bail on errors before saving. | ||
| if err := checkUpdateResults(results); err != nil { | ||
| return results, err | ||
| } | ||
|
|
||
|
|
@@ -109,8 +122,33 @@ func UpdateComponents(env *azldev.Env, options *UpdateComponentOptions) ([]Updat | |
| return results, err | ||
| } | ||
|
|
||
| // Filter results for table output: only show changed components. | ||
| return filterChangedResults(results), nil | ||
| // Prune orphan lock files when updating all components. | ||
| // Use the resolved component set (not raw config) to include | ||
| // spec-glob-discovered components that aren't in config directly. | ||
| // Lock files are version controlled, so pruning is safe even if the | ||
| // resolved set is empty (e.g., all components removed from config). | ||
| if options.ComponentFilter.IncludeAllComponents { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still a bit uncomfortable with commands whose semantics are different when used with Can we at minimum indicate it in the command help usage?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added here and to render |
||
| if len(comps) == 0 { | ||
| slog.Warn("No components resolved; all existing lock files will be treated as orphans") | ||
| } | ||
|
|
||
| resolvedNames := make(map[string]projectconfig.ComponentConfig, len(comps)) | ||
| for _, comp := range comps { | ||
| resolvedNames[comp.GetName()] = *comp.GetConfig() | ||
| } | ||
|
|
||
| pruned, pruneErr := store.PruneOrphans(resolvedNames) | ||
|
dmcilvaney marked this conversation as resolved.
|
||
| if pruneErr != nil { | ||
| return results, fmt.Errorf("pruning orphan lock files:\n%w", pruneErr) | ||
| } | ||
|
dmcilvaney marked this conversation as resolved.
|
||
|
|
||
| if pruned > 0 { | ||
| slog.Info("Pruned orphan lock files", "count", pruned) | ||
| } | ||
| } | ||
|
|
||
| // Filter results for table output: show changed and skipped components. | ||
| return filterDisplayResults(results), nil | ||
| } | ||
|
|
||
| // saveComponentLocks writes a lock file for each changed component. | ||
|
|
@@ -144,8 +182,8 @@ func saveComponentLocks(store *lockfile.Store, results []UpdateResult) error { | |
| } | ||
|
|
||
| // checkUpdateResults counts results, logs a summary, and returns an error if any | ||
| // component failed or the context was cancelled. | ||
| func checkUpdateResults(env *azldev.Env, results []UpdateResult) error { | ||
| // component failed. | ||
| func checkUpdateResults(results []UpdateResult) error { | ||
| var changed, skipped int | ||
|
|
||
| var failedNames []string | ||
|
|
@@ -171,10 +209,6 @@ func checkUpdateResults(env *azldev.Env, results []UpdateResult) error { | |
| len(failedNames), strings.Join(failedNames, "\n ")) | ||
| } | ||
|
|
||
| if env.Context().Err() != nil { | ||
| return errors.New("update cancelled; lock files not updated") | ||
| } | ||
|
|
||
| slog.Info("Update complete", | ||
| "total", len(results), | ||
| "changed", changed, | ||
|
|
@@ -183,12 +217,12 @@ func checkUpdateResults(env *azldev.Env, results []UpdateResult) error { | |
| return nil | ||
| } | ||
|
|
||
| // filterChangedResults returns only changed results for table display. | ||
| func filterChangedResults(results []UpdateResult) []UpdateResult { | ||
| // filterDisplayResults returns changed and skipped results for table display. | ||
| func filterDisplayResults(results []UpdateResult) []UpdateResult { | ||
| var tableResults []UpdateResult | ||
|
|
||
| for idx := range results { | ||
| if results[idx].Changed { | ||
| if results[idx].Changed || results[idx].Skipped { | ||
| tableResults = append(tableResults, results[idx]) | ||
| } | ||
| } | ||
|
|
@@ -263,7 +297,7 @@ func resolveUpstreamCommitsParallel( | |
|
|
||
| // checkLockChanged compares the resolved commit against the existing lock file | ||
| // to determine if the component changed. Distinguishes "not found" (new | ||
| // component) from real errors (corrupt lock file). | ||
| // component) from real errors (corrupt/unreadable lock file). | ||
| func checkLockChanged(store *lockfile.Store, componentName string, result *UpdateResult) { | ||
| exists, existsErr := store.Exists(componentName) | ||
| if existsErr != nil { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.