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
24 changes: 21 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,33 @@ Uses API version `2026-03-10` and the new usage-metrics NDJSON endpoints (signed
3. Source `lib/github-common.sh` for validation and output helpers
4. Start with the standard boilerplate (see Script Anatomy above)
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`.
6. Document in README.md following existing format:
6. **Add tests to `tests/test_script_validation.bats`** — mandatory. Add a labelled section with tests for every required env var missing (exit 1), unknown CLI args (exit 1), `--help` exits 0, and script-specific validation guards. See existing sections for the pattern.
7. Document in README.md following existing format:
- Use case description
- Required variables table
- Usage example with exports
- Output format (if applicable)
- Add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section

### Testing Approach
- **Always test on a test organization first**

The project has a bats unit-test suite in `tests/`. Run the full suite with:

```bash
bats tests/
```

| File | What it covers |
|------|----------------|
| `tests/test_common.bats` | `lib/github-common.sh` pure-logic functions and API helpers |
| `tests/test_script_validation.bats` | Every script — missing env vars, invalid args, `--help`, script-specific guards |
| `tests/mock_curl.sh` | Universal curl mock used by both test files |

Every new script **must** include a test section in `tests/test_script_validation.bats` before it is merged. At minimum test: each required env var missing exits 1, unknown CLI args exit 1, and any enum or allowlist validation specific to the script.

Additional testing approaches:
- **Always test on a test organization first** before running against production
- **Dry-run flags** — several scripts support `--dry-run` to preview changes without applying them

### Commit Messages — Conventional Commits (required)

Expand Down Expand Up @@ -285,7 +303,7 @@ When you change one of these files, you must also update the files in the "Also
| `README.md` — script documentation | Verify the script's `# ===` header comment still matches (env vars, options, requirements) |
| `.githooks/pre-commit` | `install-hooks.sh` if hook path or installation instructions change; README.md Best Practices section |
| `install-hooks.sh` | README.md Installation section |
| Add a new script | `action.yml` in the same directory; `README.md` (add use case, env var table, usage example, Available Actions table row) |
| 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) |
| Add a new domain folder | `README.md` top-level structure description; `AGENTS.md` Repository Structure section |
| `.github/workflows/ci.yml` — shellcheck flags | `.githooks/pre-commit` shellcheck invocation (keep them in sync) |
| `.github/workflows/copilot-setup-steps.yml` — tool versions | `AGENTS.md` Tech Stack table |
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,18 @@ jobs:
--severity=warning \
--exclude=SC2034,SC1091 \
--shell=bash

test:
name: Unit Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install bats
run: sudo apt-get update -qq && sudo apt-get install -y bats

- name: Run unit tests
run: bats tests/
65 changes: 55 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,58 @@ find . -name "*.sh" | xargs shellcheck --severity=warning --exclude=SC2034,SC109

## Testing

There is no automated test suite. The validation approach is:
The project has a bats unit-test suite in `tests/`. **Every new script must ship with tests.**

1. **Pre-commit hook** — shellcheck on every staged `.sh` file; gitleaks secret scan
2. **Dry-run flags** — several scripts support `--dry-run` to preview changes without applying them:
- `github-close-archived-repo-security-alerts`
- `github-enable-issues`
- `github-organize-stars`
3. **Test org first** — always run against a non-production GitHub org before production
```bash
# Run all tests
bats tests/

# Run a single file
bats tests/test_common.bats
```

### Test files

| File | What it covers |
|------|----------------|
| `tests/test_common.bats` | `lib/github-common.sh` — pure-logic functions (`validate_slug`, `require_env_var`, `require_command`, `err`, `configure_gh_auth`, `validate_token`, `get_repo_page_count`) and API helpers (`gh_api` sentinels, `gh_api_paginate`) |
| `tests/test_script_validation.bats` | Every script — missing required env vars exit 1, invalid CLI args exit 1, `--help` exits 0, script-specific enum/allowlist validation |
| `tests/mock_curl.sh` | Universal drop-in curl mock (used by both test files); response data via env vars `MOCK_CURL_CODE`, `MOCK_CURL_BODY`, `MOCK_CURL_LINK` |

### What to test for every new script

1. **Missing required env vars** — one `@test` per required variable, in the order the script checks them. Each test asserts `status -eq 1`.
2. **Invalid CLI args** — unknown flag exits 1 with an "Unknown" message.
3. **`--help` flag** — exits 0 (for scripts that implement it).
4. **Recognised flags** — `--dry-run` and other known flags do not trigger the unknown-arg error (test by asserting output does *not* contain "Unknown").
5. **Script-specific guards** — enum validation (`--type`, `DEPENDABOT_REASON`), URL allowlists (`GIT_URL_PREFIX`), required positional args.

### Mocking pattern

Tests shadow real binaries by prepending a `MOCK_BIN` directory to `PATH`:

```bash
setup() {
MOCK_BIN="$(mktemp -d)"
# Fail gh auth so GITHUB_TOKEN is never auto-resolved from a session
printf '#!/bin/sh\nexit 1\n' > "$MOCK_BIN/gh"
chmod +x "$MOCK_BIN/gh"
}

teardown() { rm -rf "$MOCK_BIN"; }

@test "my-script: exits 1 when GITHUB_TOKEN is not set" {
run bash -c "export PATH='${MOCK_BIN}:${PATH}'; unset GITHUB_TOKEN; bash '${REPO_ROOT}/domain/my-script/my-script.sh'"
[ "$status" -eq 1 ]
}
```

Use `_mock_curl_200` (defined in `test_script_validation.bats`) when a test must reach code that runs after `validate_github_token`.

### Dry-run flags and other testing approaches

- **`--dry-run`** — several scripts support it to preview changes without applying them.
- **Test org first** — always run against a non-production GitHub org before production.

---

Expand Down Expand Up @@ -185,8 +229,9 @@ done
4. **Source the shared library** using `SCRIPT_DIR`
5. **Validate all inputs** before any API calls
6. **Create `action.yml`** in the same directory — expose every env var as an input (required inputs first, optional inputs with defaults); map CLI flags (`--dry-run`, `--type`, etc.) to boolean/string inputs and construct the `ARGS` array in the `run:` step. See existing `action.yml` files for the pattern.
7. **Add to README.md** — follow the existing format: use case, env var table, usage example, output format; add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section
8. Place in the correct domain:
7. **Add tests to `tests/test_script_validation.bats`** — add a labelled section (`# ═══ github-<name> ═══`) with tests for: every required env var missing (exit 1), unknown CLI args (exit 1), `--help` exits 0, and any script-specific validation (enum guards, URL allowlists, positional args). See existing sections for the pattern.
8. **Add to README.md** — follow the existing format: use case, env var table, usage example, output format; add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section
9. Place in the correct domain:
- `org-admin/` — organization-level operations (repos, teams, members)
- `enterprise/` — enterprise-level operations (licenses, org enumeration)
- `reporting/` — read-only reports and audits
Expand All @@ -199,7 +244,7 @@ done
- **Pre-commit hook:** `.githooks/pre-commit` — runs gitleaks + shellcheck on staged `.sh` files
- **Install:** `./install-hooks.sh` or `git config core.hooksPath .githooks`
- **Bypass (emergency only):** `git commit --no-verify`
- **CI:** shellcheck runs on all `.sh` files on every PR (`.github/workflows/ci.yml`)
- **CI:** shellcheck runs on all `.sh` files on every PR (`.github/workflows/ci.yml`); bats unit tests run in a dedicated `test` job (`bats tests/`)
- **Releases:** automated by Release Please (`.github/workflows/release.yml`) — pushes to `main` trigger a release PR; merging it publishes the GitHub Release and tag

## Commit Messages — Conventional Commits (required)
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,28 @@ brew install gitleaks shellcheck
> [!TIP]
> To bypass the hooks in an emergency: `git commit --no-verify`. Use sparingly — the hooks exist to prevent secrets from reaching the remote.

### Unit Tests

The repository includes a [bats](https://github.com/bats-core/bats-core) unit-test suite in `tests/`. Install bats and run the full suite:

```bash
# macOS
brew install bats-core

# Ubuntu / Debian
sudo apt-get install -y bats

# Run all tests
bats tests/
```

| File | What it covers |
|------|----------------|
| `tests/test_common.bats` | `lib/github-common.sh` functions (`validate_slug`, `require_env_var`, `gh_api` sentinels, `gh_api_paginate`, etc.) |
| `tests/test_script_validation.bats` | Every script — missing required env vars, invalid CLI args, `--help`, and script-specific guards |

CI runs `bats tests/` on every pull request alongside shellcheck.

## Scripts

Each script is a self-contained utility designed for a specific task. Navigate to the script's directory, set the required environment variables, and execute.
Expand Down Expand Up @@ -1159,8 +1181,10 @@ Contributions are welcome! Please follow these steps:
- Start with the `# ===` header and `set -euo pipefail`
- Source `lib/github-common.sh` and validate all inputs
- Create `action.yml` in the same directory (see existing actions for the pattern)
- Add a test section for the new script in `tests/test_script_validation.bats`
4. **Update README.md** with the env var table, usage example, and a row in the Available Actions table
5. **Run shellcheck:** `shellcheck --severity=warning --exclude=SC2034,SC1091 --shell=bash your-script.sh`
6. **Test on a non-production org** before submitting
6. **Run the test suite:** `bats tests/`
7. **Test on a non-production org** before submitting
7. **Commit using [Conventional Commits](https://www.conventionalcommits.org/)** — `CHANGELOG.md` is auto-generated from commit messages; do not edit it manually
8. **Open a PR** — the PR template will guide you through the checklist
55 changes: 55 additions & 0 deletions tests/mock_curl.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/sh
# =============================================================================
# mock_curl.sh
#
# Universal drop-in curl mock for bats tests. Copy into a directory that is
# prepended to PATH; the real curl is then shadowed for the duration of a test.
#
# Response data is read from environment variables so callers never embed
# special characters in the script body:
#
# MOCK_CURL_CODE HTTP status code to return (default: 200)
# MOCK_CURL_BODY Response body (default: empty)
# MOCK_CURL_LINK Full URL for Link: next header — set to make the
# response look like a paginated "non-last" page;
# leave empty (default) to signal the final page
#
# Handles two calling conventions used in lib/github-common.sh:
#
# stdout mode (gh_api, get_repo_page_count)
# curl ... (no -o flag)
# Output: <body>\n<code> — gh_api splits on the last line
#
# file mode (gh_api_paginate, validate_token)
# curl ... -o <body-file> [-D <headers-file>]
# Output: <code> (body written to -o file; headers written to -D file)
# =============================================================================

HFILE=""
BFILE=""

# Parse only the flags we care about; everything else is ignored
while [ $# -gt 0 ]; do
case "$1" in
-D) HFILE="$2"; shift 2 ;;
-o) BFILE="$2"; shift 2 ;;
*) shift ;;
esac
done

CODE="${MOCK_CURL_CODE:-200}"

if [ -n "$BFILE" ]; then
# File mode: write body to -o target, write headers to -D target (if set)
printf '%s' "${MOCK_CURL_BODY:-}" > "$BFILE"
if [ -n "$HFILE" ]; then
printf 'HTTP/1.1 %s\r\n' "$CODE" > "$HFILE"
[ -n "${MOCK_CURL_LINK:-}" ] && \
printf 'link: <%s>; rel="next"\r\n' "${MOCK_CURL_LINK:-}" >> "$HFILE"
printf '\r\n' >> "$HFILE"
fi
printf '%s' "$CODE"
else
# Stdout mode: body on first line(s), status code on last line
printf '%s\n%s' "${MOCK_CURL_BODY:-}" "$CODE"
fi
Loading