Skip to content
Merged
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 pkg/cli/add_package_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ func parseRepositoryPackageManifest(manifestPath string, content []byte) (*repos
if !semverutil.IsValid(currentVersion) {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version validation requires a semantic-versioned compiler, but the current compiler version %q is not a valid semantic version (this indicates a build issue)", manifestPath, currentVersion)
}
currentVersion = semverutil.NormalizeGitDescribeSemver(currentVersion)
if semverutil.Compare(currentVersion, manifest.MinVersion) < 0 {
return nil, nil, fmt.Errorf("invalid Agentic Workflow manifest %q: min-version %q requires gh-aw %s or newer (current: %s)", manifestPath, manifest.MinVersion, manifest.MinVersion, currentVersion)
}
Expand Down
31 changes: 31 additions & 0 deletions pkg/cli/add_package_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,37 @@ files:
assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource)
})

t.Run("accepts git describe compiler version for matching min-version", func(t *testing.T) {
SetVersionInfo("v1.0.0-27-g117acb7f9c")
t.Cleanup(func() {
SetVersionInfo("v1.2.3")
})

downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) {
switch path {
case "aw.yml":
return []byte(`manifest-version: "1"
min-version: v1.0.0
name: Repo Assist
files:
- workflows/review.md
`), nil
case "README.md":
return []byte("# Repo Assist\n"), nil
default:
return nil, createRepositoryPackageNotFoundError(path)
}
}
listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) {
t.Fatalf("unexpected scan of %s", workflowPath)
return nil, nil
}

pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "")
require.NoError(t, err)
assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource)
})

t.Run("accepts manifest without manifest-version", func(t *testing.T) {
downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) {
switch path {
Expand Down
24 changes: 24 additions & 0 deletions pkg/semverutil/semverutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var semverLog = logger.New("semverutil:semverutil")
// It intentionally excludes prerelease and build-metadata suffixes because GitHub Actions
// version pins use only these three forms.
var actionVersionTagRegex = regexp.MustCompile(`^v[0-9]+(\.[0-9]+(\.[0-9]+)?)?$`)
var gitDescribePrereleaseRegex = regexp.MustCompile(`^[0-9]+-g[0-9a-f]+(?:-dirty)?$`)

// SemanticVersion represents a parsed semantic version.
type SemanticVersion struct {
Expand Down Expand Up @@ -123,6 +124,29 @@ func Compare(v1, v2 string) int {
return result
}

// NormalizeGitDescribeSemver collapses local git-describe compiler versions to
// their base release tag for compatibility checks.
//
// Examples:
// - v1.2.3-27-gabc1234 -> v1.2.3
// - v1.2.3-27-gabc1234-dirty -> v1.2.3
// - v1.2.3-dirty -> v1.2.3
// - v1.2.3-beta.1 -> v1.2.3-beta.1
func NormalizeGitDescribeSemver(v string) string {
v = EnsureVPrefix(strings.TrimSpace(v))
if !semver.IsValid(v) {
return v
}

canonical := semver.Canonical(v)
prerelease := strings.TrimPrefix(semver.Prerelease(canonical), "-")
if prerelease == "dirty" || gitDescribePrereleaseRegex.MatchString(prerelease) {
return strings.TrimSuffix(canonical, semver.Prerelease(canonical))
}

return canonical
}

// IsPreciseVersion returns true if the version has explicit minor and patch components
// (i.e., at least two dots in the version string, e.g. "v6.0.0" is precise, "v6" is not).
func (v *SemanticVersion) IsPreciseVersion() bool {
Expand Down
43 changes: 43 additions & 0 deletions pkg/semverutil/semverutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,49 @@ func TestCompare(t *testing.T) {
}
}

func TestNormalizeGitDescribeSemver(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "release tag unchanged",
input: "v1.2.3",
want: "v1.2.3",
},
{
name: "git describe output collapses to base tag",
input: "v1.2.3-27-g117acb7f9c",
want: "v1.2.3",
},
{
name: "dirty tag collapses to base tag",
input: "v1.2.3-dirty",
want: "v1.2.3",
},
{
name: "git describe dirty output collapses to base tag",
input: "v1.2.3-27-g117acb7f9c-dirty",
want: "v1.2.3",
},
{
name: "real prerelease remains prerelease",
input: "v1.2.3-beta.1",
want: "v1.2.3-beta.1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NormalizeGitDescribeSemver(tt.input)
if got != tt.want {
t.Errorf("NormalizeGitDescribeSemver(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

func TestIsCompatible(t *testing.T) {
tests := []struct {
pin string
Expand Down
Loading