From f01f0429a2660980a10aeb1671f520e6a88cd99e Mon Sep 17 00:00:00 2001 From: Nathan Heaps Date: Mon, 20 Jul 2026 16:22:08 -0400 Subject: [PATCH 1/4] feat(apply-repo-settings): implement collaborators section PUT /repos/{owner}/{repo}/collaborators/{username} for each {username, permission} entry in settings.yml's collaborators: list, matching the rulesets/repository implementation's style and its non-destructive philosophy (never revokes access absent from the list). Opt-in only: unlike repository/rulesets, "collaborators" is not added to the default `sections` input, since granting repo access is a higher-consequence, easier-to-miss change than a ruleset tweak. --- .github/actions/apply-repo-settings/action.sh | 72 +++++++++++++++++-- .../actions/apply-repo-settings/action.yml | 6 +- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/actions/apply-repo-settings/action.sh b/.github/actions/apply-repo-settings/action.sh index 52f778d..5e15f4e 100755 --- a/.github/actions/apply-repo-settings/action.sh +++ b/.github/actions/apply-repo-settings/action.sh @@ -3,10 +3,12 @@ # top-level sections to {OWNER}/{REPO} via the GitHub API. # # Supported sections: -# repository → PATCH /repos/{owner}/{repo} -# rulesets → POST/PUT/DELETE /repos/{owner}/{repo}/rulesets +# repository → PATCH /repos/{owner}/{repo} +# rulesets → POST/PUT/DELETE /repos/{owner}/{repo}/rulesets +# collaborators → PUT /repos/{owner}/{repo}/collaborators/{username} +# (opt-in only, see SECTIONS default below) # -# Not supported (yet): labels, collaborators, teams, environments, branches. +# Not supported (yet): labels, teams, environments, branches. # Those are handled by other workflows in nsheaps/.github today. set -euo pipefail @@ -184,12 +186,68 @@ apply_rulesets() { endlog } +############################################################################### +# collaborators: for each {username, permission} entry, PUT to add/update +# their access. We do NOT remove collaborators absent from the list (same +# non-destructive philosophy as rulesets — this action never revokes access +# it didn't explicitly grant here). +# +# Opt-in only: unlike repository/rulesets, this is not in SECTIONS' default. +# Granting repo access to a user is a higher-consequence action than a +# ruleset tweak, so a repo must explicitly add "collaborators" to its +# `sections` input even if its settings.yml already has a collaborators: +# block (e.g. merged in via the nsheaps/.github sync layer). +############################################################################### +apply_collaborators() { + log "collaborators" + + local count + count="$(yq '.collaborators | length // 0' "$SETTINGS_FILE")" + if [[ "$count" == "0" || "$count" == "null" ]]; then + info "no collaborators block, skipping" + endlog + return + fi + + local i + for ((i = 0; i < count; i++)); do + local username permission body + username="$(yq -r ".collaborators[$i].username" "$SETTINGS_FILE")" + permission="$(yq -r ".collaborators[$i].permission" "$SETTINGS_FILE")" + + if [[ -z "$username" || "$username" == "null" ]]; then + echo "::error file=$SETTINGS_FILE::collaborators[$i] is missing required 'username'" >&2 + exit 1 + fi + if [[ -z "$permission" || "$permission" == "null" ]]; then + echo "::error file=$SETTINGS_FILE::collaborators[$i] (username=$username) is missing required 'permission'" >&2 + exit 1 + fi + + body="$(jq -nc --arg permission "$permission" '{permission: $permission}')" + + info "add/update: $username ($permission)" + if [[ "$DRY_RUN" == "true" ]]; then + echo "$body" | jq '.' + else + # A 403 here most likely means the token/app lacks Administration:write + # on this repo — the `api` helper already annotates the HTTP status and + # response body, and `set -e` stops the run rather than continuing silently. + api PUT "/repos/${OWNER}/${REPO}/collaborators/${username}" "$body" >/dev/null + fi + APPLIED+=("$username") + done + + endlog +} + ############################################################################### # main ############################################################################### CREATED=() UPDATED=() UNCHANGED=() +APPLIED=() REPO_CHANGED="false" echo "Applying $SETTINGS_FILE to $OWNER/$REPO (dry-run=$DRY_RUN, sections=$SECTIONS)" @@ -202,13 +260,18 @@ if want_section "rulesets"; then apply_rulesets fi +if want_section "collaborators"; then + apply_collaborators +fi + # Summary summary="$(jq -nc \ --arg repo_changed "$REPO_CHANGED" \ --argjson created "$(printf '%s\n' "${CREATED[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ --argjson updated "$(printf '%s\n' "${UPDATED[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ --argjson unchanged "$(printf '%s\n' "${UNCHANGED[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ - '{repository: $repo_changed, rulesets_created: $created, rulesets_updated: $updated, rulesets_unchanged: $unchanged}' + --argjson collaborators_applied "$(printf '%s\n' "${APPLIED[@]:-}" | jq -R . | jq -s 'map(select(length>0))')" \ + '{repository: $repo_changed, rulesets_created: $created, rulesets_updated: $updated, rulesets_unchanged: $unchanged, collaborators_applied: $collaborators_applied}' )" echo "Summary: $summary" @@ -222,6 +285,7 @@ echo "summary=$summary" >> "$GITHUB_OUTPUT" echo "- **rulesets created**: ${#CREATED[@]}${CREATED:+: ${CREATED[*]}}" echo "- **rulesets updated**: ${#UPDATED[@]}${UPDATED:+: ${UPDATED[*]}}" echo "- **rulesets unchanged**: ${#UNCHANGED[@]}${UNCHANGED:+: ${UNCHANGED[*]}}" + echo "- **collaborators applied**: ${#APPLIED[@]}${APPLIED:+: ${APPLIED[*]}}" echo echo "Source: \`$SETTINGS_FILE\` · Dry run: \`$DRY_RUN\` · Sections: \`$SECTIONS\`" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/actions/apply-repo-settings/action.yml b/.github/actions/apply-repo-settings/action.yml index 1abe130..4ca037c 100644 --- a/.github/actions/apply-repo-settings/action.yml +++ b/.github/actions/apply-repo-settings/action.yml @@ -1,5 +1,5 @@ name: 'Apply Repo Settings' -description: 'Read .github/settings.yml and apply repository config + rulesets to a repo via the GitHub API. A minimal ephemeral alternative to the repository-settings GitHub App (https://github.com/repository-settings/app), suitable for running inside a workflow.' +description: 'Read .github/settings.yml and apply repository config, rulesets, and (opt-in) collaborators to a repo via the GitHub API. A minimal ephemeral alternative to the repository-settings GitHub App (https://github.com/repository-settings/app), suitable for running inside a workflow.' branding: icon: 'sliders' @@ -31,13 +31,13 @@ inputs: default: 'false' sections: - description: 'Comma-separated list of top-level keys to apply. Defaults to "repository,rulesets". Add others (labels, collaborators, teams) as we extend support.' + description: 'Comma-separated list of top-level keys to apply. Defaults to "repository,rulesets". "collaborators" is supported but opt-in only — add it explicitly (e.g. "repository,rulesets,collaborators") since granting repo access is higher-consequence than a ruleset tweak. Add others (labels, teams) as we extend support.' required: false default: 'repository,rulesets' outputs: summary: - description: 'JSON summary of changes applied. Keys: repository, rulesets_created, rulesets_updated, rulesets_unchanged.' + description: 'JSON summary of changes applied. Keys: repository, rulesets_created, rulesets_updated, rulesets_unchanged, collaborators_applied.' value: ${{ steps.apply.outputs.summary }} runs: From bf3507fd725588d94400ca8a7a4c712f5dd6d5cd Mon Sep 17 00:00:00 2001 From: Nathan Heaps Date: Mon, 20 Jul 2026 16:22:19 -0400 Subject: [PATCH 2/4] test(apply-repo-settings): add test coverage for collaborators No bats/shellspec harness exists yet for this repo's composite actions, so this adds a self-contained bash test.sh that stubs curl and asserts on the PUT calls (or lack thereof) action.sh makes, covering: applying entries, opt-in-only default behavior, no-op when absent, missing-field validation, and API-error propagation. Wires it into `mise run test` (new task) and a new CI job in check.yaml so the suite actually runs on every push/PR, not just locally. Adds mikefarah/yq to mise.toml as a dev/test dependency (the same yq flavor action.sh requires at runtime, already installed ad hoc inside the composite action itself for consumers). --- .github/actions/apply-repo-settings/test.sh | 197 ++++++++++++++++++++ .github/workflows/check.yaml | 17 ++ .mise/tasks/test | 14 ++ mise.toml | 3 + 4 files changed, 231 insertions(+) create mode 100755 .github/actions/apply-repo-settings/test.sh create mode 100755 .mise/tasks/test diff --git a/.github/actions/apply-repo-settings/test.sh b/.github/actions/apply-repo-settings/test.sh new file mode 100755 index 0000000..df9a467 --- /dev/null +++ b/.github/actions/apply-repo-settings/test.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# Lightweight test harness for action.sh. +# +# This repo has no bats/shellspec setup, so this is a plain bash script: +# it stubs out `curl` (the only thing action.sh shells out to for the +# actual GitHub API calls) with a recorder, runs action.sh against fixture +# settings.yml files, and asserts on the calls made (or not made) and on +# exit codes. Run directly: `.github/actions/apply-repo-settings/test.sh` +# (requires mikefarah/yq + jq on PATH, same as action.sh itself). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ACTION_SH="$SCRIPT_DIR/action.sh" + +PASS=0 +FAIL=0 + +pass() { PASS=$((PASS + 1)); echo " ok - $*"; } +fail() { + FAIL=$((FAIL + 1)) + echo " not ok - $*" +} + +# assert_contains NEEDLE HAYSTACK LABEL +assert_contains() { + local needle="$1" haystack="$2" label="$3" + if [[ "$haystack" == *"$needle"* ]]; then + pass "$label" + else + fail "$label (expected to find: $needle)" + echo " --- haystack ---" + echo "$haystack" | sed 's/^/ /' + fi +} + +# assert_not_contains NEEDLE HAYSTACK LABEL +assert_not_contains() { + local needle="$1" haystack="$2" label="$3" + if [[ "$haystack" != *"$needle"* ]]; then + pass "$label" + else + fail "$label (expected NOT to find: $needle)" + fi +} + +# assert_eq ACTUAL EXPECTED LABEL +assert_eq() { + local actual="$1" expected="$2" label="$3" + if [[ "$actual" == "$expected" ]]; then + pass "$label" + else + fail "$label (expected [$expected], got [$actual])" + fi +} + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +MOCK_BIN="$TMPDIR/bin" +mkdir -p "$MOCK_BIN" +export MOCK_CURL_LOG="$TMPDIR/curl.log" + +# Fake `curl` that records every call action.sh's `api()` helper makes and +# returns a canned response, so no real network call ever happens. Supports +# forcing a failure via MOCK_CURL_FORCE_STATUS (used by the permission-denied +# test case below). +cat >"$MOCK_BIN/curl" <<'MOCK_CURL' +#!/usr/bin/env bash +set -euo pipefail +method="GET" +url="" +body="" +outfile="/dev/null" +while [[ $# -gt 0 ]]; do + case "$1" in + -X) method="$2"; shift 2 ;; + -H) shift 2 ;; + -w) shift 2 ;; + -d) body="$2"; shift 2 ;; + -o) outfile="$2"; shift 2 ;; + -s) shift ;; + http*) url="$1"; shift ;; + *) shift ;; + esac +done + +echo "${method} ${url} ${body}" >>"$MOCK_CURL_LOG" + +if [[ -n "${MOCK_CURL_FORCE_STATUS:-}" ]]; then + echo "${MOCK_CURL_FORCE_BODY:-{}}" >"$outfile" + printf '%s' "$MOCK_CURL_FORCE_STATUS" + exit 0 +fi + +case "$url" in + */collaborators/*) echo -n "" >"$outfile"; printf '204' ;; + *) echo '{}' >"$outfile"; printf '200' ;; +esac +MOCK_CURL +chmod +x "$MOCK_BIN/curl" + +run_action() { + # run_action SETTINGS_FILE SECTIONS [extra env assignments...] + local settings_file="$1" sections="$2" + shift 2 + : >"$MOCK_CURL_LOG" + ( + cd "$TMPDIR" + export PATH="$MOCK_BIN:$PATH" + export GH_TOKEN="fake-token" + export OWNER="acme" + export REPO="widgets" + export SETTINGS_FILE="$settings_file" + export DRY_RUN="false" + export SECTIONS="$sections" + export GITHUB_OUTPUT="$TMPDIR/github_output" + export GITHUB_STEP_SUMMARY="$TMPDIR/github_step_summary" + : >"$GITHUB_OUTPUT" + : >"$GITHUB_STEP_SUMMARY" + for assignment in "$@"; do + export "${assignment?}" + done + "$ACTION_SH" + ) +} + +echo "### collaborators: applies each {username, permission} entry via PUT" +cat >"$TMPDIR/settings-collaborators.yml" <<'YAML' +collaborators: + - username: alice + permission: push + - username: bob + permission: admin +YAML + +if run_action "settings-collaborators.yml" "collaborators" >"$TMPDIR/out.log" 2>&1; then + log="$(cat "$MOCK_CURL_LOG")" + assert_contains "PUT https://api.github.com/repos/acme/widgets/collaborators/alice {\"permission\":\"push\"}" "$log" \ + "PUT issued for alice with permission=push" + assert_contains "PUT https://api.github.com/repos/acme/widgets/collaborators/bob {\"permission\":\"admin\"}" "$log" \ + "PUT issued for bob with permission=admin" + summary="$(grep '^summary=' "$TMPDIR/github_output" | sed 's/^summary=//')" + assert_contains '"collaborators_applied":["alice","bob"]' "$summary" "summary output lists both usernames" +else + fail "action.sh exited non-zero for a valid collaborators block" + cat "$TMPDIR/out.log" | sed 's/^/ /' +fi + +echo "### collaborators: opt-in only — default sections does not touch collaborators" +if run_action "settings-collaborators.yml" "repository,rulesets" >"$TMPDIR/out.log" 2>&1; then + log="$(cat "$MOCK_CURL_LOG")" + assert_not_contains "/collaborators/" "$log" \ + "no collaborators PUT issued when sections=repository,rulesets (default) even though the block exists" +else + fail "action.sh exited non-zero on default sections with a collaborators block present" + cat "$TMPDIR/out.log" | sed 's/^/ /' +fi + +echo "### collaborators: no collaborators block + section requested → no-op, not an error" +cat >"$TMPDIR/settings-empty.yml" <<'YAML' +repository: + private: true +YAML +if run_action "settings-empty.yml" "repository,rulesets,collaborators" >"$TMPDIR/out.log" 2>&1; then + log="$(cat "$MOCK_CURL_LOG")" + assert_not_contains "/collaborators/" "$log" \ + "no collaborators PUT issued when there's no collaborators: block" +else + fail "action.sh exited non-zero when collaborators is requested but absent from settings.yml" + cat "$TMPDIR/out.log" | sed 's/^/ /' +fi + +echo "### collaborators: missing 'permission' field fails loudly instead of silently skipping" +cat >"$TMPDIR/settings-bad.yml" <<'YAML' +collaborators: + - username: carol +YAML +if run_action "settings-bad.yml" "collaborators" >"$TMPDIR/out.log" 2>&1; then + fail "action.sh should have exited non-zero on a collaborators entry missing 'permission'" +else + assert_contains "missing required 'permission'" "$(cat "$TMPDIR/out.log")" \ + "clear error message on missing 'permission'" +fi + +echo "### collaborators: API error (e.g. token lacking Administration:write) propagates as a failure" +if MOCK_CURL_FORCE_STATUS=403 MOCK_CURL_FORCE_BODY='{"message":"Resource not accessible by integration"}' \ + run_action "settings-collaborators.yml" "collaborators" >"$TMPDIR/out.log" 2>&1; then + fail "action.sh should have exited non-zero when the collaborators PUT returns 403" +else + assert_contains "HTTP 403" "$(cat "$TMPDIR/out.log")" \ + "403 from the collaborators PUT surfaces in the error annotation, not swallowed" +fi + +echo +echo "$PASS passed, $FAIL failed" +[[ "$FAIL" -eq 0 ]] diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 4d6e6fd..6d84232 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -83,6 +83,23 @@ jobs: echo "Formatting changes were auto-committed. Re-running CI." exit 1 + test: + name: Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install mise + uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4 + with: + install_args: 'aqua:mikefarah/yq' + + - name: Run tests + run: mise run test + security: name: Security runs-on: ubuntu-latest diff --git a/.mise/tasks/test b/.mise/tasks/test new file mode 100755 index 0000000..c89fe60 --- /dev/null +++ b/.mise/tasks/test @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +#MISE description="Run action test scripts (composite actions have no shared test framework yet — each ships its own test.sh)" +set -euo pipefail + +status=0 +while IFS= read -r -d '' test_script; do + echo "::group::$test_script" + if ! "$test_script"; then + status=1 + fi + echo "::endgroup::" +done < <(find .github/actions -maxdepth 2 -name 'test.sh' -print0 | sort -z) + +exit "$status" diff --git a/mise.toml b/mise.toml index b040e19..676c331 100644 --- a/mise.toml +++ b/mise.toml @@ -17,3 +17,6 @@ checkov = "latest" grype = "latest" gitleaks = "latest" +# Test tools (mikefarah's yq — same one apply-repo-settings/action.sh requires at runtime) +"aqua:mikefarah/yq" = "latest" + From 8784a8242877cfba58995ee458981aad6b141fb8 Mon Sep 17 00:00:00 2001 From: Nathan Heaps Date: Mon, 20 Jul 2026 16:22:26 -0400 Subject: [PATCH 3/4] docs(apply-repo-settings): document collaborators support Documents the new collaborators section: PUT semantics, non-destructive behavior, the opt-in sections default decision and why, and the outputs/summary key. Also notes that the existing GitHub App manifest (administration:write) already covers the collaborators PUT call per GitHub's docs, so no app permission change is needed. --- .github/actions/apply-repo-settings/README.md | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/actions/apply-repo-settings/README.md b/.github/actions/apply-repo-settings/README.md index a8626ba..9599b11 100644 --- a/.github/actions/apply-repo-settings/README.md +++ b/.github/actions/apply-repo-settings/README.md @@ -6,24 +6,24 @@ Reads `.github/settings.yml` from the current repo and applies the supported sec ## Why not self-host the upstream app? -The upstream app is a Probot webhook server — it's designed to listen for `push` events on a long-running process. Adapting it for ephemeral one-shot runs is more invasive than building a minimal applier from scratch. This action covers the two sections we actually use (`repository:` config and `rulesets:`) in ~170 lines of bash + `gh api`. Other sections (`labels`, `collaborators`, `teams`, `environments`, legacy `branches`) are not yet implemented — labels are managed via a separate sync today. +The upstream app is a Probot webhook server — it's designed to listen for `push` events on a long-running process. Adapting it for ephemeral one-shot runs is more invasive than building a minimal applier from scratch. This action covers `repository:` config, `rulesets:`, and (opt-in) `collaborators:` in a small bash + `gh api` script. Other sections (`labels`, `teams`, `environments`, legacy `branches`) are not yet implemented — labels are managed via a separate sync today. ## 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. `collaborators` is supported but **opt-in only** — it is not in the default (see [What it actually does](#what-it-actually-does)). | 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 -- `summary` — JSON object: `{ repository, rulesets_created, rulesets_updated, rulesets_unchanged }`. +- `summary` — JSON object: `{ repository, rulesets_created, rulesets_updated, rulesets_unchanged, collaborators_applied }`. ## Setting up the GitHub App @@ -35,6 +35,8 @@ It builds a [GitHub App manifest](https://docs.github.com/en/apps/sharing-github Source: [`pages/index.html`](../../../pages/index.html). Deployed by [`.github/workflows/pages.yaml`](../../workflows/pages.yaml). +`PUT /repos/{owner}/{repo}/collaborators/{username}` (used by the `collaborators` section) is documented by GitHub as requiring `Administration: write` on the target repo — the same permission this manifest already grants for `repository`/`rulesets`. No additional app permission is needed to enable `collaborators`. + After creating the app: 1. Install it on every repo you want to manage. @@ -85,11 +87,17 @@ jobs: - if no ruleset with that name exists → `POST /repos/{owner}/{repo}/rulesets` - if one exists and content matches → no-op (logged as `unchanged`) - if one exists and content differs → `PUT /repos/{owner}/{repo}/rulesets/{id}` +- **`collaborators:` list** (only when `collaborators` is included in `sections` — see below) → for each `{username, permission}` entry, `PUT /repos/{owner}/{repo}/collaborators/{username}` with `{"permission": ""}`. `permission` is whatever the GitHub API accepts (`pull`, `triage`, `push`, `maintain`, `admin`, or a custom repository role name); this action does not validate it beyond checking it's present — an invalid value surfaces as an API error. + +Rulesets that exist on the repo but are absent from the YAML are **not deleted** (safer default — repos may have UI-created rulesets we don't want to wipe). Collaborators absent from the YAML are similarly **not removed** — this action only ever adds/updates, never revokes. If you want destructive sync for either, add a `--prune` mode in a follow-up. + +### Why `collaborators` is opt-in, not a default section -Rulesets that exist on the repo but are absent from the YAML are **not deleted** (safer default — repos may have UI-created rulesets we don't want to wipe). If you want destructive sync, add a `--prune` mode in a follow-up. +`repository` and `rulesets` ship in the default `sections` value, so a repo with no `repository:`/`rulesets:` block in its `settings.yml` sees no behavior change either way — the section is simply skipped. The same is true for `collaborators`. The difference is what happens once a repo's `settings.yml` _does_ gain a `collaborators:` block — for example via the `nsheaps/.github` sync layer's `collaborators` deep-merge, which a repo owner may not have driven themselves. Granting a user push/admin access is a higher-consequence, harder-to-notice change than adding a ruleset, so this action requires each repo to explicitly add `collaborators` to its `sections` input before any `PUT /repos/{owner}/{repo}/collaborators/{username}` call is made. Until a repo opts in, an incoming `collaborators:` block is inert from this action's point of view. ## Limitations - The upstream repository-settings app reads from the default branch only. This action reads from the workflow's checkout, so it works on any branch — useful for testing changes in a PR before merging. -- No support yet for `labels`, `collaborators`, `teams`, `environments`. Those are tracked separately. +- No support yet for `labels`, `teams`, `environments`. Those are tracked separately. - `bypass_actors[].actor_id` for `RepositoryRole` must be the role's numeric ID (community-documented: 1=read, 2=triage, 3=write, 4=maintain, 5=admin). Custom roles have user-assigned IDs. +- `collaborators` only manages **outside collaborators / explicit repo-level access grants** — adding an org member via this call does not add them to the organization, only grants/updates their repo-level permission. It does not touch org membership, invite state, or team-based access. From c43a880bbef9697a3244cbf6508e1fdf6149476e Mon Sep 17 00:00:00 2001 From: nsheaps-oura <300890611+nsheaps-oura@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:44:32 +0000 Subject: [PATCH 4/4] chore: `mise format` Triggered by: b4a403e847ec0be3f56dab3823af15ab6889397c Workflow run: https://github.com/nsheaps/github-actions/actions/runs/29777349915 --- .../1/1771874092/documentation/REPORT.md | 16 ++++++++-------- LICENSE.md | 2 +- pages/index.html | 3 +-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.claude/pr-reviews/nsheaps/github-actions/1/1771874092/documentation/REPORT.md b/.claude/pr-reviews/nsheaps/github-actions/1/1771874092/documentation/REPORT.md index 47964ad..65c5390 100644 --- a/.claude/pr-reviews/nsheaps/github-actions/1/1771874092/documentation/REPORT.md +++ b/.claude/pr-reviews/nsheaps/github-actions/1/1771874092/documentation/REPORT.md @@ -100,15 +100,15 @@ This PR delivers strong, well-structured documentation that significantly exceed ### Summary of Deductions -| Category | Deduction | Reason | +| Category | Deduction | Reason | | ---------------------------------- | --------- | --------------------------------------------------------------------- | --- | -------------------------------- | -| Missing troubleshooting/error docs | -5 | No guidance on common failure modes | -| Missing prerequisites | -2 | No runner/dependency requirements noted | -| Incomplete edge-case docs | -4 | maxdepth 2 limit, root-path sync naming, never-deletes implications | -| env-vars behavior mismatch | -2 | README implies compose-level injection, script does GITHUB_ENV export | -| auth-type values underdocumented | -2 | No explanation of what each auth type requires | -| Missing function docstrings | -2 | `discover_compose_files` and `export_env_vars` lack docstrings | -| Minor inline comment gaps | -1 | ` | | true`on curl,`sort -z` rationale | +| Missing troubleshooting/error docs | -5 | No guidance on common failure modes | +| Missing prerequisites | -2 | No runner/dependency requirements noted | +| Incomplete edge-case docs | -4 | maxdepth 2 limit, root-path sync naming, never-deletes implications | +| env-vars behavior mismatch | -2 | README implies compose-level injection, script does GITHUB_ENV export | +| auth-type values underdocumented | -2 | No explanation of what each auth type requires | +| Missing function docstrings | -2 | `discover_compose_files` and `export_env_vars` lack docstrings | +| Minor inline comment gaps | -1 | ` | | true`on curl,`sort -z` rationale | ## References diff --git a/LICENSE.md b/LICENSE.md index e70157b..c3fc5da 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -11,7 +11,7 @@ Copyright (c) 2026 Nathan Heaps. All rights reserved. > (https://polyformproject.org/licenses/internal-use/1.0.0), which is > purpose-built for "use the software for internal business operations, do > not distribute it." The one substantive adaptation is that PolyForm grants -> rights to *"you and your company"* (whoever accepts the license), whereas +> rights to _"you and your company"_ (whoever accepts the license), whereas > this license grants them to a **single, specifically named licensee** > (Oura, defined below). That is a real difference: the licensor here is not > a generic member of the licensee's organization, so the "permitted diff --git a/pages/index.html b/pages/index.html index 820fe74..caec1b6 100644 --- a/pages/index.html +++ b/pages/index.html @@ -319,8 +319,7 @@

Detailed setup walkthrough

 cp pages/index.html docs/index.html
-# Settings → Pages → Source: GitHub Actions (or branch /docs)
+# Settings → Pages → Source: GitHub Actions (or branch /docs)

Then use https://<your-org>.github.io/<repo>/ as the redirect. Any other HTTPS static host (Vercel, Netlify, etc.) works equally well.