From 8f618bd7a5b42a11106bc5143d736ff3c116211a Mon Sep 17 00:00:00 2001 From: nsheaps Date: Fri, 29 May 2026 02:49:51 +0000 Subject: [PATCH 1/5] feat(apply-repo-settings): make app-id optional, derive from token at runtime When a pre-generated installation token is provided, app-id is no longer required. The action now derives APP_ID from the token via GET /app when the input is omitted, so callers using checkout-as-app don't need to pass the secret twice. Auth fallback (token == '') still requires app-id + private-key. https://claude.ai/code/session_01XUJx6sKWxyKyoWdxNEsET1 --- .github/actions/apply-repo-settings/action.sh | 7 +++++++ .github/actions/apply-repo-settings/action.yml | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/actions/apply-repo-settings/action.sh b/.github/actions/apply-repo-settings/action.sh index 06a7011..810867b 100755 --- a/.github/actions/apply-repo-settings/action.sh +++ b/.github/actions/apply-repo-settings/action.sh @@ -19,6 +19,13 @@ set -euo pipefail : "${SECTIONS:=repository,rulesets}" : "${APP_ID:=}" +# Derive APP_ID from the installation token when not explicitly provided. +# An installation token can query GET /app to retrieve the App's metadata. +if [[ -z "$APP_ID" ]]; then + APP_ID=$(GH_TOKEN="$GH_TOKEN" gh api /app --jq '.id' 2>/dev/null || true) + [[ -n "$APP_ID" ]] && echo " → Derived APP_ID=$APP_ID from installation token" +fi + if [[ ! -f "$SETTINGS_FILE" ]]; then echo "::error file=$SETTINGS_FILE::settings file not found" exit 1 diff --git a/.github/actions/apply-repo-settings/action.yml b/.github/actions/apply-repo-settings/action.yml index 5cd4a21..40dedc9 100644 --- a/.github/actions/apply-repo-settings/action.yml +++ b/.github/actions/apply-repo-settings/action.yml @@ -12,8 +12,9 @@ inputs: 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.' - required: true + description: 'GitHub App ID. Used to substitute the actor_id: -1 placeholder in Integration bypass actors. Also used for authentication when token is not provided. When token is provided and app-id is omitted, the app ID is derived from the installation token at runtime via GET /app.' + required: false + default: '' private-key: description: 'GitHub App private key (PEM). Required when token is not provided.' From d4994a27dd8a8014bceb58021e0c7a7b69258b3f Mon Sep 17 00:00:00 2001 From: nsheaps Date: Fri, 29 May 2026 02:54:22 +0000 Subject: [PATCH 2/5] fix(apply-repo-settings): drop runtime actor_id derivation, note sync-time resolution actor_id: -1 placeholders are now resolved by .github/sync-repo-settings before distribution, so action.sh no longer needs to substitute them. APP_ID kept as an optional fallback for standalone use with unresolved files. https://claude.ai/code/session_01XUJx6sKWxyKyoWdxNEsET1 --- .github/actions/apply-repo-settings/action.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/actions/apply-repo-settings/action.sh b/.github/actions/apply-repo-settings/action.sh index 810867b..454bbf7 100755 --- a/.github/actions/apply-repo-settings/action.sh +++ b/.github/actions/apply-repo-settings/action.sh @@ -18,13 +18,8 @@ set -euo pipefail : "${DRY_RUN:=false}" : "${SECTIONS:=repository,rulesets}" : "${APP_ID:=}" - -# Derive APP_ID from the installation token when not explicitly provided. -# An installation token can query GET /app to retrieve the App's metadata. -if [[ -z "$APP_ID" ]]; then - APP_ID=$(GH_TOKEN="$GH_TOKEN" gh api /app --jq '.id' 2>/dev/null || true) - [[ -n "$APP_ID" ]] && echo " → Derived APP_ID=$APP_ID from installation token" -fi +# APP_ID is only needed if the settings file still contains actor_id: -1 placeholders. +# Normally the .github sync workflow resolves placeholders before distributing settings.yml. if [[ ! -f "$SETTINGS_FILE" ]]; then echo "::error file=$SETTINGS_FILE::settings file not found" From 3b0debe8b561b5b0ff1b7acb41fae8b905c88718 Mon Sep 17 00:00:00 2001 From: nsheaps Date: Fri, 29 May 2026 03:11:24 +0000 Subject: [PATCH 3/5] refactor(apply-repo-settings): remove all templating; action is now pure merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The action no longer takes app-id or private-key — it requires only a pre-generated token (with administration:write). It applies the settings file verbatim, with no placeholder substitution. Templating moved upstream to nsheaps/.github, where repo-settings.template.yaml is rendered (envsubst) into repo-settings.yaml at the source. https://claude.ai/code/session_01XUJx6sKWxyKyoWdxNEsET1 --- .github/actions/apply-repo-settings/README.md | 29 +++++++++++-------- .github/actions/apply-repo-settings/action.sh | 16 ---------- .../actions/apply-repo-settings/action.yml | 28 ++---------------- 3 files changed, 20 insertions(+), 53 deletions(-) diff --git a/.github/actions/apply-repo-settings/README.md b/.github/actions/apply-repo-settings/README.md index a934187..fcc2606 100644 --- a/.github/actions/apply-repo-settings/README.md +++ b/.github/actions/apply-repo-settings/README.md @@ -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 @@ -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 @@ -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 }} ``` diff --git a/.github/actions/apply-repo-settings/action.sh b/.github/actions/apply-repo-settings/action.sh index 454bbf7..ee7a285 100755 --- a/.github/actions/apply-repo-settings/action.sh +++ b/.github/actions/apply-repo-settings/action.sh @@ -17,9 +17,6 @@ set -euo pipefail : "${SETTINGS_FILE:=.github/settings.yml}" : "${DRY_RUN:=false}" : "${SECTIONS:=repository,rulesets}" -: "${APP_ID:=}" -# APP_ID is only needed if the settings file still contains actor_id: -1 placeholders. -# Normally the .github sync workflow resolves placeholders before distributing settings.yml. if [[ ! -f "$SETTINGS_FILE" ]]; then echo "::error file=$SETTINGS_FILE::settings file not found" @@ -132,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 diff --git a/.github/actions/apply-repo-settings/action.yml b/.github/actions/apply-repo-settings/action.yml index 40dedc9..63c5d3f 100644 --- a/.github/actions/apply-repo-settings/action.yml +++ b/.github/actions/apply-repo-settings/action.yml @@ -7,19 +7,8 @@ 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. Used to substitute the actor_id: -1 placeholder in Integration bypass actors. Also used for authentication when token is not provided. When token is provided and app-id is omitted, the app ID is derived from the installation token at runtime via GET /app.' - required: false - default: '' - - private-key: - description: 'GitHub App private key (PEM). Required when token is not provided.' - required: false - default: '' + 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 owner: description: 'Target repo owner. Defaults to the current repo owner.' @@ -54,16 +43,6 @@ 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 shell: bash run: | @@ -78,8 +57,7 @@ runs: 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 }} From 51822dd8b42b53267b74b26b63a49315fa7d74ae Mon Sep 17 00:00:00 2001 From: nsheaps Date: Fri, 29 May 2026 15:52:20 +0000 Subject: [PATCH 4/5] fix(apply-repo-settings): detect+shadow wrong yq (kislyuk/Python) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some runners pre-install kislyuk's Python yq (a jq wrapper) at /usr/bin/yq. The previous check skipped install when 'yq' was on PATH — but Python yq doesn't support '-o=json' and the action failed with exit 3 (jq compile error 'Unknown option -o=json'). Now: probe '--version' for the mikefarah marker. If absent, install mikefarah/yq to /usr/local/bin/yq and prepend it via GITHUB_PATH so subsequent steps shadow any wrong yq. --- .../actions/apply-repo-settings/action.yml | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/actions/apply-repo-settings/action.yml b/.github/actions/apply-repo-settings/action.yml index 63c5d3f..1abe130 100644 --- a/.github/actions/apply-repo-settings/action.yml +++ b/.github/actions/apply-repo-settings/action.yml @@ -43,15 +43,46 @@ outputs: runs: using: 'composite' steps: - - 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 - yq --version + # 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 + 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 From 412061870394658ceed266049dffcc90cb1f091e Mon Sep 17 00:00:00 2001 From: nsheaps <1282393+nsheaps@users.noreply.github.com> Date: Fri, 29 May 2026 15:53:11 +0000 Subject: [PATCH 5/5] chore: `mise format` Triggered by: 5fc6327d0500ad9f83cc1cc8d211af29777135a4 Workflow run: https://github.com/nsheaps/github-actions/actions/runs/26647393901 --- .github/actions/apply-repo-settings/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/actions/apply-repo-settings/README.md b/.github/actions/apply-repo-settings/README.md index fcc2606..a8626ba 100644 --- a/.github/actions/apply-repo-settings/README.md +++ b/.github/actions/apply-repo-settings/README.md @@ -10,14 +10,14 @@ The upstream app is a Probot webhook server — it's designed to listen for `pus ## Inputs -| 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. | +| 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.