Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/user/reference/cli/azldev_component.md

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

64 changes: 64 additions & 0 deletions docs/user/reference/cli/azldev_component_identity.md

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

1 change: 1 addition & 0 deletions internal/app/azldev/cmds/component/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ components defined in the project configuration.`,
addOnAppInit(app, cmd)
buildOnAppInit(app, cmd)
diffSourcesOnAppInit(app, cmd)
identityOnAppInit(app, cmd)
listOnAppInit(app, cmd)
prepareOnAppInit(app, cmd)
queryOnAppInit(app, cmd)
Expand Down
304 changes: 304 additions & 0 deletions internal/app/azldev/cmds/component/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package component

import (
"fmt"
"log/slog"
"sync"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
"github.com/microsoft/azure-linux-dev-tools/internal/fingerprint"

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Check generated code

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Unit tests

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Scenario tests (ubuntu-latest)

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / License check

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Lint

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Publish prep check

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Analyze

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Mod check

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:

Check failure on line 14 in internal/app/azldev/cmds/component/identity.go

View workflow job for this annotation

GitHub Actions / Coverage

no required module provides package github.com/microsoft/azure-linux-dev-tools/internal/fingerprint; to add it:
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
Comment on lines +11 to +16
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

This file imports github.com/microsoft/azure-linux-dev-tools/internal/fingerprint, but there is no internal/fingerprint package in the repo (so this won’t compile). Please either add the missing package in this PR or update the import/usage to the correct existing package that provides identity/fingerprint computation.

Copilot uses AI. Check for mistakes.
"github.com/spf13/cobra"
)

// Options for computing component identity fingerprints.
type IdentityComponentOptions struct {
// Standard filter for selecting components.
ComponentFilter components.ComponentFilter
}

func identityOnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
parentCmd.AddCommand(NewComponentIdentityCommand())
}

// NewComponentIdentityCommand constructs a [cobra.Command] for "component identity" CLI subcommand.
func NewComponentIdentityCommand() *cobra.Command {
options := &IdentityComponentOptions{}

cmd := &cobra.Command{
Use: "identity",
Short: "Compute identity fingerprints for components",
Long: `Compute a deterministic identity fingerprint for each selected component.

The fingerprint captures all resolved build inputs (config fields, spec file
content, overlay source files, distro context, and Affects commit count).
A change to any input produces a different fingerprint.

Use this with 'component diff-identity' to determine which components need
rebuilding between two commits.`,
Comment on lines +43 to +44
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

The help text references component diff-identity, but there is no such command in the codebase (the only match is this string). Either add that command in the same feature set, or adjust the wording to reference an existing workflow/command so users aren’t directed to a non-existent subcommand.

Suggested change
Use this with 'component diff-identity' to determine which components need
rebuilding between two commits.`,
Compare identity outputs from two commits (for example, using diff or JSON
tooling) to determine which components need rebuilding between those commits.`,

Copilot uses AI. Check for mistakes.
Example: ` # All components, JSON output for CI
azldev component identity -a -O json > identity.json

# Single component, table output for dev
azldev component identity -p curl

# Components in a group
azldev component identity -g core`,
RunE: azldev.RunFuncWithExtraArgs(func(env *azldev.Env, args []string) (interface{}, error) {
options.ComponentFilter.ComponentNamePatterns = append(
args, options.ComponentFilter.ComponentNamePatterns...,
)

return ComputeComponentIdentities(env, options)
}),
ValidArgsFunction: components.GenerateComponentNameCompletions,
}

components.AddComponentFilterOptionsToCommand(cmd, &options.ComponentFilter)

return cmd
}

// ComponentIdentityResult is the per-component output for the identity command.
type ComponentIdentityResult struct {
// Component is the component name.
Component string `json:"component" table:",sortkey"`
// Fingerprint is the overall identity hash string.
Fingerprint string `json:"fingerprint"`
// Inputs provides the individual input hashes (shown in JSON output).
Inputs fingerprint.ComponentInputs `json:"inputs" table:"-"`
}

// ComputeComponentIdentities computes fingerprints for all selected components.
func ComputeComponentIdentities(
env *azldev.Env, options *IdentityComponentOptions,
) ([]ComponentIdentityResult, error) {
resolver := components.NewResolver(env)

comps, err := resolver.FindComponents(&options.ComponentFilter)
if err != nil {
return nil, fmt.Errorf("failed to resolve components:\n%w", err)
}

distroRef := env.Config().Project.DefaultDistro

// Resolve the distro definition (fills in default version for the fingerprint).
distroRef, err = resolveDistroForIdentity(env, distroRef)
if err != nil {
slog.Debug("Could not resolve distro", "error", err)
}

return computeIdentitiesParallel(
env, comps.Components(), distroRef,
)
}
Comment on lines +78 to +100
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

This new component identity behavior is substantial (parallel execution, source identity resolution, git-history-derived inputs) but there are no unit tests added alongside it. Other component commands in this folder have focused tests; please add tests covering at least: component selection/filtering, stable output ordering, and error propagation/cancellation on a failing component (using in-memory FS / in-memory git as applicable).

Copilot generated this review using guidance from repository custom instructions.

// maxConcurrentIdentity limits the number of concurrent identity computations.
// This bounds both git ls-remote calls and file I/O.
const maxConcurrentIdentity = 32

// computeIdentitiesParallel computes fingerprints for all components concurrently,
// including source identity resolution, affects count, and overlay file hashing.
func computeIdentitiesParallel(
env *azldev.Env,
comps []components.Component,
distroRef projectconfig.DistroReference,
) ([]ComponentIdentityResult, error) {
progressEvent := env.StartEvent("Computing component identities",
"count", len(comps))
defer progressEvent.End()

// Create a cancellable child env so we can stop remaining goroutines on first error.
workerEnv, cancel := env.WithCancel()
defer cancel()

type indexedResult struct {
index int
result ComponentIdentityResult
err error
}

resultsChan := make(chan indexedResult, len(comps))
semaphore := make(chan struct{}, maxConcurrentIdentity)

var waitGroup sync.WaitGroup

for compIdx, comp := range comps {
waitGroup.Add(1)

go func() {
defer waitGroup.Done()

// Context-aware semaphore acquisition.
select {
case semaphore <- struct{}{}:
defer func() { <-semaphore }()
case <-workerEnv.Done():
resultsChan <- indexedResult{index: compIdx, err: workerEnv.Err()}

return
}

result, computeErr := computeSingleIdentity(
workerEnv, comp, distroRef,
)

resultsChan <- indexedResult{index: compIdx, result: result, err: computeErr}
}()
}

// Close channel when all goroutines complete.
go func() { waitGroup.Wait(); close(resultsChan) }()

// Collect results in order.
results := make([]ComponentIdentityResult, len(comps))
total := int64(len(comps))

var (
completed int64
firstErr error
)

for indexed := range resultsChan {
if indexed.err != nil {
if firstErr == nil {
firstErr = indexed.err

cancel()
}

// Drain remaining results so the closer goroutine can finish.
continue
}

if firstErr == nil {
results[indexed.index] = indexed.result
completed++
progressEvent.SetProgress(completed, total)
}
}

if firstErr != nil {
return nil, firstErr
}

return results, nil
}

// computeSingleIdentity computes the identity for a single component, including
// source identity resolution, affects commit counting, and overlay file hashing.
func computeSingleIdentity(
env *azldev.Env,
comp components.Component,
distroRef projectconfig.DistroReference,
) (ComponentIdentityResult, error) {
config := comp.GetConfig()
componentName := comp.GetName()

identityOpts := fingerprint.IdentityOptions{
AffectsCommitCount: countAffectsCommits(config, componentName),
}

// Resolve source identity, selecting the appropriate method based on source type (local vs. upstream etc.).
sourceIdentity, err := resolveSourceIdentityForComponent(env, comp)
if err != nil {
return ComponentIdentityResult{}, fmt.Errorf(
"source identity resolution failed for %#q:\n%w",
componentName, err)
}

identityOpts.SourceIdentity = sourceIdentity

identity, err := fingerprint.ComputeIdentity(env.FS(), *config, distroRef, identityOpts)
if err != nil {
return ComponentIdentityResult{}, fmt.Errorf("computing identity for component %#q:\n%w",
componentName, err)
}

return ComponentIdentityResult{
Component: componentName,
Fingerprint: identity.Fingerprint,
Inputs: identity.Inputs,
}, nil
}

// resolveDistroForIdentity resolves the default distro reference, filling in the
// default version when unspecified.
func resolveDistroForIdentity(
env *azldev.Env, distroRef projectconfig.DistroReference,
) (projectconfig.DistroReference, error) {
distroDef, _, err := env.ResolveDistroRef(distroRef)
if err != nil {
return distroRef,
fmt.Errorf("resolving distro %#q:\n%w", distroRef.Name, err)
}

// Fill in the resolved version if the ref didn't specify one.
if distroRef.Version == "" {
distroRef.Version = distroDef.DefaultVersion
}

return distroRef, nil
}

// countAffectsCommits counts the number of "Affects: <componentName>" commits in the
// project repo. Returns 0 if the count cannot be determined (e.g., no git repo).
func countAffectsCommits(config *projectconfig.ComponentConfig, componentName string,
) int {
configFile := config.SourceConfigFile
if configFile == nil || configFile.SourcePath() == "" {
return 0
}

repo, err := sources.OpenProjectRepo(configFile.SourcePath())
if err != nil {
slog.Debug("Could not open project repo for Affects commits; defaulting to 0",
"component", componentName, "error", err)
Comment on lines +258 to +262
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

sources.OpenProjectRepo is called here, but no such exported function exists in internal/app/azldev/core/sources (only an unexported openProjectRepo helper exists in synthistory.go). As written this won’t compile; consider exporting a helper from sources (or duplicating the small PlainOpenWithOptions logic here) so identity can open the project repo reliably.

Copilot uses AI. Check for mistakes.

return 0
}

commits, err := sources.FindAffectsCommits(repo, componentName)
if err != nil {
slog.Debug("Could not count Affects commits; defaulting to 0",
"component", componentName, "error", err)

return 0
}

return len(commits)
}
Comment on lines +250 to +276
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

countAffectsCommits walks the full git history once per component via sources.FindAffectsCommits, making this command O(#components × #commits). On large repos/components this can become very slow. Consider scanning the git log once per run and building a map[componentName]int (or map[string]struct{count int}) for all Affects: trailers, then looking up counts per component.

Copilot uses AI. Check for mistakes.

// resolveSourceIdentityForComponent returns a deterministic identity string for the
// component's source by delegating to [sourceproviders.SourceManager.ResolveSourceIdentity].
func resolveSourceIdentityForComponent(
env *azldev.Env, comp components.Component,
) (string, error) {
distro, err := sourceproviders.ResolveDistro(env, comp)
if err != nil {
return "", fmt.Errorf("resolving distro for component %#q:\n%w",
comp.GetName(), err)
}

// A new source manager is created per component because each may reference a different
// upstream distro.
srcManager, err := sourceproviders.NewSourceManager(env, distro)
if err != nil {
return "", fmt.Errorf("creating source manager for component %#q:\n%w",
comp.GetName(), err)
}

identity, err := srcManager.ResolveSourceIdentity(env.Context(), comp)
if err != nil {
return "", fmt.Errorf("resolving source identity for %#q:\n%w",
comp.GetName(), err)
}

return identity, nil
Comment on lines +279 to +303
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

sourceproviders.SourceManager (and the concrete returned by NewSourceManager) does not define a ResolveSourceIdentity method anywhere in the repo, so this call won’t compile. If source identity is required for fingerprints, add a real API for it in sourceproviders (and implement it), or switch to an existing mechanism already used for source resolution.

Suggested change
// component's source by delegating to [sourceproviders.SourceManager.ResolveSourceIdentity].
func resolveSourceIdentityForComponent(
env *azldev.Env, comp components.Component,
) (string, error) {
distro, err := sourceproviders.ResolveDistro(env, comp)
if err != nil {
return "", fmt.Errorf("resolving distro for component %#q:\n%w",
comp.GetName(), err)
}
// A new source manager is created per component because each may reference a different
// upstream distro.
srcManager, err := sourceproviders.NewSourceManager(env, distro)
if err != nil {
return "", fmt.Errorf("creating source manager for component %#q:\n%w",
comp.GetName(), err)
}
identity, err := srcManager.ResolveSourceIdentity(env.Context(), comp)
if err != nil {
return "", fmt.Errorf("resolving source identity for %#q:\n%w",
comp.GetName(), err)
}
return identity, nil
// component's source. Currently this is derived solely from the component name to avoid
// relying on unresolved source provider APIs.
func resolveSourceIdentityForComponent(
env *azldev.Env, comp components.Component,
) (string, error) {
if comp == nil {
return "", fmt.Errorf("resolving source identity:\n%v", "component is nil")
}
// Use the component name as a stable, deterministic identity. This avoids depending on
// a non-existent [sourceproviders.SourceManager.ResolveSourceIdentity] API while still
// providing a repeatable identifier for fingerprinting.
return comp.GetName(), nil

Copilot uses AI. Check for mistakes.
}
Loading