feat: branch protection audit workflow and apply script#1
Conversation
Adds a reusable workflow for drift detection on GitHub rulesets (comparing live config against a checked-in JSON) and an idempotent shell script for creating/updating rulesets via the API. Supports SOC 2 / ISO 27001 evidence collection with 365-day artifact retention.
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds an idempotent Bash script to create or update GitHub repository rulesets from JSON (optional GitHub Actions bypass) and a reusable GitHub Actions workflow that audits live repository rulesets against an expected config, uploads evidence, and fails on detected drift. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Script as "apply-ruleset.sh"
participant CLI as "gh CLI"
participant GHAPI as "GitHub API"
participant Workflow as "audit-branch-protection (GHA)"
participant Store as "Artifact Store"
User->>Script: run apply-ruleset.sh with payload (and optional flag)
Script->>CLI: gh api repos/{owner/repo}/rulesets (list)
CLI->>GHAPI: request rulesets
GHAPI-->>CLI: return rulesets
CLI-->>Script: ruleset list
alt ruleset exists
Script->>CLI: PUT repos/{owner/repo}/rulesets/{id} with PAYLOAD
CLI->>GHAPI: update ruleset
GHAPI-->>CLI: updated ruleset response
CLI-->>Script: success (updated_at, id)
else ruleset missing
Script->>CLI: POST repos/{owner/repo}/rulesets with PAYLOAD
CLI->>GHAPI: create ruleset
GHAPI-->>CLI: created ruleset response
CLI-->>Script: success (created_at, id)
end
Note right of Workflow: Audit workflow flow
User->>Workflow: trigger audit-branch-protection (workflow_call)
Workflow->>GHAPI: create GitHub App token / fetch live rulesets
GHAPI-->>Workflow: live rulesets
Workflow->>Workflow: normalize & compare live vs expected config
alt drift detected
Workflow->>Store: upload evidence (live JSON + diff)
Store-->>Workflow: artifact stored
Workflow->>User: job fails (drift)
else no drift
Workflow->>Store: upload evidence (live JSON)
Store-->>Workflow: artifact stored
Workflow->>User: job succeeds
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
Summary of ChangesHello @jan-kubica, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces robust automation for managing and auditing GitHub branch protection rulesets, crucial for compliance with standards like SOC 2 Type II and ISO 27001. By providing a reusable workflow for drift detection and an idempotent script for applying rulesets, it streamlines the process of maintaining consistent and auditable security configurations across repositories. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a reusable workflow for auditing branch protection rulesets and an accompanying script to apply these rulesets. The changes are well-structured and include documentation updates. My review focuses on a functional bug in the apply script and improving the clarity of usage examples in both the script's comments and the README.
- Fix GitHub Actions installation lookup (wrong API endpoint) - Use jq --arg for safe variable binding (prevent injection) - Add guard for duplicate ruleset names - Fix paginated API response with --slurp (prevents incomplete evidence) - Clarify example paths to run from repository root
| INSTALLATION_ID=$( | ||
| gh api "repos/${REPO}/installations" \ | ||
| --paginate \ | ||
| --jq '.[] | select(.app_slug == "github-actions") | .id' \ | ||
| 2>/dev/null || true | ||
| ) |
There was a problem hiding this comment.
🔴 Wrong jq filter for repos/{owner}/{repo}/installations API response structure
The --github-actions-bypass flag silently never works because the jq filter assumes the API returns a bare array, but GET /repos/{owner}/{repo}/installations returns an object {"total_count": N, "installations": [...]}. The filter .[] | select(.app_slug == "github-actions") | .id iterates over the top-level object values (a number and an array), causing a jq error when it tries to index a number with .app_slug. Since 2>/dev/null || true suppresses the error, INSTALLATION_ID is always empty, and the script prints the warning and skips adding the bypass actor.
Root Cause and Impact
At apply-ruleset.sh:83, the jq filter is:
.[] | select(.app_slug == "github-actions") | .id
But the API response shape is:
{"total_count": 1, "installations": [{"id": 123, "app_slug": "github-actions"}]}So .[] yields 1 (the total_count number) and [{...}] (the installations array). Applying select(.app_slug == ...) on the number 1 causes a jq type error, which is swallowed by 2>/dev/null || true at line 84.
The correct filter should be .installations[] | select(.app_slug == "github-actions") | .id.
Impact: The --github-actions-bypass flag is completely non-functional. Users who rely on it (e.g., for SBOM workflows that push to main) will not get the bypass actor added, potentially blocking those workflows.
| INSTALLATION_ID=$( | |
| gh api "repos/${REPO}/installations" \ | |
| --paginate \ | |
| --jq '.[] | select(.app_slug == "github-actions") | .id' \ | |
| 2>/dev/null || true | |
| ) | |
| INSTALLATION_ID=$( | |
| gh api "repos/${REPO}/installations" \ | |
| --paginate \ | |
| --jq '.installations[] | select(.app_slug == "github-actions") | .id' \ | |
| 2>/dev/null || true | |
| ) | |
Was this helpful? React with 👍 or 👎 to provide feedback.
…#27) * fix(actions): treat setup-node's NODE_AUTH_TOKEN placeholder as unset `actions/setup-node@v6` with `registry-url:` configured unconditionally exports `NODE_AUTH_TOKEN=XXXXX-XXXXX-XXXXX-XXXXX` as a literal placeholder so its `.npmrc` template `_authToken=${NODE_AUTH_TOKEN}` expands to a non-empty (but useless) string. The previous guard treated that placeholder as a real token and refused to run, breaking every standard setup-node + publish flow — the placeholder is what the stdnum dispatch hit on attempt #1. Unset NODE_AUTH_TOKEN if and only if it matches the placeholder. Real legacy tokens still trip the refuse-and-exit branch. Surfaced by the stdnum#95 release dispatch against v0.0.1. * fix(actions): set placeholder NODE_AUTH_TOKEN to empty string, not unset Codex caught a real subtlety on the previous fix attempt. npm config does env-var expansion when reading .npmrc, and an unset NODE_AUTH_TOKEN can let npm pass the literal `${NODE_AUTH_TOKEN}` syntax through to the Authorization header instead of treating it as absent — which defeats OIDC and risks the placeholder-token bearer auth that this action exists to prevent. Set NODE_AUTH_TOKEN to an empty string instead. `.npmrc`'s `_authToken=${NODE_AUTH_TOKEN}` expands cleanly to `_authToken=` (no auth), and npm's OIDC trusted-publishing path takes over via the ACTIONS_ID_TOKEN_REQUEST_* env vars. Also adopts gemini's suggestion to declare the placeholder constant as `readonly`. Addresses bot reviews on PR #27.
Summary
audit-branch-protection.yml) that compares live GitHub rulesets against a checked-in expected config, detects drift, and uploads compliance evidence as an artifact (365-day retention)apply-ruleset.sh) that creates or updates a GitHub ruleset from a JSON file viagh apiContext
Stella handles privileged legal data and targets SOC 2 Type II / ISO 27001 compliance. Auditors need exportable evidence that branch protection is correctly configured. This workflow runs weekly and on manual dispatch, producing a timestamped JSON artifact.
The apply script supports a
--github-actions-bypassflag to automatically look up the GitHub Actions installation ID and add it as a bypass actor (needed for workflows like SBOM that push tomain).Prerequisites (manual, one-time)
BRANCH_PROTECTION_APP_ID+BRANCH_PROTECTION_APP_KEYas org-level secretsCompanion PR
The repo-specific caller workflow and
ruleset-main.jsonconfig will be added tostella/stellain a separate PR.Test plan
shellcheck .github/branch-protection/apply-ruleset.shSummary by CodeRabbit
New Features
Documentation