From b239275fdbfe09f7a778188ffbba79422dfbdccb Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 00:14:51 -0600 Subject: [PATCH 1/9] feat: add composite actions, enhanced governance, and GitHub Issue reports - Add composite actions: security-scan, sync-settings, update-pre-commit - Replace SMTP email with GitHub Issues for drift reporting - Add label sync, default branch check, vulnerability alerts, metadata checks - Enhance README with detailed purpose for every enforced setting - Add baseline schema validation to quality checks - Refactor workflows to use composite actions --- .github/actions/security-scan/action.yml | 30 ++ .github/actions/sync-settings/action.yml | 46 +++ .../update-pre-commit-composite/action.yml | 36 +++ .github/workflows/quality-checks.yml | 16 ++ .github/workflows/security.yml | 20 +- .github/workflows/sync-settings.yml | 153 ++++++---- .github/workflows/update-pre-commit-hooks.yml | 28 +- .secrets.baseline | 2 +- CLAUDE.md | 44 ++- README.md | 268 ++++++++++++++---- config/baseline.json | 35 ++- scripts/sync-repo-settings.sh | 144 ++++++++++ 12 files changed, 657 insertions(+), 165 deletions(-) create mode 100644 .github/actions/security-scan/action.yml create mode 100644 .github/actions/sync-settings/action.yml create mode 100644 .github/actions/update-pre-commit-composite/action.yml diff --git a/.github/actions/security-scan/action.yml b/.github/actions/security-scan/action.yml new file mode 100644 index 0000000..e2f1db7 --- /dev/null +++ b/.github/actions/security-scan/action.yml @@ -0,0 +1,30 @@ +name: Security Scan +description: Run SAST and SCA security scans + +inputs: + scan-path: + description: Path to scan + required: false + default: "." + +runs: + using: composite + steps: + - name: Run Semgrep SAST + uses: semgrep/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1.0.0 + with: + config: auto + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e7f3b01483832965 # v0.31.0 + with: + scan-type: fs + scan-ref: ${{ inputs.scan-path }} + format: sarif + output: trivy-results.sarif + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + if: always() + with: + sarif_file: trivy-results.sarif diff --git a/.github/actions/sync-settings/action.yml b/.github/actions/sync-settings/action.yml new file mode 100644 index 0000000..d723846 --- /dev/null +++ b/.github/actions/sync-settings/action.yml @@ -0,0 +1,46 @@ +name: Sync Repository Settings +description: Compare and apply GitHub settings across all repos against a baseline + +inputs: + mode: + description: "Run mode: --dry-run or --apply" + required: true + default: "--dry-run" + github_token: + description: PAT with repo and admin scopes + required: true + +outputs: + report_file: + description: Path to the generated report + value: ${{ steps.sync.outputs.report_file }} + total_repos: + description: Number of repos scanned + value: ${{ steps.parse.outputs.total_repos }} + compliant: + description: Number of compliant repos + value: ${{ steps.parse.outputs.compliant }} + drift: + description: Number of repos with drift + value: ${{ steps.parse.outputs.drift }} + has_drift: + description: Whether any drift was detected + value: ${{ steps.parse.outputs.has_drift }} + +runs: + using: composite + steps: + - name: Run settings sync + id: sync + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + REPORT_FILE: reports/sync-report.md + run: | + ./scripts/sync-repo-settings.sh "${{ inputs.mode }}" + echo "report_file=reports/sync-report.md" >> "$GITHUB_OUTPUT" + + - name: Parse report + id: parse + shell: bash + run: ./scripts/generate-report.sh reports/sync-report.md diff --git a/.github/actions/update-pre-commit-composite/action.yml b/.github/actions/update-pre-commit-composite/action.yml new file mode 100644 index 0000000..50471d8 --- /dev/null +++ b/.github/actions/update-pre-commit-composite/action.yml @@ -0,0 +1,36 @@ +name: Update Pre-commit Hooks Composite Action +description: Updates pre-commit hook versions and creates a PR + +inputs: + github_token: + description: GitHub token for creating PRs + required: true + +runs: + using: composite + steps: + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.x" + + - name: Install pre-commit + shell: bash + run: pip install pre-commit + + - name: Update hooks + shell: bash + run: pre-commit autoupdate + + - name: Create Pull Request + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + token: ${{ inputs.github_token }} + commit-message: "chore: update pre-commit hook versions" + title: "chore: update pre-commit hook versions" + body: | + Automated update of pre-commit hook versions. + + Review the changes to `.pre-commit-config.yaml` and merge if CI passes. + branch: chore/update-pre-commit-hooks + delete-branch: true diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index fae9d8a..d4b3c4a 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -79,6 +79,22 @@ jobs: fi done + - name: Validate baseline schema + run: | + ERRORS=0 + for section in repo_settings security branch_protection labels required_files; do + if ! jq -e ".$section" config/baseline.json > /dev/null 2>&1; then + echo "ERROR: Missing section '$section' in baseline.json" + ERRORS=$((ERRORS + 1)) + else + echo "OK: section '$section' present" + fi + done + if [ "$ERRORS" -gt 0 ]; then + echo "ERROR: baseline.json schema validation failed" + exit 1 + fi + actions-security: name: Actions Security runs-on: ubuntu-latest diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 38501aa..ec5a734 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -9,6 +9,7 @@ on: permissions: contents: read security-events: write + actions: read jobs: security-scan: @@ -17,21 +18,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Run Semgrep - uses: returntocorp/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1.0.0 - with: - config: auto - - - name: Run Trivy - uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e7f3b01483832965 # v0.31.0 with: - scan-type: fs - format: sarif - output: trivy-results.sarif + persist-credentials: false - - name: Upload Trivy SARIF - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 - if: always() + - name: Run Security Scan + uses: ./.github/actions/security-scan with: - sarif_file: trivy-results.sarif + scan-path: "." diff --git a/.github/workflows/sync-settings.yml b/.github/workflows/sync-settings.yml index 16becc8..f0b4e3c 100644 --- a/.github/workflows/sync-settings.yml +++ b/.github/workflows/sync-settings.yml @@ -17,104 +17,139 @@ on: permissions: contents: read + issues: write jobs: sync: name: Sync Settings runs-on: ubuntu-latest - permissions: - contents: read steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Sync repository settings - env: - GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} - REPORT_FILE: reports/sync-report.md - run: | - MODE="${{ github.event.inputs.mode || '--apply' }}" - ./scripts/sync-repo-settings.sh "$MODE" - - - name: Parse report - id: report - run: ./scripts/generate-report.sh reports/sync-report.md + - name: Run settings sync + id: sync + uses: ./.github/actions/sync-settings + with: + mode: ${{ github.event.inputs.mode || '--apply' }} + github_token: ${{ secrets.ORG_SETTINGS_PAT }} - name: Upload report artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sync-report + name: sync-report-${{ github.run_number }} path: reports/sync-report.md retention-days: 90 - - name: Send email report + - name: Post job summary if: always() - uses: dawidd6/action-send-mail@v3.12.0 - with: - server_address: smtp.gmail.com - server_port: 587 - username: ${{ secrets.EMAIL_USERNAME }} - password: ${{ secrets.EMAIL_PASSWORD }} - subject: | - GitHub Settings Sync Report — ${{ steps.report.outputs.drift > 0 && 'Drift Detected' || 'All Compliant' }} - to: gamaware@gmail.com - from: GitHub Org Settings <${{ secrets.EMAIL_USERNAME }}> - body: | - GitHub Organization Settings Sync Report - ========================================== - - Date: ${{ github.event.head_commit.timestamp || github.event.repository.updated_at }} - Mode: ${{ github.event.inputs.mode || '--apply' }} - Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - - Repositories scanned: ${{ steps.report.outputs.total_repos }} - Compliant: ${{ steps.report.outputs.compliant }} - Drift detected: ${{ steps.report.outputs.drift }} - - Full report is attached and available as a workflow artifact. - attachments: reports/sync-report.md + run: | + { + echo "## Settings Sync Results" + echo "" + echo "| Metric | Value |" + echo "| --- | --- |" + echo "| Repositories scanned | ${{ steps.sync.outputs.total_repos }} |" + echo "| Compliant | ${{ steps.sync.outputs.compliant }} |" + echo "| Drift detected | ${{ steps.sync.outputs.drift }} |" + echo "| Mode | ${{ github.event.inputs.mode || '--apply' }} |" + echo "" + echo "### Full Report" + echo "" + cat reports/sync-report.md + } >> "$GITHUB_STEP_SUMMARY" + + - name: Create or update drift issue + if: steps.sync.outputs.has_drift == 'true' + env: + GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} + run: | + TITLE="chore: settings drift detected — $(date '+%Y-%m-%d')" + BODY=$(cat <<'ISSUE_EOF' + ## Settings Drift Report + + **Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + **Mode**: ${{ github.event.inputs.mode || '--apply' }} + **Repos with drift**: ${{ steps.sync.outputs.drift }} / ${{ steps.sync.outputs.total_repos }} + + See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for the full report. + + ISSUE_EOF + ) + + # Close previous drift issues + gh issue list --label "settings-drift" --state open --json number --jq '.[].number' | while read -r num; do + gh issue close "$num" --comment "Superseded by new sync run." + done + + # Create new issue + gh issue create --title "$TITLE" --body "$BODY" --label "settings-drift" + + - name: Close drift issue if compliant + if: steps.sync.outputs.has_drift == 'false' + env: + GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} + run: | + gh issue list --label "settings-drift" --state open --json number --jq '.[].number' | while read -r num; do + gh issue close "$num" --comment "All repositories are now compliant." + done new-repo-check: name: Discover New Repositories runs-on: ubuntu-latest - permissions: - contents: read steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check for new repos + id: newrepos env: GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} run: | - echo "## New Repository Discovery" > reports/new-repos.md - echo "" >> reports/new-repos.md - - # Get all current repos - gh repo list gamaware --no-archived --json name,createdAt --jq '.[] | "\(.name) (created: \(.createdAt))"' --limit 200 > /tmp/all-repos.txt - - # Check repos created in the last 7 days - WEEK_AGO=$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -v-7d '+%Y-%m-%dT%H:%M:%SZ') + mkdir -p reports + { + echo "## New Repository Discovery" + echo "" + } > reports/new-repos.md + + WEEK_AGO=$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \ + || date -u -v-7d '+%Y-%m-%dT%H:%M:%SZ') NEW_REPOS=$(gh repo list gamaware --no-archived --json name,createdAt \ - --jq "[.[] | select(.createdAt > \"$WEEK_AGO\")] | .[].name" --limit 200 || echo "") + --jq "[.[] | select(.createdAt > \"$WEEK_AGO\")] | .[].name" \ + --limit 200 || echo "") if [ -n "$NEW_REPOS" ]; then - echo "New repositories found in the last 7 days:" >> reports/new-repos.md - echo "" >> reports/new-repos.md - echo "$NEW_REPOS" | while read -r repo; do - echo "- **$repo**" >> reports/new-repos.md - done - echo "" >> reports/new-repos.md - echo "These repositories will be included in the next settings sync." >> reports/new-repos.md + echo "has_new=true" >> "$GITHUB_OUTPUT" + { + echo "New repositories found in the last 7 days:" + echo "" + echo "$NEW_REPOS" | while read -r repo; do + echo "- **$repo**" + done + echo "" + echo "These repositories will be included in the next settings sync." + } >> reports/new-repos.md else + echo "has_new=false" >> "$GITHUB_OUTPUT" echo "No new repositories found in the last 7 days." >> reports/new-repos.md fi - cat reports/new-repos.md + cat reports/new-repos.md >> "$GITHUB_STEP_SUMMARY" + + - name: Create issue for new repos + if: steps.newrepos.outputs.has_new == 'true' + env: + GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} + run: | + BODY=$(cat reports/new-repos.md) + gh issue create \ + --title "chore: new repositories discovered — $(date '+%Y-%m-%d')" \ + --body "$BODY" \ + --label "new-repo" - name: Upload new repos report uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: new-repos-report + name: new-repos-report-${{ github.run_number }} path: reports/new-repos.md retention-days: 30 diff --git a/.github/workflows/update-pre-commit-hooks.yml b/.github/workflows/update-pre-commit-hooks.yml index 6acb3a7..307f9af 100644 --- a/.github/workflows/update-pre-commit-hooks.yml +++ b/.github/workflows/update-pre-commit-hooks.yml @@ -10,32 +10,16 @@ permissions: pull-requests: write jobs: - update: - name: Update Hooks + update-hooks: + name: Update Pre-commit Hooks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: "3.x" - - - name: Install pre-commit - run: pip install pre-commit + persist-credentials: false - - name: Auto-update hooks - run: pre-commit autoupdate - - - name: Create PR - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + - name: Update Hooks + uses: ./.github/actions/update-pre-commit-composite with: - commit-message: "chore: update pre-commit hook versions" - title: "chore: update pre-commit hook versions" - body: | - Automated update of pre-commit hook versions. - - Review the changes to `.pre-commit-config.yaml` and merge if CI passes. - branch: chore/update-pre-commit-hooks - delete-branch: true + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.secrets.baseline b/.secrets.baseline index d83305e..cc7d043 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -123,5 +123,5 @@ } ], "results": {}, - "generated_at": "2026-03-10T05:54:17Z" + "generated_at": "2026-03-10T06:12:31Z" } diff --git a/CLAUDE.md b/CLAUDE.md index 36c5160..b4d8922 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,15 +3,17 @@ ## Repository Overview Automated governance for GitHub repository settings across the -`gamaware` organization. Contains shell scripts and GitHub Actions -workflows that discover repos, compare settings against a baseline, -apply corrections, and send email reports. +`gamaware` organization. Contains shell scripts, composite GitHub +Actions, and workflows that discover repos, compare settings against +a baseline, apply corrections, and report drift via GitHub Issues. ## Repository Structure - `scripts/` — Shell scripts for syncing settings and generating reports - `config/` — Baseline settings JSON and per-repo overrides - `.github/workflows/` — CI/CD and scheduled sync workflows +- `.github/actions/` — Composite actions (security-scan, sync-settings, + update-pre-commit-composite) - `docs/adr/` — Architecture Decision Records ## Git Workflow @@ -56,13 +58,41 @@ commit hooks — see `.pre-commit-config.yaml` for the full list. ## CI/CD Pipelines -- `sync-settings.yml` — weekly settings sync + email report -- `quality-checks.yml` — markdown, YAML, shell, structure validation -- `security.yml` — Semgrep SAST + Trivy SCA -- `update-pre-commit-hooks.yml` — weekly auto-update via PR +- `sync-settings.yml` — weekly settings sync + GitHub Issue reports +- `quality-checks.yml` — markdown, YAML, shell, structure, JSON + schema validation +- `security.yml` — Semgrep SAST + Trivy SCA (via composite action) +- `update-pre-commit-hooks.yml` — weekly auto-update via PR (via + composite action) + +## Composite Actions + +- `.github/actions/security-scan/` — reusable Semgrep + Trivy scan +- `.github/actions/sync-settings/` — reusable settings sync with + outputs for drift detection +- `.github/actions/update-pre-commit-composite/` — reusable + pre-commit autoupdate + PR creation ## Code Review - CodeRabbit auto-review via `.coderabbit.yaml` - GitHub Copilot auto-review via ruleset - Both reviewers run on every PR + +## Settings Sync Details + +The sync script (`scripts/sync-repo-settings.sh`) enforces: + +1. **Repo settings**: merge strategy, features, auto-merge, branch + cleanup +2. **Security**: secret scanning, push protection, vulnerability + alerts +3. **Branch protection**: reviews, CODEOWNERS, linear history, + conversation resolution +4. **Labels**: standard issue labels across all repos +5. **Default branch**: ensures all repos use `main` +6. **Metadata**: flags missing descriptions and topics (advisory) +7. **Required files**: LICENSE, README, CODEOWNERS, etc. + +Configuration lives in `config/baseline.json` with per-repo +overrides in `config/overrides.json`. diff --git a/README.md b/README.md index bf70009..c9227b5 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,165 @@ # GitHub Organization Settings Automated governance for GitHub repository settings across the -`gamaware` organization. Ensures consistent configuration, branch -protection, and security practices across all repositories. +`gamaware` organization. Discovers all repositories, compares their +configuration against a defined baseline, applies corrections, and +reports drift via GitHub Issues. -## What It Does +## Why This Exists -- Discovers all repositories in the `gamaware` GitHub account -- Compares current settings against a defined baseline -- Applies standardized settings (merge strategies, branch protection, - security scanning) -- Runs weekly on a schedule and on every push for validation -- Sends an email report summarizing changes and drift +Manually configuring GitHub repository settings is error-prone and +does not scale. When you create a new repository or change a policy +(e.g., requiring linear history), every repo needs updating. This +automation ensures that all repositories under `gamaware` converge to +a single standard — the same merge strategies, branch protection +rules, security scanning, and scaffolding files. -## Settings Enforced +## How It Works -| Category | Setting | Value | +```text +Weekly (Sunday 00:00 UTC) + | + v ++-------------------+ +| Discover repos | -- gh repo list (excludes archived + excluded) ++-------------------+ + | + v ++-------------------+ +| Compare settings | -- current state vs config/baseline.json ++-------------------+ + | + v ++-------------------+ +| Apply corrections | -- PATCH/PUT via GitHub API (--apply mode) ++-------------------+ + | + v ++-------------------+ +| Generate report | -- Markdown report + GitHub Issue if drift found ++-------------------+ +``` + +The workflow also checks for repositories created in the last 7 days +and opens a GitHub Issue to flag them. + +## What Gets Enforced + +### Merge Strategy + +Every repository is locked to squash-merge only. This keeps `main` +history linear and readable. + +| Setting | Value | Why | | --- | --- | --- | -| Merge | Squash merge only | `true` | -| Merge | Merge commit | `false` | -| Merge | Rebase merge | `false` | -| Merge | Squash commit title | `PR_TITLE` | -| Merge | Delete branch on merge | `true` | -| Merge | Allow auto merge | `true` | -| Merge | Allow update branch | `true` | -| Features | Wiki | `false` | -| Features | Projects | `false` | -| Features | Discussions | `false` | -| Features | Issues | `true` | -| Security | Secret scanning | `enabled` | -| Security | Push protection | `enabled` | -| Branch Protection | Required reviews | `1` | -| Branch Protection | Dismiss stale reviews | `true` | -| Branch Protection | Require CODEOWNERS | `true` | -| Branch Protection | Required status checks (strict) | `true` | -| Branch Protection | Required linear history | `true` | -| Branch Protection | Required conversation resolution | `true` | -| Branch Protection | Enforce admins | `false` | +| `allow_squash_merge` | `true` | Single clean commit per PR | +| `allow_merge_commit` | `false` | Prevents noisy merge commits | +| `allow_rebase_merge` | `false` | Prevents rebase without squash | +| `squash_merge_commit_title` | `PR_TITLE` | Consistent commit messages | +| `squash_merge_commit_message` | `PR_BODY` | PR description becomes commit body | +| `delete_branch_on_merge` | `true` | Auto-cleanup merged branches | +| `allow_auto_merge` | `true` | Merge automatically when checks pass | +| `allow_update_branch` | `true` | Keep PR branches current with base | + +### Repository Features + +Unused features are disabled to reduce attack surface and clutter. + +| Setting | Value | Why | +| --- | --- | --- | +| `has_wiki` | `false` | Documentation lives in the repo | +| `has_projects` | `false` | Not used for project tracking | +| `has_discussions` | `false` | Not used for discussions | +| `has_issues` | `true` | Primary issue tracker | + +### Security + +Secret scanning catches leaked credentials before they reach `main`. +Vulnerability alerts flag known CVEs in dependencies. + +| Setting | Value | Why | +| --- | --- | --- | +| Secret scanning | `enabled` | Detect leaked tokens and keys | +| Push protection | `enabled` | Block pushes containing secrets | +| Vulnerability alerts | `enabled` | Dependabot CVE notifications | + +### Branch Protection (main) + +All changes go through PRs with at least one review. Linear history +ensures clean `git log` and bisectability. + +| Setting | Value | Why | +| --- | --- | --- | +| Required reviews | `1` | At least one approval before merge | +| Dismiss stale reviews | `true` | New pushes invalidate old approvals | +| Require CODEOWNERS | `true` | Owners must review their areas | +| Required status checks | `strict` | Branch must be up to date | +| Required linear history | `true` | No merge commits on main | +| Required conversation resolution | `true` | All comments must be resolved | +| Enforce admins | `false` | Admins can bypass when needed | + +### Labels + +Standard labels are created on every repo for consistent issue +triage. + +| Label | Color | Purpose | +| --- | --- | --- | +| `bug` | red | Something is broken | +| `enhancement` | cyan | New feature or improvement | +| `documentation` | blue | Docs updates | +| `security` | yellow | Security-related | +| `settings-drift` | gold | Settings mismatch (used by this repo) | +| `new-repo` | green | New repo discovered (used by this repo) | + +### Required Files + +Every repo must contain these files. Missing files are flagged in the +report (not auto-created, since content is repo-specific). + +| File | Purpose | +| --- | --- | +| `LICENSE` | Legal terms (MIT) | +| `README.md` | Project overview | +| `.gitignore` | Exclude build artifacts and secrets | +| `CODEOWNERS` | Assign default reviewers | +| `CONTRIBUTING.md` | Contribution guidelines | +| `SECURITY.md` | Vulnerability disclosure policy | +| `.pre-commit-config.yaml` | Local linting and validation | +| `.github/dependabot.yml` | Automated dependency updates | + +### Metadata Checks (Advisory) + +Repos missing a description or topics are flagged in the report for +manual attention. These are not auto-fixed because they require +human judgment. + +### Default Branch Check + +Repos not using `main` as the default branch are flagged and can be +corrected in `--apply` mode. ## Repository Structure ```text github-org-settings/ ├── .claude/ -│ ├── settings.json +│ ├── settings.json # Claude Code hooks config │ └── hooks/ -│ └── post-edit.sh +│ └── post-edit.sh # Auto-format on edit ├── .github/ -│ ├── workflows/ -│ │ ├── sync-settings.yml -│ │ ├── quality-checks.yml -│ │ ├── security.yml -│ │ └── update-pre-commit-hooks.yml │ ├── actions/ -│ │ ├── update-pre-commit-composite/ +│ │ ├── security-scan/ # Composite: Semgrep + Trivy │ │ │ └── action.yml -│ │ └── security-scan/ +│ │ ├── sync-settings/ # Composite: settings sync +│ │ │ └── action.yml +│ │ └── update-pre-commit-composite/ # Composite: hook updates │ │ └── action.yml +│ ├── workflows/ +│ │ ├── sync-settings.yml # Weekly settings sync + reports +│ │ ├── quality-checks.yml # PR/push linting and validation +│ │ ├── security.yml # SAST + SCA scanning +│ │ └── update-pre-commit-hooks.yml # Weekly hook updates │ ├── ISSUE_TEMPLATE/ │ │ ├── settings-bug.md │ │ └── settings-request.md @@ -64,23 +167,24 @@ github-org-settings/ │ ├── copilot-instructions.md │ └── dependabot.yml ├── scripts/ -│ ├── sync-repo-settings.sh -│ └── generate-report.sh +│ ├── sync-repo-settings.sh # Main sync logic +│ └── generate-report.sh # Report parser for CI ├── config/ -│ ├── baseline.json -│ └── overrides.json +│ ├── baseline.json # Settings enforced on all repos +│ └── overrides.json # Per-repo exceptions ├── docs/ │ └── adr/ │ ├── README.md -│ └── 001-settings-governance.md -├── .coderabbit.yaml +│ ├── 001-settings-governance.md +│ └── 002-sync-architecture.md +├── .coderabbit.yaml # CodeRabbit auto-review config ├── .gitignore ├── .markdownlint.yaml ├── .yamllint.yml ├── .pre-commit-config.yaml ├── .secrets.baseline -├── zizmor.yml -├── CLAUDE.md +├── zizmor.yml # GitHub Actions security config +├── CLAUDE.md # Claude Code project instructions ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE @@ -90,19 +194,36 @@ github-org-settings/ ## Usage -### Manual Run +### Automatic (Weekly) + +The `sync-settings.yml` workflow runs every Sunday at midnight UTC in +`--apply` mode. It: + +1. Discovers all non-archived repos +2. Compares settings against `config/baseline.json` +3. Applies corrections via the GitHub API +4. Posts a Job Summary with the full report +5. Opens a GitHub Issue if drift was detected +6. Closes previous drift issues if all repos are compliant +7. Flags new repos created in the last 7 days + +### Manual Trigger ```bash -gh workflow run sync-settings.yml +# Dry run (validation only) +gh workflow run sync-settings.yml -f mode="--dry-run" + +# Apply settings +gh workflow run sync-settings.yml -f mode="--apply" ``` ### Local Testing ```bash -# Dry run (validate only, no changes applied) +# Dry run ./scripts/sync-repo-settings.sh --dry-run -# Apply settings +# Apply ./scripts/sync-repo-settings.sh --apply ``` @@ -110,22 +231,49 @@ gh workflow run sync-settings.yml ### Baseline Settings -Edit `config/baseline.json` to change the enforced settings across all -repositories. +Edit `config/baseline.json` to change the enforced settings. Changes +go through PR review like any code change. ### Per-Repo Overrides -Edit `config/overrides.json` to exempt specific repositories from -certain settings. +Edit `config/overrides.json` to set repo-specific exceptions. The +most common override is `required_status_checks.contexts` since +each repo has different CI jobs. + +### Excluding Repositories + +Add repo names to the `excluded` array in `config/overrides.json`: + +```json +{ + "excluded": ["some-repo-to-skip"] +} +``` + +## Secrets Required + +| Secret | Purpose | Scopes | +| --- | --- | --- | +| `ORG_SETTINGS_PAT` | GitHub PAT for API access | `repo`, `admin:org` | + +## Code Review + +- **CodeRabbit**: auto-review on PRs via `.coderabbit.yaml` +- **GitHub Copilot**: auto-review via ruleset with custom instructions -### Excluded Repositories +## CI/CD Pipelines -Repositories can be excluded entirely by adding them to the `excluded` -array in `config/overrides.json`. +| Workflow | Trigger | Purpose | +| --- | --- | --- | +| `sync-settings.yml` | Weekly + manual | Settings enforcement | +| `quality-checks.yml` | PR + push to main | Markdown, YAML, shell, structure | +| `security.yml` | PR + push to main | Semgrep SAST + Trivy SCA | +| `update-pre-commit-hooks.yml` | Weekly + manual | Auto-update hook versions | ## Author -Jorge Alejandro Garcia Martinez ([@gamaware](https://github.com/gamaware)) +Jorge Alejandro Garcia Martinez +([@gamaware](https://github.com/gamaware)) ## License diff --git a/config/baseline.json b/config/baseline.json index 7ba5dbe..de3a9f2 100644 --- a/config/baseline.json +++ b/config/baseline.json @@ -15,7 +15,8 @@ }, "security": { "secret_scanning": true, - "secret_scanning_push_protection": true + "secret_scanning_push_protection": true, + "vulnerability_alerts": true }, "branch_protection": { "branch": "main", @@ -33,6 +34,38 @@ "allow_force_pushes": false, "allow_deletions": false }, + "labels": [ + { + "name": "bug", + "color": "d73a4a", + "description": "Something is not working" + }, + { + "name": "enhancement", + "color": "a2eeef", + "description": "New feature or improvement" + }, + { + "name": "documentation", + "color": "0075ca", + "description": "Documentation updates" + }, + { + "name": "security", + "color": "e4e669", + "description": "Security-related issue" + }, + { + "name": "settings-drift", + "color": "fbca04", + "description": "Repository settings do not match baseline" + }, + { + "name": "new-repo", + "color": "0e8a16", + "description": "New repository discovered" + } + ], "required_files": [ "LICENSE", "README.md", diff --git a/scripts/sync-repo-settings.sh b/scripts/sync-repo-settings.sh index fa4dc02..bdb57ae 100755 --- a/scripts/sync-repo-settings.sh +++ b/scripts/sync-repo-settings.sh @@ -134,6 +134,37 @@ SECURITY_EOF echo -e "$changes" } +# Enable Dependabot vulnerability alerts +sync_vulnerability_alerts() { + local repo="$1" + local effective + effective=$(get_effective_settings "$repo") + local changes="" + + local want_alerts + want_alerts=$(echo "$effective" | jq -r '.security.vulnerability_alerts // false') + if [ "$want_alerts" != "true" ]; then + return + fi + + local current_alerts + current_alerts=$(gh api "repos/$OWNER/$repo/vulnerability-alerts" -i 2>/dev/null | head -1 || echo "") + + if echo "$current_alerts" | grep -q "204"; then + log "OK: vulnerability alerts for $repo" + else + changes="- Vulnerability alerts: \`disabled\` -> \`enabled\`\n" + if [ "$MODE" = "--apply" ]; then + gh api -X PUT "repos/$OWNER/$repo/vulnerability-alerts" > /dev/null 2>&1 \ + || log "WARN: Could not enable vulnerability alerts for $repo" + log "APPLIED vulnerability alerts for $repo" + else + log "DRIFT detected in vulnerability alerts for $repo" + fi + fi + echo -e "$changes" +} + # Compare and optionally apply branch protection sync_branch_protection() { local repo="$1" @@ -278,6 +309,91 @@ PROTECT_EOF ) > /dev/null 2>&1 } +# Sync standard labels across repos +sync_labels() { + local repo="$1" + local effective + effective=$(get_effective_settings "$repo") + local changes="" + + local label_count + label_count=$(echo "$effective" | jq -r '.labels // [] | length') + if [ "$label_count" = "0" ]; then + return + fi + + local current_labels + current_labels=$(gh api "repos/$OWNER/$repo/labels" --jq '.[].name' 2>/dev/null || echo "") + + echo "$effective" | jq -c '.labels[]' 2>/dev/null | while read -r label_json; do + local name color description + name=$(echo "$label_json" | jq -r '.name') + color=$(echo "$label_json" | jq -r '.color') + description=$(echo "$label_json" | jq -r '.description') + + if ! echo "$current_labels" | grep -qx "$name"; then + changes="${changes}- Label missing: \`$name\`\n" + if [ "$MODE" = "--apply" ]; then + gh api -X POST "repos/$OWNER/$repo/labels" \ + -f name="$name" -f color="$color" -f description="$description" \ + > /dev/null 2>&1 || log "WARN: Could not create label $name for $repo" + fi + fi + done + echo -e "$changes" +} + +# Check default branch is 'main' +check_default_branch() { + local repo="$1" + local changes="" + + local default_branch + default_branch=$(gh api "repos/$OWNER/$repo" --jq '.default_branch' 2>/dev/null || echo "") + + if [ "$default_branch" != "main" ] && [ -n "$default_branch" ]; then + changes="- Default branch: \`$default_branch\` (expected \`main\`)\n" + if [ "$MODE" = "--apply" ]; then + gh api -X PATCH "repos/$OWNER/$repo" -f default_branch="main" > /dev/null 2>&1 \ + || log "WARN: Could not rename default branch for $repo (may need manual rename)" + log "APPLIED default branch rename for $repo" + else + log "DRIFT detected in default branch for $repo" + fi + else + log "OK: default branch for $repo" + fi + echo -e "$changes" +} + +# Check for missing description and topics +check_repo_metadata() { + local repo="$1" + local changes="" + + local current + current=$(gh api "repos/$OWNER/$repo" 2>/dev/null) || return + + local description + description=$(echo "$current" | jq -r '.description // ""') + if [ -z "$description" ] || [ "$description" = "null" ]; then + changes="${changes}- **Missing repository description**\n" + fi + + local topics + topics=$(echo "$current" | jq -r '.topics | length') + if [ "$topics" = "0" ]; then + changes="${changes}- **No repository topics configured**\n" + fi + + if [ -n "$changes" ]; then + log "WARN: metadata gaps for $repo" + else + log "OK: metadata for $repo" + fi + echo -e "$changes" +} + # Check for required files check_required_files() { local repo="$1" @@ -328,6 +444,13 @@ REPORT_HEADER local repo_drift="" + # Default branch + local branch_changes + branch_changes=$(check_default_branch "$repo") + if [ -n "$branch_changes" ]; then + repo_drift="${repo_drift}### Default Branch\n\n${branch_changes}\n" + fi + # Repo settings local repo_changes repo_changes=$(sync_repo_settings "$repo") @@ -342,6 +465,13 @@ REPORT_HEADER repo_drift="${repo_drift}### Security\n\n${security_changes}\n" fi + # Vulnerability alerts + local vuln_changes + vuln_changes=$(sync_vulnerability_alerts "$repo") + if [ -n "$vuln_changes" ]; then + repo_drift="${repo_drift}### Vulnerability Alerts\n\n${vuln_changes}\n" + fi + # Branch protection local protection_changes protection_changes=$(sync_branch_protection "$repo") @@ -349,6 +479,20 @@ REPORT_HEADER repo_drift="${repo_drift}### Branch Protection\n\n${protection_changes}\n" fi + # Labels + local label_changes + label_changes=$(sync_labels "$repo") + if [ -n "$label_changes" ]; then + repo_drift="${repo_drift}### Labels\n\n${label_changes}\n" + fi + + # Metadata (advisory only — not auto-fixed) + local metadata_changes + metadata_changes=$(check_repo_metadata "$repo") + if [ -n "$metadata_changes" ]; then + repo_drift="${repo_drift}### Metadata (Manual Action Needed)\n\n${metadata_changes}\n" + fi + # Required files local missing_files missing_files=$(check_required_files "$repo") From d83bc29de4a5be411ad9fa8a1b5d409340043075 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 00:33:53 -0600 Subject: [PATCH 2/9] feat: add architecture docs, runbooks, and Claude Code skills - Add docs/architecture.md with system context and component overview - Add docs/runbooks/ with 4 operational procedures (exclude repo, add setting, handle drift, onboard repo) - Add 3 Claude Code skills: /audit, /add-repo-override, /exclude-repo - Update README and CLAUDE.md with new structure --- .claude/skills/add-repo-override/SKILL.md | 26 ++++++ .claude/skills/audit/SKILL.md | 24 +++++ .claude/skills/exclude-repo/SKILL.md | 21 +++++ CLAUDE.md | 11 +++ README.md | 24 +++-- docs/architecture.md | 104 ++++++++++++++++++++++ docs/runbooks/README.md | 10 +++ docs/runbooks/add-setting.md | 41 +++++++++ docs/runbooks/exclude-repo.md | 28 ++++++ docs/runbooks/handle-drift.md | 40 +++++++++ docs/runbooks/onboard-repo.md | 60 +++++++++++++ 11 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/add-repo-override/SKILL.md create mode 100644 .claude/skills/audit/SKILL.md create mode 100644 .claude/skills/exclude-repo/SKILL.md create mode 100644 docs/architecture.md create mode 100644 docs/runbooks/README.md create mode 100644 docs/runbooks/add-setting.md create mode 100644 docs/runbooks/exclude-repo.md create mode 100644 docs/runbooks/handle-drift.md create mode 100644 docs/runbooks/onboard-repo.md diff --git a/.claude/skills/add-repo-override/SKILL.md b/.claude/skills/add-repo-override/SKILL.md new file mode 100644 index 0000000..b1d6ab6 --- /dev/null +++ b/.claude/skills/add-repo-override/SKILL.md @@ -0,0 +1,26 @@ +--- +name: add-repo-override +description: Add a per-repo settings override to overrides.json +user-invocable: true +--- + +# Add Repository Override + +Add a per-repo exception to `config/overrides.json`. + +## Arguments + +`$ARGUMENTS` should be in the format: ` ` + +Examples: +- `my-repo branch_protection.required_status_checks.contexts '["Build","Test"]'` +- `my-repo repo_settings.has_wiki true` + +## Steps + +1. Read the current `config/overrides.json` +2. Parse `$ARGUMENTS` to extract repo name, setting path, and value +3. Add or update the override for the specified repo +4. Validate the resulting JSON with `jq empty` +5. Show the diff of what changed +6. Remind the user to create a PR for the change diff --git a/.claude/skills/audit/SKILL.md b/.claude/skills/audit/SKILL.md new file mode 100644 index 0000000..755dae2 --- /dev/null +++ b/.claude/skills/audit/SKILL.md @@ -0,0 +1,24 @@ +--- +name: audit +description: Run a dry-run settings audit across all repositories +user-invocable: true +disable-model-invocation: true +--- + +# Audit Repository Settings + +Run a dry-run sync to check for drift without applying changes. + +## Steps + +1. Run the sync script in dry-run mode: + +```bash +./scripts/sync-repo-settings.sh --dry-run +``` + +2. Display the report: + +```bash +cat reports/sync-report.md +``` diff --git a/.claude/skills/exclude-repo/SKILL.md b/.claude/skills/exclude-repo/SKILL.md new file mode 100644 index 0000000..00e07d6 --- /dev/null +++ b/.claude/skills/exclude-repo/SKILL.md @@ -0,0 +1,21 @@ +--- +name: exclude-repo +description: Exclude a repository from settings governance +user-invocable: true +--- + +# Exclude Repository + +Add a repository to the exclusion list in `config/overrides.json`. + +## Arguments + +`$ARGUMENTS` should be the repository name to exclude. + +## Steps + +1. Read the current `config/overrides.json` +2. Add `$ARGUMENTS` to the `excluded` array (if not already present) +3. Validate the resulting JSON with `jq empty` +4. Show the updated exclusion list +5. Remind the user to create a PR for the change diff --git a/CLAUDE.md b/CLAUDE.md index b4d8922..6573ac7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,12 @@ a baseline, apply corrections, and report drift via GitHub Issues. - `.github/workflows/` — CI/CD and scheduled sync workflows - `.github/actions/` — Composite actions (security-scan, sync-settings, update-pre-commit-composite) +- `.claude/skills/` — Reusable skills (`/audit`, `/add-repo-override`, + `/exclude-repo`) +- `docs/architecture.md` — System architecture overview - `docs/adr/` — Architecture Decision Records +- `docs/runbooks/` — Operational procedures (exclude repo, add setting, + handle drift, onboard repo) ## Git Workflow @@ -73,6 +78,12 @@ commit hooks — see `.pre-commit-config.yaml` for the full list. - `.github/actions/update-pre-commit-composite/` — reusable pre-commit autoupdate + PR creation +## Claude Code Skills + +- `/audit` — run a dry-run settings check across all repos +- `/add-repo-override` — add a per-repo exception to overrides.json +- `/exclude-repo` — exclude a repository from governance + ## Code Review - CodeRabbit auto-review via `.coderabbit.yaml` diff --git a/README.md b/README.md index c9227b5..ab6a05e 100644 --- a/README.md +++ b/README.md @@ -145,8 +145,15 @@ corrected in `--apply` mode. github-org-settings/ ├── .claude/ │ ├── settings.json # Claude Code hooks config -│ └── hooks/ -│ └── post-edit.sh # Auto-format on edit +│ ├── hooks/ +│ │ └── post-edit.sh # Auto-format on edit +│ └── skills/ +│ ├── audit/ # /audit — dry-run settings check +│ │ └── SKILL.md +│ ├── add-repo-override/ # /add-repo-override — add exception +│ │ └── SKILL.md +│ └── exclude-repo/ # /exclude-repo — exclude a repo +│ └── SKILL.md ├── .github/ │ ├── actions/ │ │ ├── security-scan/ # Composite: Semgrep + Trivy @@ -173,10 +180,17 @@ github-org-settings/ │ ├── baseline.json # Settings enforced on all repos │ └── overrides.json # Per-repo exceptions ├── docs/ -│ └── adr/ +│ ├── architecture.md # System architecture overview +│ ├── adr/ +│ │ ├── README.md +│ │ ├── 001-settings-governance.md +│ │ └── 002-sync-architecture.md +│ └── runbooks/ │ ├── README.md -│ ├── 001-settings-governance.md -│ └── 002-sync-architecture.md +│ ├── add-setting.md # How to add a new setting +│ ├── exclude-repo.md # How to exclude a repo +│ ├── handle-drift.md # How to respond to drift +│ └── onboard-repo.md # How to onboard a new repo ├── .coderabbit.yaml # CodeRabbit auto-review config ├── .gitignore ├── .markdownlint.yaml diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..bb11f4c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,104 @@ +# Architecture Overview + +## System Context + +This repository is the single source of truth for GitHub repository +settings across all `gamaware` repositories. It acts as a control +plane that reads a desired state from JSON configuration and +reconciles it against the actual state via the GitHub API. + +```text ++---------------------+ +------------------+ +| config/ | | GitHub API | +| baseline.json |------>| (repos, branches,| +| overrides.json | | labels, security| ++---------------------+ +------------------+ + | ^ + v | ++---------------------+ | +| scripts/ | | +| sync-repo-settings |---------------+ +| generate-report | ++---------------------+ + | + v ++---------------------+ +| GitHub Issues | +| Job Summaries | +| Artifacts (90 days) | ++---------------------+ +``` + +## Components + +### Configuration Layer (`config/`) + +- `baseline.json` — settings enforced on every repository +- `overrides.json` — per-repo exceptions (status checks, exclusions) + +The effective configuration for a repo is the baseline deep-merged +with its overrides. If no override exists, the baseline is used +as-is. + +### Sync Engine (`scripts/`) + +`sync-repo-settings.sh` is the core reconciliation loop: + +1. **Discovery** — `gh repo list` finds all non-archived repos +2. **Comparison** — for each repo, current state is fetched via API + and compared field-by-field against the effective config +3. **Remediation** — in `--apply` mode, PATCH/PUT calls correct drift +4. **Reporting** — a markdown report is generated with per-repo diffs + +The script handles 7 categories independently: + +| Category | API Endpoint | Method | +| --- | --- | --- | +| Repo settings | `repos/{owner}/{repo}` | PATCH | +| Security | `repos/{owner}/{repo}` | PATCH | +| Vulnerability alerts | `repos/{owner}/{repo}/vulnerability-alerts` | PUT | +| Branch protection | `repos/{owner}/{repo}/branches/main/protection` | PUT | +| Labels | `repos/{owner}/{repo}/labels` | POST | +| Default branch | `repos/{owner}/{repo}` | PATCH | +| Metadata | (advisory only) | — | + +### Composite Actions (`.github/actions/`) + +Reusable building blocks consumed by workflows: + +- `security-scan/` — Semgrep SAST + Trivy SCA with SARIF upload +- `sync-settings/` — wraps the sync script with structured outputs +- `update-pre-commit-composite/` — autoupdates hooks and creates PR + +### Workflows (`.github/workflows/`) + +| Workflow | Schedule | Purpose | +| --- | --- | --- | +| `sync-settings.yml` | Weekly Sunday 00:00 UTC | Settings enforcement | +| `quality-checks.yml` | PR + push | Linting and validation | +| `security.yml` | PR + push | SAST + SCA | +| `update-pre-commit-hooks.yml` | Weekly Sunday 00:00 UTC | Hook version updates | + +### Reporting + +Drift is communicated through three channels: + +1. **GitHub Issues** — auto-created with `settings-drift` label when + drift is found, auto-closed when all repos are compliant +2. **Job Summaries** — visible in the Actions run page +3. **Artifacts** — full markdown report stored for 90 days + +## Security Model + +- The `ORG_SETTINGS_PAT` secret provides API access with `repo` and + `admin:org` scopes +- Workflows use least-privilege `permissions` blocks +- All third-party actions are pinned to SHA commits +- Secret scanning and push protection are enforced on this repo too + +## Design Decisions + +Detailed rationale is documented in Architecture Decision Records: + +- [ADR 001: Settings Governance](adr/001-settings-governance.md) +- [ADR 002: Sync Architecture](adr/002-sync-architecture.md) diff --git a/docs/runbooks/README.md b/docs/runbooks/README.md new file mode 100644 index 0000000..1d712e2 --- /dev/null +++ b/docs/runbooks/README.md @@ -0,0 +1,10 @@ +# Runbooks + +Operational procedures for managing GitHub organization settings. + +| Runbook | Purpose | +| --- | --- | +| [Exclude a Repository](exclude-repo.md) | Stop syncing settings to a repo | +| [Add a New Setting](add-setting.md) | Enforce a new setting across all repos | +| [Handle Drift](handle-drift.md) | Respond to drift detection issues | +| [Onboard New Repository](onboard-repo.md) | Prepare a new repo for governance | diff --git a/docs/runbooks/add-setting.md b/docs/runbooks/add-setting.md new file mode 100644 index 0000000..9016f8c --- /dev/null +++ b/docs/runbooks/add-setting.md @@ -0,0 +1,41 @@ +# Add a New Setting + +Enforce a new GitHub setting across all repositories. + +## Steps + +1. Identify the GitHub API field name for the setting. Check the + [GitHub REST API docs](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository) + +2. Add the field to the appropriate section in `config/baseline.json`: + - `repo_settings` — repository-level toggles + - `security` — security features + - `branch_protection` — branch protection rules + - `labels` — standard labels + - `required_files` — files that must exist + +3. Update `scripts/sync-repo-settings.sh` if the new setting requires + a different API endpoint or comparison logic + +4. Update `README.md` to document the new setting and its purpose + +5. Test locally: + + ```bash + ./scripts/sync-repo-settings.sh --dry-run + ``` + +6. Create a PR. Review the dry-run output to confirm the expected + changes + +7. After merge, the next weekly run will apply the setting. For + immediate application, trigger manually: + + ```bash + gh workflow run sync-settings.yml -f mode="--apply" + ``` + +## Per-Repo Exceptions + +If a repo legitimately needs a different value, add an override in +`config/overrides.json` under the repo name. diff --git a/docs/runbooks/exclude-repo.md b/docs/runbooks/exclude-repo.md new file mode 100644 index 0000000..6658052 --- /dev/null +++ b/docs/runbooks/exclude-repo.md @@ -0,0 +1,28 @@ +# Exclude a Repository + +Stop syncing settings to a specific repository. + +## When to Use + +- Third-party forks that must keep upstream settings +- Temporary repos that will be deleted soon +- Repos with fundamentally different governance needs + +## Steps + +1. Open `config/overrides.json` +2. Add the repo name to the `excluded` array: + + ```json + { + "excluded": ["repo-to-exclude"] + } + ``` + +3. Create a PR with the change +4. After merge, the next sync run will skip this repo + +## Reverting + +Remove the repo name from the `excluded` array and merge the PR. +The next sync run will pick it up again. diff --git a/docs/runbooks/handle-drift.md b/docs/runbooks/handle-drift.md new file mode 100644 index 0000000..98449e8 --- /dev/null +++ b/docs/runbooks/handle-drift.md @@ -0,0 +1,40 @@ +# Handle Drift Detection + +Respond to a `settings-drift` issue created by the sync workflow. + +## Triage + +1. Open the linked workflow run from the issue body +2. Review the Job Summary to see which repos drifted and what changed +3. Download the report artifact for full details + +## Common Causes + +| Cause | Action | +| --- | --- | +| Manual settings change via GitHub UI | Let the next `--apply` run fix it | +| New repo created without governance | The sync will apply baseline settings | +| Legitimate exception needed | Add an override in `config/overrides.json` | +| PAT expired or lacks scopes | Rotate the `ORG_SETTINGS_PAT` secret | + +## Manual Fix + +If you need to fix drift immediately without waiting for the +weekly run: + +```bash +gh workflow run sync-settings.yml -f mode="--apply" +``` + +Or locally: + +```bash +./scripts/sync-repo-settings.sh --apply +``` + +## Issue Lifecycle + +- The workflow auto-creates a new issue each time drift is found +- Previous drift issues are auto-closed with a superseded comment +- When all repos are compliant, open drift issues are auto-closed + with a compliant comment diff --git a/docs/runbooks/onboard-repo.md b/docs/runbooks/onboard-repo.md new file mode 100644 index 0000000..6d91589 --- /dev/null +++ b/docs/runbooks/onboard-repo.md @@ -0,0 +1,60 @@ +# Onboard a New Repository + +Prepare a new repository for settings governance. + +## Automatic Discovery + +New repos are discovered automatically by the weekly sync. The +workflow creates a GitHub Issue with the `new-repo` label listing +repos created in the last 7 days. + +## What the Sync Handles Automatically + +- Repository settings (merge strategy, features, auto-merge) +- Security settings (secret scanning, push protection, vulnerability + alerts) +- Branch protection rules +- Standard labels + +## What Needs Manual Setup + +The sync reports missing files but does not create them, since their +content is repo-specific: + +1. `LICENSE` — choose the appropriate license +2. `README.md` — write project overview +3. `.gitignore` — configure for the project's languages +4. `CODEOWNERS` — assign default reviewers +5. `CONTRIBUTING.md` — contribution guidelines +6. `SECURITY.md` — vulnerability disclosure policy +7. `.pre-commit-config.yaml` — configure hooks for the project's stack +8. `.github/dependabot.yml` — configure dependency updates + +## Adding Status Check Overrides + +Each repo has different CI jobs. Add the required status check names +to `config/overrides.json`: + +```json +{ + "repos": { + "new-repo-name": { + "branch_protection": { + "required_status_checks": { + "contexts": ["Build", "Test", "Lint"] + } + } + } + } +} +``` + +## Adding Description and Topics + +The sync flags repos without a description or topics. Set them via: + +```bash +gh repo edit gamaware/new-repo-name \ + --description "Short description" \ + --add-topic topic1 --add-topic topic2 +``` From 5ea0394e4b3bf3ef80aee38c3f2080724d61baa7 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 00:35:58 -0600 Subject: [PATCH 3/9] fix: preserve existing status check contexts during branch protection sync The PUT endpoint replaces all branch protection. Without this fix, repos without an explicit override in overrides.json would have their status check contexts wiped to an empty array. Now the script reads the current contexts from the repo and carries them forward. --- scripts/sync-repo-settings.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/sync-repo-settings.sh b/scripts/sync-repo-settings.sh index bdb57ae..a56e2be 100755 --- a/scripts/sync-repo-settings.sh +++ b/scripts/sync-repo-settings.sh @@ -261,11 +261,22 @@ apply_branch_protection() { local branch="$2" local effective="$3" + # Preserve existing status check contexts if no override is defined. + # The PUT endpoint replaces all protection, so we must carry forward + # the repo's current contexts to avoid wiping repo-specific CI checks. local contexts="[]" local override_contexts override_contexts=$(jq -r --arg r "$repo" '.repos[$r].branch_protection.required_status_checks.contexts // empty' "$OVERRIDES" 2>/dev/null || echo "") if [ -n "$override_contexts" ]; then contexts=$(jq -r --arg r "$repo" '.repos[$r].branch_protection.required_status_checks.contexts' "$OVERRIDES") + else + # No override — read current contexts from the repo so we don't wipe them + local current_contexts + current_contexts=$(gh api "repos/$OWNER/$repo/branches/$branch/protection/required_status_checks" --jq '.contexts // []' 2>/dev/null || echo "[]") + if [ -n "$current_contexts" ] && [ "$current_contexts" != "[]" ]; then + contexts="$current_contexts" + log "INFO: Preserving existing status check contexts for $repo" + fi fi local strict From fd965c9d2f038eff862a50825c777864c6e300e0 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 10:08:05 -0600 Subject: [PATCH 4/9] fix: address review comments and absorb Dependabot version bumps Review fixes (CodeRabbit + Copilot): - Fix sync_labels subshell scoping: use process substitution instead of piped while loop so changes variable propagates correctly - Use grep -qxF instead of grep -qx to avoid regex interpretation - Read default branch from config instead of hardcoding "main" - Handle explicit false overrides for vulnerability_alerts - Remove unused issues: write permission from workflow - Create drift issue before closing old ones to prevent gap - Add concurrency group to prevent parallel sync runs - Raise repo list limit from 200 to 1000 - Add label field structure validation to baseline schema check Dependabot bumps absorbed: - actions/checkout v4.2.2 -> v6.0.2 - actions/setup-python v5.6.0 -> v6.2.0 - actions/upload-artifact v4.6.2 -> v7.0.0 - github/codeql-action v3.28.18 -> v4.32.6 - peter-evans/create-pull-request v7.0.8 -> v8.1.0 Lint fixes: - Fix MD032 (blanks around lists) in skill SKILL.md - Fix MD029 (ordered list prefix) in audit SKILL.md - Fix MD041 (first line heading) in copilot-instructions and PR template - Fix MD034 (bare URLs) in CONTRIBUTING.md and SECURITY.md - Remove unused drift_count variable (shellcheck SC2034) --- .claude/skills/add-repo-override/SKILL.md | 1 + .claude/skills/audit/SKILL.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/actions/security-scan/action.yml | 2 +- .../update-pre-commit-composite/action.yml | 4 +- .github/copilot-instructions.md | 2 +- .github/workflows/quality-checks.yml | 16 +++-- .github/workflows/security.yml | 2 +- .github/workflows/sync-settings.yml | 59 +++++++++++-------- .github/workflows/update-pre-commit-hooks.yml | 2 +- .secrets.baseline | 2 +- CONTRIBUTING.md | 2 +- SECURITY.md | 2 +- scripts/generate-report.sh | 1 - scripts/sync-repo-settings.sh | 59 +++++++++++++------ 15 files changed, 100 insertions(+), 58 deletions(-) diff --git a/.claude/skills/add-repo-override/SKILL.md b/.claude/skills/add-repo-override/SKILL.md index b1d6ab6..7f5cba8 100644 --- a/.claude/skills/add-repo-override/SKILL.md +++ b/.claude/skills/add-repo-override/SKILL.md @@ -13,6 +13,7 @@ Add a per-repo exception to `config/overrides.json`. `$ARGUMENTS` should be in the format: ` ` Examples: + - `my-repo branch_protection.required_status_checks.contexts '["Build","Test"]'` - `my-repo repo_settings.has_wiki true` diff --git a/.claude/skills/audit/SKILL.md b/.claude/skills/audit/SKILL.md index 755dae2..4dfcddd 100644 --- a/.claude/skills/audit/SKILL.md +++ b/.claude/skills/audit/SKILL.md @@ -17,7 +17,7 @@ Run a dry-run sync to check for drift without applying changes. ./scripts/sync-repo-settings.sh --dry-run ``` -2. Display the report: +1. Display the report: ```bash cat reports/sync-report.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e480ec7..06dff58 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,5 @@ +# Pull Request + ## What changed diff --git a/.github/actions/security-scan/action.yml b/.github/actions/security-scan/action.yml index e2f1db7..d447719 100644 --- a/.github/actions/security-scan/action.yml +++ b/.github/actions/security-scan/action.yml @@ -24,7 +24,7 @@ runs: output: trivy-results.sarif - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 if: always() with: sarif_file: trivy-results.sarif diff --git a/.github/actions/update-pre-commit-composite/action.yml b/.github/actions/update-pre-commit-composite/action.yml index 50471d8..7524af9 100644 --- a/.github/actions/update-pre-commit-composite/action.yml +++ b/.github/actions/update-pre-commit-composite/action.yml @@ -10,7 +10,7 @@ runs: using: composite steps: - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" @@ -23,7 +23,7 @@ runs: run: pre-commit autoupdate - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ inputs.github_token }} commit-message: "chore: update pre-commit hook versions" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9474943..d642b9e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,4 @@ -Review priorities for this repository: +# Review Priorities 1. Shell script quality: shellcheck and shellharden compliance, proper quoting, error handling (set -euo pipefail), no hardcoded tokens diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index d4b3c4a..f47db9f 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Lint markdown uses: DavidAnson/markdownlint-cli2-action@db4c2f7b1e4a6de4660458dd8d547f94deaac667 # v22.0.0 @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Lint YAML uses: ibiqlik/action-yamllint@2576f72e4b4e5aef56e60fc8a24fa17e25be1fef # v3.1.1 @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run ShellCheck uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check required files run: | @@ -90,6 +90,12 @@ jobs: echo "OK: section '$section' present" fi done + # Validate label structure + LABEL_ERRORS=$(jq '[.labels[] | select(.name == null or .color == null or .description == null)] | length' config/baseline.json) + if [ "$LABEL_ERRORS" -gt 0 ]; then + echo "ERROR: $LABEL_ERRORS labels missing required fields (name, color, description)" + ERRORS=$((ERRORS + LABEL_ERRORS)) + fi if [ "$ERRORS" -gt 0 ]; then echo "ERROR: baseline.json schema validation failed" exit 1 @@ -100,7 +106,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install zizmor run: | diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index ec5a734..7730105 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/sync-settings.yml b/.github/workflows/sync-settings.yml index f0b4e3c..446f615 100644 --- a/.github/workflows/sync-settings.yml +++ b/.github/workflows/sync-settings.yml @@ -15,9 +15,12 @@ on: - "--dry-run" - "--apply" +concurrency: + group: settings-sync + cancel-in-progress: false + permissions: contents: read - issues: write jobs: sync: @@ -25,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run settings sync id: sync @@ -35,7 +38,7 @@ jobs: github_token: ${{ secrets.ORG_SETTINGS_PAT }} - name: Upload report artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: sync-report-${{ github.run_number }} path: reports/sync-report.md @@ -49,42 +52,50 @@ jobs: echo "" echo "| Metric | Value |" echo "| --- | --- |" - echo "| Repositories scanned | ${{ steps.sync.outputs.total_repos }} |" - echo "| Compliant | ${{ steps.sync.outputs.compliant }} |" - echo "| Drift detected | ${{ steps.sync.outputs.drift }} |" - echo "| Mode | ${{ github.event.inputs.mode || '--apply' }} |" + echo "| Repositories scanned | $TOTAL_REPOS |" + echo "| Compliant | $COMPLIANT |" + echo "| Drift detected | $DRIFT |" + echo "| Mode | $MODE |" echo "" echo "### Full Report" echo "" cat reports/sync-report.md } >> "$GITHUB_STEP_SUMMARY" + env: + TOTAL_REPOS: ${{ steps.sync.outputs.total_repos }} + COMPLIANT: ${{ steps.sync.outputs.compliant }} + DRIFT: ${{ steps.sync.outputs.drift }} + MODE: ${{ github.event.inputs.mode || '--apply' }} - - name: Create or update drift issue + - name: Create drift issue if: steps.sync.outputs.has_drift == 'true' env: GH_TOKEN: ${{ secrets.ORG_SETTINGS_PAT }} + DRIFT: ${{ steps.sync.outputs.drift }} + TOTAL_REPOS: ${{ steps.sync.outputs.total_repos }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SYNC_MODE: ${{ github.event.inputs.mode || '--apply' }} run: | TITLE="chore: settings drift detected — $(date '+%Y-%m-%d')" - BODY=$(cat <<'ISSUE_EOF' - ## Settings Drift Report + BODY="## Settings Drift Report - **Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - **Mode**: ${{ github.event.inputs.mode || '--apply' }} - **Repos with drift**: ${{ steps.sync.outputs.drift }} / ${{ steps.sync.outputs.total_repos }} + **Run**: $RUN_URL + **Mode**: $SYNC_MODE + **Repos with drift**: $DRIFT / $TOTAL_REPOS - See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for the full report. + See the [workflow run]($RUN_URL) for the full report." - ISSUE_EOF - ) + # Create new issue first, then close old ones + gh issue create --title "$TITLE" --body "$BODY" --label "settings-drift" - # Close previous drift issues + # Close previous drift issues (all except the one just created) + LATEST=$(gh issue list --label "settings-drift" --state open --json number --jq '.[0].number') gh issue list --label "settings-drift" --state open --json number --jq '.[].number' | while read -r num; do - gh issue close "$num" --comment "Superseded by new sync run." + if [ "$num" != "$LATEST" ]; then + gh issue close "$num" --comment "Superseded by new sync run." + fi done - # Create new issue - gh issue create --title "$TITLE" --body "$BODY" --label "settings-drift" - - name: Close drift issue if compliant if: steps.sync.outputs.has_drift == 'false' env: @@ -99,7 +110,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check for new repos id: newrepos @@ -116,7 +127,7 @@ jobs: || date -u -v-7d '+%Y-%m-%dT%H:%M:%SZ') NEW_REPOS=$(gh repo list gamaware --no-archived --json name,createdAt \ --jq "[.[] | select(.createdAt > \"$WEEK_AGO\")] | .[].name" \ - --limit 200 || echo "") + --limit 1000 || echo "") if [ -n "$NEW_REPOS" ]; then echo "has_new=true" >> "$GITHUB_OUTPUT" @@ -148,7 +159,7 @@ jobs: --label "new-repo" - name: Upload new repos report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: new-repos-report-${{ github.run_number }} path: reports/new-repos.md diff --git a/.github/workflows/update-pre-commit-hooks.yml b/.github/workflows/update-pre-commit-hooks.yml index 307f9af..3331a7a 100644 --- a/.github/workflows/update-pre-commit-hooks.yml +++ b/.github/workflows/update-pre-commit-hooks.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.secrets.baseline b/.secrets.baseline index cc7d043..70c93b1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -123,5 +123,5 @@ } ], "results": {}, - "generated_at": "2026-03-10T06:12:31Z" + "generated_at": "2026-03-10T16:02:48Z" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f4b836..692d440 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,4 +25,4 @@ Open an issue using the appropriate template: ## Contact -alejandrogarcia@iteso.mx + diff --git a/SECURITY.md b/SECURITY.md index cf0321f..3df2eba 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ manage is critical. If you discover a security issue, please report it privately: -- Email: alejandrogarcia@iteso.mx +- Email: - Include: affected file, description, reproduction steps Do not open a public issue for security vulnerabilities. diff --git a/scripts/generate-report.sh b/scripts/generate-report.sh index f4d1e38..5e7e9af 100755 --- a/scripts/generate-report.sh +++ b/scripts/generate-report.sh @@ -13,7 +13,6 @@ if [ ! -f "$REPORT_FILE" ]; then fi # Extract summary metrics for the workflow -drift_count=$(grep -c "^## " "$REPORT_FILE" | tr -d ' ' || echo "0") total_repos=$(grep "Total repositories" "$REPORT_FILE" | grep -oE '[0-9]+' || echo "0") compliant=$(grep "Compliant" "$REPORT_FILE" | grep -oE '[0-9]+' || echo "0") drift=$(grep "Drift detected" "$REPORT_FILE" | grep -oE '[0-9]+' || echo "0") diff --git a/scripts/sync-repo-settings.sh b/scripts/sync-repo-settings.sh index a56e2be..8ff73f1 100755 --- a/scripts/sync-repo-settings.sh +++ b/scripts/sync-repo-settings.sh @@ -22,8 +22,8 @@ log() { get_repos() { local excluded excluded=$(jq -r '.excluded[]' "$OVERRIDES" 2>/dev/null || echo "") - gh repo list "$OWNER" --no-archived --json name --jq '.[].name' --limit 200 | while read -r repo; do - if ! echo "$excluded" | grep -qx "$repo"; then + gh repo list "$OWNER" --no-archived --json name --jq '.[].name' --limit 1000 | while read -r repo; do + if ! echo "$excluded" | grep -qxF "$repo"; then echo "$repo" fi done @@ -134,7 +134,7 @@ SECURITY_EOF echo -e "$changes" } -# Enable Dependabot vulnerability alerts +# Enable or disable Dependabot vulnerability alerts sync_vulnerability_alerts() { local repo="$1" local effective @@ -142,17 +142,16 @@ sync_vulnerability_alerts() { local changes="" local want_alerts - want_alerts=$(echo "$effective" | jq -r '.security.vulnerability_alerts // false') - if [ "$want_alerts" != "true" ]; then - return - fi + want_alerts=$(echo "$effective" | jq -r '.security.vulnerability_alerts // "true"') local current_alerts current_alerts=$(gh api "repos/$OWNER/$repo/vulnerability-alerts" -i 2>/dev/null | head -1 || echo "") - + local is_enabled=false if echo "$current_alerts" | grep -q "204"; then - log "OK: vulnerability alerts for $repo" - else + is_enabled=true + fi + + if [ "$want_alerts" = "true" ] && [ "$is_enabled" = "false" ]; then changes="- Vulnerability alerts: \`disabled\` -> \`enabled\`\n" if [ "$MODE" = "--apply" ]; then gh api -X PUT "repos/$OWNER/$repo/vulnerability-alerts" > /dev/null 2>&1 \ @@ -161,6 +160,17 @@ sync_vulnerability_alerts() { else log "DRIFT detected in vulnerability alerts for $repo" fi + elif [ "$want_alerts" = "false" ] && [ "$is_enabled" = "true" ]; then + changes="- Vulnerability alerts: \`enabled\` -> \`disabled\`\n" + if [ "$MODE" = "--apply" ]; then + gh api -X DELETE "repos/$OWNER/$repo/vulnerability-alerts" > /dev/null 2>&1 \ + || log "WARN: Could not disable vulnerability alerts for $repo" + log "APPLIED vulnerability alerts for $repo" + else + log "DRIFT detected in vulnerability alerts for $repo" + fi + else + log "OK: vulnerability alerts for $repo" fi echo -e "$changes" } @@ -320,7 +330,8 @@ PROTECT_EOF ) > /dev/null 2>&1 } -# Sync standard labels across repos +# Sync standard labels across repos. +# Uses process substitution to avoid subshell scoping issues with piped loops. sync_labels() { local repo="$1" local effective @@ -330,19 +341,21 @@ sync_labels() { local label_count label_count=$(echo "$effective" | jq -r '.labels // [] | length') if [ "$label_count" = "0" ]; then + echo "" return fi local current_labels current_labels=$(gh api "repos/$OWNER/$repo/labels" --jq '.[].name' 2>/dev/null || echo "") - echo "$effective" | jq -c '.labels[]' 2>/dev/null | while read -r label_json; do + while read -r label_json; do + [ -z "$label_json" ] && continue local name color description name=$(echo "$label_json" | jq -r '.name') color=$(echo "$label_json" | jq -r '.color') description=$(echo "$label_json" | jq -r '.description') - if ! echo "$current_labels" | grep -qx "$name"; then + if ! echo "$current_labels" | grep -qxF "$name"; then changes="${changes}- Label missing: \`$name\`\n" if [ "$MODE" = "--apply" ]; then gh api -X POST "repos/$OWNER/$repo/labels" \ @@ -350,22 +363,32 @@ sync_labels() { > /dev/null 2>&1 || log "WARN: Could not create label $name for $repo" fi fi - done + done < <(echo "$effective" | jq -c '.labels[]' 2>/dev/null) + + if [ -n "$changes" ]; then + log "DRIFT detected in labels for $repo" + else + log "OK: labels for $repo" + fi echo -e "$changes" } -# Check default branch is 'main' +# Check default branch matches config check_default_branch() { local repo="$1" + local effective + effective=$(get_effective_settings "$repo") local changes="" + local expected_branch + expected_branch=$(echo "$effective" | jq -r '.branch_protection.branch') local default_branch default_branch=$(gh api "repos/$OWNER/$repo" --jq '.default_branch' 2>/dev/null || echo "") - if [ "$default_branch" != "main" ] && [ -n "$default_branch" ]; then - changes="- Default branch: \`$default_branch\` (expected \`main\`)\n" + if [ "$default_branch" != "$expected_branch" ] && [ -n "$default_branch" ]; then + changes="- Default branch: \`$default_branch\` (expected \`$expected_branch\`)\n" if [ "$MODE" = "--apply" ]; then - gh api -X PATCH "repos/$OWNER/$repo" -f default_branch="main" > /dev/null 2>&1 \ + gh api -X PATCH "repos/$OWNER/$repo" -f default_branch="$expected_branch" > /dev/null 2>&1 \ || log "WARN: Could not rename default branch for $repo (may need manual rename)" log "APPLIED default branch rename for $repo" else From a494e5d904e2785c82750678f51eb7d35f8ce69f Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 10:15:38 -0600 Subject: [PATCH 5/9] fix: update stale action SHA pins and zizmor version - markdownlint-cli2-action: update SHA to match v22.0.0 tag - action-yamllint: update SHA to match v3.1.1 tag - trivy-action: update from v0.31.0 to v0.35.0 with correct SHA - zizmor: update from v1.5.0 to v1.23.1, use find for binary extraction --- .github/actions/security-scan/action.yml | 2 +- .github/workflows/quality-checks.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/security-scan/action.yml b/.github/actions/security-scan/action.yml index d447719..82cb3a6 100644 --- a/.github/actions/security-scan/action.yml +++ b/.github/actions/security-scan/action.yml @@ -16,7 +16,7 @@ runs: config: auto - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e7f3b01483832965 # v0.31.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: scan-type: fs scan-ref: ${{ inputs.scan-path }} diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index f47db9f..ca1a543 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Lint markdown - uses: DavidAnson/markdownlint-cli2-action@db4c2f7b1e4a6de4660458dd8d547f94deaac667 # v22.0.0 + uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 yaml-lint: name: YAML Validation @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Lint YAML - uses: ibiqlik/action-yamllint@2576f72e4b4e5aef56e60fc8a24fa17e25be1fef # v3.1.1 + uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # v3.1.1 with: config_file: .yamllint.yml @@ -110,11 +110,11 @@ jobs: - name: Install zizmor run: | - ZIZMOR_VERSION="1.5.0" + ZIZMOR_VERSION="1.23.1" curl -sL "https://github.com/woodruffw/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" -o /tmp/zizmor.tar.gz mkdir -p /tmp/zizmor-extract tar -xzf /tmp/zizmor.tar.gz -C /tmp/zizmor-extract - sudo mv /tmp/zizmor-extract/zizmor /usr/local/bin/zizmor + find /tmp/zizmor-extract -name zizmor -type f -exec sudo mv {} /usr/local/bin/zizmor \; chmod +x /usr/local/bin/zizmor - name: Run zizmor From b98ffb257e63a6b0764913162270d8ae8b213446 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 10:18:43 -0600 Subject: [PATCH 6/9] fix: add persist-credentials false to all checkout steps (zizmor artipacked) --- .github/workflows/quality-checks.yml | 10 ++++++++++ .github/workflows/sync-settings.yml | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index ca1a543..a9a3d6d 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Lint markdown uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 @@ -26,6 +28,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Lint YAML uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # v3.1.1 @@ -38,6 +42,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Run ShellCheck uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 @@ -48,6 +54,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Check required files run: | @@ -107,6 +115,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install zizmor run: | diff --git a/.github/workflows/sync-settings.yml b/.github/workflows/sync-settings.yml index 446f615..97b5589 100644 --- a/.github/workflows/sync-settings.yml +++ b/.github/workflows/sync-settings.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Run settings sync id: sync @@ -111,6 +113,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Check for new repos id: newrepos From 6570f77d5638d2dbdbd729ae7767cb79248652ae Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 12:44:33 -0600 Subject: [PATCH 7/9] fix: resolve Semgrep shell injection and zizmor warnings - Use env var instead of inline ${{ inputs.mode }} in sync-settings composite action to prevent shell injection (Semgrep finding) - Add if: always() to Trivy step so it runs regardless of Semgrep result, preventing missing SARIF upload - Set zizmor secrets-outside-env to info level (acceptable for single-admin personal account without GitHub Environments) --- .github/actions/security-scan/action.yml | 1 + .github/actions/sync-settings/action.yml | 3 ++- zizmor.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/security-scan/action.yml b/.github/actions/security-scan/action.yml index 82cb3a6..c1dba5c 100644 --- a/.github/actions/security-scan/action.yml +++ b/.github/actions/security-scan/action.yml @@ -16,6 +16,7 @@ runs: config: auto - name: Run Trivy vulnerability scanner + if: always() uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: scan-type: fs diff --git a/.github/actions/sync-settings/action.yml b/.github/actions/sync-settings/action.yml index d723846..b79b15a 100644 --- a/.github/actions/sync-settings/action.yml +++ b/.github/actions/sync-settings/action.yml @@ -36,8 +36,9 @@ runs: env: GH_TOKEN: ${{ inputs.github_token }} REPORT_FILE: reports/sync-report.md + SYNC_MODE: ${{ inputs.mode }} run: | - ./scripts/sync-repo-settings.sh "${{ inputs.mode }}" + ./scripts/sync-repo-settings.sh "$SYNC_MODE" echo "report_file=reports/sync-report.md" >> "$GITHUB_OUTPUT" - name: Parse report diff --git a/zizmor.yml b/zizmor.yml index 00ea2bb..e85580c 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -3,3 +3,4 @@ rules: config: policies: "*": ref-pin + secrets-outside-env: info From 86835e77d299fab60ea6dca69e5fd39e0fe0e521 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 21:44:02 -0600 Subject: [PATCH 8/9] fix: correct zizmor config syntax for secrets-outside-env Use ignore directive with specific workflow files instead of unsupported severity level string. These workflows use secrets without GitHub Environments, which is acceptable for a single-admin personal account. --- zizmor.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zizmor.yml b/zizmor.yml index e85580c..b65970c 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -3,4 +3,7 @@ rules: config: policies: "*": ref-pin - secrets-outside-env: info + secrets-outside-env: + ignore: + - sync-settings.yml + - update-pre-commit-hooks.yml From 60cf8e3b2a205cef9711253055b807c1da95c240 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Tue, 10 Mar 2026 21:59:29 -0600 Subject: [PATCH 9/9] fix: address code review feedback on zizmor config and drift issue creation - Scope zizmor secrets-outside-env ignore to line-level where practical (update-pre-commit-hooks.yml:25), file-level for sync-settings.yml (5 secret usages make line-level unmaintainable) - Add error handling for gh issue create in drift detection step to prevent closing existing issues when creation fails --- .github/workflows/sync-settings.yml | 5 ++++- zizmor.yml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-settings.yml b/.github/workflows/sync-settings.yml index 97b5589..150fba7 100644 --- a/.github/workflows/sync-settings.yml +++ b/.github/workflows/sync-settings.yml @@ -88,7 +88,10 @@ jobs: See the [workflow run]($RUN_URL) for the full report." # Create new issue first, then close old ones - gh issue create --title "$TITLE" --body "$BODY" --label "settings-drift" + if ! gh issue create --title "$TITLE" --body "$BODY" --label "settings-drift"; then + echo "::error::Failed to create drift issue" + exit 1 + fi # Close previous drift issues (all except the one just created) LATEST=$(gh issue list --label "settings-drift" --state open --json number --jq '.[0].number') diff --git a/zizmor.yml b/zizmor.yml index b65970c..68f89d0 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -6,4 +6,4 @@ rules: secrets-outside-env: ignore: - sync-settings.yml - - update-pre-commit-hooks.yml + - update-pre-commit-hooks.yml:25