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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* @jflowers @jpower432 @marcusburghardt
safe-settings/ @jflowers @jpower432 @marcusburghardt
132 changes: 132 additions & 0 deletions .github/workflows/safe_settings_sync.yml
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
249 changes: 249 additions & 0 deletions MAINTAINING.md
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`.
Loading
Loading