From 73c842d7efe7f73d1d2db1059c47718f1ae95f20 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:55:47 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20implement=20issue=20#397=20?= =?UTF-8?q?=E2=80=94=20Compliance:=20copilot-setup-steps-invalid-job-name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/copilot-setup-steps.yml | 125 ++++++++++++++++++++ .github/workflows/test.yml | 3 + tests/test_copilot_setup_steps.sh | 133 ++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 tests/test_copilot_setup_steps.sh diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..ad9d6964d --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,125 @@ +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/copilot-setup-steps.yml +# Standard: petry-projects/.github/standards/ci-standards.md#11-copilot-cloud-agent-setup +# +# ADOPTING THIS WORKFLOW: +# 1. Copy this file to .github/workflows/copilot-setup-steps.yml in your repo. +# 2. Keep every REQUIRED section — do NOT remove the checkout or verify steps. +# 3. Uncomment and adapt the stack blocks that match your repo's tech stack. +# 4. Delete (not just comment out) stacks that do not apply after initial setup. +# 5. Merge to the default branch — the workflow only triggers from the default branch. +# 6. Run manually from Actions → "Copilot Setup Steps" → "Run workflow" to verify. +# +# WHAT THIS FILE DOES: +# Bootstraps Copilot cloud agent's ephemeral environment BEFORE the agent starts +# working on your repo. Pre-installing dependencies deterministically: +# • Speeds up every agent session (no trial-and-error dependency discovery) +# • Makes private/internal packages available (impossible for the agent alone) +# • Ensures exact tool versions that match your CI pipeline +# Without this file the agent installs dependencies itself — slower, non-deterministic, +# and unreliable for repos with private packages or complex build graphs. +# +# SEE ALSO: +# AGENTS.md — authoritative development standards for this repo +# .github/copilot-instructions.md — always-on Copilot instructions (summary of AGENTS.md) +# .github/instructions/ — path-scoped instruction files per language +# +# CONSTRAINTS (enforced by GitHub, documented at docs.github.com): +# • Job MUST be named `copilot-setup-steps` to be recognized by Copilot cloud agent +# • timeout-minutes maximum: 59 (hard limit) +# • Customizable fields: steps, permissions, runs-on, services, snapshot, timeout-minutes +# • All other job-level settings are silently ignored by GitHub +# • fetch-depth on checkout is always overridden by the agent — do not rely on it here +# • This file MUST be present on the default branch to take effect +# +# AGENTS — READ BEFORE EDITING: +# This file is a PER-REPO TEMPLATE (Tier 2). There is no central reusable workflow +# because setup steps are inherently tech-stack-specific. Each repo owns its copy. +# Stay within the patterns documented in ci-standards.md §11. Do not change: +# • the job name (`copilot-setup-steps`) +# • the fork guard condition +# • the trigger events +# • the verify-environment step +# For changes to the org-wide pattern, open a PR against this file in +# petry-projects/.github and propagate to the fleet. +# ───────────────────────────────────────────────────────────────────────────── + +name: "Copilot Setup Steps" + +# Run automatically when this file changes (validates the setup steps work), +# and allow manual runs from the Actions tab at any time. +# NOT a standard CI trigger — this file should NOT be added to push: branches: [main]. +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +# Defense-in-depth: reset top-level permissions; set exact grants per-job. +# Pattern from envoyproxy/envoy and the org Permissions Policy (ci-standards.md). +permissions: {} + +jobs: + # ── REQUIRED: Job name must be exactly `copilot-setup-steps` ──────────────── + copilot-setup-steps: + # Skip on fork-origin pull requests — forks cannot access org secrets required for private packages. + # `github.event.repository.fork` reflects the base repo (always false for org repos); the + # correct guard is on the PR head repo so that external fork PRs are skipped while direct + # pushes and manual runs always proceed. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + + runs-on: ubuntu-latest + + # Adjust upward only if your build or download steps genuinely need more time. + # Hard maximum enforced by GitHub is 59 minutes. + timeout-minutes: 30 + + # Minimum permissions — Copilot receives its own separate token for its operations. + permissions: + contents: read + + steps: + # ── REQUIRED ────────────────────────────────────────────────────────────── + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # ── REQUIRED ────────────────────────────────────────────────────────────── + # Environment verification — always runs last. Fails loud if AGENTS.md is + # missing so the problem is caught here rather than mid-agent-session. + - name: Verify environment + run: | + echo "=== petry-projects Copilot cloud agent environment ===" + echo "Repository : ${{ github.repository }}" + echo "Ref : ${{ github.ref }}" + echo "Runner : ${{ runner.os }} / ${{ runner.arch }}" + echo "" + echo "--- Installed tool versions ---" + git --version + gh --version 2>/dev/null | head -1 || echo "gh: not installed" + node --version 2>/dev/null || echo "node: not installed" + npm --version 2>/dev/null || echo "npm: not installed" + go version 2>/dev/null || echo "go: not installed" + python --version 2>/dev/null || echo "python: not installed" + echo "" + echo "--- Agent instruction files ---" + if [ -f "AGENTS.md" ]; then + echo "AGENTS.md ($(wc -l < AGENTS.md) lines)" + else + echo "AGENTS.md — MISSING. Add this file per agent-standards.md." + exit 1 + fi + if [ -f ".github/copilot-instructions.md" ]; then + echo ".github/copilot-instructions.md" + else + echo ".github/copilot-instructions.md — not present (optional but recommended)" + fi + INSTR_COUNT=0 + if [ -d ".github/instructions" ]; then + INSTR_COUNT=$(find .github/instructions -name "*.instructions.md" | wc -l) + fi + echo ".github/instructions/: ${INSTR_COUNT} file(s)" + echo "" + echo "Setup complete — Copilot cloud agent is ready to work" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c20eb4b57..5152eaf31 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,3 +20,6 @@ jobs: - name: Run list-prs sort tests run: bash tests/test_list_prs_sort.sh + + - name: Run copilot-setup-steps workflow tests + run: bash tests/test_copilot_setup_steps.sh diff --git a/tests/test_copilot_setup_steps.sh b/tests/test_copilot_setup_steps.sh new file mode 100644 index 000000000..1debb31b2 --- /dev/null +++ b/tests/test_copilot_setup_steps.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# tests/test_copilot_setup_steps.sh +# +# Validates that .github/workflows/copilot-setup-steps.yml exists and contains +# a job named exactly `copilot-setup-steps`, which GitHub requires to recognise +# the file for the Copilot cloud agent setup feature. +# +# Run: bash tests/test_copilot_setup_steps.sh + +set -euo pipefail + +PASS=0 +FAIL=0 +ERRORS="" + +ok() { + local name="$1" + PASS=$((PASS + 1)) + printf 'PASS %s\n' "$name" +} + +fail() { + local name="$1" + local msg="$2" + FAIL=$((FAIL + 1)) + ERRORS="${ERRORS}FAIL ${name}: ${msg}\n" + printf 'FAIL %s: %s\n' "$name" "$msg" +} + +WORKFLOW=".github/workflows/copilot-setup-steps.yml" + +# --------------------------------------------------------------------------- +# Test 1: File exists +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + ok "copilot-setup-steps.yml exists" +else + fail "copilot-setup-steps.yml exists" "file not found at $WORKFLOW" +fi + +# --------------------------------------------------------------------------- +# Test 2: Job named `copilot-setup-steps` is present +# GitHub requires this exact name to activate the workflow for Copilot agent. +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + if python3 - "$WORKFLOW" << 'PY' +import sys, yaml +with open(sys.argv[1]) as f: + wf = yaml.safe_load(f) +jobs = wf.get("jobs", {}) +if "copilot-setup-steps" not in jobs: + print(f"jobs keys found: {list(jobs.keys())}", file=sys.stderr) + sys.exit(1) +PY + then + ok "job named 'copilot-setup-steps' is present" + else + fail "job named 'copilot-setup-steps' is present" \ + "no job with that exact name — GitHub will not recognise the workflow" + fi +fi + +# --------------------------------------------------------------------------- +# Test 3: No other job names (GitHub only uses the copilot-setup-steps job) +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + JOB_COUNT=$(python3 - "$WORKFLOW" << 'PY' +import sys, yaml +with open(sys.argv[1]) as f: + wf = yaml.safe_load(f) +print(len(wf.get("jobs", {}))) +PY +) + if [ "$JOB_COUNT" -eq 1 ]; then + ok "workflow contains exactly 1 job" + else + fail "workflow contains exactly 1 job" "found $JOB_COUNT jobs — expected only copilot-setup-steps" + fi +fi + +# --------------------------------------------------------------------------- +# Test 4: timeout-minutes does not exceed 59 (GitHub hard limit) +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + TIMEOUT=$(python3 - "$WORKFLOW" << 'PY' +import sys, yaml +with open(sys.argv[1]) as f: + wf = yaml.safe_load(f) +job = wf.get("jobs", {}).get("copilot-setup-steps", {}) +print(job.get("timeout-minutes", 0)) +PY +) + if [ "$TIMEOUT" -le 59 ] && [ "$TIMEOUT" -gt 0 ]; then + ok "timeout-minutes ($TIMEOUT) is within GitHub's 59-minute limit" + else + fail "timeout-minutes within limit" \ + "timeout-minutes=$TIMEOUT is invalid (must be 1–59)" + fi +fi + +# --------------------------------------------------------------------------- +# Test 5: Top-level permissions are reset to {} (least-privilege policy) +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + PERMS=$(python3 - "$WORKFLOW" << 'PY' +import sys, yaml +with open(sys.argv[1]) as f: + wf = yaml.safe_load(f) +perms = wf.get("permissions", "MISSING") +# {} or None (null) both represent "reset to empty" in YAML +if perms == {} or perms is None: + print("empty") +else: + print(repr(perms)) +PY +) + if [ "$PERMS" = "empty" ]; then + ok "top-level permissions reset to {}" + else + fail "top-level permissions reset to {}" \ + "got: $PERMS — ci-standards.md requires top-level permissions: {}" + fi +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "Results: $PASS passed, $FAIL failed" +if [ "$FAIL" -gt 0 ]; then + printf '%b' "$ERRORS" + exit 1 +fi From 7368dea3a0da38f2a55d8eea82a3eaf38c273aee Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:03:18 +0000 Subject: [PATCH 2/4] chore: apply manual instructions [skip ci-relay] --- .github/workflows/test.yml | 3 ++ tests/test_copilot_setup_steps.sh | 55 +++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5152eaf31..9e4180652 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,5 +21,8 @@ jobs: - name: Run list-prs sort tests run: bash tests/test_list_prs_sort.sh + - name: Install test dependencies + run: pip install pyyaml + - name: Run copilot-setup-steps workflow tests run: bash tests/test_copilot_setup_steps.sh diff --git a/tests/test_copilot_setup_steps.sh b/tests/test_copilot_setup_steps.sh index 1debb31b2..e50377a14 100644 --- a/tests/test_copilot_setup_steps.sh +++ b/tests/test_copilot_setup_steps.sh @@ -9,6 +9,16 @@ set -euo pipefail +if ! command -v python3 &>/dev/null; then + echo "ERROR: python3 is required to run this script." >&2 + exit 1 +fi + +if ! python3 -c "import yaml" &>/dev/null; then + echo "ERROR: python3 'yaml' module (PyYAML) is required to run this script." >&2 + exit 1 +fi + PASS=0 FAIL=0 ERRORS="" @@ -28,6 +38,7 @@ fail() { } WORKFLOW=".github/workflows/copilot-setup-steps.yml" +YAML_VALID=0 # --------------------------------------------------------------------------- # Test 1: File exists @@ -38,16 +49,33 @@ else fail "copilot-setup-steps.yml exists" "file not found at $WORKFLOW" fi +# --------------------------------------------------------------------------- +# Test 1.5: YAML is valid +# --------------------------------------------------------------------------- +if [ -f "$WORKFLOW" ]; then + if python3 - "$WORKFLOW" << 'PY' 2>/dev/null +import sys, yaml +with open(sys.argv[1]) as f: + yaml.safe_load(f) +PY + then + ok "copilot-setup-steps.yml is valid YAML" + YAML_VALID=1 + else + fail "copilot-setup-steps.yml is valid YAML" "failed to parse YAML file" + fi +fi + # --------------------------------------------------------------------------- # Test 2: Job named `copilot-setup-steps` is present # GitHub requires this exact name to activate the workflow for Copilot agent. # --------------------------------------------------------------------------- -if [ -f "$WORKFLOW" ]; then +if [ "$YAML_VALID" -eq 1 ]; then if python3 - "$WORKFLOW" << 'PY' import sys, yaml with open(sys.argv[1]) as f: - wf = yaml.safe_load(f) -jobs = wf.get("jobs", {}) + wf = yaml.safe_load(f) or {} +jobs = wf.get("jobs") or {} if "copilot-setup-steps" not in jobs: print(f"jobs keys found: {list(jobs.keys())}", file=sys.stderr) sys.exit(1) @@ -63,15 +91,15 @@ fi # --------------------------------------------------------------------------- # Test 3: No other job names (GitHub only uses the copilot-setup-steps job) # --------------------------------------------------------------------------- -if [ -f "$WORKFLOW" ]; then +if [ "$YAML_VALID" -eq 1 ]; then JOB_COUNT=$(python3 - "$WORKFLOW" << 'PY' import sys, yaml with open(sys.argv[1]) as f: - wf = yaml.safe_load(f) -print(len(wf.get("jobs", {}))) + wf = yaml.safe_load(f) or {} +print(len(wf.get("jobs") or {})) PY ) - if [ "$JOB_COUNT" -eq 1 ]; then + if [[ "$JOB_COUNT" == "1" ]]; then ok "workflow contains exactly 1 job" else fail "workflow contains exactly 1 job" "found $JOB_COUNT jobs — expected only copilot-setup-steps" @@ -81,16 +109,17 @@ fi # --------------------------------------------------------------------------- # Test 4: timeout-minutes does not exceed 59 (GitHub hard limit) # --------------------------------------------------------------------------- -if [ -f "$WORKFLOW" ]; then +if [ "$YAML_VALID" -eq 1 ]; then TIMEOUT=$(python3 - "$WORKFLOW" << 'PY' import sys, yaml with open(sys.argv[1]) as f: - wf = yaml.safe_load(f) -job = wf.get("jobs", {}).get("copilot-setup-steps", {}) + wf = yaml.safe_load(f) or {} +jobs = wf.get("jobs") or {} +job = jobs.get("copilot-setup-steps") or {} print(job.get("timeout-minutes", 0)) PY ) - if [ "$TIMEOUT" -le 59 ] && [ "$TIMEOUT" -gt 0 ]; then + if [[ "$TIMEOUT" =~ ^[0-9]+$ ]] && [ "$TIMEOUT" -le 59 ] && [ "$TIMEOUT" -gt 0 ]; then ok "timeout-minutes ($TIMEOUT) is within GitHub's 59-minute limit" else fail "timeout-minutes within limit" \ @@ -101,11 +130,11 @@ fi # --------------------------------------------------------------------------- # Test 5: Top-level permissions are reset to {} (least-privilege policy) # --------------------------------------------------------------------------- -if [ -f "$WORKFLOW" ]; then +if [ "$YAML_VALID" -eq 1 ]; then PERMS=$(python3 - "$WORKFLOW" << 'PY' import sys, yaml with open(sys.argv[1]) as f: - wf = yaml.safe_load(f) + wf = yaml.safe_load(f) or {} perms = wf.get("permissions", "MISSING") # {} or None (null) both represent "reset to empty" in YAML if perms == {} or perms is None: From f74949261f4c15dc7e8f46dcf132231c297fe84f Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:30:18 +0000 Subject: [PATCH 3/4] chore: apply manual instructions [skip ci-relay] --- .github/workflows/copilot-setup-steps.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ad9d6964d..b71f5e82e 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -85,6 +85,8 @@ jobs: # ── REQUIRED ────────────────────────────────────────────────────────────── - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # ── REQUIRED ────────────────────────────────────────────────────────────── # Environment verification — always runs last. Fails loud if AGENTS.md is From 74bb0b7ac18c0198b32b24321ee8b9a919c94d79 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:10:27 +0000 Subject: [PATCH 4/4] fix(reviews): address review comments [skip ci-relay] --- .github/workflows/copilot-setup-steps.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b71f5e82e..b849c2cdc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -89,14 +89,16 @@ jobs: persist-credentials: false # ── REQUIRED ────────────────────────────────────────────────────────────── - # Environment verification — always runs last. Fails loud if AGENTS.md is + # Environment verification — always runs last. Fails loud if something is # missing so the problem is caught here rather than mid-agent-session. + # Also surfaces the agent instruction files so session logs confirm they + # were found before the agent began working. - name: Verify environment run: | echo "=== petry-projects Copilot cloud agent environment ===" - echo "Repository : ${{ github.repository }}" - echo "Ref : ${{ github.ref }}" - echo "Runner : ${{ runner.os }} / ${{ runner.arch }}" + echo "Repository : ${GITHUB_REPOSITORY}" + echo "Ref : ${GITHUB_REF}" + echo "Runner : ${RUNNER_OS} / ${RUNNER_ARCH}" echo "" echo "--- Installed tool versions ---" git --version @@ -108,20 +110,21 @@ jobs: echo "" echo "--- Agent instruction files ---" if [ -f "AGENTS.md" ]; then - echo "AGENTS.md ($(wc -l < AGENTS.md) lines)" + echo "✅ AGENTS.md ($(wc -l < AGENTS.md) lines)" else - echo "AGENTS.md — MISSING. Add this file per agent-standards.md." + echo "❌ AGENTS.md — MISSING. Add this file per agent-standards.md." exit 1 fi if [ -f ".github/copilot-instructions.md" ]; then - echo ".github/copilot-instructions.md" + echo "✅ .github/copilot-instructions.md" else - echo ".github/copilot-instructions.md — not present (optional but recommended)" + echo "ℹ️ .github/copilot-instructions.md — not present (optional but recommended)" fi + # Count path-scoped instruction files (guard against missing directory) INSTR_COUNT=0 if [ -d ".github/instructions" ]; then INSTR_COUNT=$(find .github/instructions -name "*.instructions.md" | wc -l) fi - echo ".github/instructions/: ${INSTR_COUNT} file(s)" + echo "ℹ️ .github/instructions/: ${INSTR_COUNT} file(s)" echo "" - echo "Setup complete — Copilot cloud agent is ready to work" + echo "✅ Setup complete — Copilot cloud agent is ready to work"