Skip to content

feat: branch protection audit workflow and apply script#1

Merged
jan-kubica merged 3 commits into
mainfrom
feat/audit-branch-protection
Feb 15, 2026
Merged

feat: branch protection audit workflow and apply script#1
jan-kubica merged 3 commits into
mainfrom
feat/audit-branch-protection

Conversation

@jan-kubica
Copy link
Copy Markdown
Contributor

@jan-kubica jan-kubica commented Feb 15, 2026

Summary

  • Adds a reusable workflow (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)
  • Adds an idempotent apply script (apply-ruleset.sh) that creates or updates a GitHub ruleset from a JSON file via gh api
  • Updates README with usage docs for both

Context

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-bypass flag to automatically look up the GitHub Actions installation ID and add it as a bypass actor (needed for workflows like SBOM that push to main).

Prerequisites (manual, one-time)

  1. Create a GitHub App ("Stella Branch Protection") with Administration: Read and write
  2. Store BRANCH_PROTECTION_APP_ID + BRANCH_PROTECTION_APP_KEY as org-level secrets

Companion PR

The repo-specific caller workflow and ruleset-main.json config will be added to stella/stella in a separate PR.

Test plan

  • Verify reusable workflow syntax is valid (actionlint)
  • Run shellcheck .github/branch-protection/apply-ruleset.sh
  • After GitHub App setup: run apply script against a test repo
  • After apply: trigger audit workflow manually, verify evidence artifact

Open with Devin

Summary by CodeRabbit

  • New Features

    • Added an idempotent script to create or update repository branch-protection rulesets, with input validation and optional GitHub Actions bypass.
    • Added a reusable audit workflow that compares live rulesets to an expected config, produces evidence, and fails when drift is detected.
  • Documentation

    • Updated docs with usage examples and guidance for branch-protection automation and auditing.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 15, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Branch Protection Script
​.github/branch-protection/apply-ruleset.sh
New executable Bash script. Reads a ruleset JSON payload, validates a non-empty name, checks repository rulesets via gh api, updates (PUT) if found or creates (POST) otherwise. Optional --github-actions-bypass resolves the GitHub Actions app installation ID and injects it. Uses jq and prints id/timestamps.
Audit Workflow
​.github/workflows/audit-branch-protection.yml
New reusable workflow_call workflow. Authenticates as a GitHub App, fetches live repo rulesets, matches by name against an expected config, normalizes observable fields, computes drift diff, uploads JSON evidence (and diff if drift), and fails the job when drift is detected.
Documentation
README.md
Adds documentation entries for the audit workflow and the apply-ruleset.sh script, usage examples, inputs/secrets for the workflow, and artifact/evidence explanation.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped in with a payload clutched tight,

I matched names by moon and updated by light.
I sniffed for drift, left artifacts neat—
Idempotent hops keep protections complete. 🥕


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @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 Reusable Workflow: Introduced audit-branch-protection.yml, a reusable GitHub Actions workflow designed to compare live GitHub rulesets against a checked-in configuration. This workflow detects drift, fails if discrepancies are found, and uploads compliance evidence as an artifact with a 365-day retention period. It runs weekly and can be manually dispatched.
  • Idempotent Ruleset Apply Script: Added apply-ruleset.sh, an idempotent shell script that creates or updates GitHub repository rulesets from a JSON file using the gh api. It supports an optional --github-actions-bypass flag to automatically add the GitHub Actions app as a bypass actor.
  • Documentation Updates: The README.md has been updated to include comprehensive usage documentation for both the new audit-branch-protection.yml workflow and the apply-ruleset.sh script, detailing their purpose, required secrets, inputs, and example usage.

🧠 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
  • .github/branch-protection/apply-ruleset.sh
    • Added a new shell script for idempotent creation and updating of GitHub rulesets.
    • Implemented logic to check for existing rulesets by name and perform either a POST (create) or PUT (update) operation.
    • Included support for an optional --github-actions-bypass flag to automatically add the GitHub Actions app as a bypass actor.
    • Added error handling for missing ruleset files and JSON payloads without a 'name' field.
  • README.md
    • Updated the 'Reusable Workflows' table to include audit-branch-protection.yml.
    • Added a new section 'Branch Protection Scripts' detailing apply-ruleset.sh.
    • Included a detailed 'Audit Branch Protection' section with usage examples, required secrets, and inputs for the new workflow.
    • Added an 'Apply Ruleset' section with basic and advanced usage examples for the apply-ruleset.sh script.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/audit-branch-protection.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/branch-protection/apply-ruleset.sh
Comment thread .github/branch-protection/apply-ruleset.sh Outdated
Comment thread README.md Outdated
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread .github/branch-protection/apply-ruleset.sh
Comment thread .github/branch-protection/apply-ruleset.sh
Comment thread .github/workflows/audit-branch-protection.yml Outdated
- 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
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment thread .github/branch-protection/apply-ruleset.sh
@jan-kubica jan-kubica merged commit ce0297b into main Feb 15, 2026
1 check was pending
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +80 to +85
INSTALLATION_ID=$(
gh api "repos/${REPO}/installations" \
--paginate \
--jq '.[] | select(.app_slug == "github-actions") | .id' \
2>/dev/null || true
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
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
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

jan-kubica added a commit that referenced this pull request May 13, 2026
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant