Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .github/actions/apply-repo-settings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ The upstream app is a Probot webhook server — it's designed to listen for `pus

## Inputs

| Input | Required | Default | Description |
| --------------- | -------- | ---------------------- | ------------------------------------------------------------------ |
| `app-id` | yes | — | GitHub App ID. Needs `Administration: write` and `Contents: read`. |
| `private-key` | yes | — | The App's PEM private key. |
| `owner` | no | current owner | Target repo owner. |
| `repo` | no | current repo | Target repo name. |
| `settings-file` | no | `.github/settings.yml` | Path to the YAML to apply. |
| `dry-run` | no | `false` | Print what would change without applying. |
| `sections` | no | `repository,rulesets` | Comma-separated section names to apply. |
| Input | Required | Default | Description |
| --------------- | -------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `token` | yes | — | GitHub token with `Administration: write` on the target repo (typically from `checkout-as-app` or `github-app-auth`). |
| `owner` | no | current owner | Target repo owner. |
| `repo` | no | current repo | Target repo name. |
| `settings-file` | no | `.github/settings.yml` | Path to the YAML to apply. |
| `dry-run` | no | `false` | Print what would change without applying. |
| `sections` | no | `repository,rulesets` | Comma-separated section names to apply. |

The action does **not** do any templating or placeholder substitution on `settings-file` — it applies the YAML as-is. If you need env-var-based substitution (e.g. resolving a GitHub App ID into `bypass_actors[].actor_id`), render the file upstream (e.g. with `envsubst`) before invoking this action.

## Outputs

Expand All @@ -37,7 +38,7 @@ Source: [`pages/index.html`](../../../pages/index.html). Deployed by [`.github/w
After creating the app:

1. Install it on every repo you want to manage.
2. Add `APPLY_REPO_SETTINGS_APP_ID` and `APPLY_REPO_SETTINGS_PRIVATE_KEY` as secrets.
2. Add `APPLY_REPO_SETTINGS_APP_ID` and `APPLY_REPO_SETTINGS_PRIVATE_KEY` as secrets (consumed by `checkout-as-app` / `github-app-auth` — this action only takes the resulting token).

## Example workflow

Expand Down Expand Up @@ -65,11 +66,15 @@ jobs:
apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: nsheaps/github-actions/.github/actions/apply-repo-settings@main
- name: Checkout as GitHub App
id: checkout
uses: nsheaps/github-actions/.github/actions/checkout-as-app@main
with:
app-id: ${{ secrets.APPLY_REPO_SETTINGS_APP_ID }}
private-key: ${{ secrets.APPLY_REPO_SETTINGS_PRIVATE_KEY }}
- uses: nsheaps/github-actions/.github/actions/apply-repo-settings@main
with:
token: ${{ steps.checkout.outputs.token }}
dry-run: ${{ inputs.dry-run || false }}
```

Expand Down
14 changes: 0 additions & 14 deletions .github/actions/apply-repo-settings/action.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ set -euo pipefail
: "${SETTINGS_FILE:=.github/settings.yml}"
: "${DRY_RUN:=false}"
: "${SECTIONS:=repository,rulesets}"
: "${APP_ID:=}"

if [[ ! -f "$SETTINGS_FILE" ]]; then
echo "::error file=$SETTINGS_FILE::settings file not found"
Expand Down Expand Up @@ -130,19 +129,6 @@ apply_rulesets() {
name="$(yq -r ".rulesets[$i].name" "$SETTINGS_FILE")"
body="$(yq -o=json ".rulesets[$i]" "$SETTINGS_FILE")"

# Substitute actor_id -1 placeholder for Integration bypass actors with the real App ID.
# actor_id must be the GitHub App ID (integer), not the installation ID.
# Gracefully skip Integration bypass actors if APP_ID is not configured.
if [[ -n "$APP_ID" ]]; then
body="$(echo "$body" | jq \
--argjson app_id "$APP_ID" \
'.bypass_actors //= [] | .bypass_actors |= map(if .actor_type == "Integration" and .actor_id == -1 then .actor_id = $app_id else . end)')"
else
body="$(echo "$body" | jq \
'if .bypass_actors then .bypass_actors |= map(select(not (.actor_type == "Integration" and .actor_id == -1))) else . end')"
info "APP_ID not set — skipping placeholder Integration bypass actors for: $name"
fi

existing_id="$(echo "$existing" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id // empty')"

if [[ -z "$existing_id" ]]; then
Expand Down
66 changes: 38 additions & 28 deletions .github/actions/apply-repo-settings/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,9 @@ branding:

inputs:
token:
description: 'Pre-generated GitHub App installation token. When provided, app-id/private-key are not used for authentication. app-id is still required for substituting the actor_id: -1 placeholder in Integration bypass actors.'
required: false
default: ''

app-id:
description: 'GitHub App ID. Required. Used to substitute the actor_id: -1 placeholder in Integration bypass actors. Also used for authentication when token is not provided. See docs/setup.html in this action directory for an HTML helper that builds the app via the GitHub manifest flow.'
description: 'GitHub token with administration:write on the target repo (typically a GitHub App installation token from checkout-as-app or github-app-auth).'
required: true

private-key:
description: 'GitHub App private key (PEM). Required when token is not provided.'
required: false
default: ''

owner:
description: 'Target repo owner. Defaults to the current repo owner.'
required: false
Expand Down Expand Up @@ -53,32 +43,52 @@ outputs:
runs:
using: 'composite'
steps:
- name: Authenticate as GitHub App
id: app-token
if: ${{ inputs.token == '' }}
uses: nsheaps/github-actions/.github/actions/github-app-auth@603052841cd49b9fdc99bc32979ff9c45c9b669e # 2026-05-22
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}
owner: ${{ inputs.owner }}
skip-checkout: 'true'

- name: Ensure yq is available
- name: Ensure mikefarah/yq is available
shell: bash
run: |
if ! command -v yq >/dev/null 2>&1; then
echo "Installing yq..."
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
set -euo pipefail
# The action needs mikefarah's Go yq (supports `-o=json` for
# YAML→JSON conversion). Some runners pre-install kislyuk's Python
# yq — a `jq` wrapper that does NOT understand `-o=json` and exits
# with cryptic "Unknown option" errors. Detect, and if the resolved
# `yq` is the wrong one, install mikefarah's to /usr/local/bin/yq
# and prepend that path for subsequent steps so it shadows the
# pre-installed wrapper.
install_mikefarah() {
echo "Installing mikefarah/yq to /usr/local/bin/yq..."
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
}
yq_is_mikefarah() {
# mikefarah's --version line contains the github.com/mikefarah URL.
"$1" --version 2>&1 | grep -q 'mikefarah'
}
if command -v yq >/dev/null 2>&1 && yq_is_mikefarah "$(command -v yq)"; then
echo "mikefarah/yq already on PATH: $(command -v yq)"
else
if command -v yq >/dev/null 2>&1; then
echo "Wrong yq detected at $(command -v yq) ($(yq --version 2>&1)); shadowing with mikefarah's."
fi
install_mikefarah
echo "/usr/local/bin" >> "$GITHUB_PATH"
fi
# Sanity check: must be mikefarah's yq when action.sh runs in the next step.
# We can't rely on $GITHUB_PATH having taken effect in THIS step, so
# validate by absolute path if we just installed, else use what's on PATH.
check_bin=$(command -v yq)
if [[ -x /usr/local/bin/yq ]] && yq_is_mikefarah /usr/local/bin/yq; then
check_bin=/usr/local/bin/yq
fi
yq --version
echo "Verifying $check_bin..."
"$check_bin" --version
yq_is_mikefarah "$check_bin" || { echo "::error::yq sanity check failed: not mikefarah"; exit 1; }

- name: Apply settings
id: apply
shell: bash
env:
GH_TOKEN: ${{ inputs.token || steps.app-token.outputs.token }}
APP_ID: ${{ inputs.app-id }}
GH_TOKEN: ${{ inputs.token }}
OWNER: ${{ inputs.owner }}
REPO: ${{ inputs.repo }}
SETTINGS_FILE: ${{ inputs.settings-file }}
Expand Down
Loading