Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d3b56e1
Plan copilot compat-driven installer update
Copilot Jun 3, 2026
547e903
Use compat matrix for Copilot CLI resolution
Copilot Jun 3, 2026
950c7a2
Harden compat semver parsing and test constants
Copilot Jun 3, 2026
d5cded8
Align compat install flow with runtime plan
Copilot Jun 3, 2026
5788468
Harden compat fallback and cache window matching
Copilot Jun 3, 2026
74e0502
Add GitHub Actions warning when using bundled compat fallback
Copilot Jun 3, 2026
b2aefd5
Remove dangerous fallback to latest Copilot CLI when compat resolutio…
Copilot Jun 3, 2026
d378dfa
Make compat resolution failure error more actionable
Copilot Jun 3, 2026
a17dc79
Implement cache TTL expiry based on compat.json cache-ttl-days
Copilot Jun 3, 2026
680f2cd
Improve cache TTL implementation readability
Copilot Jun 3, 2026
e301163
Document partial day truncation in cache age calculation
Copilot Jun 3, 2026
5cb3c3f
Add jq-based compat resolution to support environments without python3
Copilot Jun 3, 2026
8701923
Merge remote-tracking branch 'origin/main' into copilot/update-instal…
Copilot Jun 4, 2026
71c97cc
Remove obsolete releases.json and schema files after migration to com…
Copilot Jun 4, 2026
e7ec812
Document canary push model for compat matrix updates
Copilot Jun 4, 2026
c81a758
Apply remaining changes
Copilot Jun 4, 2026
87eaedb
Plan dispatch-driven compat bump workflow
Copilot Jun 4, 2026
ba68870
Add canary repository_dispatch compat bump workflow
Copilot Jun 4, 2026
df83eba
Refine dispatch workflow constants and branch name sanitization
Copilot Jun 4, 2026
1ae521e
revert: undo canary dispatch auto-bump changes and restore releases.json
Copilot Jun 4, 2026
3f0e29d
chore: remove ADR reference from compat.schema.json description
Copilot Jun 4, 2026
6ab648d
Apply remaining changes
Copilot Jun 4, 2026
24198ef
fix: remove double network fallback in compat.json download; add sing…
Copilot Jun 4, 2026
464ad27
chore: plan remove python fallback from install_copilot_cli.sh
Copilot Jun 4, 2026
acc6845
refactor: remove python fallback from compat resolution
Copilot Jun 4, 2026
a93a978
fix: improve jq resolution error reporting
Copilot Jun 4, 2026
376e0e1
fix: clarify compat resolver errors and simplify jq stderr capture
Copilot Jun 4, 2026
c37df0c
fix: consolidate jq and compat resolution errors
Copilot Jun 4, 2026
91abc54
chore: prepare updated PR messaging
Copilot Jun 4, 2026
ca3e20d
Merge remote-tracking branch 'origin/main' into copilot/update-instal…
Copilot Jun 4, 2026
36b62d9
fix: source compat config from gh-aw-actions
Copilot Jun 4, 2026
0c498aa
Merge branch 'main' into copilot/update-install-copilot-cli-script
github-actions[bot] Jun 4, 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
3 changes: 3 additions & 0 deletions .github/aw/compat.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"$schema": "./compat.schema.json",
"blockedVersions": [],
"minimumVersion": "v0.65.3",
"minRecommendedVersion": "v0.65.3",
"agent-compat-v1": {
"cache-ttl-days": 14,
"copilot": [
Expand Down
25 changes: 24 additions & 1 deletion .github/aw/compat.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,37 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/github/gh-aw/.github/aw/compat.schema.json",
"title": "gh-aw agent compatibility matrix",
"description": "Pins agentic CLI versions to gh-aw release ranges. Consumed by the setup action to install a known-good agent version per gh-aw release. See ADR for the design rationale.",
"description": "Pins agentic CLI versions to gh-aw release ranges. Consumed by the setup action to install a known-good agent version per gh-aw release.",
"type": "object",
"required": ["agent-compat-v1"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string"
},
"blockedVersions": {
"type": "array",
"description": "List of blocked compile-agentic versions that are not allowed to run (e.g. due to a security compromise). Workflows compiled with a blocked version will fail at activation.",
"items": {
"type": "string",
"pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "A blocked version string in vMAJOR.MINOR.PATCH format (e.g. 'v1.2.3')"
},
"uniqueItems": true,
"default": []
},
"minimumVersion": {
"type": "string",
"description": "The minimum supported compile-agentic version in vMAJOR.MINOR.PATCH format. Workflows compiled with a version below this will fail at activation. Use an empty string to disable this check.",
"pattern": "^(v[0-9]+\\.[0-9]+\\.[0-9]+)?$",
"default": ""
},
"minRecommendedVersion": {
"type": "string",
"description": "The minimum recommended compile-agentic version in vMAJOR.MINOR.PATCH format. Workflows compiled with a version below this will emit a warning (but not fail) at activation, nudging users to upgrade. Use an empty string to disable this check.",
"pattern": "^(v[0-9]+\\.[0-9]+\\.[0-9]+)?$",
"default": ""
},
"agent-compat-v1": {
"type": "object",
"additionalProperties": false,
Expand Down
40 changes: 39 additions & 1 deletion .github/workflows/cgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -669,13 +669,51 @@ jobs:
core.info(`✅ ${CONFIG_FILE} is valid JSON`);

const errors = [];
const allowedTopKeys = new Set(['$schema', 'agent-compat-v1']);
const allowedTopKeys = new Set(['$schema', 'blockedVersions', 'minimumVersion', 'minRecommendedVersion', 'agent-compat-v1']);
for (const key of Object.keys(config)) {
if (!allowedTopKeys.has(key)) {
errors.push(`Unknown top-level property: '${key}'`);
}
}

const releaseVersionRe = /^v[0-9]+\.[0-9]+\.[0-9]+$/;
if ('blockedVersions' in config) {
if (!Array.isArray(config.blockedVersions)) {
errors.push("'blockedVersions' must be an array");
} else {
const seenBlocked = new Set();
config.blockedVersions.forEach((v, i) => {
if (typeof v !== 'string') {
errors.push(`blockedVersions[${i}] must be a string`);
return;
}
if (!releaseVersionRe.test(v)) {
errors.push(`blockedVersions[${i}] ('${v}') must match vMAJOR.MINOR.PATCH`);
}
if (seenBlocked.has(v)) {
errors.push(`blockedVersions contains duplicate entry '${v}'`);
}
seenBlocked.add(v);
});
}
}
if ('minimumVersion' in config) {
const mv = config.minimumVersion;
if (typeof mv !== 'string') {
errors.push("'minimumVersion' must be a string");
} else if (!(mv === '' || releaseVersionRe.test(mv))) {
errors.push(`'minimumVersion' ('${mv}') does not match expected version pattern (vMAJOR.MINOR.PATCH or empty string)`);
}
}
if ('minRecommendedVersion' in config) {
const mrv = config.minRecommendedVersion;
if (typeof mrv !== 'string') {
errors.push("'minRecommendedVersion' must be a string");
} else if (!(mrv === '' || releaseVersionRe.test(mrv))) {
errors.push(`'minRecommendedVersion' ('${mrv}') does not match expected version pattern (vMAJOR.MINOR.PATCH or empty string)`);
}
}

const matrix = config['agent-compat-v1'];
if (typeof matrix !== 'object' || matrix === null || Array.isArray(matrix)) {
core.setFailed(`ERROR: 'agent-compat-v1' must be an object`);
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/check_version_updates.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* This script:
* 1. Reads the compiled version from GH_AW_COMPILED_VERSION env var.
* 2. Skips the check if the version is not in vMAJOR.MINOR.PATCH official release format.
* 3. Fetches .github/aw/releases.json from the gh-aw repository via raw.githubusercontent.com.
* 3. Fetches .github/aw/compat.json from the gh-aw-actions repository via raw.githubusercontent.com.
* - Uses withRetry to handle transient network failures.
* 4. If the download fails or config is invalid JSON, the check is skipped (soft failure).
* 5. Validates that the compiled version is not in the blocked list.
Expand All @@ -18,7 +18,7 @@

const { withRetry, isTransientError } = require("./error_recovery.cjs");

const CONFIG_URL = "https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/releases.json";
const CONFIG_URL = "https://raw.githubusercontent.com/github/gh-aw-actions/main/.github/aw/compat.json";

/**
* Parse an official version string (must be in vMAJOR.MINOR.PATCH format).
Expand Down
225 changes: 223 additions & 2 deletions actions/setup/sh/install_copilot_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@ set +o histexpand
set -euo pipefail

# Configuration
SECONDS_PER_DAY=86400
VERSION="${1:-}"
COPILOT_REPO="github/copilot-cli"
INSTALL_DIR="/usr/local/bin"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Smoke test: nice work using a single remote fetch here to avoid double network latency — consider asserting a non-empty response before falling back.

COPILOT_DIR="${HOME}/.copilot"
COPILOT_TOOLCACHE_MAX_DEPTH=4
COMPAT_URL="${COPILOT_COMPAT_URL:-https://raw.githubusercontent.com/github/gh-aw-actions/main/.github/aw/compat.json}"
COMPILED_GH_AW_VERSION="${GH_AW_COMPILED_VERSION:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Smoke test: good defensive fallback to the bundled compat.json with a ::warning:: — clear and resilient.

REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
COMPAT_BUNDLED_PATH="${COPILOT_COMPAT_BUNDLED_PATH:-${REPO_ROOT}/.github/aw/compat.json}"
COMPAT_MATCHED_MIN_AGENT=""
COMPAT_MATCHED_MAX_AGENT=""
COMPAT_CACHE_TTL_DAYS=""

# Fix directory ownership before installation
# This is needed because a previous AWF run on the same runner may have used
Expand Down Expand Up @@ -132,9 +141,172 @@ version_is_greater() {
return 1
}

# Download compatibility matrix with bundled fallback.
download_compat_json() {
local compat_file="$1"
local source_file="$2"

echo "Attempting to download compatibility matrix from ${COMPAT_URL}..." >&2
if curl -fsSL --retry 3 --retry-delay 5 -o "$compat_file" "$COMPAT_URL"; then
echo "$COMPAT_URL" > "$source_file"
echo "Successfully downloaded compatibility matrix from ${COMPAT_URL}" >&2
return 0
fi
echo "Compatibility matrix download failed from ${COMPAT_URL}" >&2

if [ -f "$COMPAT_BUNDLED_PATH" ]; then
echo "::warning::Compatibility matrix network fetch failed; using bundled fallback at ${COMPAT_BUNDLED_PATH}"
echo "Falling back to bundled compatibility matrix at ${COMPAT_BUNDLED_PATH}" >&2
Comment thread
pelikhan marked this conversation as resolved.
cp "$COMPAT_BUNDLED_PATH" "$compat_file"
echo "bundled:${COMPAT_BUNDLED_PATH}" > "$source_file"
return 0
fi

echo "Bundled compatibility matrix not found at ${COMPAT_BUNDLED_PATH}" >&2
return 1
}

# Resolve compat using jq.
# Returns: "max_agent|row_index|min_aw|max_aw|min_agent|max_agent|cache_ttl_days"
resolve_compat_with_jq() {
local compat_file="$1"
local compiled_version="$2"
local compiled_no_v="${compiled_version#v}"

jq -r --arg compiled "$compiled_no_v" '
# Semver comparison: returns -1 if a<b, 0 if equal, 1 if a>b
Comment thread
pelikhan marked this conversation as resolved.
def semver_cmp(a; b):
(a | split(".") | map(tonumber)) as $a_parts |
(b | split(".") | map(tonumber)) as $b_parts |
if ($a_parts[0] < $b_parts[0]) then -1
elif ($a_parts[0] > $b_parts[0]) then 1
elif ($a_parts[1] < $b_parts[1]) then -1
elif ($a_parts[1] > $b_parts[1]) then 1
elif ($a_parts[2] < $b_parts[2]) then -1
elif ($a_parts[2] > $b_parts[2]) then 1
else 0 end;

.["agent-compat-v1"] as $compat |
($compat["cache-ttl-days"] // "") as $cache_ttl |
($compat.copilot // []) as $rows |

# Find first matching row
$rows | to_entries | map(
.value as $row |
.key as $idx |
$row["min-gh-aw"] as $min_aw |
$row["max-gh-aw"] as $max_aw |
$row["min-agent"] as $min_agent |
$row["max-agent"] as $max_agent |

# Check if gh-aw version is in range
if (semver_cmp($compiled; $min_aw) >= 0) and
(($max_aw == "*") or (semver_cmp($compiled; $max_aw) <= 0)) then
"\($max_agent)|\($idx)|\($min_aw)|\($max_aw)|\($min_agent)|\($max_agent)|\($cache_ttl)"
else empty end
) | first // ""
' "$compat_file"
}

# Resolve Copilot version from compat matrix using GH_AW_COMPILED_VERSION.
resolve_version_from_compat() {
local compiled_version="${1:-}"
local compat_file="$2"
local resolved_info=""
local compat_source=""

if [ -z "$compiled_version" ]; then
echo "No GH_AW_COMPILED_VERSION provided, skipping compatibility matrix resolution." >&2
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
return 1
fi

compat_source="${compat_file}.source"
if ! download_compat_json "$compat_file" "$compat_source"; then
echo "Could not resolve compatibility matrix from network or bundled fallback." >&2
return 1
fi

if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required for compatibility matrix resolution." >&2
echo "ERROR: Install jq from https://jqlang.github.io/jq/download/ or pass an explicit Copilot CLI version to bypass compat resolution." >&2
return 1
fi

if ! resolved_info="$(resolve_compat_with_jq "$compat_file" "$compiled_version" 2>&1)"; then
if [ -n "$resolved_info" ]; then
echo "ERROR: Compatibility matrix resolution failed: ${resolved_info}" >&2
else
echo "ERROR: Compatibility matrix resolution failed." >&2
fi
return 1
fi

if [ -z "$resolved_info" ]; then
echo "Compatibility matrix lookup found no matching copilot window for gh-aw ${compiled_version}." >&2
return 1
fi

IFS='|' read -r resolved_version row_index row_min_aw row_max_aw row_min_agent row_max_agent cache_ttl_days <<< "$resolved_info"
echo "Compatibility matrix source: $(cat "$compat_source")" >&2
echo "Compatibility matrix matched row ${row_index}: gh-aw ${row_min_aw}..${row_max_aw}, copilot ${row_min_agent}..${row_max_agent}" >&2
echo "Resolved Copilot CLI version from compatibility matrix: ${resolved_version}" >&2
if [ -n "$cache_ttl_days" ]; then
echo "Cache TTL: ${cache_ttl_days} days" >&2
fi
printf '%s|%s|%s|%s\n' "$resolved_version" "$row_min_agent" "$row_max_agent" "$cache_ttl_days"
return 0
}

# Check if a cached binary has exceeded the cache TTL (in days).
# Returns 0 (expired) or 1 (not expired).
is_cache_expired() {
local cached_binary="$1"
local ttl_days="$2"
local now_epoch=""
local file_epoch=""
local age_days=""

# If TTL is not set or not numeric, consider cache as not expired
if [ -z "$ttl_days" ] || ! [[ "$ttl_days" =~ ^[0-9]+$ ]]; then
return 1
fi

# Get current time and file modification time as epoch seconds
now_epoch="$(date +%s)"

# Try to get file modification time (platform-portable)
if file_epoch="$(stat -c %Y "$cached_binary" 2>/dev/null)"; then
: # Linux stat format worked
elif file_epoch="$(stat -f %m "$cached_binary" 2>/dev/null)"; then
: # macOS stat format worked
else
# Cannot determine file age, consider not expired
return 1
fi

# Calculate age in days (integer division truncates partial days, e.g., 1.9 days → 1 day)
age_days=$(( (now_epoch - file_epoch) / SECONDS_PER_DAY ))

if [ "$age_days" -ge "$ttl_days" ]; then
echo " Cache age: ${age_days} days (exceeds TTL of ${ttl_days} days)" >&2
return 0 # Expired
else
echo " Cache age: ${age_days} days (within TTL of ${ttl_days} days)" >&2
return 1 # Not expired
fi
}

# Look up a compatible Copilot CLI from the Actions toolcache before downloading a release tarball.
find_cached_copilot_bin() {
local requested_version="${1:-latest}"
local min_version="${2:-}"
local max_version="${3:-}"
local cache_ttl_days="${4:-}"
local requested_version_normalized=""
local tool_cache_root=""
local candidate=""
Expand All @@ -145,7 +317,10 @@ find_cached_copilot_bin() {
local best_candidate=""
local best_version=""

echo "Searching toolcache for GitHub Copilot CLI (requested: ${requested_version}, arch: ${ARCH_NAME})..." >&2
echo "Searching toolcache for GitHub Copilot CLI (requested: ${requested_version}, arch: ${ARCH_NAME}, range: ${min_version:-none}..${max_version:-none})..." >&2
if [ -n "$cache_ttl_days" ]; then
echo " Cache TTL enabled: ${cache_ttl_days} days" >&2
fi

if [ "$requested_version" != "latest" ]; then
requested_version_normalized="$(normalize_version "$requested_version")"
Expand Down Expand Up @@ -195,6 +370,32 @@ find_cached_copilot_bin() {
continue
fi

if [ -n "$min_version" ] && version_is_greater "$min_version" "$candidate_version_normalized"; then
echo " Skipping candidate (below compat minimum: ${candidate_version_normalized} < ${min_version})" >&2
continue
fi

if [ -n "$max_version" ] && version_is_greater "$candidate_version_normalized" "$max_version"; then
echo " Skipping candidate (above compat maximum: ${candidate_version_normalized} > ${max_version})" >&2
continue
fi

# Apply cache TTL expiry check UNLESS:
# 1. Cached version equals max-agent (already latest in compat window), OR
# 2. Explicit version was requested (requested_version != "latest")
if [ -n "$cache_ttl_days" ] && [ "$requested_version" = "latest" ] && [ -n "$max_version" ]; then
# Check if candidate version equals max-agent
if [ "$candidate_version_normalized" = "$max_version" ]; then
echo " Cache TTL skipped (candidate equals max-agent: ${candidate_version_normalized})" >&2
else
# Candidate is not max-agent, apply TTL check
if is_cache_expired "$candidate" "$cache_ttl_days"; then
echo " Skipping candidate (cache expired and not max-agent: ${candidate_version_normalized} != ${max_version})" >&2
continue
fi
fi
fi

if [ -z "$best_candidate" ] || version_is_greater "$candidate_version_normalized" "$best_version"; then
echo " New best candidate: ${candidate} (${candidate_version_normalized} > ${best_version:-none})" >&2
best_candidate="$candidate"
Expand Down Expand Up @@ -246,8 +447,28 @@ EOF
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

# Resolve a compatible Copilot version from compat matrix unless the caller passed an explicit version.
if [ -z "$VERSION" ]; then
echo "No explicit Copilot CLI version requested. Attempting compat-driven version resolution..."
if RESOLVED_COMPAT_INFO="$(resolve_version_from_compat "$COMPILED_GH_AW_VERSION" "${TEMP_DIR}/compat.json")"; then
IFS='|' read -r RESOLVED_COMPAT_VERSION COMPAT_MATCHED_MIN_AGENT COMPAT_MATCHED_MAX_AGENT COMPAT_CACHE_TTL_DAYS <<< "$RESOLVED_COMPAT_INFO"
VERSION="$RESOLVED_COMPAT_VERSION"
REQUESTED_VERSION="latest"
echo "Using compat-resolved Copilot CLI window: ${COMPAT_MATCHED_MIN_AGENT}..${COMPAT_MATCHED_MAX_AGENT}"
echo "Will install compat max-agent ${VERSION} if no cached version satisfies the window."
else
echo "ERROR: Failed to resolve Copilot CLI version from compatibility matrix." >&2
echo "ERROR: Cannot install without a compatible version." >&2
echo "To fix: Pass an explicit version as an argument (e.g., 'install_copilot_cli.sh 1.0.51')" >&2
echo " or ensure GH_AW_COMPILED_VERSION matches a row in .github/aw/compat.json" >&2
exit 1
fi
else
echo "Explicit Copilot CLI version argument provided (${VERSION}); skipping compat matrix resolution."
fi

# Prefer the runner toolcache when a compatible Copilot CLI is already available.
if CACHED_COPILOT_BIN="$(find_cached_copilot_bin "$REQUESTED_VERSION")"; then
if CACHED_COPILOT_BIN="$(find_cached_copilot_bin "$REQUESTED_VERSION" "${COMPAT_MATCHED_MIN_AGENT}" "${COMPAT_MATCHED_MAX_AGENT}" "${COMPAT_CACHE_TTL_DAYS}")"; then
echo "Using cached GitHub Copilot CLI from ${CACHED_COPILOT_BIN}"
activate_cached_copilot_bin "$CACHED_COPILOT_BIN"

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Every compiled `.lock.yml` embeds the gh-aw version used to produce it:
GH_AW_INFO_AWF_VERSION: "v0.64.5"
```

At runtime, the activation job fetches `.github/aw/releases.json` and compares the embedded version against three policies:
At runtime, the activation job fetches `.github/aw/compat.json` and compares the embedded version against three policies:

| Policy | Effect |
|--------|--------|
Expand Down
Loading
Loading