Branch and Tag Protection#
The canonical API repository relies on two layers of protection to ensure that only reviewed, validated changes reach main and only CI creates release tags. APX can configure both automatically via apx init canonical --setup-github, or you can set them up manually through the GitHub UI.
Why Protection Matters#
Without protection rules, any contributor with write access could:
Push directly to
main, bypassing lint and breaking-change checksCreate release tags manually, skipping the validated CI pipeline
Delete or overwrite existing tags, corrupting consumer dependency resolution
APX’s protection model prevents all three:
Threat |
Protection Layer |
|---|---|
Unreviewed code on |
Branch protection with required PR reviews |
Invalid schemas merged |
Required status checks ( |
Bypassing CODEOWNERS |
Required Code Owner reviews |
Manual tag creation |
Tag protection ruleset ( |
Tag deletion/overwrite |
Tag protection ruleset (restrict deletions) |
Branch Protection on main#
What APX Configures#
When you run --setup-github, APX calls the GitHub Branch Protection API (PUT repos/{owner}/{repo}/branches/main/protection) with these settings:
{
"required_status_checks": {
"strict": true,
"contexts": ["validate"]
},
"enforce_admins": false,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"require_code_owner_reviews": true,
"dismiss_stale_reviews": true
},
"restrictions": null
}
Settings Breakdown#
Setting |
Value |
Purpose |
|---|---|---|
Required status checks |
|
The |
Required approving reviews |
1 |
At least one team member must approve the PR. |
Require Code Owner reviews |
|
If the changed path has a CODEOWNERS entry, a member of that team must approve. |
Dismiss stale reviews |
|
Pushing new commits to the PR dismisses previous approvals, requiring re-review. |
Enforce admins |
|
Org admins can bypass in emergencies (e.g., hotfixes). Set to |
Restrictions |
|
No push restrictions beyond the PR requirement — anyone with write access can open a PR. |
The validate Status Check#
The validate job comes from the generated .github/workflows/ci.yml:
name: APX Schema CI
on:
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: infobloxopen/apx@v1
- name: Lint schemas
run: apx lint
- name: Check for breaking changes
run: apx breaking --against origin/main
This ensures every PR is linted and checked for breaking changes before it can be merged.
Idempotent Behavior#
EnsureBranchProtection is idempotent — it checks whether protection already exists before creating it:
Already exists: Reported as
âś“ Already configured: branch protection on mainCreated: Reported as
âś“ Created: branch protection on mainPermission denied (403): Reported as
âš Requires admin: branch protection on main
Tag Protection Ruleset#
Why Rulesets (Not Legacy Tag Protection)#
GitHub offers two tag protection mechanisms:
Legacy tag protection rules — Simple pattern matching, limited control
Repository rulesets — Fine-grained control with bypass actors, enforcement levels, and audit logging
APX uses repository rulesets because they support bypass actors, allowing the GitHub App (via CI) and organization admins to create tags while blocking everyone else.
What APX Configures#
APX creates a ruleset named apx-tag-protection via the GitHub Rulesets API (POST repos/{owner}/{repo}/rulesets):
{
"name": "apx-tag-protection",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~ALL"],
"exclude": []
}
},
"rules": [
{ "type": "creation" },
{ "type": "deletion" }
],
"bypass_actors": [
{
"actor_type": "OrganizationAdmin",
"actor_id": 1,
"bypass_mode": "always"
}
]
}
Settings Breakdown#
Setting |
Value |
Purpose |
|---|---|---|
Target |
|
Applies to git tags, not branches |
Enforcement |
|
Rules are enforced (not in “evaluate” audit-only mode) |
Ref pattern |
|
Protects all tags — not just APX-formatted ones |
Restrict creation |
Enabled |
Prevents direct |
Restrict deletion |
Enabled |
Prevents |
Bypass: OrgAdmin |
Always |
Organization admins can bypass for emergencies |
Tag Patterns#
APX’s tagging convention uses subdirectory-scoped tags:
<format>/<domain>/<api>/<line>/v<semver>
Examples:
proto/payments/ledger/v1/v1.0.0
proto/payments/ledger/v1/v1.2.3
openapi/users/v1/v1.0.0
avro/events/v1/v1.5.0
The ~ALL pattern protects every tag, which is the safest default. If you need to allow non-APX tags (e.g., application release tags), you can edit the ruleset conditions to target specific patterns:
"conditions": {
"ref_name": {
"include": [
"refs/tags/proto/**",
"refs/tags/openapi/**",
"refs/tags/avro/**",
"refs/tags/jsonschema/**",
"refs/tags/parquet/**"
],
"exclude": []
}
}
Idempotent Behavior#
EnsureTagProtection checks for an existing ruleset named apx-tag-protection before creating:
Already exists: Reported as
âś“ Already configured: tag protection rulesetName collision: Detected via
Name must be uniqueerror, reported as skippedCreated: Reported as
âś“ Created: tag protection rulesetPermission denied: Reported as
âš Requires admin: tag protection ruleset: <detail>
GitHub App & Org Secrets#
The protection model depends on a GitHub App providing CI with elevated permissions without sharing personal access tokens.
Required Org Secrets#
Secret |
Purpose |
Visibility |
|---|---|---|
|
GitHub App numeric ID |
All repositories |
|
GitHub App private key (PEM) |
All repositories |
These secrets are consumed by actions/create-github-app-token@v1 in the CI workflows to generate short-lived installation tokens.
App Permissions#
The GitHub App created by --setup-github requests these permissions:
Permission |
Level |
Purpose |
|---|---|---|
|
|
Push tags, commit catalog updates |
|
|
Create release PRs |
|
|
Basic repo metadata access |
Why a GitHub App (Not a PAT)#
Aspect |
Personal Access Token |
GitHub App |
|---|---|---|
Scope |
Tied to a user account |
Org-level installation |
Audit |
Actions appear as the user |
Actions appear as |
Rotation |
Manual |
Automatic (short-lived tokens) |
Revocation |
Affects all repos using that PAT |
Per-installation control |
Offboarding |
Token invalid when user leaves |
Survives personnel changes |
Automated Setup#
Run the full automated setup:
apx init canonical --org=<org> --repo=apis --setup-github
This performs all four steps in sequence:
Creates (or reuses) the GitHub App
Sets
APX_APP_IDandAPX_APP_PRIVATE_KEYas org secretsConfigures branch protection on
mainCreates the
apx-tag-protectiontag ruleset
See Canonical Repository Setup for the complete walkthrough.
Manual Setup#
If you lack org admin access or prefer manual configuration:
Branch Protection (GitHub UI)#
Go to Settings → Branches → Add branch protection rule
Branch name pattern:
mainEnable:
Require a pull request before merging
Required approvals: 1
Require review from Code Owners
Dismiss stale pull request approvals when new commits are pushed
Require status checks to pass before merging
Require branches to be up to date before merging
Status checks: add
validate
Require signed commits (optional, recommended)
Click Create
Tag Ruleset (GitHub UI)#
Go to Settings → Rules → Rulesets → New ruleset → New tag ruleset
Configure:
Ruleset name:
apx-tag-protectionEnforcement status: Active
Bypass list: Add “Organization admin” with “Always” bypass mode
Target tags: All tags
Rules: Enable “Restrict creations” and “Restrict deletions”
Click Create
Org Secrets (GitHub UI or CLI)#
If someone else created the GitHub App, get the App ID and PEM from them and set:
# Via gh CLI (requires admin:org scope)
gh secret set APX_APP_ID --org <org> --visibility all --body "<app-id>"
gh secret set APX_APP_PRIVATE_KEY --org <org> --visibility all < private-key.pem
Or via Organization Settings → Secrets and variables → Actions → New organization secret.
Verifying Protection#
Branch Protection#
# Check branch protection exists
gh api repos/<org>/apis/branches/main/protection \
--jq '{
reviews: .required_pull_request_reviews.required_approving_review_count,
codeowners: .required_pull_request_reviews.require_code_owner_reviews,
status_checks: .required_status_checks.contexts
}'
Expected output:
{
"reviews": 1,
"codeowners": true,
"status_checks": ["validate"]
}
Tag Ruleset#
# List rulesets
gh api repos/<org>/apis/rulesets --jq '.[].name'
# → apx-tag-protection
# Verify tag push is blocked (should fail for non-admin users)
git tag test-protection && git push origin test-protection
# → remote: error: GH013: Repository rule violations found ...
Org Secrets#
gh secret list --org <org> | grep APX_
# → APX_APP_ID Updated 2026-03-08
# → APX_APP_PRIVATE_KEY Updated 2026-03-08
Customization#
Stricter Branch Protection#
To also enforce rules on admins:
gh api repos/<org>/apis/branches/main/protection \
--method PUT --input - <<'EOF'
{
"required_status_checks": { "strict": true, "contexts": ["validate"] },
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 2,
"require_code_owner_reviews": true,
"dismiss_stale_reviews": true
},
"restrictions": null
}
EOF
Adding More Status Checks#
If you add custom CI jobs (e.g., integration tests), add them to the required contexts:
gh api repos/<org>/apis/branches/main/protection \
--method PUT --input - <<'EOF'
{
"required_status_checks": {
"strict": true,
"contexts": ["validate", "integration-tests", "security-scan"]
},
"enforce_admins": false,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"require_code_owner_reviews": true,
"dismiss_stale_reviews": true
},
"restrictions": null
}
EOF
Scoped Tag Protection#
To protect only APX tag patterns while allowing other tags:
Go to Settings → Rules → Rulesets → apx-tag-protection → Edit
Change target from “All tags” to “Include by pattern”
Add patterns:
proto/**,openapi/**,avro/**,jsonschema/**,parquet/**
Troubleshooting#
“403 Forbidden” on Branch Protection#
You need admin access to the repository. Ask an org admin to run:
apx init canonical --org=<org> --repo=apis --setup-github
Or configure manually in the GitHub UI under Settings → Branches.
“admin:org scope” Error#
Your gh token needs the admin:org scope for org-level secrets:
gh auth refresh -h github.com -s admin:org
Tag Push Rejected#
If git push --tags fails with a ruleset violation, the tag protection is working correctly. Tags should only be created by CI via the on-merge.yml workflow. If you need to create a tag manually in an emergency, use an org admin account.
Stale Review Dismissal Confusion#
If approvals keep getting dismissed, it’s because dismiss_stale_reviews is enabled. Any new push to the PR requires re-approval. This is intentional — it prevents approving a PR and then pushing additional unreviewed changes.