diff --git a/.github/workflows/auto-merge.yaml b/.github/workflows/auto-merge.yaml deleted file mode 100644 index 343e493..0000000 --- a/.github/workflows/auto-merge.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# Caller for the org's reusable auto-merge workflow — the set-and-forget -# companion to merge.yaml. A maintainer arms a PR once (comments `/auto-merge` -# or adds the `auto-merge` label) and it is fast-forwarded automatically the -# moment it is approved and every required check is green, via the same -# signature-preserving "FF Merge" App as the `/merge` flow. `/merge` merges now; -# `/auto-merge` merges when ready. Remove the label to cancel. -# -# All four triggers are required, one per job in the reusable workflow: -# - issue_comment + pull_request(labeled) arm the PR; -# - pull_request_review(submitted) and workflow_run(completed) attempt the -# merge once the PR's state changes. -# `workflows:` lists every workflow that must be green before merging. This repo's -# only PR-gating check is CodeQL (public-code-quality requires code scanning); -# release-please runs on push to main, not on PRs, so it is not listed. Whichever -# run finishes last triggers the attempt, and the App re-verifies all checks, -# approval, and the fast-forward before moving the ref. -# -# The label and review jobs use pull_request / pull_request_review (not the -# _target variants), so they arm same-repo PRs only; fork PRs arm via the -# `/auto-merge` comment and merge through that comment's attempt or workflow_run. -# -# Org prerequisites (see bitwise-media-group/ff-merge): the FF_MERGE_CLIENT_ID -# variable + FF_MERGE_PRIVATE_KEY secret, and the "FF Merge" App in main's -# ruleset bypass list — the same App as the `/merge` flow. - -name: Auto-merge - -on: - issue_comment: - types: [created] - pull_request: - types: [labeled] - pull_request_review: - types: [submitted] - workflow_run: - workflows: ["CodeQL analysis"] - types: [completed] - -# the App token does the privileged work; the caller grants nothing -permissions: {} - -jobs: - auto-merge: - uses: bitwise-media-group/github-workflows/.github/workflows/auto-merge.yaml@077a003a620f49bd0062c73fc761dbea05d7fb70 # v1.1.0 - with: - app-client-id: ${{ vars.FF_MERGE_CLIENT_ID }} - secrets: - app-private-key: ${{ secrets.FF_MERGE_PRIVATE_KEY }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index e666112..0000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: CodeQL analysis -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - # Weekly, so newly-published CodeQL queries surface issues between changes. - - cron: "27 4 * * 1" - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - permissions: - # upload code scanning results - security-events: write - # required to analyze the `actions` language - actions: read - # checkout the repository - contents: read - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependabot-merge.yaml b/.github/workflows/dependabot-merge.yaml index 85a167d..720df05 100644 --- a/.github/workflows/dependabot-merge.yaml +++ b/.github/workflows/dependabot-merge.yaml @@ -1,7 +1,7 @@ # Caller for the org's reusable Dependabot auto-merge workflow. Dependabot opens -# weekly github-actions pin-bump PRs (see .github/dependabot.yaml); this -# auto-approves the minor/patch ones and fast-forwards them into main once CI is -# green, via the same signature-preserving "FF Merge" App as the `/merge` flow. +# weekly github-actions pin-bump PRs (see .github/dependabot.yaml); once a PR's +# checks are green this auto-approves the minor/patch ones and fast-forwards them +# into main via the same signature-preserving "FF Merge" App as the `/merge` flow. # Major updates are never approved, so they wait for a human. # # Both the approval and the merge use the App token, so approval works regardless @@ -10,23 +10,22 @@ # Integration bypass actor. The approval is what satisfies # public-pull-request-required's required review. # -# Both triggers are required: pull_request_target approves on open, and -# workflow_run fast-forwards once CI finishes green. `workflows:` lists this -# repo's only PR-gating check, CodeQL (public-code-quality); check_suite is not -# usable — GitHub does not fire it for a repo's own Actions CI. pull_request_target -# is safe here: the reusable workflow never checks out or runs PR code. +# Trigger: workflow_run only — v3 dropped the pull_request_target trigger. This +# event attaches no check run to the PR, so it leaves no skipped-job clutter (and +# needs no pull_request_target). `workflows:` lists this repo's only PR-gating +# check, the CodeQL-based "Security Analysis"; whichever run finishes last triggers +# the merge attempt. check_suite is not usable — GitHub does not fire it for a +# repo's own Actions CI. # # Org prerequisites (see bitwise-media-group/ff-merge): the FF_MERGE_CLIENT_ID -# variable + FF_MERGE_PRIVATE_KEY secret, and the "FF Merge" App in main's -# ruleset bypass list — the same App as the `/merge` flow. +# variable + FF_MERGE_PRIVATE_KEY secret, and the "FF Merge" App in main's ruleset +# bypass list — the same App as the `/merge` flow. -name: Dependabot auto-merge +name: Dependabot Auto-Merge on: - pull_request_target: - types: [opened, reopened, synchronize] workflow_run: - workflows: ["CodeQL analysis"] + workflows: ["Security Analysis"] types: [completed] # the App token does the privileged work; the caller grants nothing @@ -34,7 +33,7 @@ permissions: {} jobs: auto-merge: - uses: bitwise-media-group/github-workflows/.github/workflows/dependabot-merge.yaml@077a003a620f49bd0062c73fc761dbea05d7fb70 # v1.1.0 + uses: bitwise-media-group/github-workflows/.github/workflows/dependabot-merge.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 with: app-client-id: ${{ vars.FF_MERGE_CLIENT_ID }} secrets: diff --git a/.github/workflows/merge-notice.yaml b/.github/workflows/merge-notice.yaml index b34d681..db3e16d 100644 --- a/.github/workflows/merge-notice.yaml +++ b/.github/workflows/merge-notice.yaml @@ -8,7 +8,7 @@ # and never checks out or runs PR code, so the elevated context is safe. No # secret needed. -name: Fast-forward merge notice +name: Merge Notice on: pull_request_target: @@ -20,6 +20,6 @@ permissions: jobs: notice: - uses: bitwise-media-group/github-workflows/.github/workflows/merge-notice.yaml@077a003a620f49bd0062c73fc761dbea05d7fb70 # v1.1.0 + uses: bitwise-media-group/github-workflows/.github/workflows/merge-notice.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 with: pr-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/merge-review-ack.yaml b/.github/workflows/merge-review-ack.yaml new file mode 100644 index 0000000..3b4bd40 --- /dev/null +++ b/.github/workflows/merge-review-ack.yaml @@ -0,0 +1,25 @@ +# Caller for the org's reusable merge-review-ack workflow — the v3 companion that +# makes an approval complete an armed auto-merge. An approval on a fork PR carries +# no secrets, so merge.yaml's review path can't merge it directly; and if every +# check is already green, the approval is the last event, so nothing else would +# fire to finish the merge. This workflow completes on an approving review purely +# so its workflow_run(completed) re-enters merge.yaml in the base-repo context, +# where the "FF Merge" App token is minted and the fast-forward retried. Harmless +# for same-repo PRs (those merge via the review path directly). +# +# Wire-up: this workflow's name, "Merge Review Ack", is listed in merge.yaml's +# workflow_run.workflows. It does no privileged work, so it grants nothing and +# needs no secret — the completed run is the only signal. + +name: Merge Review Ack + +on: + pull_request_review: + types: [submitted] + +# does no privileged work; grants nothing +permissions: {} + +jobs: + ack: + uses: bitwise-media-group/github-workflows/.github/workflows/merge-review-ack.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 82756ca..968c8cf 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -1,37 +1,55 @@ -# Caller for the org's reusable fast-forward `/merge` workflow. A maintainer -# comments `/merge` on an approved, green PR and the base branch is -# fast-forwarded to the PR head, preserving each commit's signature — so the -# result still satisfies `required_signatures` on main. +# Caller for the org's reusable fast-forward merge workflow. As of v2 the old +# auto-merge.yaml is folded in here, so this one file drives both flows: a +# maintainer comments `/merge` on an approved, green PR to fast-forward main now, +# or arms set-and-forget auto-merge with `/auto-merge` (or the `auto-merge` label) +# so the PR merges itself the moment it is approved and every required check is +# green. Either way the "FF Merge" GitHub App moves the ref, preserving each +# commit's signature — so the result still satisfies `required_signatures` on main. # -# The privileged ref move is done by the "FF Merge" GitHub App, which sits in -# main's ruleset bypass list (the Integration bypass actor in -# public-pull-request-required / public-code-quality). The reusable workflow -# mints the App token, re-verifies the commenter's write access, that the PR is -# approved and every required check is green, and that the move is a genuine -# fast-forward before touching the ref — so the caller's GITHUB_TOKEN needs no -# permissions of its own. +# The App sits in main's ruleset bypass list (the Integration bypass actor in +# public-pull-request-required / public-code-quality). The reusable workflow mints +# the App token, re-verifies write access, approval, and that every required check +# is green, and that the move is a genuine fast-forward before touching the ref — +# so the caller's GITHUB_TOKEN needs no permissions of its own. # -# Trigger: issue_comment runs in the base-repo context, so the App credentials -# are available even for fork PRs. +# Every trigger here attaches NO check run to the PR head, so the merge machinery +# leaves no skipped-job clutter on the PR's checks list: +# - issue_comment(created) drives `/merge` and arms `/auto-merge`; it runs in the +# base-repo context, so the App credentials are available even for fork PRs. +# - workflow_run(completed) re-attempts an armed auto-merge once a gating run +# finishes. `workflows:` lists every check that must be green PLUS "Merge Review +# Ack" (the companion in merge-review-ack.yaml that turns an approval into a +# merge attempt, for fork and same-repo PRs alike). This repo's only PR-gating +# check is the CodeQL-based "Security Analysis"; release runs on push to main, +# not on PRs, so it is not listed. Whichever run finishes last triggers the +# attempt, and the App re-verifies all checks before merging. +# - schedule sweeps armed PRs hourly as a backstop for any missed event. As of +# v3, adding the `auto-merge` label arms the PR; the merge then happens on the +# next workflow_run or this sweep, not immediately. # # Org prerequisites (see bitwise-media-group/ff-merge): the FF_MERGE_CLIENT_ID -# variable + FF_MERGE_PRIVATE_KEY secret, and the "FF Merge" App in main's -# ruleset bypass list. +# variable + FF_MERGE_PRIVATE_KEY secret, and the "FF Merge" App in main's ruleset +# bypass list — the same App as the Dependabot auto-merge flow. -name: Fast-forward merge +name: Merge on: issue_comment: types: [created] + workflow_run: + workflows: ["Security Analysis", "Merge Review Ack"] + types: [completed] + schedule: + # Hourly backstop sweep of armed PRs for any missed event trigger. + - cron: "17 * * * *" # the App token does the privileged work; the job's GITHUB_TOKEN needs nothing permissions: {} jobs: - fast-forward: - uses: bitwise-media-group/github-workflows/.github/workflows/merge.yaml@077a003a620f49bd0062c73fc761dbea05d7fb70 # v1.1.0 + merge: + uses: bitwise-media-group/github-workflows/.github/workflows/merge.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 with: - pr-number: ${{ github.event.issue.number }} app-client-id: ${{ vars.FF_MERGE_CLIENT_ID }} secrets: app-private-key: ${{ secrets.FF_MERGE_PRIVATE_KEY }} diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml deleted file mode 100644 index 8c26826..0000000 --- a/.github/workflows/release-please.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: Release - -# Drives releases within the org rulesets for public repos: -# - opens a `release-please--*` release PR branch, the prefix that -# `public-fork-only` allows to be created directly in the repo; -# - tags releases as vX.Y.Z, the only tags `public-version-tags-only` -# permits, which `public-immutable-tags` then freezes; -# - commits with the GITHUB_TOKEN, which GitHub signs as verified, so the -# release commit satisfies `required_signatures` on protected branches. -# -# Manifest mode is used: configuration lives in release-please-config.json and -# the current version in .release-please-manifest.json at the repo root. - -on: - push: - branches: [main] - -permissions: - # create release commits, tags, and GitHub Releases - contents: write - # open and update the release PR - pull-requests: write - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - name: Run release-please - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..b163770 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,49 @@ +# Caller for the org's reusable release workflow. This repo has no .goreleaser.yaml +# and no zensical.toml, and ships no build artifacts, so a release here is just the +# release-please cut (the workflow's goreleaser/publish, docs, and vanity-tags jobs +# skip). It drives releases +# within the org rulesets for public repos: +# - opens a `release-please--*` release PR branch, the prefix that +# `public-fork-only` allows to be created directly in the repo; +# - tags releases as vX.Y.Z, the only tags `public-version-tags-only` permits, +# which `public-immutable-tags` then freezes; +# - release-please commits with the GITHUB_TOKEN, which GitHub signs as verified, +# so the release commit satisfies `required_signatures` on protected branches. +# +# Manifest mode: configuration lives in release-please-config.json and the current +# version in .release-please-manifest.json at the repo root. vanity-tags is left at +# its default (false) — nothing pins this repo as a reusable workflow, so there are +# no floating major tags to move. + +name: Release + +on: + push: + branches: [main] + +# Ceiling for the reusable workflow's jobs. GitHub resolves a reusable workflow's +# permissions as the union of every job and ignores `if:`, so the caller must grant +# every scope any job declares — even the goreleaser/publish and docs jobs that are +# skipped here (no .goreleaser.yaml, no zensical.toml) — or the run fails at startup. +# This block is only a ceiling: each reusable job narrows to its own declared scopes, +# so the only job that runs here, release-please, still gets just +# contents/issues/pull-requests; the rest exist solely to clear that startup check. +permissions: + # create release commits, tags, and GitHub Releases (release-please) + contents: write + # release-please creates PR labels via the issues API + issues: write + # open and update the release PR + pull-requests: write + # cosign keyless signing (goreleaser) + the docs job's Pages OIDC deploy + id-token: write + # GitHub build-provenance attestation (goreleaser/publish) + attestations: write + # artifact storage record for the attestation (goreleaser/publish) + artifact-metadata: write + # publish the docs site to GitHub Pages (docs job, when a zensical.toml exists) + pages: write + +jobs: + release: + uses: bitwise-media-group/github-workflows/.github/workflows/release.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..6f45315 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,39 @@ +# Caller for the org's reusable security (CodeQL) workflow — the v2 rename of the +# old codeql.yaml. It owns the triggers and grants the analyze permissions; the +# reusable workflow detects languages at the repo root and scans `actions` +# (build-free), plus go (when a root go.mod exists) and javascript-typescript +# (when package.json exists). This repo is shell + JSON config with neither, so +# `actions` is the only language analyzed — the same coverage the previous inline +# CodeQL workflow gave, and what public-code-quality's required code scanning +# expects (it gates on the "CodeQL" tool, which codeql-action reports regardless +# of this workflow's name). +# +# The workflow name "Security Analysis" is load-bearing: merge.yaml and +# dependabot-merge.yaml list it in their workflow_run triggers as this repo's only +# PR-gating check. Keep it in sync there if you rename it. + +name: Security Analysis + +on: + push: + branches: [main] + pull_request: + branches: ["main", "releases/*"] + schedule: + # Weekly, so newly-published CodeQL queries surface issues between changes. + - cron: "27 4 * * 1" + +# ceiling for the reusable workflow's analyze job +permissions: + # upload analysis results to code scanning + security-events: write + # fetch internal or private CodeQL packs + packages: read + # required to analyze the `actions` language + actions: read + # check out the repository + contents: read + +jobs: + analyze: + uses: bitwise-media-group/github-workflows/.github/workflows/security.yaml@4a154ffd7efbb6aff856386ec1def4b6dc364672 # v4.0.0 diff --git a/org-config.sh b/org-config.sh index 2238eaf..7afe4f5 100755 --- a/org-config.sh +++ b/org-config.sh @@ -9,12 +9,15 @@ set -eu # ./org-config.sh import [dir] # apply config <- dir # ./org-config.sh labels-sync [--public|--private] [dir] # ./org-config.sh sync [--public|--private] [dir] +# ./org-config.sh teams-sync [--public|--private] # -# org defaults to bitwise-media-group. The two *-sync commands fan config out -# across the org's repos (default dir: ./repo-config) and take --public / -# --private to limit which repos by visibility: +# org defaults to bitwise-media-group. The *-sync commands fan config out across +# the org's repos and take --public / --private to limit which repos by +# visibility. labels-sync and sync read a snapshot dir (default: ./repo-config): # labels-sync applies only /labels.json to each repo. # sync runs the full repo-config.sh import (settings, rulesets, labels). +# teams-sync grants a team (default: bitwise-maintainers) a permission +# (default: maintain) on every repo; takes no dir. # # What's covered: # - Organisation-level rulesets only (full definitions: conditions, rules, @@ -48,11 +51,21 @@ set -eu # deleted from each repo, which removes it from that repo's issues/PRs. Set # KEEP_EXTRA=1 to only add/update and never delete. # +# Teams: teams-sync grants one org team a single permission on EVERY non-archived +# repo in the org, so a standing maintainer team gets access to repos created +# after it was set up. Defaults to the bitwise-maintainers team at maintain (the +# "as maintainers" access level); override with TEAM / TEAM_PERMISSION. Purely +# additive and idempotent: GitHub's team-repo PUT upserts the grant, and the +# command never removes a team from a repo (no mirror/delete pass). +# # Env: # STRIP_BYPASS=1 drop ruleset bypass_actors on export — use when the bypass # actors (teams, apps, custom roles) won't exist in the target. # KEEP_EXTRA=1 labels-sync only: add/update labels but never delete ones a # repo has that aren't in labels.json (additive, not mirror). +# TEAM= teams-sync only: org team to grant (default bitwise-maintainers). +# TEAM_PERMISSION teams-sync only: pull|triage|push|maintain|admin or a custom +# role name (default maintain). # # Requires: gh (authenticated, org owner), jq. # Note: reading/writing org settings and rulesets requires organisation owner; @@ -81,6 +94,7 @@ usage() { echo " $0 import [dir]" >&2 echo " $0 labels-sync [--public|--private] [dir] (dir default: repo-config)" >&2 echo " $0 sync [--public|--private] [dir] (dir default: repo-config)" >&2 + echo " $0 teams-sync [--public|--private] (team default: bitwise-maintainers)" >&2 echo " (org defaults to bitwise-media-group)" >&2 exit 2 } @@ -88,6 +102,10 @@ usage() { gh=/opt/homebrew/bin/gh here=$(dirname "$0") +# teams-sync target: which org team gets which permission on every repo. +team=${TEAM:-bitwise-maintainers} +team_permission=${TEAM_PERMISSION:-maintain} + cmd="${1:-}" [ -n "$cmd" ] || usage shift @@ -288,6 +306,24 @@ repo_sync() { done } +# Grant $team the $team_permission level on every non-archived repo in the org, +# optionally filtered by visibility. GitHub's team-repo PUT is an upsert, so this +# is idempotent and re-asserts access on repos that already have it; it never +# removes the team (additive, no mirror/delete pass). Archived repos are +# read-only and skipped. Per-repo failures are reported but don't abort the run. +teams_sync() { + ${gh} api --paginate "orgs/$org/repos?type=$visibility" \ + --jq '.[] | select(.archived | not) | .name' | while read -r name; do + [ -n "$name" ] || continue + if ${gh} api -X PUT "orgs/$org/teams/$team/repos/$org/$name" \ + -f permission="$team_permission" >/dev/null; then + echo "granted $team ($team_permission) -> $name" + else + echo "FAILED team -> $name (see error above)" >&2 + fi + done +} + case "$cmd" in export) dir="${dir:-org-config}" @@ -305,5 +341,8 @@ sync) dir="${dir:-repo-config}" repo_sync ;; +teams-sync) + teams_sync + ;; *) usage ;; esac diff --git a/org-config/rulesets/public-code-quality.json b/org-config/rulesets/public-code-quality.json index f1c9abc..d429a5e 100644 --- a/org-config/rulesets/public-code-quality.json +++ b/org-config/rulesets/public-code-quality.json @@ -12,10 +12,21 @@ "conditions": { "ref_name": { "exclude": [], - "include": [] + "include": [ + "~DEFAULT_BRANCH", + "refs/heads/releases/*" + ] }, "repository_property": { - "exclude": [], + "exclude": [ + { + "name": "bypass-policies", + "source": "custom", + "property_values": [ + "true" + ] + } + ], "include": [ { "name": "visibility", diff --git a/org-config/rulesets/public-fork-only.json b/org-config/rulesets/public-fork-only.json index da83475..677d6db 100644 --- a/org-config/rulesets/public-fork-only.json +++ b/org-config/rulesets/public-fork-only.json @@ -10,17 +10,21 @@ { "name": "visibility", "source": "system", - "property_values": ["public"] + "property_values": [ + "public" + ] } ] }, "ref_name": { "exclude": [ "refs/heads/release-please--*", - "refs/heads/dependabot/**", + "refs/heads/dependabot/**/*", "refs/heads/main" ], - "include": ["~ALL"] + "include": [ + "~ALL" + ] } }, "rules": [ diff --git a/org-config/rulesets/public-pull-request-required.json b/org-config/rulesets/public-pull-request-required.json index c134b1d..f16d74d 100644 --- a/org-config/rulesets/public-pull-request-required.json +++ b/org-config/rulesets/public-pull-request-required.json @@ -8,11 +8,6 @@ "actor_type": "OrganizationAdmin", "bypass_mode": "always" }, - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - }, { "actor_id": 4048320, "actor_type": "Integration", @@ -21,7 +16,15 @@ ], "conditions": { "repository_property": { - "exclude": [], + "exclude": [ + { + "name": "bypass-policies", + "source": "custom", + "property_values": [ + "true" + ] + } + ], "include": [ { "name": "visibility", diff --git a/org-config/rulesets/public-release-branch-security.json b/org-config/rulesets/public-release-branch-security.json index 9a557b7..d90b832 100644 --- a/org-config/rulesets/public-release-branch-security.json +++ b/org-config/rulesets/public-release-branch-security.json @@ -12,7 +12,15 @@ ] }, "repository_property": { - "exclude": [], + "exclude": [ + { + "name": "bypass-policies", + "source": "custom", + "property_values": [ + "true" + ] + } + ], "include": [ { "name": "visibility", diff --git a/repo-config.sh b/repo-config.sh index 0b5e760..bfc356f 100755 --- a/repo-config.sh +++ b/repo-config.sh @@ -14,10 +14,14 @@ set -eu # bypass_actors). Org-level rulesets that merely apply to this repo are # ignored in both directions — manage those with org-config.sh. # - Labels (name, color, description) as a single labels.json array. +# - Pages build source (Settings > Pages > "Build and deployment > Source") +# as pages.json: build_type ("workflow" = GitHub Actions, "legacy" = deploy +# from a branch) plus the source branch/path when building from a branch. # - General settings, grouped as: # features has_issues, has_projects, has_wiki, has_discussions # pull-requests allow_squash_merge, allow_merge_commit, allow_rebase_merge, -# allow_auto_merge, allow_update_branch, delete_branch_on_merge +# allow_auto_merge, allow_update_branch, delete_branch_on_merge, +# pull_request_creation_policy (all | collaborators_only) # commits squash/merge commit title+message templates, # web_commit_signoff_required # @@ -32,6 +36,10 @@ set -eu # - labels.json mirrors the same way by entry: update/create the labels listed, # delete any repo label absent from it. A missing labels.json is left # untouched; an empty array ([]) means "remove them all". +# - pages.json is applied, not mirrored: import enables/updates Pages to match +# it (create when off, update when on) but never disables Pages, and export +# only overwrites it while Pages is on — a missing pages.json, or a repo with +# Pages off, is left untouched so a hand-authored template isn't clobbered. # # Env: # STRIP_BYPASS=1 drop ruleset bypass_actors on export — use when the bypass @@ -45,6 +53,7 @@ SETTINGS_FILTER='{ has_issues, has_projects, has_wiki, has_discussions, allow_squash_merge, allow_merge_commit, allow_rebase_merge, allow_auto_merge, allow_update_branch, delete_branch_on_merge, + pull_request_creation_policy, squash_merge_commit_title, squash_merge_commit_message, merge_commit_title, merge_commit_message, }' @@ -71,6 +80,21 @@ export_config() { gh api "repos/$repo" | jq "$SETTINGS_FILTER" >"$dir/settings.json" echo "exported settings -> $dir/settings.json" + # Pages build source. Reduce to the create/update payload: build_type, plus + # source branch/path only for branch ("legacy") deploys. When Pages is off + # (404) leave any existing pages.json alone rather than deleting a template. + if pages=$(gh api "repos/$repo/pages" 2>/dev/null); then + printf '%s' "$pages" | + jq '{build_type} + + (if .build_type == "legacy" + then {source: {branch: .source.branch, path: .source.path}} + else {} end)' \ + >"$dir/pages.json" + echo "exported pages -> $dir/pages.json" + else + echo "pages off on $repo; leaving $dir/pages.json untouched" >&2 + fi + # Mirror reality: drop previously-exported rulesets so any removed upstream # don't linger here as stale files. rm -f "$dir"/rulesets/*.json @@ -108,6 +132,22 @@ import_config() { echo "no $dir/settings.json; skipping settings" >&2 fi + # Pages: apply pages.json onto the repo. PUT updates an existing site, POST + # creates one when Pages is off; a missing file leaves Pages alone. We never + # disable Pages here (no DELETE) — that stays a deliberate manual action. + if [ -f "$dir/pages.json" ]; then + if gh api "repos/$repo/pages" >/dev/null 2>&1; then + verb=PUT + else + verb=POST + fi + if gh api -X "$verb" "repos/$repo/pages" --input "$dir/pages.json" >/dev/null; then + echo "applied pages <- $dir/pages.json" + else + echo "FAILED pages <- $dir/pages.json (see error above)" >&2 + fi + fi + # Rulesets: mirror the files onto the repo. A missing rulesets dir is left # alone; an empty dir is a real instruction to remove every ruleset. if [ -d "$dir/rulesets" ]; then diff --git a/repo-config/pages.json b/repo-config/pages.json new file mode 100644 index 0000000..cb604f3 --- /dev/null +++ b/repo-config/pages.json @@ -0,0 +1,3 @@ +{ + "build_type": "workflow" +} diff --git a/repo-config/settings.json b/repo-config/settings.json index b2c348d..f0392c4 100644 --- a/repo-config/settings.json +++ b/repo-config/settings.json @@ -8,5 +8,6 @@ "allow_rebase_merge": false, "allow_auto_merge": false, "allow_update_branch": false, - "delete_branch_on_merge": true + "delete_branch_on_merge": true, + "pull_request_creation_policy": "collaborators_only" }