Skip to content

Commit 459718e

Browse files
fix(scripts): use App ID (not installation ID) for tag ruleset bypass actor
The GitHub Rulesets API expects the App's numeric ID as �ctor_id for Integration bypass actors, not the installation ID. The installation ID endpoint also requires App JWT auth rather than a user token. Removes the installation ID lookup step entirely — APP_ID (available as a script parameter) is passed directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 912e942 commit 459718e

2 files changed

Lines changed: 55 additions & 77 deletions

File tree

scripts/setup-release.ps1

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -236,59 +236,51 @@ if (Test-RulesetExists $TagRulesetName) {
236236
} else {
237237
Write-Info "Configuring tag ruleset for v* ..."
238238

239-
if ($TokenType -eq 'app' -and $AppId -and $AppPem) {
240-
Write-Info "GitHub App detected — fetching installation ID ..."
239+
if ($TokenType -eq 'app' -and $AppId) {
240+
# Use App ID directly as the Integration bypass actor.
241+
# The actor_id for an Integration (GitHub App) bypass is the App's numeric ID,
242+
# NOT the installation ID. The App must be installed on the repo/org first.
243+
Write-Info "GitHub App detected — creating tag ruleset with App ID $AppId as bypass actor ..."
244+
245+
$TagRulesetBody = @{
246+
name = $TagRulesetName
247+
target = 'tag'
248+
enforcement = 'active'
249+
conditions = @{
250+
ref_name = @{ include = @('refs/tags/v*'); exclude = @() }
251+
}
252+
rules = @(
253+
@{ type = 'creation' }
254+
@{ type = 'deletion' }
255+
@{ type = 'non_fast_forward' }
256+
@{ type = 'update' }
257+
)
258+
bypass_actors = @(
259+
@{
260+
actor_id = [int]$AppId
261+
actor_type = 'Integration'
262+
bypass_mode = 'always'
263+
}
264+
)
265+
} | ConvertTo-Json -Depth 10
241266

242-
$InstallationId = $null
267+
$created = $false
243268
if (-not $DryRun) {
244-
$installation = Invoke-GhApi "repos/$FullRepo/installation" -Silent
245-
$InstallationId = $installation?.id
269+
try {
270+
Invoke-GhApi "repos/$FullRepo/rulesets" -Method POST -Body $TagRulesetBody | Out-Null
271+
$created = $true
272+
} catch {
273+
Write-Warn "Tag ruleset API creation failed — ensure the App is installed on this repo/org."
274+
}
246275
} else {
247-
$InstallationId = '<INSTALLATION_ID>'
276+
Write-Dry "POST repos/$FullRepo/rulesets (tag ruleset, App bypass actor_id=$AppId)"
277+
$created = $true
248278
}
249279

250-
if (-not $InstallationId) {
251-
Write-Warn "Could not retrieve installation ID for App $AppId."
252-
Write-Warn "Ensure the app is installed on ${FullRepo} before re-running."
253-
Show-TagRulesetUiInstructions
280+
if ($created) {
281+
Write-OK "Tag ruleset created with GitHub App bypass (App ID $AppId)."
254282
} else {
255-
Write-Info "Installation ID: $InstallationId — creating tag ruleset with App bypass ..."
256-
257-
$TagRulesetBody = @{
258-
name = $TagRulesetName
259-
target = 'tag'
260-
enforcement = 'active'
261-
conditions = @{
262-
ref_name = @{ include = @('refs/tags/v*'); exclude = @() }
263-
}
264-
rules = @( @{ type = 'creation' } )
265-
bypass_actors = @(
266-
@{
267-
actor_id = $InstallationId
268-
actor_type = 'Integration'
269-
bypass_mode = 'always'
270-
}
271-
)
272-
} | ConvertTo-Json -Depth 10
273-
274-
$created = $false
275-
if (-not $DryRun) {
276-
try {
277-
Invoke-GhApi "repos/$FullRepo/rulesets" -Method POST -Body $TagRulesetBody | Out-Null
278-
$created = $true
279-
} catch {
280-
Write-Warn "Tag ruleset API creation failed (may need admin:org scope)."
281-
}
282-
} else {
283-
Write-Dry "POST repos/$FullRepo/rulesets (tag ruleset, App bypass actor_id=$InstallationId)"
284-
$created = $true
285-
}
286-
287-
if ($created) {
288-
Write-OK "Tag ruleset created with GitHub App bypass (installation $InstallationId)."
289-
} else {
290-
Show-TagRulesetUiInstructions
291-
}
283+
Show-TagRulesetUiInstructions
292284
}
293285
} else {
294286
Write-Warn "Tag ruleset bypass for github-actions[bot] requires the GitHub UI."

scripts/setup-release.sh

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -200,53 +200,39 @@ if ruleset_exists "${TAG_RULESET_NAME}"; then
200200
else
201201
info "Configuring tag ruleset for v* ..."
202202

203-
if [[ "${TOKEN_TYPE}" == "app" && -n "${APP_ID}" && -n "${APP_PEM}" ]]; then
204-
# ── Option A: GitHub App — get installation ID and use it as bypass actor ──
205-
info "GitHub App detected — fetching installation ID..."
203+
if [[ "${TOKEN_TYPE}" == "app" && -n "${APP_ID}" ]]; then
204+
# ── Option A: GitHub App — use App ID directly as the Integration bypass actor ──
205+
# The bypass actor actor_id for an Integration (GitHub App) is the App's numeric ID,
206+
# NOT the installation ID. The App must be installed on the repo (or org) first.
207+
info "GitHub App detected — creating tag ruleset with App ID ${APP_ID} as bypass actor..."
206208

207-
if [[ "${DRY_RUN}" == true ]]; then
208-
dry "gh api repos/${FULL_REPO}/installation --jq '.id'"
209-
INSTALLATION_ID="<INSTALLATION_ID>"
210-
else
211-
INSTALLATION_ID=$(gh api "repos/${FULL_REPO}/installation" --jq '.id' 2>/dev/null || true)
212-
fi
213-
214-
if [[ -z "${INSTALLATION_ID}" || "${INSTALLATION_ID}" == "null" ]]; then
215-
warning "Could not determine installation ID for GitHub App ${APP_ID}."
216-
warning "Ensure the app is installed on the repository first."
217-
warning "Falling back to UI instructions for tag ruleset."
218-
_tag_ruleset_ui_instructions
219-
else
220-
info "Installation ID: ${INSTALLATION_ID} — creating tag ruleset with App bypass..."
221-
222-
TAG_RULESET_JSON=$(cat <<ENDJSON
209+
TAG_RULESET_JSON=$(cat <<ENDJSON
223210
{
224211
"name": "Protect release tags",
225212
"target": "tag",
226213
"enforcement": "active",
227214
"conditions": {
228215
"ref_name": { "include": ["refs/tags/v*"], "exclude": [] }
229216
},
230-
"rules": [{ "type": "creation" }],
217+
"rules": [{ "type": "creation" }, { "type": "deletion" }, { "type": "non_fast_forward" }, { "type": "update" }],
231218
"bypass_actors": [{
232-
"actor_id": ${INSTALLATION_ID},
219+
"actor_id": ${APP_ID},
233220
"actor_type": "Integration",
234221
"bypass_mode": "always"
235222
}]
236223
}
237224
ENDJSON
238225
)
239226

240-
if [[ "${DRY_RUN}" == true ]]; then
241-
dry "POST repos/${FULL_REPO}/rulesets (tag ruleset with App bypass, actor_id=${INSTALLATION_ID})"
227+
if [[ "${DRY_RUN}" == true ]]; then
228+
dry "POST repos/${FULL_REPO}/rulesets (tag ruleset with App bypass, actor_id=${APP_ID})"
229+
else
230+
if echo "${TAG_RULESET_JSON}" | gh api "repos/${FULL_REPO}/rulesets" \
231+
--method POST --header "Content-Type: application/json" --input - > /dev/null 2>&1; then
232+
success "Tag ruleset created with GitHub App bypass (App ID ${APP_ID})."
242233
else
243-
if echo "${TAG_RULESET_JSON}" | gh api "repos/${FULL_REPO}/rulesets" \
244-
--method POST --header "Content-Type: application/json" --input - > /dev/null 2>&1; then
245-
success "Tag ruleset created with GitHub App bypass (installation ${INSTALLATION_ID})."
246-
else
247-
warning "Tag ruleset creation via API failed — admin:org scope may be required."
248-
_tag_ruleset_ui_instructions
249-
fi
234+
warning "Tag ruleset creation via API failed — ensure the App is installed on this repo/org."
235+
_tag_ruleset_ui_instructions
250236
fi
251237
fi
252238

0 commit comments

Comments
 (0)