Skip to content

Commit 01891ec

Browse files
locus313Patrick LewisCopilot
authored
chore: tighten agent environment and test suite (#65)
- stub sleep in bats tests (suite ~44s -> ~14s) - add tests/test_repo_structure.bats enforcing the new-script checklist - add preToolUse hook blocking heredoc file writes (with bats tests) - move per-script behaviors to docs/script-behaviors.md; slim AGENTS.md - prune instruction files irrelevant to a bash-only repo - document mkdir-before-create step for new scripts Co-authored-by: Patrick Lewis <plewis@ontinue.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2c3193c commit 01891ec

15 files changed

Lines changed: 257 additions & 2637 deletions

‎.github/copilot-instructions.md‎

Lines changed: 4 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -145,87 +145,13 @@ Scripts follow this validation sequence:
145145

146146
## Script-Specific Behaviors
147147

148-
### `github-add-enterprise-team-read-permissions`
149-
150-
Uses GraphQL cursor-based pagination to enumerate all enterprise orgs, then assigns the `all_repo_read` org role (configurable via `ALL_REPO_READ_ROLE_NAME`) to the enterprise team in each org. Requires Bearer token authentication with `admin:enterprise` scope. The `ENTERPRISE_TEAM_SLUG` is the slug without the `"ent:"` prefix.
151-
152-
### `github-add-repo-collaborators-by-pattern`
153-
154-
Adds individual collaborators to org repos whose names match `REPO_NAME_REGEX` (ERE). Accepts a comma-separated `COLLABORATORS` list and an optional `REPO_EXCLUDE_REGEX` to skip matching repos. Uses `token` auth for all REST calls. Iterates all org repos paginated at 100 per page.
155-
156-
### `github-add-repo-permissions`
157-
158-
Grants team permissions across all repos in an org. Reads five space-separated team-slug lists (`REPO_ADMIN`, `REPO_MAINTAIN`, `REPO_PUSH`, `REPO_TRIAGE`, `REPO_PULL`). Optionally filters repos by name prefix via `REPO_NAME_FILTER`. Sleeps 5 s between each repo to stay within rate limits. Uses legacy `Accept: application/vnd.github.v3+json` and `token` auth.
159-
160-
### `github-archive-old-repos`
161-
162-
Calculates a cutoff date based on `YEARS_THRESHOLD` (default 5). Generates a timestamped CSV report under `reports/`, shows the top 10 oldest repos, and prompts for confirmation before archiving. Handles both BSD (`date -v`, macOS) and GNU (`date -d`, Linux) date syntax for cross-platform compatibility.
163-
164-
### `github-auto-repo-creation`
165-
166-
Creates private repos from a comma-separated `REPO_NAMES` list. Configures branch protection on the default branch, creates a CODEOWNERS file (Base64-encoded via API), and grants admin access to comma-separated `ADMIN_TEAMS`. All slug values in `REPO_NAMES`, `ADMIN_TEAMS`, and `REPO_OWNERS` are validated before use. Requires `base64` in addition to `curl` and `jq`.
167-
168-
### `github-close-archived-repo-security-alerts`
169-
170-
Dismisses open Dependabot, code-scanning, and secret-scanning alerts on all archived repos. Supports `--type` to target a single alert type and `--dry-run` to preview without changes. Generates a timestamped CSV report under `reports/`. Requires a token with `security_events` and `repo` scopes.
171-
172-
### `github-dockerfile-discovery`
173-
174-
Uses GitHub code-search API to find Dockerfiles across all enterprise orgs, then fetches and parses each file to extract `FROM` instructions (including multi-stage builds). Produces three timestamped reports in `REPORT_DIR`: a detail CSV, a summary CSV, and a plain-text summary. Supports `ORGS` override, `ORG_FILTER`/`ORG_EXCLUDE` regex filters (validated as syntactically correct ERE before use), and configurable sleep intervals (`SEARCH_SLEEP`, `CONTENT_SLEEP`). Both `gh_api` call sites handle sentinels: `__422__` or `__404__` on the code-search endpoint skips that org with a warning; `__404__` or `__422__` on the file-contents endpoint skips that individual file.
175-
176-
### `github-enable-issues`
177-
178-
Iterates all non-archived repos in an org and enables the Issues feature on any repo where it is disabled. Supports `--dry-run` to list affected repos without making changes.
179-
180-
### `github-get-consumed-licenses`
181-
182-
Calls the enterprise consumed-licenses endpoint using Bearer authentication. Requires `read:enterprise` scope. Returns seat consumption and purchase counts. Token-only script; no pagination or retry logic beyond what the single API call provides.
183-
184-
### `github-get-public-repos`
185-
186-
Discovers all enterprise orgs (three-tier fallback via `get_enterprise_orgs`), fetches all repos in each, and filters to public visibility in `jq` (does not rely on `?type=public` query param, which is unreliable for enterprise-managed orgs). Writes a timestamped CSV to `REPORT_DIR`. Supports `ORGS` override and `ORG_FILTER`/`ORG_EXCLUDE` ERE regex filters.
187-
188-
### `github-get-repo-list`
189-
190-
Outputs a CSV row per repository (full name, owner, visibility, URL, description, fork flag, pushed/created/updated timestamps) to stdout. Pagination handled at 100 repos per page via `get_repo_page_count`.
191-
192-
### `github-import-repo`
193-
194-
Performs a full bare clone (`git clone --mirror`) of a source repo and pushes all branches, tags, and history to a new private destination repo. Validates `GIT_URL_PREFIX` against a GitHub-host allowlist before running any git operations to prevent credential leakage. Repo names and `OWNER_USERNAME` are validated as slugs.
195-
196-
### `github-migrate-internal-repos-to-private`
197-
198-
Fetches all repos with internal visibility (paginated) and converts each to private via PATCH. Logs success or failure per repo. This operation cannot be undone via the API — converting internal to private removes access from members of other enterprise orgs.
199-
200-
### `github-monthly-issues-report`
201-
202-
Generates an HTML report of issues created in a date range (`MONTH_START`/`MONTH_END`), filtered by a hardcoded label (`Linked [AC]` — edit the script to change). Uses the timeline API to track who applied labels. URL-encodes labels for API calls (e.g., `Linked [AC]` → `Linked%20[AC]`). Outputs to `output.txt`.
203-
204-
### `github-organize-stars`
205-
206-
Uses `gh` CLI GraphQL (not `curl`) to fetch all starred repos. Accepts `GITHUB_TOKEN` env var or an active `gh` auth session for authentication. Categorizes by primary language, GitHub topics, and name keywords using a `RULES` array (pipe-delimited: `List Name|LANGUAGES|TOPICS|NAME_KEYWORDS`; first matching rule wins). Caches stars at `~/.cache/gh-star-organizer/stars.json`. Supports `--dry-run`, `-y` (skip confirm), `--show-repos`, and `--no-cache`. Adds repos to Lists in batches of 25.
207-
208-
### `github-repo-from-template`
209-
210-
Creates a private repo from `TEMPLATE_REPO` (including all branches), assigns admin permissions to space-separated `REPO_ADMIN` teams, write permissions to `REPO_WRITE` teams, then invites `CD_USERNAME` as a collaborator using `CD_GITHUB_TOKEN` and auto-accepts the invitation. All slug values are validated before use.
211-
212-
### `github-repo-permissions-report`
213-
214-
Uses `curl` for all API calls. Accepts `GITHUB_TOKEN` env var or an active `gh` auth session (token auto-resolved at lib source time) for authentication. Accepts `-r OWNER/REPO`, `-b BRANCH`, and `-o FILE` flags. Outputs a CSV with two record types: `permission` (all users/teams) and `bypass_actor` (explicit PR approval bypass entries). Reports both branch protection rules and repository rulesets.
215-
216-
### `github-copilot-report`
217-
218-
Uses `curl` for all GitHub API calls. Accepts `GITHUB_TOKEN` env var (PAT with `read:enterprise` and `manage_billing:enterprise` scopes) or an active `gh` auth session (token auto-resolved at lib source time) for authentication. Also requires `az` to be **installed** when Entra ID enrichment is needed — `az` is checked at runtime only if `--no-entra` is not set and `az` is not already skipped. If `az` is not installed or not logged in, Entra lookup is silently skipped. When `az` is logged in, enriches each user with Entra ID department and job title via `az rest`.
219-
220-
Auto-detects credits per seat from a promo/standard table keyed on plan type and today's date (promo period Jun 1 – Sep 1, 2026); override with `--credits N` or `$CREDITS_PER_SEAT_OVERRIDE`. Credits are pooled enterprise-wide, not per-user buckets. Code completions are not billed in AI credits.
221-
222-
Uses API version `2026-03-10` and the new usage-metrics NDJSON endpoints (signed download links). The legacy `/copilot/metrics` and `/copilot/usage` endpoints were closed Apr 2, 2026.
148+
Per-script quirks (auth style, pagination, sentinels, reports) live in [`docs/script-behaviors.md`](../docs/script-behaviors.md). Read the matching section before changing an existing script.
223149

224150
## Development Guidelines
225151

226152
### Adding New Scripts
227-
1. Create directory: `github-<action-verb-object>/`
228-
2. Create script: `github-<action-verb-object>.sh` (match directory name)
153+
1. Create directory `<domain>/github-<action-verb-object>/` with `mkdir -p` in its own bash call, before any `create` call (the `create` tool needs an existing parent)
154+
2. Create script: `github-<action-verb-object>.sh` (match directory name); add a section to `docs/script-behaviors.md`. `tests/test_repo_structure.bats` fails until every checklist item below exists
229155
3. Source `lib/github-common.sh` for validation and output helpers
230156
4. Start with the standard boilerplate (see Script Anatomy above)
231157
5. Create `action.yml` in the same directory — expose every env var as a named input (required inputs without defaults, optional inputs with sensible defaults); map CLI flags such as `--dry-run` and `--type` to boolean/string inputs and build an `ARGS` array in the `run:` step. Mirror the pattern of any existing `action.yml`.
@@ -305,7 +231,7 @@ When you change one of these files, you must also update the files in the "Also
305231
| `README.md` — script documentation | Verify the script's `# ===` header comment still matches (env vars, options, requirements) |
306232
| `.githooks/pre-commit` | `install-hooks.sh` if hook path or installation instructions change; README.md Best Practices section |
307233
| `install-hooks.sh` | README.md Installation section |
308-
| Add a new script | `tests/test_script_validation.bats` (add a test section for the new script); `action.yml` in the same directory; `README.md` (add use case, env var table, usage example, Available Actions table row) |
234+
| Add a new script | `tests/test_script_validation.bats`; `action.yml` in the same directory; `README.md` (use case, env var table, usage example, Available Actions row); `docs/script-behaviors.md`. `tests/test_repo_structure.bats` enforces the first four |
309235
| Add a new domain folder | `README.md` top-level structure description; `AGENTS.md` Repository Structure section |
310236
| `.github/workflows/ci.yml` — shellcheck flags | `.githooks/pre-commit` shellcheck invocation (keep them in sync) |
311237
| `.github/workflows/copilot-setup-steps.yml` — tool versions | `AGENTS.md` Tech Stack table |
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": 1,
3+
"hooks": {
4+
"preToolUse": [
5+
{
6+
"matcher": "bash",
7+
"type": "command",
8+
"bash": "./.github/hooks/scripts/block-file-writes-via-bash.sh",
9+
"cwd": ".",
10+
"timeoutSec": 5
11+
}
12+
]
13+
}
14+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# block-file-writes-via-bash.sh
4+
#
5+
# preToolUse hook for the bash tool. Denies commands that write file contents
6+
# through heredocs or tee, steering the agent to the create/edit tools.
7+
# =============================================================================
8+
9+
set -euo pipefail
10+
11+
payload="$(cat)"
12+
tool_name="$(printf '%s' "${payload}" | jq -r '.toolName // ""')"
13+
[[ "${tool_name}" == "bash" ]] || exit 0
14+
15+
command="$(printf '%s' "${payload}" | jq -r '.toolArgs // "{}" | fromjson | .command // ""')"
16+
17+
# Heredoc redirected into a file or tee: cat > f <<EOF, tee f <<'EOF', cat >> f <<-EOF
18+
if printf '%s' "${command}" | grep -Eq '(>|tee)[^<|;&]*<<-?[[:space:]]*['\''"\\]?[A-Za-z_]'; then
19+
jq -nc '{permissionDecision: "deny",
20+
permissionDecisionReason: "Write files with the create or edit tools; bash heredoc writes are blocked."}'
21+
fi
22+
exit 0

0 commit comments

Comments
 (0)