diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f488ac3..98aa60d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,14 @@ body: attributes: value: |
- Thank you for taking the time to report a bug! Please answer each question below to the best of your ability; it is okay to leave questions blank if you have to. + Thank you for taking the time to report a bug! Please answer each question below to the best of your ability; it is okay to leave questions blank if you have to. + + ⚠️ **Security notice**: This is a public repository. Before sharing logs or files, please review them for: + - API keys, tokens, or credentials + - Personal identifiable information (PII) + - Internal URLs, hostnames, or filesystem paths + + Please avoid sharing contents from `~/.copilot/` unless you've reviewed and redacted sensitive data. - type: textarea id: description attributes: diff --git a/.github/workflows/close-invalid.yml b/.github/workflows/close-invalid.yml index 4078bd8..eb45191 100644 --- a/.github/workflows/close-invalid.yml +++ b/.github/workflows/close-invalid.yml @@ -15,22 +15,29 @@ permissions: jobs: close-on-adding-invalid-label: - if: - github.repository == 'github/copilot-cli' && github.event.label.name == - 'invalid' + if: | + github.repository == 'github/copilot-cli' && + github.event.label.name == 'invalid' && + ( + github.event_name == 'issues' || + github.event.pull_request.head.repo.full_name == github.repository + ) runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Close issue if: ${{ github.event_name == 'issues' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - URL: ${{ github.event.issue.html_url }} - run: gh issue close $URL + NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + run: gh issue close "$NUMBER" --repo "$REPO" - name: Close PR if: ${{ github.event_name == 'pull_request_target' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - URL: ${{ github.event.pull_request.html_url }} - run: gh pr close $URL + NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: gh pr close "$NUMBER" --repo "$REPO" diff --git a/.github/workflows/close-single-word-issues.yml b/.github/workflows/close-single-word-issues.yml index f2ef0da..111f285 100644 --- a/.github/workflows/close-single-word-issues.yml +++ b/.github/workflows/close-single-word-issues.yml @@ -11,10 +11,11 @@ permissions: jobs: close-issue: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Close Single-Word Issue - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -25,8 +26,10 @@ jobs: const issueNumber = context.payload.issue.number; const repo = context.repo.repo; + console.log(`Closing single-word issue #${issueNumber}: "${issueTitle}"`); + // Close the issue and add the invalid label - github.rest.issues.update({ + await github.rest.issues.update({ owner: context.repo.owner, repo: repo, issue_number: issueNumber, @@ -41,4 +44,6 @@ jobs: issue_number: issueNumber, body: `This issue may have been opened accidentally. I'm going to close it now, but feel free to open a new issue with a more descriptive title.` }); + + console.log(`Successfully closed issue #${issueNumber}`); } diff --git a/.github/workflows/feature-request-comment.yml b/.github/workflows/feature-request-comment.yml index f347eb6..f8cf311 100644 --- a/.github/workflows/feature-request-comment.yml +++ b/.github/workflows/feature-request-comment.yml @@ -9,8 +9,9 @@ permissions: jobs: add-comment-to-enhancement-issues: - if: github.event.label.name == 'enhancement' + if: github.repository == 'github/copilot-cli' && github.event.label.name == 'enhancement' runs-on: ubuntu-latest + timeout-minutes: 5 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} @@ -29,3 +30,6 @@ jobs: contribute. steps: - run: gh issue comment "$NUMBER" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index 2a864dd..83c62f9 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -11,9 +11,11 @@ permissions: jobs: noResponse: + if: github.repository == 'github/copilot-cli' runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-issue-labels: 'more-info-needed' diff --git a/.github/workflows/on-issue-close.yml b/.github/workflows/on-issue-close.yml index e768226..24eb621 100644 --- a/.github/workflows/on-issue-close.yml +++ b/.github/workflows/on-issue-close.yml @@ -5,7 +5,9 @@ on: - closed jobs: label_issues: + if: github.repository == 'github/copilot-cli' runs-on: ubuntu-latest + timeout-minutes: 5 permissions: issues: write steps: diff --git a/.github/workflows/remove-triage-label.yml b/.github/workflows/remove-triage-label.yml index b4834e0..b9700e8 100644 --- a/.github/workflows/remove-triage-label.yml +++ b/.github/workflows/remove-triage-label.yml @@ -9,10 +9,9 @@ permissions: jobs: remove-triage-label-from-issues: - if: - github.event.label.name != 'triage' && github.event.label.name != - 'more-info-needed' + if: github.repository == 'github/copilot-cli' && github.event.label.name != 'triage' && github.event.label.name != 'more-info-needed' runs-on: ubuntu-latest + timeout-minutes: 5 steps: - run: gh issue edit "$NUMBER" --remove-label "$LABELS" env: diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 3ec537c..f2dc9c7 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -8,9 +8,11 @@ permissions: jobs: stale: + if: github.repository == 'github/copilot-cli' runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: stale-issue-label: 'stale, triage' # The label that will be added to the issues when automatically marked as stale start-date: '2025-01-01T00:00:00Z' # Skip stale action for issues created before it diff --git a/.github/workflows/triage-issues.yml b/.github/workflows/triage-issues.yml index f73bb29..e15a9ce 100644 --- a/.github/workflows/triage-issues.yml +++ b/.github/workflows/triage-issues.yml @@ -9,10 +9,15 @@ on: permissions: issues: write +concurrency: + group: triage-${{ github.event.issue.number }} + cancel-in-progress: true + jobs: label_incoming_issues: + if: github.repository == 'github/copilot-cli' && (github.event.action == 'opened' || github.event.action == 'reopened') runs-on: ubuntu-latest - if: github.event.action == 'opened' || github.event.action == 'reopened' + timeout-minutes: 5 steps: - run: gh issue edit "$NUMBER" --add-label "$LABELS" env: @@ -21,10 +26,9 @@ jobs: NUMBER: ${{ github.event.issue.number }} LABELS: triage label_more_info_issues: - if: - github.event.action == 'unlabeled' && github.event.label.name == - 'more-info-needed' + if: github.repository == 'github/copilot-cli' && github.event.action == 'unlabeled' && github.event.label.name == 'more-info-needed' runs-on: ubuntu-latest + timeout-minutes: 5 steps: - run: gh issue edit "$NUMBER" --add-label "$LABELS" env: diff --git a/.github/workflows/unable-to-reproduce-comment.yml b/.github/workflows/unable-to-reproduce-comment.yml index 195739a..f6c119d 100644 --- a/.github/workflows/unable-to-reproduce-comment.yml +++ b/.github/workflows/unable-to-reproduce-comment.yml @@ -9,8 +9,9 @@ permissions: jobs: add-comment-to-unable-to-reproduce-issues: - if: github.event.label.name == 'unable-to-reproduce' + if: github.repository == 'github/copilot-cli' && github.event.label.name == 'unable-to-reproduce' runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Update issue env: diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 094defe..26e9b47 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -6,39 +6,75 @@ on: jobs: publish-winget: + if: github.repository == 'github/copilot-cli' name: Submit to WinGet repository # GitHub token permissions needed for winget-create to submit a PR permissions: contents: read - pull-requests: write # winget-create is only supported on Windows runs-on: windows-latest + timeout-minutes: 30 # winget-create will read the following environment variable to access the GitHub token needed for submitting a PR # See https://aka.ms/winget-create-token env: WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + RELEASE_ASSETS_JSON: ${{ toJSON(github.event.release.assets) }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + IS_PRERELEASE: ${{ github.event.release.prerelease }} steps: + - name: Validate required secret + shell: pwsh + run: | + if ([string]::IsNullOrWhiteSpace($env:WINGET_CREATE_GITHUB_TOKEN)) { + Write-Error "WINGET_CREATE_GITHUB_TOKEN secret is not configured" + exit 1 + } + - name: Submit package using wingetcreate + shell: pwsh run: | # Set the package ID based on the release info - $packageId = if ('${{ !github.event.release.prerelease }}' -eq 'true') { 'GitHub.Copilot' } else { 'GitHub.Copilot.Prerelease' } - - # Get installer info from release event - $assets = '${{ toJSON(github.event.release.assets) }}' | ConvertFrom-Json - $packageVersion = (${{ toJSON(github.event.release.tag_name) }}) - + $isPrerelease = ($env:IS_PRERELEASE -eq 'true') + $packageId = if ($isPrerelease) { 'GitHub.Copilot.Prerelease' } else { 'GitHub.Copilot' } + + # Get installer info from release event (passed via env to avoid script injection patterns) + $assets = $env:RELEASE_ASSETS_JSON | ConvertFrom-Json + $packageVersion = $env:RELEASE_TAG + # Find the download URLs for the x64 and arm64 installers separately # This allows overrides to be used so that wingetcreate does not have to guess the architecture from the filename $installerUrlx64 = $assets | Where-Object -Property name -like '*win32-x64.zip' | Select-Object -ExpandProperty browser_download_url $installerUrlarm64 = $assets | Where-Object -Property name -like '*win32-arm64.zip' | Select-Object -ExpandProperty browser_download_url - + + if ([string]::IsNullOrWhiteSpace($installerUrlx64) -or [string]::IsNullOrWhiteSpace($installerUrlarm64)) { + Write-Error "Could not determine installer URLs from release assets" + exit 1 + } + + # Download wingetcreate (with retries) + $maxRetries = 3 + $retryDelaySeconds = 5 + for ($i = 1; $i -le $maxRetries; $i++) { + try { + curl.exe -JLO https://aka.ms/wingetcreate/latest + if (Test-Path "wingetcreate.exe") { break } + } catch { + Write-Warning "Download attempt $i failed: $($_.Exception.Message)" + } + Start-Sleep -Seconds $retryDelaySeconds + } + + if (-not (Test-Path "wingetcreate.exe")) { + Write-Error "Failed to download wingetcreate after $maxRetries attempts" + exit 1 + } + # Update package using wingetcreate - curl.exe -JLO https://aka.ms/wingetcreate/latest .\wingetcreate.exe update $packageId ` - --version $packageVersion ` + --version "$packageVersion" ` --urls "$installerUrlx64|x64" "$installerUrlarm64|arm64" ` --submit diff --git a/README.md b/README.md index 28050a4..5a7fc34 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # GitHub Copilot CLI (Public Preview) +## Workflow Status +![Stale Issues](https://github.com/github/copilot-cli/actions/workflows/stale-issues.yml/badge.svg) +![No Response](https://github.com/github/copilot-cli/actions/workflows/no-response.yml/badge.svg) + The power of GitHub Copilot, now in your terminal. GitHub Copilot CLI brings AI-powered coding assistance directly to your command line, enabling you to build, debug, and understand code through natural language conversations. Powered by the same agentic harness as GitHub's Copilot coding agent, it provides intelligent assistance while staying deeply integrated with your GitHub workflow. diff --git a/install.sh b/install.sh index fb3d3d4..2b5f2fb 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -euo pipefail # GitHub Copilot CLI Installation Script # Usage: curl -fsSL https://gh.io/copilot-install | bash @@ -39,16 +39,34 @@ if [ "${VERSION}" = "latest" ] || [ -z "$VERSION" ]; then DOWNLOAD_URL="https://github.com/github/copilot-cli/releases/latest/download/copilot-${PLATFORM}-${ARCH}.tar.gz" CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/latest/download/SHA256SUMS.txt" elif [ "${VERSION}" = "prerelease" ]; then - # Get the latest prerelease tag - if ! command -v git >/dev/null 2>&1; then - echo "Error: git is required to install prerelease versions" >&2 - exit 1 + # Get the latest prerelease tag. + # Prefer GitHub Releases API; fallback to git tags with a conservative prerelease regex. + VERSION="" + + if command -v curl >/dev/null 2>&1; then + VERSION="$(curl -fsSL https://api.github.com/repos/github/copilot-cli/releases \ + | awk -F'"' '/"prerelease": true/ {p=1} p && /"tag_name":/ {print $4; exit}')" || true fi - VERSION="$(git ls-remote --tags https://github.com/github/copilot-cli | tail -1 | awk -F/ '{print $NF}')" - if [ -z "$VERSION" ]; then + + if [ -z "${VERSION}" ]; then + if ! command -v git >/dev/null 2>&1; then + echo "Error: git is required to install prerelease versions (or install curl)." >&2 + exit 1 + fi + + VERSION="$(git ls-remote --tags https://github.com/github/copilot-cli \ + | awk -F/ '{print $NF}' \ + | sed 's/\^{}$//' \ + | grep -E '^v?[0-9].*-(rc|beta|alpha|pre|preview)' \ + | sort -V \ + | tail -1)" || true + fi + + if [ -z "${VERSION}" ]; then echo "Error: Could not determine prerelease version" >&2 exit 1 fi + echo "Latest prerelease version: $VERSION" DOWNLOAD_URL="https://github.com/github/copilot-cli/releases/download/${VERSION}/copilot-${PLATFORM}-${ARCH}.tar.gz" CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/${VERSION}/SHA256SUMS.txt" @@ -65,14 +83,14 @@ echo "Downloading from: $DOWNLOAD_URL" # Download and extract with error handling TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT TMP_TARBALL="$TMP_DIR/copilot-${PLATFORM}-${ARCH}.tar.gz" if command -v curl >/dev/null 2>&1; then - curl -fsSL "$DOWNLOAD_URL" -o "$TMP_TARBALL" + curl -fsSL --retry 3 --retry-delay 2 "$DOWNLOAD_URL" -o "$TMP_TARBALL" elif command -v wget >/dev/null 2>&1; then - wget -qO "$TMP_TARBALL" "$DOWNLOAD_URL" + wget --tries=3 -qO "$TMP_TARBALL" "$DOWNLOAD_URL" else - echo "Error: Neither curl nor wget found. Please install one of them." - rm -rf "$TMP_DIR" + echo "Error: Neither curl nor wget found. Please install one of them." >&2 exit 1 fi @@ -80,37 +98,49 @@ fi TMP_CHECKSUMS="$TMP_DIR/SHA256SUMS.txt" CHECKSUMS_AVAILABLE=false if command -v curl >/dev/null 2>&1; then - curl -fsSL "$CHECKSUMS_URL" -o "$TMP_CHECKSUMS" 2>/dev/null && CHECKSUMS_AVAILABLE=true + curl -fsSL --retry 3 --retry-delay 2 "$CHECKSUMS_URL" -o "$TMP_CHECKSUMS" 2>/dev/null && CHECKSUMS_AVAILABLE=true elif command -v wget >/dev/null 2>&1; then - wget -qO "$TMP_CHECKSUMS" "$CHECKSUMS_URL" 2>/dev/null && CHECKSUMS_AVAILABLE=true + wget --tries=3 -qO "$TMP_CHECKSUMS" "$CHECKSUMS_URL" 2>/dev/null && CHECKSUMS_AVAILABLE=true fi if [ "$CHECKSUMS_AVAILABLE" = true ]; then - if command -v sha256sum >/dev/null 2>&1; then - if (cd "$TMP_DIR" && sha256sum -c --ignore-missing SHA256SUMS.txt >/dev/null 2>&1); then - echo "✓ Checksum validated" + target_filename="copilot-${PLATFORM}-${ARCH}.tar.gz" + expected="$(grep -E "${target_filename}$" "$TMP_CHECKSUMS" | head -n1 | awk '{print $1}')" || true + + if [ -z "$expected" ]; then + echo "Warning: checksum entry not found for ${target_filename}; skipping checksum validation." >&2 + else + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$TMP_TARBALL" | awk '{print $1}')" + elif command -v shasum >/dev/null 2>&1; then + actual="$(shasum -a 256 "$TMP_TARBALL" | awk '{print $1}')" else - echo "Error: Checksum validation failed." >&2 - rm -rf "$TMP_DIR" - exit 1 + echo "Error: No sha256sum or shasum found. Cannot validate download integrity." >&2 + echo "Install sha256sum or shasum, or set SKIP_CHECKSUM=1 to bypass (not recommended)." >&2 + if [ "${SKIP_CHECKSUM:-0}" != "1" ]; then + exit 1 + fi + actual="" fi - elif command -v shasum >/dev/null 2>&1; then - if (cd "$TMP_DIR" && shasum -a 256 -c --ignore-missing SHA256SUMS.txt >/dev/null 2>&1); then + + if [ -n "${actual}" ] && [ "$expected" = "$actual" ]; then echo "✓ Checksum validated" - else + elif [ -n "${actual}" ]; then echo "Error: Checksum validation failed." >&2 - rm -rf "$TMP_DIR" exit 1 fi - else - echo "Warning: No sha256sum or shasum found, skipping checksum validation." fi fi # Check that the file is a valid tarball if ! tar -tzf "$TMP_TARBALL" >/dev/null 2>&1; then echo "Error: Downloaded file is not a valid tarball or is corrupted." >&2 - rm -rf "$TMP_DIR" + exit 1 +fi + +# Validate tarball contents (avoid extracting unexpected files into bin/) +if ! tar -tzf "$TMP_TARBALL" | grep -qx 'copilot'; then + echo "Error: tarball contents unexpected (expected a single 'copilot' binary)." >&2 exit 1 fi @@ -134,7 +164,6 @@ fi tar -xz -C "$INSTALL_DIR" -f "$TMP_TARBALL" chmod +x "$INSTALL_DIR/copilot" echo "✓ GitHub Copilot CLI installed to $INSTALL_DIR/copilot" -rm -rf "$TMP_DIR" # Check if installed binary is accessible if ! command -v copilot >/dev/null 2>&1; then @@ -144,7 +173,9 @@ if ! command -v copilot >/dev/null 2>&1; then # Detect shell rc file case "$(basename "${SHELL:-/bin/sh}")" in zsh) RC_FILE="$HOME/.zshrc" ;; - bash) RC_FILE="$HOME/.bashrc" ;; + bash) + if [ -f "$HOME/.bashrc" ]; then RC_FILE="$HOME/.bashrc"; else RC_FILE="$HOME/.bash_profile"; fi + ;; *) RC_FILE="$HOME/.profile" ;; esac