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
5 changes: 5 additions & 0 deletions .changeset/patch-secure-install-assets.md

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

12 changes: 6 additions & 6 deletions actions/setup/js/upload_assets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ async function main() {
try {
// Check if orphaned branch already exists, if not create it
try {
await exec.exec(`git rev-parse --verify origin/${normalizedBranchName}`);
await exec.exec(`git checkout -B ${normalizedBranchName} origin/${normalizedBranchName}`);
await exec.exec("git", ["rev-parse", "--verify", `origin/${normalizedBranchName}`]);
Copy link
Contributor

Choose a reason for hiding this comment

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

✅ Great fix: using the array form of exec.exec() prevents shell injection by ensuring normalizedBranchName is passed as a literal argument rather than being interpolated into a shell command string.

await exec.exec("git", ["checkout", "-B", normalizedBranchName, `origin/${normalizedBranchName}`]);
core.info(`Checked out existing branch from origin: ${normalizedBranchName}`);
} catch (originError) {
// Validate that branch starts with "assets/" prefix before creating orphaned branch
Expand All @@ -111,9 +111,9 @@ async function main() {

// Branch doesn't exist on origin and has valid prefix, create orphaned branch
core.info(`Creating new orphaned branch: ${normalizedBranchName}`);
await exec.exec(`git checkout --orphan ${normalizedBranchName}`);
await exec.exec(`git rm -rf .`);
await exec.exec(`git clean -fdx`);
await exec.exec("git", ["checkout", "--orphan", normalizedBranchName]);
await exec.exec("git", ["rm", "-rf", "."]);
await exec.exec("git", ["clean", "-fdx"]);
}

// Process each asset
Expand Down Expand Up @@ -171,7 +171,7 @@ async function main() {
if (isStaged) {
core.summary.addRaw("## 🎭 Staged Mode: Asset Publication Preview");
} else {
await exec.exec(`git push origin ${normalizedBranchName}`);
await exec.exec("git", ["push", "origin", normalizedBranchName]);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The PR description claims to have converted "all 6 call sites" to array form, but only 6 calls are shown in the diff (lines 98, 99, 114, 115, 116, 174). However, line 155 await exec.exec(\git add "${targetFileName}"`)` also uses shell-interpreted form with a template string and was not converted. This represents an incomplete security fix - the shell injection vulnerability still exists at line 155 where targetFileName could potentially contain shell metacharacters. Additionally, the PR description states "Converted all 6 call sites" but there are actually 8 exec.exec calls in this file (including lines 155 and 170), so the count appears incorrect.

Copilot uses AI. Check for mistakes.
core.summary.addRaw("## Assets").addRaw(`Successfully uploaded **${uploadCount}** assets to branch \`${normalizedBranchName}\``).addRaw("");
core.info(`Successfully uploaded ${uploadCount} assets to branch ${normalizedBranchName}`);
}
Expand Down
140 changes: 90 additions & 50 deletions actions/setup/sh/install_copilot_cli.sh
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
#!/usr/bin/env bash
# Install GitHub Copilot CLI with retry logic
# Install GitHub Copilot CLI with SHA256 checksum verification
# Usage: install_copilot_cli.sh [VERSION]
#
# This script downloads and installs the GitHub Copilot CLI from the official
# installer script with retry logic to handle transient network failures.
# This script downloads and installs the GitHub Copilot CLI directly from GitHub
# releases with SHA256 checksum verification, following the secure pattern from
# install_awf_binary.sh to avoid executing unverified downloaded scripts.
#
# Arguments:
# VERSION - Optional Copilot CLI version to install (default: latest from installer)
# VERSION - Optional Copilot CLI version to install (default: latest release)
#
# Features:
# - Retries download up to 3 times with exponential backoff
# - Verifies installation after completion
# - Downloads to temp file for security
# - Cleans up temp files after installation
# Security features:
# - Downloads binary directly from GitHub releases (no installer script execution)
# - Verifies SHA256 checksum against official SHA256SUMS.txt
# - Fails fast if checksum verification fails

set -euo pipefail

# Configuration
VERSION="${1:-}"
INSTALLER_URL="https://raw.githubusercontent.com/github/copilot-cli/main/install.sh"
INSTALLER_TEMP="/tmp/copilot-install.sh"
MAX_ATTEMPTS=3
COPILOT_REPO="github/copilot-cli"
INSTALL_DIR="/usr/local/bin"
COPILOT_DIR="/home/runner/.copilot"

# Fix directory ownership before installation
Expand All @@ -32,52 +31,93 @@ echo "Ensuring correct ownership of $COPILOT_DIR..."
mkdir -p "$COPILOT_DIR"
sudo chown -R runner:runner "$COPILOT_DIR"

# Function to download installer with retry logic
download_installer_with_retry() {
local attempt=1
local wait_time=5

while [ $attempt -le $MAX_ATTEMPTS ]; do
echo "Attempt $attempt of $MAX_ATTEMPTS: Downloading Copilot CLI installer..."

if curl -fsSL "$INSTALLER_URL" -o "$INSTALLER_TEMP" 2>&1; then
echo "Successfully downloaded installer"
return 0
fi

if [ $attempt -lt $MAX_ATTEMPTS ]; then
echo "Failed to download installer. Retrying in ${wait_time}s..."
sleep $wait_time
wait_time=$((wait_time * 2)) # Exponential backoff
else
echo "ERROR: Failed to download installer after $MAX_ATTEMPTS attempts"
return 1
fi
attempt=$((attempt + 1))
done
# Detect OS and architecture
OS="$(uname -s)"
ARCH="$(uname -m)"

# Map architecture to Copilot CLI naming
case "$ARCH" in
x86_64|amd64) ARCH_NAME="x64" ;;
aarch64|arm64) ARCH_NAME="arm64" ;;
*) echo "ERROR: Unsupported architecture: ${ARCH}"; exit 1 ;;
esac

# Map OS to Copilot CLI naming
case "$OS" in
Linux) PLATFORM="linux" ;;
Darwin) PLATFORM="darwin" ;;
*) echo "ERROR: Unsupported operating system: ${OS}"; exit 1 ;;
esac

TARBALL_NAME="copilot-${PLATFORM}-${ARCH_NAME}.tar.gz"

# Build download URLs
if [ -z "$VERSION" ]; then
BASE_URL="https://github.com/${COPILOT_REPO}/releases/latest/download"
else
# Prefix version with 'v' if not already present
case "$VERSION" in
v*) ;;
*) VERSION="v$VERSION" ;;
esac
BASE_URL="https://github.com/${COPILOT_REPO}/releases/download/${VERSION}"
fi

TARBALL_URL="${BASE_URL}/${TARBALL_NAME}"
CHECKSUMS_URL="${BASE_URL}/SHA256SUMS.txt"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The script downloads from SHA256SUMS.txt but the referenced documentation file docs/copilot-cli-checksum-verification.md and the pattern from install_awf_binary.sh both use checksums.txt (without the SHA256 prefix and .txt extension pattern). Please verify that the github/copilot-cli repository actually publishes a file named exactly SHA256SUMS.txt in its releases. If the actual filename is checksums.txt, this script will fail to download the checksums file.

Copilot uses AI. Check for mistakes.

echo "Installing GitHub Copilot CLI${VERSION:+ version $VERSION} (os: ${OS}, arch: ${ARCH})..."

# Platform-portable SHA256 function
sha256_hash() {
local file="$1"
if command -v sha256sum &>/dev/null; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum &>/dev/null; then
shasum -a 256 "$file" | awk '{print $1}'
else
echo "ERROR: No sha256sum or shasum found" >&2
exit 1
fi
}

# Main installation flow
echo "Installing GitHub Copilot CLI${VERSION:+ version $VERSION}..."
# Create temp directory with cleanup on exit
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

# Download checksums
echo "Downloading checksums from ${CHECKSUMS_URL}..."
curl -fsSL -o "${TEMP_DIR}/SHA256SUMS.txt" "${CHECKSUMS_URL}"
Copy link
Contributor

Choose a reason for hiding this comment

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

🔒 This new approach of downloading checksums separately and verifying before execution is a solid supply chain security improvement over the old pattern of running a downloaded installer script directly with sudo bash.


# Download binary tarball
echo "Downloading binary from ${TARBALL_URL}..."
curl -fsSL -o "${TEMP_DIR}/${TARBALL_NAME}" "${TARBALL_URL}"
Comment on lines +90 to +94
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The curl commands for downloading the checksums and tarball files lack explicit error messages. While the -f flag will cause curl to exit with non-zero status on HTTP errors (triggering the set -e behavior), users won't see helpful error messages explaining what went wrong. Consider adding error handling with descriptive messages, similar to how install_awf_binary.sh could be improved, or at minimum add || { echo "ERROR: Failed to download ..."; exit 1; } blocks after each curl command.

Suggested change
curl -fsSL -o "${TEMP_DIR}/SHA256SUMS.txt" "${CHECKSUMS_URL}"
# Download binary tarball
echo "Downloading binary from ${TARBALL_URL}..."
curl -fsSL -o "${TEMP_DIR}/${TARBALL_NAME}" "${TARBALL_URL}"
curl -fsSL -o "${TEMP_DIR}/SHA256SUMS.txt" "${CHECKSUMS_URL}" || { echo "ERROR: Failed to download checksums from ${CHECKSUMS_URL}"; exit 1; }
# Download binary tarball
echo "Downloading binary from ${TARBALL_URL}..."
curl -fsSL -o "${TEMP_DIR}/${TARBALL_NAME}" "${TARBALL_URL}" || { echo "ERROR: Failed to download Copilot CLI tarball from ${TARBALL_URL}"; exit 1; }

Copilot uses AI. Check for mistakes.

# Verify checksum
echo "Verifying SHA256 checksum for ${TARBALL_NAME}..."
EXPECTED_CHECKSUM=$(awk -v fname="${TARBALL_NAME}" '$2 == fname {print $1; exit}' "${TEMP_DIR}/SHA256SUMS.txt" | tr 'A-F' 'a-f')
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The AWK pattern assumes the checksum file format has the filename in the second field (space-separated), but this should be verified against the actual format of copilot-cli's SHA256SUMS.txt file. Common formats include:

  • BSD format: SHA256 (filename) = checksum
  • GNU format: checksum filename (two spaces)
  • Simple format: checksum filename (single space)

The current pattern '$2 == fname' will only work if the format is exactly checksum filename with a single space. If the format uses two spaces (GNU style) or has the filename in parentheses (BSD style), this will fail to extract the checksum.

Suggested change
EXPECTED_CHECKSUM=$(awk -v fname="${TARBALL_NAME}" '$2 == fname {print $1; exit}' "${TEMP_DIR}/SHA256SUMS.txt" | tr 'A-F' 'a-f')
EXPECTED_CHECKSUM=$(awk -v fname="${TARBALL_NAME}" '
# BSD format: SHA256 (filename) = checksum
$1 == "SHA256" {
fname_field = $2
gsub(/^\(/, "", fname_field)
gsub(/\)$/, "", fname_field)
if (fname_field == fname) {
print $4
exit
}
}
# GNU/simple format: checksum[ ]filename
$NF == fname {
print $1
exit
}
' "${TEMP_DIR}/SHA256SUMS.txt" | tr 'A-F' 'a-f')

Copilot uses AI. Check for mistakes.

# Download installer with retry logic
if ! download_installer_with_retry; then
echo "ERROR: Could not download Copilot CLI installer"
if [ -z "$EXPECTED_CHECKSUM" ]; then
echo "ERROR: Could not find checksum for ${TARBALL_NAME} in SHA256SUMS.txt"
exit 1
fi

# Execute the installer with the specified version
# Pass VERSION directly to sudo to ensure it's available to the installer script
if [ -n "$VERSION" ]; then
echo "Installing Copilot CLI version $VERSION..."
sudo VERSION="$VERSION" bash "$INSTALLER_TEMP"
else
echo "Installing latest Copilot CLI version..."
sudo bash "$INSTALLER_TEMP"
ACTUAL_CHECKSUM=$(sha256_hash "${TEMP_DIR}/${TARBALL_NAME}" | tr 'A-F' 'a-f')

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
echo "ERROR: Checksum verification failed!"
echo " Expected: $EXPECTED_CHECKSUM"
echo " Got: $ACTUAL_CHECKSUM"
echo " The downloaded file may be corrupted or tampered with"
exit 1
fi

# Cleanup temp file
rm -f "$INSTALLER_TEMP"
echo "✓ Checksum verification passed for ${TARBALL_NAME}"

# Extract and install binary
echo "Installing binary to ${INSTALL_DIR}..."
sudo tar -xz -C "${INSTALL_DIR}" -f "${TEMP_DIR}/${TARBALL_NAME}"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The script uses sudo tar to extract directly to /usr/local/bin, but this will extract all contents of the tarball to that directory. If the tarball contains multiple files or has a directory structure (e.g., copilot-linux-x64/copilot), this could fail or extract files to unexpected locations. Consider extracting to the temp directory first, then moving only the copilot binary to the install directory, similar to how install_awf_binary.sh handles this with chmod +x and sudo mv separately.

Suggested change
sudo tar -xz -C "${INSTALL_DIR}" -f "${TEMP_DIR}/${TARBALL_NAME}"
tar -xz -C "${TEMP_DIR}" -f "${TEMP_DIR}/${TARBALL_NAME}"
# Locate the copilot binary within the extracted contents
COPILOT_SRC=""
if [ -f "${TEMP_DIR}/copilot" ]; then
COPILOT_SRC="${TEMP_DIR}/copilot"
else
COPILOT_SRC="$(find "${TEMP_DIR}" -maxdepth 3 -type f -name 'copilot' | head -n 1 || true)"
fi
if [ -z "${COPILOT_SRC}" ] || [ ! -f "${COPILOT_SRC}" ]; then
echo "ERROR: Could not find 'copilot' binary in extracted tarball"
exit 1
fi
sudo mv "${COPILOT_SRC}" "${INSTALL_DIR}/copilot"

Copilot uses AI. Check for mistakes.
sudo chmod +x "${INSTALL_DIR}/copilot"

# Verify installation
echo "Verifying Copilot CLI installation..."
Expand Down