Skip to content

Commit 8547a98

Browse files
authored
Merge pull request #31 from naipi11/codex/release-dependabot-token
Harden release Dependabot alert gate
2 parents a1ea21f + 40cbe1d commit 8547a98

5 files changed

Lines changed: 81 additions & 23 deletions

File tree

.github/CI.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ tag; it uses only the repository `GITHUB_TOKEN`, never a PAT. Release notes
4949
mark the build as unsigned when no Authenticode certificate was supplied.
5050
Winget submission is a separate, manual step outside this workflow.
5151

52+
The Dependabot release gate uses the repository secret
53+
`DEPENDABOT_ALERTS_TOKEN`, not `GITHUB_TOKEN`: GitHub's automatic Actions
54+
token cannot reliably read the Dependabot alerts REST endpoint. Configure it
55+
as a fine-grained token restricted to this repository with **Dependabot
56+
alerts: Read** permission, then rotate it using `gh secret set
57+
DEPENDABOT_ALERTS_TOKEN -R naipi11/codex-barbar`. A missing or unreadable
58+
secret fails the release before artifacts are built; there is no release-time
59+
bypass for that security gate.
60+
5261
### Interaction guard — `.github/workflows/interaction-guard.yml`
5362

5463
Runs on `ubuntu-24.04` for untrusted issue/PR authors. Permissions are

.github/workflows/pr-check.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ jobs:
8080
shell: pwsh
8181
run: .\scripts\assert-v1-boundaries.ps1
8282

83+
- name: Release workflow policy guard
84+
shell: pwsh
85+
run: .\scripts\assert-release-workflow.ps1
86+
8387
- name: Production dependency audit
8488
run: pnpm --dir apps/desktop-tauri audit --prod --audit-level high
8589

.github/workflows/release.yml

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ on:
1212
required: false
1313
default: false
1414
type: boolean
15-
allow_unreadable_dependabot:
16-
description: Allow a manual release when the Dependabot API is unavailable (requires external alert review)
17-
required: false
18-
default: false
19-
type: boolean
2015
push:
2116
tags:
2217
- 'v*'
@@ -104,30 +99,32 @@ jobs:
10499
shell: pwsh
105100
run: .\scripts\audit-licenses.ps1
106101

102+
- name: Release workflow policy guard
103+
shell: pwsh
104+
run: .\scripts\assert-release-workflow.ps1
105+
107106
- name: Dependabot alert gate
108107
shell: pwsh
109108
env:
110-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109+
GH_TOKEN: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }}
111110
run: |
112-
$allowUnreadable = $false
113-
if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') {
114-
$allowUnreadable = [bool]::Parse("${{ inputs.allow_unreadable_dependabot }}")
111+
if ([string]::IsNullOrWhiteSpace($env:GH_TOKEN)) {
112+
throw "DEPENDABOT_ALERTS_TOKEN is not configured. Add a repository secret with Dependabot alerts: Read."
115113
}
116-
$alerts = gh api "/repos/$env:GITHUB_REPOSITORY/dependabot/alerts" --paginate 2>$null
114+
115+
$alertsJson = gh api "/repos/$env:GITHUB_REPOSITORY/dependabot/alerts?state=open&per_page=100" --paginate --slurp 2>$null
117116
if ($LASTEXITCODE -ne 0) {
118-
if (-not $allowUnreadable) {
119-
throw "Dependabot alerts API is not readable. Enable Dependabot alerts / security-events read access before publishing."
120-
}
121-
Write-Warning "Dependabot alerts API is unavailable; manual override was explicitly supplied after external alert review."
122-
$global:LASTEXITCODE = 0
123-
} else {
124-
$critical = @($alerts | ConvertFrom-Json | Where-Object {
125-
$_.state -eq 'open' -and $_.security_advisory.severity -in @('high', 'critical')
126-
})
127-
if ($critical.Count -gt 0) {
128-
$names = $critical | ForEach-Object { $_.dependency.package.ecosystem + ':' + $_.dependency.package.name } | Sort-Object -Unique
129-
throw "Open high/critical Dependabot alerts block release: $($names -join ', ')"
130-
}
117+
throw "Dependabot alerts API is not readable with DEPENDABOT_ALERTS_TOKEN."
118+
}
119+
120+
$pages = @($alertsJson | ConvertFrom-Json)
121+
$alerts = @($pages | ForEach-Object { $_ })
122+
$critical = @($alerts | Where-Object {
123+
$_.state -eq 'open' -and $_.security_advisory.severity -in @('high', 'critical')
124+
})
125+
if ($critical.Count -gt 0) {
126+
$names = $critical | ForEach-Object { $_.dependency.package.ecosystem + ':' + $_.dependency.package.name } | Sort-Object -Unique
127+
throw "Open high/critical Dependabot alerts block release: $($names -join ', ')"
131128
}
132129
133130
- name: Build release artifacts
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Static policy guard for the release workflow.
2+
#
3+
# Dependabot's alerts endpoint is not reliably readable through the automatic
4+
# Actions GITHUB_TOKEN. A dedicated repository secret is mandatory, otherwise
5+
# tag-triggered releases would fail after all build checks had already passed.
6+
7+
Set-StrictMode -Version Latest
8+
$ErrorActionPreference = 'Stop'
9+
10+
$repoRoot = Split-Path -Parent $PSScriptRoot
11+
$workflowPath = Join-Path $repoRoot '.github\workflows\release.yml'
12+
if (-not (Test-Path -LiteralPath $workflowPath)) {
13+
throw "Release workflow is missing: $workflowPath"
14+
}
15+
16+
$workflow = Get-Content -Raw -Encoding utf8 -LiteralPath $workflowPath
17+
$gateMatch = [regex]::Match(
18+
$workflow,
19+
'(?ms)^ - name: Dependabot alert gate\r?\n.*?(?=^ - name:|\z)'
20+
)
21+
if (-not $gateMatch.Success) {
22+
throw 'Release workflow has no Dependabot alert gate.'
23+
}
24+
25+
$gate = $gateMatch.Value
26+
$required = @(
27+
'GH_TOKEN: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }}',
28+
'DEPENDABOT_ALERTS_TOKEN is not configured',
29+
'/repos/$env:GITHUB_REPOSITORY/dependabot/alerts'
30+
)
31+
foreach ($fragment in $required) {
32+
if (-not $gate.Contains($fragment)) {
33+
throw "Dependabot gate must contain: $fragment"
34+
}
35+
}
36+
37+
if ($gate.Contains('GITHUB_TOKEN')) {
38+
throw 'Dependabot gate must not use the automatic GITHUB_TOKEN.'
39+
}
40+
if ($workflow.Contains('allow_unreadable_dependabot')) {
41+
throw 'Release workflow must not allow an unreadable Dependabot bypass.'
42+
}
43+
if ($gate.Contains('manual override')) {
44+
throw 'Dependabot gate must not contain a manual override path.'
45+
}
46+
47+
Write-Host '[assert-release-workflow] OK - Dependabot release credential policy is enforced'

scripts/local-check.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ if (-not ($Rust -or $Tauri -or $Frontend -or $Format -or $Clippy -or $ReleaseDoc
5656
Push-Location $RepoRoot
5757
try {
5858
Invoke-Step "V1 boundary guard" "powershell.exe" @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts\assert-v1-boundaries.ps1")
59+
Invoke-Step "Release workflow policy guard" "powershell.exe" @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts\assert-release-workflow.ps1")
5960
if ($All -or $Format) {
6061
Invoke-Step "Rust format" "cargo" @("fmt", "--all", "--check")
6162
}

0 commit comments

Comments
 (0)