-
Notifications
You must be signed in to change notification settings - Fork 14
feat: adopt safe-settings for repository configuration management #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marcusburghardt
wants to merge
5
commits into
complytime:main
Choose a base branch
from
marcusburghardt:opsx/adopt-safe-settings
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cc8e345
docs: add change proposal for safe-settings adoption
marcusburghardt 755079f
feat: add initial safe-settings configuration
marcusburghardt 35506bd
fix: address spec review findings across all artifacts
marcusburghardt 3a7df00
feat: implement safe-settings workflow, boundary tests, and docs
marcusburghardt 69f1850
fix(spec): require Organization Administration write for org rulesets
marcusburghardt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| * @jflowers @jpower432 @marcusburghardt | ||
| safe-settings/ @jflowers @jpower432 @marcusburghardt |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # Workflow to apply safe-settings org configuration. | ||
| # Manual dispatch only — add push/schedule triggers after initial validation. | ||
| # Uses a dedicated safe-settings-bot GitHub App for authentication. | ||
| # | ||
| # safe-settings reads config from the admin repo's default branch (main). | ||
| # Config must be merged to main before this workflow can apply it. | ||
| # | ||
| # See MAINTAINING.md for operational documentation. | ||
| name: Safe Settings Sync | ||
|
|
||
| "on": | ||
| workflow_dispatch: | ||
| inputs: | ||
| dry-run: | ||
| description: 'Preview changes without applying (NOP mode)' | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| repos: | ||
| description: >- | ||
| Comma-separated list of repos to target (e.g. "complytime-demos"). | ||
| Leave empty to apply to all managed repos. | ||
| required: false | ||
| type: string | ||
| default: '' | ||
|
|
||
| concurrency: | ||
| group: safe-settings-sync | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| # Pin to a specific release tag. Update via a deliberate PR. | ||
| # TODO: Replace with a commit SHA after initial validation. | ||
| SAFE_SETTINGS_VERSION: "2.1.17" | ||
| SAFE_SETTINGS_CODE_DIR: ".safe-settings-code" | ||
|
|
||
| jobs: | ||
| sync: | ||
| if: github.repository_owner == 'complytime' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Checkout complytime/.github repo | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Validate safe-settings YAML syntax | ||
| run: yamllint safe-settings/ | ||
|
|
||
| - name: Generate scoped deployment-settings | ||
| if: inputs.repos != '' | ||
| env: | ||
| TARGET_REPOS: ${{ inputs.repos }} | ||
| run: | | ||
| echo "Scoping safe-settings to repos: $TARGET_REPOS" | ||
|
|
||
| # Read all repo names from peribolos.yaml | ||
| ALL_REPOS=$(yq -r '.orgs[].repos | keys[]' peribolos.yaml) | ||
|
|
||
| # Build the exclude list: all repos EXCEPT the target ones | ||
| EXCLUDE_LIST="" | ||
| for repo in $ALL_REPOS; do | ||
| SKIP=false | ||
| IFS=',' read -ra TARGETS <<< "$TARGET_REPOS" | ||
| for target in "${TARGETS[@]}"; do | ||
| target=$(echo "$target" | xargs) # trim whitespace | ||
| if [ "$repo" = "$target" ]; then | ||
| SKIP=true | ||
| break | ||
| fi | ||
| done | ||
| if [ "$SKIP" = "false" ]; then | ||
| EXCLUDE_LIST="${EXCLUDE_LIST} - ${repo}"$'\n' | ||
| fi | ||
| done | ||
|
|
||
| # Always exclude the admin repo | ||
| EXCLUDE_LIST="${EXCLUDE_LIST} - .github"$'\n' | ||
| EXCLUDE_LIST="${EXCLUDE_LIST} - admin"$'\n' | ||
| EXCLUDE_LIST="${EXCLUDE_LIST} - safe-settings"$'\n' | ||
|
|
||
| # Write scoped deployment-settings.yml (preserving validators | ||
| # from the original file) | ||
| { | ||
| echo "# Auto-generated: scoped to repos: $TARGET_REPOS" | ||
| echo "restrictedRepos:" | ||
| echo " exclude:" | ||
| echo -n "$EXCLUDE_LIST" | ||
| # Append validators from original file | ||
| echo "" | ||
| yq -r 'del(.restrictedRepos) | select(. != null)' \ | ||
| safe-settings/deployment-settings.yml | ||
| } > /tmp/scoped-deployment-settings.yml | ||
|
|
||
| echo "--- Generated deployment-settings.yml ---" | ||
| cat /tmp/scoped-deployment-settings.yml | ||
|
|
||
| - name: Checkout safe-settings code | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: github/safe-settings | ||
| ref: ${{ env.SAFE_SETTINGS_VERSION }} | ||
| path: ${{ env.SAFE_SETTINGS_CODE_DIR }} | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642195f572a2b2b9a5e172c # v4.4.0 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install safe-settings dependencies | ||
| working-directory: ${{ env.SAFE_SETTINGS_CODE_DIR }} | ||
| run: npm install | ||
|
|
||
| - name: Run safe-settings sync | ||
| working-directory: ${{ env.SAFE_SETTINGS_CODE_DIR }} | ||
| env: | ||
| APP_ID: ${{ vars.SAFE_SETTINGS_APP_ID }} | ||
| PRIVATE_KEY: ${{ secrets.SAFE_SETTINGS_PRIVATE_KEY }} | ||
| GH_ORG: complytime | ||
| ADMIN_REPO: .github | ||
| CONFIG_PATH: safe-settings | ||
| DEPLOYMENT_CONFIG_FILE: >- | ||
| ${{ inputs.repos != '' && '/tmp/scoped-deployment-settings.yml' | ||
| || format('{0}/safe-settings/deployment-settings.yml', github.workspace) }} | ||
| FULL_SYNC_NOP: ${{ inputs.dry-run }} | ||
| LOG_LEVEL: ${{ inputs.dry-run && 'debug' || 'info' }} | ||
| run: | | ||
| if [ "$FULL_SYNC_NOP" = "true" ]; then | ||
| echo "=== DRY-RUN MODE: Showing what would change ===" | ||
| fi | ||
| npm run full-sync | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| # Maintaining the complytime GitHub Organization | ||
|
|
||
| This document covers operational workflows for managing the complytime | ||
| GitHub organization using two complementary tools: **peribolos** and | ||
| **safe-settings**. | ||
|
|
||
| ## Tool Boundary | ||
|
|
||
| | Area | Tool | Config Location | | ||
| |------|------|----------------| | ||
| | Org membership (admins, members) | peribolos | `peribolos.yaml` | | ||
| | Team creation, membership, privacy | peribolos | `peribolos.yaml` | | ||
| | Team-to-repo permission mappings | peribolos | `peribolos.yaml` | | ||
| | Repo description | peribolos | `peribolos.yaml` | | ||
| | Repo has_projects | peribolos | `peribolos.yaml` | | ||
| | Repo default_branch | peribolos | `peribolos.yaml` | | ||
| | Repo merge strategies | safe-settings | `safe-settings/settings.yml` | | ||
| | Repo auto-merge, delete-branch | safe-settings | `safe-settings/settings.yml` | | ||
| | Repo has_wiki | safe-settings | `safe-settings/settings.yml` | | ||
| | Dependabot alerts and fixes | safe-settings | `safe-settings/settings.yml` | | ||
| | Branch protection rules | safe-settings | `safe-settings/settings.yml` | | ||
| | Rulesets | safe-settings | `safe-settings/settings.yml` | | ||
| | `.github` repo ruleset | **manual** | GitHub UI | | ||
|
|
||
| **Why two tools?** Peribolos manages org-level concerns (who is a member, | ||
| what teams exist, what permissions teams have). Safe-settings manages | ||
| repo-level concerns (how branches are protected, what merge strategies | ||
| are allowed, what security features are enabled). This separation follows | ||
| the principle of least privilege for their respective GitHub App | ||
| permissions. | ||
|
|
||
| **Boundary enforcement:** Go tests in `config/boundary_test.go` validate | ||
| that neither tool manages fields owned by the other. These tests run on | ||
| every PR via CI. | ||
|
|
||
| ## Common Workflows | ||
|
|
||
| ### Add or Remove an Org Member | ||
|
|
||
| 1. Edit `peribolos.yaml` — add/remove the username from the `admins` or | ||
| `members` list (keep sorted alphabetically). | ||
| 2. If adding, add to the appropriate team(s) as well. | ||
| 3. Submit a PR. CI validates the config automatically. | ||
| 4. After merge, peribolos applies the change (push-triggered or daily | ||
| at 05:30 UTC). | ||
|
|
||
| ### Create a New Team or Change Team Membership | ||
|
|
||
| 1. Edit `peribolos.yaml` — add/modify the team under the `teams` section. | ||
| 2. Ensure team members are org members (CI validates this). | ||
| 3. Ensure admins are listed as `maintainers`, not `members` (CI validates). | ||
| 4. Submit a PR and merge. | ||
|
|
||
| ### Add a New Repository to Safe-settings Management | ||
|
|
||
| 1. Add the repo to `peribolos.yaml` with `description`, `has_projects`, | ||
| and `default_branch` (peribolos-owned fields). | ||
| 2. Add the repo to the appropriate suborg file: | ||
| - `safe-settings/suborgs/code-repos.yml` for code repositories | ||
| - `safe-settings/suborgs/non-code-repos.yml` for non-code repositories | ||
| 3. Add the repo to the matching ruleset `repository_name.include` list | ||
| in `safe-settings/settings.yml`. **Both files must be updated** — the | ||
| suborg controls settings inheritance, the ruleset controls branch | ||
| protection. | ||
| 4. Submit a PR. CI boundary tests validate consistency. | ||
| 5. After merge, trigger `workflow_dispatch` on the "Safe Settings Sync" | ||
| workflow to apply. | ||
|
|
||
| ### Change Branch Protection Rules or Rulesets | ||
|
|
||
| 1. Edit `safe-settings/settings.yml` — modify the ruleset under `rulesets`. | ||
| 2. The `safe-settings: code repos` ruleset applies to code repos. | ||
| 3. The `safe-settings: non-code repos` ruleset applies to non-code repos. | ||
| 4. Submit a PR and merge. | ||
| 5. Trigger `workflow_dispatch` to apply. | ||
|
|
||
| ### Add a Repo-Specific Override | ||
|
|
||
| Use repo overrides sparingly. Only create one when a repo needs settings | ||
| that differ from its suborg defaults. | ||
|
|
||
| 1. Create `safe-settings/repos/<repo-name>.yml`. | ||
| 2. Set only the fields that differ from the suborg/org defaults. | ||
| 3. Do NOT set peribolos-owned fields (`description`, `has_projects`, | ||
| `default_branch`). | ||
| 4. Submit a PR. CI boundary tests validate the override. | ||
|
|
||
| See `safe-settings/repos/complyctl.yml` for an example (complyctl requires | ||
| 2 approvers instead of the org default of 1). | ||
|
|
||
| ## Override Validator Policies | ||
|
|
||
| Override validators in `safe-settings/deployment-settings.yml` enforce | ||
| a security floor: | ||
|
|
||
| - **Approver count floor**: Suborg or repo configs cannot lower | ||
| `required_approving_review_count` below the org default. Setting it | ||
| higher is allowed. | ||
| - **No admin collaborators**: The `admin` permission cannot be granted | ||
| to collaborators via safe-settings. Use peribolos team membership | ||
| with admin role instead. | ||
|
|
||
| **Requesting an exception:** If a legitimate use case requires bypassing | ||
| a validator, discuss with org admins. Exceptions require modifying the | ||
| validator script in `deployment-settings.yml` via a reviewed PR. | ||
|
|
||
| ## Local Validation | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| - Go (version in `go.mod`) | ||
| - `yamllint` (for YAML validation) | ||
|
|
||
| ### Commands | ||
|
|
||
| ```bash | ||
| # Validate all YAML (peribolos + safe-settings) | ||
| make lint | ||
|
|
||
| # Run all Go tests (peribolos + boundary) | ||
| make test-unit | ||
|
|
||
| # Validate only safe-settings YAML | ||
| make safe-settings-validate | ||
|
|
||
| # Full validation: format, vet, lint, tests, diff check | ||
| make sanity | ||
| ``` | ||
|
|
||
| ## Applying Safe-settings Changes | ||
|
|
||
| safe-settings reads its config from the `.github` repo's default branch | ||
| via the GitHub API. Config changes must be **merged to main** before | ||
| safe-settings can apply them. | ||
|
|
||
| ### Testing sequence | ||
|
|
||
| 1. **Local validation** (before PR): | ||
| ```bash | ||
| make test-unit # boundary tests | ||
| make safe-settings-validate # YAML syntax | ||
| ``` | ||
|
|
||
| 2. **Submit PR** — CI runs boundary tests and YAML validation. | ||
|
|
||
| 3. **Merge PR** — config lands on main. | ||
|
|
||
| 4. **Dry-run against a single repo** — go to Actions > "Safe Settings | ||
| Sync" > "Run workflow": | ||
| - Set `dry-run` to `true` | ||
| - Set `repos` to a single repo (e.g., `complytime-demos`) | ||
| - Review the workflow output to see what would change | ||
|
|
||
| 5. **Apply to a single repo** — same workflow: | ||
| - Set `dry-run` to `false` | ||
| - Set `repos` to the same repo | ||
| - Verify the changes in the GitHub UI | ||
|
|
||
| 6. **Apply to all repos** — same workflow: | ||
| - Set `dry-run` to `false` | ||
| - Leave `repos` empty (applies to all managed repos) | ||
|
|
||
| ### Rollback | ||
|
|
||
| If safe-settings applies incorrect settings: | ||
| 1. `git revert` the config change and push to main | ||
| 2. Trigger `workflow_dispatch` with `dry-run=false` — safe-settings | ||
| reverts to the previous config state | ||
| 3. Or fix settings manually via the GitHub UI (safe-settings will | ||
| re-apply them on the next sync) | ||
|
|
||
| ## Triggering Manual Sync | ||
|
|
||
| ### Peribolos | ||
|
|
||
| Go to Actions > "Apply Peribolos" > "Run workflow". Set `dry-run` to | ||
| `true` for a preview, or `false` to apply. | ||
|
|
||
| ### Safe-settings | ||
|
|
||
| Go to Actions > "Safe Settings Sync" > "Run workflow": | ||
| - **dry-run**: `true` to preview, `false` to apply (defaults to `true`) | ||
| - **repos**: comma-separated list of repos to target (e.g., | ||
| `complytime-demos,community`). Leave empty to apply to all managed | ||
| repos. | ||
|
|
||
| ### Future automation | ||
|
|
||
| After initial validation, the workflow can be extended with: | ||
| - `push` trigger on `safe-settings/**` path changes to main | ||
| - `schedule` trigger (daily at 06:00 UTC) for drift correction | ||
|
|
||
| These triggers are intentionally disabled during the initial rollout to | ||
| ensure full manual control. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Settings not applied after merge | ||
|
|
||
| 1. Trigger `workflow_dispatch` manually — safe-settings only runs on | ||
| manual dispatch during initial rollout (no push/schedule triggers). | ||
| 2. Check the "Safe Settings Sync" workflow run in the Actions tab. | ||
| 3. Look for errors in the workflow logs (credential expiry, API errors). | ||
|
|
||
| ### Boundary test failures | ||
|
|
||
| Boundary tests fail when: | ||
| - A repo in a suborg file does not exist in `peribolos.yaml` — add it | ||
| to peribolos first. | ||
| - A repo appears in multiple suborg files — each repo belongs to exactly | ||
| one suborg. | ||
| - A safe-settings config sets `description`, `has_projects`, or | ||
| `default_branch` — these are peribolos-owned fields. | ||
| - A suborg repo list does not match the corresponding ruleset | ||
| `repository_name.include` — update both files together. | ||
|
|
||
| ### safe-settings sync errors | ||
|
|
||
| Common causes: | ||
| - **Credential expiry**: The GitHub App private key may need rotation. | ||
| Update the `SAFE_SETTINGS_PRIVATE_KEY` secret. | ||
| - **API rate limits**: The sync may fail if it hits GitHub API rate | ||
| limits. Wait and re-trigger. | ||
| - **Invalid YAML**: The workflow validates YAML before applying. Check | ||
| the yamllint output in the workflow logs. | ||
| - **safe-settings version issue**: If safe-settings behavior changes, | ||
| check the pinned version in the workflow file. | ||
|
|
||
| ## Excluded Repos | ||
|
|
||
| The following repos are excluded from safe-settings management: | ||
|
|
||
| - `.github` — the admin repo (avoids circular dependency). Its | ||
| ruleset ("verify") is managed manually via the GitHub UI. | ||
| - `complyscribe` — archived. | ||
| - `gemara-content-service` — pending archival. | ||
|
|
||
| These are listed in `safe-settings/deployment-settings.yml` under | ||
| `restrictedRepos` and/or excluded from suborg files. | ||
|
|
||
| ## Migration Notes | ||
|
|
||
| Existing repo-level rulesets (created manually via the GitHub UI) coexist | ||
| with the new org-level rulesets managed by safe-settings. GitHub evaluates | ||
| all active rulesets and the most restrictive rule wins. | ||
|
|
||
| After verifying the org-level rulesets work correctly, the old repo-level | ||
| rulesets should be deleted via the GitHub UI. The full list is documented | ||
| in comments at the top of `safe-settings/settings.yml`. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM]
SAFE_SETTINGS_VERSIONis pinned to a mutable tag (2.1.17), not a commit SHA. Tags can be force-pushed in the upstreamgithub/safe-settingsrepo. Theactions/checkoutsteps in this same file correctly pin to commit SHAs — the same pattern should be applied here. The existing TODO comment tracks this; consider resolving it before enabling automated triggers (push/schedule) in a follow-up.