This document describes the complete release process for this repository.
It is designed to be reusable — copy it (along with the scripts in scripts/) to any other repository and substitute the org/repo names.
- Overview
- Developer Workflow
- Quick Setup (DSC Scripts)
- Manual Setup Guide
- Token Reference by Scope
- DSC Script Reference
- Adapting to Another Repository
- Troubleshooting
Releases are fully automated and gated. No human ever pushes a version tag or manually creates a GitHub release. The flow is:
Feature branch
→ just changeset (describe your change)
→ PR to main (changeset-check + ci-gate must pass)
→ merge (release bot creates Version PR)
→ Version PR (ci-gate must pass)
→ merge (CD pipeline publishes the release)
| Risk | Mitigation |
|---|---|
| Tag pushed manually before CD runs | Tag ruleset: only github-actions[bot] or your GitHub App can create v* tags |
| Release created without build artifacts | Draft-first: NuGet assets attached before --draft=false |
| Published release tampered with | Immutable releases: published releases are locked |
| Mid-release failure leaves broken state | ERR trap: deletes draft + tag on any error → clean retry |
| Double-release on re-run | CD checks tag existence first; skips if already released |
| PR merged without a changeset | changeset-check required status check blocks merging |
| Version PR bypasses CI | CHANGESET_TOKEN / GitHub App ensures CI triggers on bot-created PR |
| Component | File | Purpose |
|---|---|---|
| Changeset bot | .github/workflows/release-pr.yml |
Creates/updates Version PR on push to main |
| CD pipeline | .github/workflows/cd.yml |
Triggers on package.json change; builds, tests, packs, releases |
| Changeset check | .github/workflows/ci.yml (changeset-check job) |
Blocks PRs without a changeset file |
| CI gate | .github/workflows/ci.yml (ci-gate job) |
Single required status check |
| Changeset config | .changeset/config.json |
Tells the bot which repo and changelog format to use |
| Version script | package.json version-packages |
Runs changeset version then update-version.ts |
git checkout -b feat/my-change
# ... make changes ...just changesetThis opens an interactive prompt. Select the bump type:
| Type | When to use |
|---|---|
patch |
Bug fixes, internal refactors, dependency bumps |
minor |
New features, new options, backwards-compatible additions |
major |
Breaking changes — any change that requires consumer code changes |
A .changeset/<random-name>.md file is created. Commit it with your changes.
git add .
git commit -m "feat: my change"
git push origin feat/my-change
# Open a PR to mainThe Changeset Check status check will verify a .changeset/*.md file is present.
If your PR needs no release note (docs-only, CI-only), add the skip-changeset label.
Once CI Gate and Changeset Check are green, merge the PR.
The release-pr.yml workflow creates (or updates) a Version Packages PR:
- Branch:
changeset-release/main - Bumps
package.jsonversion, updatesCHANGELOG.md, deletes consumed.changeset/*.mdfiles, syncs docs/wiki
Review, ensure CI passes, then merge it.
Merging the Version PR triggers cd.yml, which:
- Reads the new version from
package.json - Skips if the release tag already exists (idempotent)
- Runs format check, build, tests (main + sample solution)
- Packs NuGet artifacts
- Creates a draft GitHub Release with assets attached
- Publishes the draft → becomes immutable
- On any failure: ERR trap deletes the draft + tag → clean retry
just changeset-statusTwo idempotent scripts automate the repository configuration. Safe to re-run.
# Personal repo, fine-grained PAT
./scripts/setup-release.sh \
--owner myuser --repo myrepo \
--scope personal --token-type fine-grained
# Organisation repo, GitHub App (fully automated)
./scripts/setup-release.sh \
--owner myorg --repo myrepo \
--scope org --token-type app \
--app-id 123456 --app-pem ./my-app.private-key.pem
# GitHub Enterprise Server, classic PAT
./scripts/setup-release.sh \
--owner myorg --repo myrepo \
--scope ghes --token-type classic \
--ghes-host github.example.com
# Dry run — see what would happen without making changes
./scripts/setup-release.sh --owner myorg --repo myrepo --dry-run# Personal repo, fine-grained PAT
.\scripts\setup-release.ps1 `
-Owner myuser -Repo myrepo `
-Scope personal -TokenType fine-grained
# Organisation repo, GitHub App (fully automated)
.\scripts\setup-release.ps1 `
-Owner myorg -Repo myrepo `
-Scope org -TokenType app `
-AppId 123456 -AppPem .\my-app.private-key.pem
# GitHub Enterprise Server, classic PAT
.\scripts\setup-release.ps1 `
-Owner myorg -Repo myrepo `
-Scope ghes -TokenType classic `
-GhesHost github.example.com
# Dry run
.\scripts\setup-release.ps1 -Owner myorg -Repo myrepo -DryRun| Step | Automatable | Notes |
|---|---|---|
Branch ruleset (main) |
✅ Fully automated | Uses gh api |
Tag ruleset (v*) |
✅ With GitHub App | App installation ID used as bypass actor |
Tag ruleset (v*) |
⚠ UI step with PAT | github-actions[bot] bypass requires UI |
skip-changeset label |
✅ Fully automated | Used by changeset-check job |
CHANGESET_TOKEN secret |
Guided | Script prints exact URL + instructions |
APP_ID / APP_PRIVATE_KEY secrets |
✅ If --app-id and --app-pem provided |
|
| Immutable releases | ⚠ UI step | No API endpoint exists |
The Version PR bot uses GITHUB_TOKEN by default. But GitHub's anti-loop protection means GITHUB_TOKEN-created PRs do not trigger CI — so CI Gate never passes and the Version PR can never merge.
You must configure one of the following three options:
GitHub Apps issue short-lived tokens per workflow run. The private key never expires; no rotation needed.
When to use:
- Organisation or enterprise repos
- Any environment where token expiry management is undesirable
- When you also want the tag ruleset bypass to be automatable
Setup:
-
Create the app
Scope URL Personal https://github.com/settings/apps/new Organisation https://github.com/organizations/{org}/settings/apps/newGHEC Same as organisation on github.com GHES https://{hostname}/settings/apps/new -
Fill in fields:
- App name:
{org}-release-bot(or any unique name) - Homepage URL: your repository URL
- Webhook: uncheck "Active"
- App name:
-
Set permissions:
Permission Level Why Contents Read & write Push version-bump commits, CHANGELOG Pull requests Read & write Create and update Version PR Workflows Read & write Update workflow files if changeset touches .github/Metadata Read Always required (auto-set) -
Click Create GitHub App
-
Download private key:
- On the app settings page → scroll to Private keys → Generate a private key
- Save the downloaded
.pemfile securely
-
Note the App ID shown on the settings page
-
Install the app on the repository:
- App settings → Install App → select your org/account → Only select repositories → pick the repo
-
Set secrets:
gh secret set APP_ID --repo {owner}/{repo} --body "123456" gh secret set APP_PRIVATE_KEY --repo {owner}/{repo} < /path/to/app.private-key.pem
PowerShell:
gh secret set APP_ID --repo {owner}/{repo} --body "123456" Get-Content .\app.private-key.pem -Raw | gh secret set APP_PRIVATE_KEY --repo {owner}/{repo}
-
Get installation ID (needed for the tag ruleset bypass — see Step 3):
# Via gh CLI (once the app is installed on the repo): gh api repos/{owner}/{repo}/installation --jq '.id' # Alternatively: App settings → Installations → click the installed org/account # The installation ID appears in the URL: /installations/{id}
The release-pr.yml workflow auto-detects APP_ID and uses the App token when present.
Scoped to a specific repository with exact permissions. Maximum expiry: 1 year. Set a calendar reminder to rotate before expiry.
When to use:
- Personal repos or small teams
- GHES 3.10+
- When a GitHub App is not practical
Required permissions:
| Permission | Level | Why |
|---|---|---|
| Contents | Read & write | Push version-bump commits |
| Pull requests | Read & write | Create/update Version PR |
| Workflows | Read & write | Update .github/workflows/ if touched |
| Metadata | Read | Auto-required |
Setup by scope:
Personal account
- Navigate to: https://github.com/settings/personal-access-tokens/new
- Resource owner: your personal account
- Repository access: Only select repositories → pick the repo
- Set permissions (table above) and an expiry
- Copy the token and set the secret:
gh secret set CHANGESET_TOKEN --repo {owner}/{repo} # paste when prompted
Organisation-owned repository
-
Ensure the org allows fine-grained PATs:
- Navigate to:
https://github.com/organizations/{org}/settings/personal-access-tokens - Set policy to Allow fine-grained personal access tokens
- If approval is required, submit a request and wait for admin approval
- Navigate to:
-
Navigate to: https://github.com/settings/personal-access-tokens/new
-
Resource owner: select the organisation (not your personal account)
-
Repository access: Only select repositories → pick the repo
-
Set permissions (table above) and an expiry
-
SAML SSO: Fine-grained PATs do not require a separate SSO authorization step
-
Copy the token:
gh secret set CHANGESET_TOKEN --repo {owner}/{repo}
GitHub Enterprise Cloud (GHEC)
Same as organisation above. Additionally:
- Enterprise admins can enforce a fine-grained PAT policy at the enterprise level
- Check with your enterprise admin if you get "policy does not allow" errors
- Enterprise-managed users (EMU): token creation may be restricted to the IdP — follow your enterprise's provisioning process
GitHub Enterprise Server (GHES 3.10+)
-
Ensure the org allows fine-grained PATs:
- Navigate to:
https://{hostname}/organizations/{org}/settings/personal-access-tokens
- Navigate to:
-
Navigate to:
https://{hostname}/settings/personal-access-tokens/new -
Set permissions (table above) and an expiry
-
Copy the token:
GH_HOST={hostname} gh secret set CHANGESET_TOKEN --repo {owner}/{repo}
⚠ GHES < 3.10: Fine-grained PATs are not available. Use Option C.
Grants broad repo scope across all repositories the token owner has access to. Use only when Options A and B are unavailable.
When to use:
- GHES < 3.10
- Organisation policy explicitly blocks fine-grained PATs and GitHub Apps
Setup by scope:
Personal account
- Navigate to: https://github.com/settings/tokens/new
- Scope:
repo - Set an expiry (recommended, even though it's optional)
- Copy the token:
gh secret set CHANGESET_TOKEN --repo {owner}/{repo}
Organisation-owned repository
- Navigate to: https://github.com/settings/tokens/new
- Scope:
repo - SAML SSO: After creating the token, click "Authorize" next to the organisation name
- Copy the token:
gh secret set CHANGESET_TOKEN --repo {owner}/{repo}
GitHub Enterprise Cloud (GHEC)
Same as organisation. SAML SSO authorization is required. If the enterprise uses IP allow lists, ensure the CI runner IPs are allowed.
GitHub Enterprise Server (GHES any version)
- Navigate to:
https://{hostname}/settings/tokens/new - Scope:
repo - Copy the token:
GH_HOST={hostname} gh secret set CHANGESET_TOKEN --repo {owner}/{repo}
| Scenario | Recommended option | Token expiry | Tag ruleset bypass automatable |
|---|---|---|---|
| Organisation or enterprise | A — GitHub App | Never (private key) | ✅ Yes |
| Personal repo, small team | B — Fine-grained PAT | Max 1 year | ⚠ UI step |
| GHES < 3.10 | C — Classic PAT | Optional | ⚠ UI step |
| Org blocks fine-grained PATs | A or C | — | ✅ A / ⚠ C |
Requires a PR and CI Gate to pass before any commit reaches main.
Automated (already applied to this repo):
gh api repos/{owner}/{repo}/rulesets \
--method POST \
--header "Content-Type: application/json" \
--input - <<'EOF'
{
"name": "Protect main branch",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": { "include": ["refs/heads/main"], "exclude": [] }
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [{ "context": "CI Gate" }]
}
}
]
}
EOFPowerShell equivalent:
$body = @{
name = "Protect main branch"; target = "branch"; enforcement = "active"
conditions = @{ ref_name = @{ include = @("refs/heads/main"); exclude = @() } }
rules = @(
@{ type = "deletion" }
@{ type = "non_fast_forward" }
@{ type = "pull_request"
parameters = @{ required_approving_review_count = 0 } }
@{ type = "required_status_checks"
parameters = @{ strict_required_status_checks_policy = $false
required_status_checks = @(@{ context = "CI Gate" }) } }
)
} | ConvertTo-Json -Depth 10
$body | gh api repos/{owner}/{repo}/rulesets --method POST --header "Content-Type: application/json" --input -Via UI: Settings → Rules → Rulesets → New ruleset → New branch ruleset
| Setting | Value |
|---|---|
| Ruleset name | Protect main branch |
| Target branches | refs/heads/main |
| Rules | ✅ Restrict deletions |
| ✅ Block force pushes | |
| ✅ Require a pull request before merging (0 approvals) | |
✅ Require status checks → add CI Gate |
For GHES: Rulesets require GHES 3.11+. On earlier versions use branch protection rules:
GH_HOST={hostname} gh api repos/{owner}/{repo}/branches/main/protection \ --method PUT \ --input - <<'EOF' { "required_status_checks": { "strict": false, "contexts": ["CI Gate"] }, "enforce_admins": false, "required_pull_request_reviews": null, "restrictions": null } EOF
Prevents anyone from pushing v* tags manually. Tags can only be created by the CD pipeline.
This is the most important security guard in the release flow.
# 1. Get the app's installation ID on the repo:
INSTALLATION_ID=$(gh api repos/{owner}/{repo}/installation --jq '.id')
# 2. Create the tag ruleset with the App as bypass actor:
gh api repos/{owner}/{repo}/rulesets \
--method POST \
--header "Content-Type: application/json" \
--input - <<EOF
{
"name": "Protect release tags",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": { "include": ["refs/tags/v*"], "exclude": [] }
},
"rules": [{ "type": "creation" }],
"bypass_actors": [{
"actor_id": ${INSTALLATION_ID},
"actor_type": "Integration",
"bypass_mode": "always"
}]
}
EOFPowerShell:
$installationId = (gh api repos/{owner}/{repo}/installation | ConvertFrom-Json).id
$body = @{
name = "Protect release tags"; target = "tag"; enforcement = "active"
conditions = @{ ref_name = @{ include = @("refs/tags/v*"); exclude = @() } }
rules = @( @{ type = "creation" } )
bypass_actors = @( @{ actor_id = $installationId; actor_type = "Integration"; bypass_mode = "always" } )
} | ConvertTo-Json -Depth 10
$body | gh api repos/{owner}/{repo}/rulesets --method POST --header "Content-Type: application/json" --input -The github-actions[bot] bypass actor cannot be specified programmatically without admin:org OAuth scope (which is unavailable with fine-grained or classic PATs). This step must be done via the GitHub UI:
- Navigate to:
https://github.com/{owner}/{repo}/settings/rules/new?target=tag - Ruleset name:
Protect release tags - Target tags:
refs/tags/v* - Rules: ✅ Restrict creations
- Bypass actors: Add → search "GitHub Actions" → select → Save
For GHES: Rulesets require GHES 3.11+. On earlier versions, the tag ruleset cannot be enforced via rules. Enforce via policy instead: document that tags must only be created via the CD pipeline.
Immutable releases prevent any modification to a published release — assets cannot be replaced or deleted, and the release body cannot be changed.
Why it's safe with this release flow:
gh release create --draftcreates a draft — drafts are never immutable- Assets are uploaded to the draft (fully mutable at this point)
gh release edit --draft=falsepublishes and locks the release- The
trap cleanup ERRfires on any error before the--draft=falseline - Therefore: failures always clean up a mutable draft; a locked release is never left broken
Enable via UI:
Settings → General → Releases → ✅ Immutable releases
There is no REST API endpoint for this setting.
- Create a feature branch, run
just changeset, commit and open a PR - Verify
Changeset CheckandCI Gateappear as required status checks on the PR - Merge the PR; verify a
changeset-release/mainPR appears within minutes - Verify CI runs on the Version PR (
CI Gatepasses) - Merge the Version PR; verify the CD pipeline runs and publishes a GitHub Release with NuGet assets attached
- Verify the release is marked as immutable (lock icon on the release page)
- Attempt to push a
v*tag manually — verify it is rejected by the tag ruleset
Quick reference covering all combinations of scope and token type.
| Scope | Token type | Where to create | Org admin action needed? | SAML SSO step? | Notes |
|---|---|---|---|---|---|
| Personal | GitHub App | https://github.com/settings/apps/new | No | No | Preferred; no expiry |
| Personal | Fine-grained PAT | https://github.com/settings/personal-access-tokens/new | No | No | Max 1 year; set reminder |
| Personal | Classic PAT | https://github.com/settings/tokens/new | No | No | Broad scope; avoid if possible |
| Org | GitHub App | https://github.com/organizations/{org}/settings/apps/new | To install on org | No | Best for teams |
| Org | Fine-grained PAT | https://github.com/settings/personal-access-tokens/new | Yes — allow fine-grained PATs | No | Org approval may be required |
| Org | Classic PAT | https://github.com/settings/tokens/new | No (but SSO authz needed) | Yes — authorize for org | Broad scope; fallback only |
| GHEC | GitHub App | https://github.com/organizations/{org}/settings/apps/new |
Yes — enterprise may restrict | No | EMU: check enterprise policy |
| GHEC | Fine-grained PAT | https://github.com/settings/personal-access-tokens/new | Yes — org and enterprise must allow | No | Enterprise policy may block |
| GHEC | Classic PAT | https://github.com/settings/tokens/new | No (SSO authz needed) | Yes | Broad scope |
| GHES 3.10+ | Fine-grained PAT | https://{hostname}/settings/personal-access-tokens/new |
Yes — org must allow | Depends on GHES config | GHES 3.10+ only |
| GHES any | Classic PAT | https://{hostname}/settings/tokens/new |
No | Depends on GHES config | Works on all GHES versions |
| GHES any | GitHub App | https://{hostname}/settings/apps/new |
To install | No | Fully supported on all GHES |
The release-pr.yml workflow auto-detects which authentication method to use:
# Prefers GitHub App when APP_ID is present; falls back to CHANGESET_TOKEN
- uses: actions/create-github-app-token@v1
id: app-token
if: ${{ secrets.APP_ID != '' }}
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Token used for checkout and changeset action:
token: ${{ steps.app-token.outputs.token || secrets.CHANGESET_TOKEN }}| Secret name | Used when | Option |
|---|---|---|
APP_ID + APP_PRIVATE_KEY |
GitHub App token is preferred | A |
CHANGESET_TOKEN |
Fallback when APP_ID is not set |
B or C |
Both scripts (setup-release.sh and setup-release.ps1) are idempotent DSC-style configuration scripts. Safe to re-run at any time.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--owner / -Owner |
string | ✅ | — | GitHub user or org name |
--repo / -Repo |
string | ✅ | — | Repository name |
--scope / -Scope |
enum | — | personal |
personal · org · enterprise · ghes |
--token-type / -TokenType |
enum | — | app |
app · fine-grained · classic |
--ghes-host / -GhesHost |
string | When scope=ghes | — | GHES hostname, e.g. github.example.com |
--app-id / -AppId |
string | — | — | GitHub App ID (used to set secret and get installation) |
--app-pem / -AppPem |
path | — | — | Path to .pem private key (used to set secret) |
--dry-run / -DryRun |
flag | — | false | Print actions without applying |
| Step | Action | Idempotency check |
|---|---|---|
| 1. Branch ruleset | POST /repos/{r}/rulesets |
Checks for existing ruleset by name |
| 2. Tag ruleset | POST /repos/{r}/rulesets (with bypass if App) |
Checks for existing ruleset by name |
3. skip-changeset label |
POST /repos/{r}/labels |
Checks for existing label by name |
| 4. Secrets | gh secret set |
Checks GET /repos/{r}/actions/secrets/{name} |
| 5. Immutable releases | Prints UI instructions | Always prints (no API check possible) |
# personal × app
./scripts/setup-release.sh --owner myuser --repo myrepo \
--scope personal --token-type app \
--app-id 123456 --app-pem ./app.pem
# personal × fine-grained
./scripts/setup-release.sh --owner myuser --repo myrepo \
--scope personal --token-type fine-grained
# personal × classic
./scripts/setup-release.sh --owner myuser --repo myrepo \
--scope personal --token-type classic
# org × app
./scripts/setup-release.sh --owner myorg --repo myrepo \
--scope org --token-type app \
--app-id 123456 --app-pem ./app.pem
# org × fine-grained
./scripts/setup-release.sh --owner myorg --repo myrepo \
--scope org --token-type fine-grained
# enterprise (GHEC) × app
./scripts/setup-release.sh --owner myenterprise --repo myrepo \
--scope enterprise --token-type app \
--app-id 123456 --app-pem ./app.pem
# ghes × classic (GHES < 3.10)
./scripts/setup-release.sh --owner myorg --repo myrepo \
--scope ghes --token-type classic \
--ghes-host github.example.com
# ghes × fine-grained (GHES 3.10+)
./scripts/setup-release.sh --owner myorg --repo myrepo \
--scope ghes --token-type fine-grained \
--ghes-host github.example.com
# dry run — inspect without changing anything
./scripts/setup-release.sh --owner myorg --repo myrepo \
--scope org --token-type app --dry-run-
Copy files:
.changeset/config.json → update "repo" to "{owner}/{repo}" .changeset/README.md .github/workflows/release-pr.yml .github/workflows/cd.yml → update MAIN_SOLUTION, SAMPLE_SOLUTION, PACKAGE_PROJECT scripts/setup-release.sh scripts/setup-release.ps1Plus add the
changeset-checkjob andci-gateupdates to your existing CI workflow. -
Install dependencies:
bun add -D @changesets/cli @changesets/changelog-github
-
Add scripts to
package.json:{ "scripts": { "changeset": "changeset", "version-packages": "changeset version" } }If you have a version-sync script (like
update-version.ts), append&& bun .build/update-version.ts. -
Add Justfile recipes:
changeset: bun changeset changeset-status: bun changeset status
-
Configure the repository (run the DSC script):
./scripts/setup-release.sh --owner {owner} --repo {repo} \ --scope {personal|org|enterprise|ghes} --token-type {app|fine-grained|classic} -
Add secrets (see Step 1 above for your chosen token type)
-
Complete manual steps:
- Tag ruleset bypass (if using PAT): GitHub UI → add "GitHub Actions" bypass actor
- Immutable releases: Settings → General → ✅ Immutable releases
-
Adapt
cd.ymlfor your project type. Key variables:env: MAIN_SOLUTION: ./src/MyProject.slnx SAMPLE_SOLUTION: ./samples/SampleApp/SampleApp.slnx # remove if no samples PACKAGE_PROJECT: ./src/MyProject/MyProject.csproj ARTIFACTS_DIR: ./artifacts/nuget
For non-.NET projects, replace the build/test/pack steps with your equivalent commands.
The CHANGESET_TOKEN secret is missing, expired, or has insufficient permissions. The bot falls back to GITHUB_TOKEN which cannot trigger workflow runs.
Diagnose: Check the release-pr.yml run logs for "Generate GitHub App token" step — if it was skipped (APP_ID not set) and CHANGESET_TOKEN is empty, the PR was created with GITHUB_TOKEN.
Fix:
- Set or rotate the
CHANGESET_TOKENsecret (orAPP_ID+APP_PRIVATE_KEY) - Close and reopen the Version PR, or trigger the workflow manually:
gh workflow run release-pr.yml --repo {owner}/{repo}
The release tag already exists. This is intentional idempotency.
# Check what's there:
gh release view v{version} --repo {owner}/{repo}
# If the existing release is broken (no assets), delete it:
gh release delete v{version} --cleanup-tag --yes --repo {owner}/{repo}
# Re-trigger the CD pipeline:
gh workflow run cd.yml --repo {owner}/{repo}The ERR trap should have cleaned up automatically. Verify:
# Check no draft release was left behind:
gh release list --repo {owner}/{repo}
# Check no orphaned tag was left:
git ls-remote --tags https://github.com/{owner}/{repo} 'refs/tags/v*'If a draft or tag was left, clean up manually:
gh release delete v{version} --cleanup-tag --yes --repo {owner}/{repo}Then re-trigger:
gh workflow run cd.yml --repo {owner}/{repo}remote: error: GH013: Repository rule violations found for refs/tags/v4.3.0
This is the tag ruleset working correctly. Tags must be created via the CD pipeline (by merging a Version PR). Do not push tags manually.
Error: No changeset file found.
Run just changeset on your feature branch. If the PR is genuinely docs-only or CI-only, add the skip-changeset label.
The Version PR will be created with GITHUB_TOKEN (no CI runs) or the bot will fail entirely.
Fix: Rotate the token, update the secret, then re-trigger release-pr.yml:
gh workflow run release-pr.yml --repo {owner}/{repo}The org requires admin approval for fine-grained PATs. You'll see an error in the workflow logs mentioning "approval" or "policy".
Fix: Contact an org admin to approve the PAT at:
https://github.com/organizations/{org}/settings/personal-access-tokens
Or switch to Option A (GitHub App), which doesn't require per-token approval.
Ensure GH_HOST is set:
export GH_HOST=github.example.com
gh api repos/{owner}/{repo}/rulesetsOr pass it inline:
GH_HOST=github.example.com gh api repos/{owner}/{repo}/rulesets