Skip to content
Open
2 changes: 1 addition & 1 deletion .github/aw/compat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"min-gh-aw": "0.72.0",
"max-gh-aw": "*",
"min-agent": "1.0.21",
"max-agent": "1.0.56",
"max-agent": "1.0.75",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@copilot since it's open max-agent should be *

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f51cf40. The open catch-all now uses max-agent: "*"; schema/CI validation enforce that invariant, and the installer treats the wildcard as an unbounded range.

"open": true
},
{
Expand Down
21 changes: 16 additions & 5 deletions actions/setup/sh/install_copilot_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ resolve_version_from_compat() {
return 1
fi

if [[ ! "$compiled_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "GH_AW_COMPILED_VERSION '${compiled_version}' is not in vMAJOR.MINOR.PATCH format; skipping compatibility matrix resolution." >&2
if [[ ! "$compiled_version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "GH_AW_COMPILED_VERSION '${compiled_version}' is not in MAJOR.MINOR.PATCH format; skipping compatibility matrix resolution." >&2
return 1
fi

Expand Down Expand Up @@ -422,8 +422,13 @@ find_cached_copilot_bin() {
printf '%s\n' "$candidate"
return 0
fi
echo " Skipping candidate (version mismatch: want ${requested_version_normalized}, got ${candidate_version_normalized})" >&2
continue
# If no compat range is available, an exact match is required
if [ -z "$min_version" ] && [ -z "$max_version" ]; then
echo " Skipping candidate (version mismatch: want ${requested_version_normalized}, got ${candidate_version_normalized})" >&2
continue
fi
echo " No exact match (want ${requested_version_normalized}); checking compat range ${min_version}..${max_version}" >&2
Comment thread
pelikhan marked this conversation as resolved.
Outdated
# Fall through to range check below
fi

if [ -n "$min_version" ] && version_is_greater "$min_version" "$candidate_version_normalized"; then
Expand Down Expand Up @@ -520,7 +525,13 @@ if [ -z "$VERSION" ]; then
exit 1
fi
else
echo "Explicit Copilot CLI version argument provided (${VERSION}); skipping compat matrix resolution."
echo "Explicit Copilot CLI version argument provided (${VERSION}); resolving compat window for toolcache range matching..."
if RESOLVED_COMPAT_INFO="$(resolve_version_from_compat "$COMPILED_GH_AW_VERSION" "${TEMP_DIR}/compat.json")"; then
IFS='|' read -r _UNUSED COMPAT_MATCHED_MIN_AGENT COMPAT_MATCHED_MAX_AGENT COMPAT_CACHE_TTL_DAYS <<< "$RESOLVED_COMPAT_INFO"
Comment thread
pelikhan marked this conversation as resolved.
Outdated
echo "Compat window resolved: ${COMPAT_MATCHED_MIN_AGENT}..${COMPAT_MATCHED_MAX_AGENT} (toolcache range matching enabled)"
else
echo "Compat window unavailable; exact toolcache match required for version ${VERSION}." >&2
fi
fi

# Prefer the runner toolcache when a compatible Copilot CLI is already available.
Expand Down
104 changes: 104 additions & 0 deletions pkg/constants/version_constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
package constants

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
Expand All @@ -28,3 +35,100 @@ func TestDefaultPlaywrightCLIVersionOutsideCooldownWindow(t *testing.T) {
t.Fatalf("@playwright/cli@%s is only %s old, but Playwright CLI installs enforce a %s npm release-age cooldown", DefaultPlaywrightCLIVersion, age.Round(time.Second), minReleaseAge)
}
}

// TestDefaultCopilotVersionWithinCompatWindow asserts that DefaultCopilotVersion falls
// within the declared compat.json window so the runner toolcache can satisfy installs
// without a network download. Failures here indicate that either DefaultCopilotVersion
// or the compat.json max-agent needs to be updated.
func TestDefaultCopilotVersionWithinCompatWindow(t *testing.T) {
// Locate compat.json relative to this test file (three directories up from
// pkg/constants/ → repo root → .github/aw/compat.json).
_, testFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
compatPath := filepath.Join(filepath.Dir(testFile), "..", "..", ".github", "aw", "compat.json")
compatPath = filepath.Clean(compatPath)

data, err := os.ReadFile(compatPath)
if err != nil {
t.Fatalf("read %s: %v", compatPath, err)
}

var compat struct {
AgentCompatV1 struct {
Copilot []struct {
MinGhAw string `json:"min-gh-aw"`
MaxGhAw string `json:"max-gh-aw"`
MinAgent string `json:"min-agent"`
MaxAgent string `json:"max-agent"`
Open bool `json:"open"`
} `json:"copilot"`
} `json:"agent-compat-v1"`
}
if err := json.Unmarshal(data, &compat); err != nil {
t.Fatalf("parse %s: %v", compatPath, err)
}

version := string(DefaultCopilotVersion)

// Find the first open row (open: true means it covers the current gh-aw release).
for _, row := range compat.AgentCompatV1.Copilot {
if !row.Open {
continue
}
Comment thread
pelikhan marked this conversation as resolved.
if row.MinAgent == "" || row.MaxAgent == "" {
t.Fatalf("compat row missing min-agent or max-agent: %+v", row)
}
if cmp, err := semverCmp(version, row.MinAgent); err != nil {
t.Fatalf("semverCmp(%q, %q): %v", version, row.MinAgent, err)
} else if cmp < 0 {
t.Fatalf("DefaultCopilotVersion %q is below compat min-agent %q; bump min-agent or lower DefaultCopilotVersion", version, row.MinAgent)
}
if cmp, err := semverCmp(version, row.MaxAgent); err != nil {
Comment thread
pelikhan marked this conversation as resolved.
Outdated
t.Fatalf("semverCmp(%q, %q): %v", version, row.MaxAgent, err)
} else if cmp > 0 {
t.Fatalf("DefaultCopilotVersion %q exceeds compat max-agent %q; update .github/aw/compat.json max-agent or lower DefaultCopilotVersion to prevent toolcache bypass", version, row.MaxAgent)
}
return // found and validated
}
t.Fatalf("no open compat row found in %s; add an open row for the current gh-aw release", compatPath)
}

// semverCmp compares two semver strings (without leading "v") and returns -1, 0, or 1.
func semverCmp(a, b string) (int, error) {
pa, err := parseSemver(a)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", a, err)
}
pb, err := parseSemver(b)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", b, err)
}
for i := range pa {
if pa[i] < pb[i] {
return -1, nil
}
if pa[i] > pb[i] {
return 1, nil
}
}
return 0, nil
}

func parseSemver(v string) ([3]int, error) {
v = strings.TrimPrefix(v, "v")
parts := strings.SplitN(v, ".", 3)
if len(parts) != 3 {
return [3]int{}, fmt.Errorf("expected MAJOR.MINOR.PATCH, got %q", v)
}
var out [3]int
for i, p := range parts {
n, err := strconv.Atoi(p)
if err != nil {
return [3]int{}, fmt.Errorf("part %d of %q is not a number: %w", i, v, err)
}
out[i] = n
}
return out, nil
}
10 changes: 10 additions & 0 deletions pkg/workflow/compiler_main_job_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ func (c *Compiler) buildMainJobEnv(data *WorkflowData) map[string]string {
env["GH_AW_PROJECT_UTC"] = fmt.Sprintf("%q", utcOffset)
}

// Expose the compiler version so the Copilot install script can resolve a
// compatible toolcache entry via compat-matrix range matching rather than
// requiring an exact version download on every job.
if IsRelease() {
Comment thread
pelikhan marked this conversation as resolved.
if env == nil {
env = make(map[string]string)
}
env["GH_AW_COMPILED_VERSION"] = c.version
}

return env
}

Expand Down
11 changes: 11 additions & 0 deletions pkg/workflow/threat_detection_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,24 @@ func (c *Compiler) buildDetectionJob(data *WorkflowData) (*Job, error) {
environment = "environment: " + data.SafeOutputs.ThreatDetection.Environment
}

var detectionJobEnv map[string]string
// Expose the compiler version so the Copilot install script can resolve a
// compatible toolcache entry via compat-matrix range matching rather than
// requiring an exact version download on every job.
if IsRelease() {
detectionJobEnv = map[string]string{
"GH_AW_COMPILED_VERSION": c.version,
}
}

job := &Job{
Name: string(constants.DetectionJobName),
Needs: needs,
If: jobCondition,
RunsOn: c.indentYAMLLines(runsOn, " "),
Environment: c.indentYAMLLines(environment, " "),
Permissions: permissions,
Env: detectionJobEnv,
Steps: steps,
Outputs: outputs,
}
Expand Down
Loading