Skip to content

Commit 0d0d99a

Browse files
vaindclaude
andauthored
feat(updater): Add SSH key support and comprehensive authentication validation (#134)
* fix(updater): Add token validation and git credential configuration Addresses GitHub Actions checkout authentication issues by: - Adding early token validation with clear error messages - Configuring git credentials explicitly to prevent "terminal prompts disabled" errors This helps prevent and diagnose common token issues like: - Expired tokens - Missing expiration dates - Insufficient scopes - Incorrect secret references Related to actions/checkout#664 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: Escape template expression in error message * fix(updater): Remove token syntax echo from validation error message * fix: Improve token validation to detect malformed tokens * refactor: Use PowerShell for token validation and git config * feat: Add token scope validation Checks token scopes using x-oauth-scopes header: - Reports scopes for classic PATs - Warns if repo/public_repo scope missing - Provides guidance for fine-grained PATs Based on https://github.com/orgs/community/discussions/25259 * fix: Reintroduce token validity and access checks in the validation process * fix(updater): Remove token syntax echo from validation error message * feat: Enhance whitespace detection in token validation Shows detailed information when whitespace is detected: - Token length - Position of whitespace character - Type of whitespace (newline, space, tab, etc) This helps quickly identify malformed token secrets. * fix: Remove debug output for token preview in error handling * feat: Add explicit check for SSH keys in token validation Detects when an SSH private key is mistakenly passed as api-token. Provides clear error message explaining the difference between SSH keys and GitHub tokens. This catches the error before the generic whitespace check. * feat: Add SSH key support as alternative to token authentication Changes: - Add ssh-key input parameter - Make api-token optional when ssh-key is provided - Pass ssh-key to actions/checkout steps - Skip token validation when using SSH key - Skip git credential config when using SSH key - Validate that only one auth method is provided This allows the action to work with deploy keys, matching the functionality of the previous reusable workflow implementation. Refs: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#push-using-ssh-deploy-keys * fix: Allow both api-token and ssh-key together SSH key can be used for git operations while token is used for GitHub API calls (gh commands, PR creation, etc). This is a valid and useful configuration. * refactor: Split authentication validation into separate steps Changes: - Step 1: Validate authentication inputs (checks at least one is present) - Step 2: Validate API token (runs only if token provided) - Step 3: Validate SSH key (runs only if SSH key provided) Benefits: - Clearer separation of concerns - Easier to read and maintain - Each validation only runs when relevant - SSH key validation now checks format * refactor: Remove manual git credential configuration The actions/checkout action already handles git credential configuration when token or ssh-key is provided. Manual configuration was redundant and could potentially interfere with checkout's credential handling. * docs: Add changelog entry and update v3 breaking changes - Add feature and fix entries for SSH key support and authentication validation - Add note to v3 breaking changes about SSH key support in v3.1 - Reference issue #128 and PR #134 * docs: Remove commented-out api-token option from changelog * fix: Fallback to github.token when api-token is empty When using only ssh-key (no api-token), GH_TOKEN was set to empty string, causing gh CLI to refuse authentication instead of falling back to the default GITHUB_TOKEN. This broke critical steps that use gh api: - Parse existing PR URL - Get changelog - Update dependency (when filtering by GH release titles) Changed all instances of: GH_TOKEN: ${{ inputs.api-token }} To: GH_TOKEN: ${{ inputs.api-token || github.token }} This ensures gh CLI always has valid authentication. Fixes seer-by-sentry review comment: #134 (comment) * fix: Update updater version to use latest stable release --------- Co-authored-by: Claude <[email protected]>
1 parent 6272a50 commit 0d0d99a

File tree

2 files changed

+149
-10
lines changed

2 files changed

+149
-10
lines changed

CHANGELOG.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
- Scripts receive original and new version as arguments
99
- Support both bash (`.sh`) and PowerShell (`.ps1`) scripts
1010
- Enables workflows like updating lock files, running code generators, or modifying configuration files
11+
- Updater - Add SSH key support and comprehensive authentication validation ([#134](https://github.com/getsentry/github-workflows/pull/134))
12+
- Add `ssh-key` input parameter for deploy key authentication
13+
- Support using both `ssh-key` (for git) and `api-token` (for GitHub API) together
14+
- Add detailed token validation with actionable error messages
15+
- Detect common token issues: expiration, whitespace, SSH keys in wrong input, missing scopes
16+
- Validate SSH key format when provided
1117

1218
### Fixes
1319

1420
- Updater - Fix boolean input handling for `changelog-entry` parameter and add input validation ([#127](https://github.com/getsentry/github-workflows/pull/127))
21+
- Updater - Fix cryptic authentication errors with better validation and error messages ([#134](https://github.com/getsentry/github-workflows/pull/134), closes [#128](https://github.com/getsentry/github-workflows/issues/128))
1522

1623
### Dependencies
1724

@@ -52,7 +59,7 @@
5259
# If a custom token is used instead, a CI would be triggered on a created PR.
5360
api-token: ${{ secrets.CI_DEPLOY_KEY }}
5461
55-
### After
62+
### After (v3.0)
5663
native:
5764
runs-on: ubuntu-latest
5865
steps:
@@ -63,6 +70,21 @@
6370
api-token: ${{ secrets.CI_DEPLOY_KEY }}
6471
```
6572

73+
**Note**: If you were using SSH deploy keys with the v2 reusable workflow, the v3.0 composite action initially only supported tokens.
74+
SSH key support was restored in v3.1 ([#134](https://github.com/getsentry/github-workflows/pull/134)). To use SSH keys, update to v3.1+ and use the `ssh-key` input:
75+
76+
```yaml
77+
### With SSH key (v3.1+)
78+
native:
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: getsentry/github-workflows/updater@v3
82+
with:
83+
path: scripts/update-sentry-native-ndk.sh
84+
name: Native SDK
85+
ssh-key: ${{ secrets.CI_DEPLOY_KEY }}
86+
```
87+
6688
To update your existing Danger workflows:
6789

6890
```yaml

updater/action.yml

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ inputs:
3434
required: false
3535
default: ''
3636
api-token:
37-
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
38-
required: true
37+
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}. Not required if ssh-key is provided, but can be used together with ssh-key for GitHub API operations.'
38+
required: false
39+
default: ''
40+
ssh-key:
41+
description: 'SSH private key for repository authentication. Can be used alone or together with api-token (SSH for git, token for GitHub API).'
42+
required: false
43+
default: ''
3944
post-update-script:
4045
description: 'Optional script to run after successful dependency update. Can be a bash script (.sh) or PowerShell script (.ps1). The script will be executed in the caller-repo directory before PR creation.'
4146
required: false
@@ -117,6 +122,116 @@ runs:
117122
}
118123
Write-Output "✓ Post-update script path '${{ inputs.post-update-script }}' is valid"
119124
125+
- name: Validate authentication inputs
126+
shell: pwsh
127+
env:
128+
GH_TOKEN: ${{ inputs.api-token || github.token }}
129+
SSH_KEY: ${{ inputs.ssh-key }}
130+
run: |
131+
$hasToken = -not [string]::IsNullOrEmpty($env:GH_TOKEN)
132+
$hasSshKey = -not [string]::IsNullOrEmpty($env:SSH_KEY)
133+
134+
if (-not $hasToken -and -not $hasSshKey) {
135+
Write-Output "::error::Either api-token or ssh-key must be provided for authentication."
136+
exit 1
137+
}
138+
139+
if ($hasToken -and $hasSshKey) {
140+
Write-Output "✓ Using both SSH key (for git) and token (for GitHub API)"
141+
} elseif ($hasToken) {
142+
Write-Output "✓ Using token authentication"
143+
} else {
144+
Write-Output "✓ Using SSH key authentication"
145+
}
146+
147+
- name: Validate API token
148+
if: ${{ inputs.api-token != '' }}
149+
shell: pwsh
150+
env:
151+
GH_TOKEN: ${{ inputs.api-token || github.token }}
152+
run: |
153+
# Check if token is actually an SSH key
154+
if ($env:GH_TOKEN -match '-----BEGIN') {
155+
Write-Output "::error::The api-token input appears to contain an SSH private key."
156+
Write-Output "::error::Please use the ssh-key input for SSH authentication instead of api-token."
157+
exit 1
158+
}
159+
160+
# Check for whitespace
161+
if ($env:GH_TOKEN -match '\s') {
162+
$tokenLength = $env:GH_TOKEN.Length
163+
$whitespaceMatch = [regex]::Match($env:GH_TOKEN, '\s')
164+
$position = $whitespaceMatch.Index
165+
$char = $whitespaceMatch.Value
166+
$charName = switch ($char) {
167+
"`n" { "newline (LF)" }
168+
"`r" { "carriage return (CR)" }
169+
"`t" { "tab" }
170+
" " { "space" }
171+
default { "whitespace character (code: $([int][char]$char))" }
172+
}
173+
Write-Output "::error::GitHub token contains whitespace at position $position of $tokenLength characters: $charName"
174+
Write-Output "::error::This suggests the token secret may be malformed. Check for extra newlines when setting the secret."
175+
exit 1
176+
}
177+
178+
# Check token scopes (works for classic PATs only)
179+
$headers = curl -sS -I -H "Authorization: token $env:GH_TOKEN" https://api.github.com 2>&1
180+
$scopeLine = $headers | Select-String -Pattern '^x-oauth-scopes:' -CaseSensitive:$false
181+
if ($scopeLine) {
182+
$scopes = $scopeLine -replace '^x-oauth-scopes:\s*', '' -replace '\r', ''
183+
if ([string]::IsNullOrWhiteSpace($scopes)) {
184+
Write-Output "::warning::Token has no scopes. If using a fine-grained PAT, ensure it has Contents (write) and Pull Requests (write) permissions."
185+
} else {
186+
Write-Output "Token scopes: $scopes"
187+
if ($scopes -notmatch '\brepo\b' -and $scopes -notmatch '\bpublic_repo\b') {
188+
Write-Output "::warning::Token may be missing 'repo' or 'public_repo' scope. This may cause issues with private repositories."
189+
}
190+
}
191+
} else {
192+
Write-Output "::notice::Could not detect token scopes (this is normal for fine-grained PATs). Ensure token has Contents (write) and Pull Requests (write) permissions."
193+
}
194+
195+
# Check token validity and access
196+
gh api repos/${{ github.repository }} --silent 2>&1 | Out-Null
197+
if ($LASTEXITCODE -ne 0) {
198+
Write-Output "::error::GitHub token validation failed. Please verify:"
199+
Write-Output " 1. Token is not empty or malformed"
200+
Write-Output " 2. Token has not expired"
201+
Write-Output " 3. Token has an expiration date set"
202+
Write-Output " 4. Token has 'repo' and 'workflow' scopes"
203+
exit 1
204+
}
205+
206+
Write-Output "✓ GitHub token is valid and has access to this repository"
207+
208+
- name: Validate SSH key
209+
if: ${{ inputs.ssh-key != '' }}
210+
shell: pwsh
211+
env:
212+
SSH_KEY: ${{ inputs.ssh-key }}
213+
run: |
214+
# Check if SSH key looks valid
215+
if ($env:SSH_KEY -notmatch '-----BEGIN') {
216+
Write-Output "::warning::SSH key does not appear to start with a PEM header (-----BEGIN). Please verify the key format."
217+
}
218+
219+
# Check for common SSH key types
220+
$validKeyTypes = @('RSA', 'OPENSSH', 'DSA', 'EC', 'PRIVATE KEY')
221+
$hasValidType = $false
222+
foreach ($type in $validKeyTypes) {
223+
if ($env:SSH_KEY -match "-----BEGIN.*$type") {
224+
$hasValidType = $true
225+
break
226+
}
227+
}
228+
229+
if (-not $hasValidType) {
230+
Write-Output "::warning::SSH key type not recognized. Supported types: RSA, OPENSSH, DSA, EC, PRIVATE KEY"
231+
}
232+
233+
Write-Output "✓ SSH key format appears valid"
234+
120235
# What we need to accomplish:
121236
# * update to the latest tag
122237
# * create a PR
@@ -137,7 +252,8 @@ runs:
137252
- name: Checkout repository
138253
uses: actions/checkout@v4
139254
with:
140-
token: ${{ inputs.api-token }}
255+
token: ${{ inputs.api-token || github.token }}
256+
ssh-key: ${{ inputs.ssh-key }}
141257
ref: ${{ inputs.target-branch || github.ref }}
142258
path: caller-repo
143259

@@ -150,7 +266,7 @@ runs:
150266
DEPENDENCY_PATTERN: ${{ inputs.pattern }}
151267
GH_TITLE_PATTERN: ${{ inputs.gh-title-pattern }}
152268
POST_UPDATE_SCRIPT: ${{ inputs.post-update-script }}
153-
GH_TOKEN: ${{ inputs.api-token }}
269+
GH_TOKEN: ${{ inputs.api-token || github.token }}
154270
run: ${{ github.action_path }}/scripts/update-dependency.ps1 -Path $env:DEPENDENCY_PATH -Pattern $env:DEPENDENCY_PATTERN -GhTitlePattern $env:GH_TITLE_PATTERN -PostUpdateScript $env:POST_UPDATE_SCRIPT
155271

156272
- name: Get the base repo info
@@ -194,7 +310,7 @@ runs:
194310
shell: pwsh
195311
working-directory: caller-repo
196312
env:
197-
GH_TOKEN: ${{ inputs.api-token }}
313+
GH_TOKEN: ${{ inputs.api-token || github.token }}
198314
run: |
199315
$urls = @(gh api 'repos/${{ github.repository }}/pulls?base=${{ steps.root.outputs.baseBranch }}&head=${{ github.repository_owner }}:${{ steps.root.outputs.prBranch }}' --jq '.[].html_url')
200316
if ($urls.Length -eq 0)
@@ -221,7 +337,7 @@ runs:
221337
shell: pwsh
222338
working-directory: caller-repo
223339
env:
224-
GH_TOKEN: ${{ inputs.api-token }}
340+
GH_TOKEN: ${{ inputs.api-token || github.token }}
225341
run: |
226342
$changelog = ${{ github.action_path }}/scripts/get-changelog.ps1 `
227343
-RepoUrl '${{ steps.target.outputs.url }}' `
@@ -276,7 +392,8 @@ runs:
276392
if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }}
277393
uses: actions/checkout@v4
278394
with:
279-
token: ${{ inputs.api-token }}
395+
token: ${{ inputs.api-token || github.token }}
396+
ssh-key: ${{ inputs.ssh-key }}
280397
ref: ${{ inputs.target-branch || github.ref }}
281398
path: caller-repo
282399

@@ -287,7 +404,7 @@ runs:
287404
env:
288405
DEPENDENCY_PATH: ${{ inputs.path }}
289406
POST_UPDATE_SCRIPT: ${{ inputs.post-update-script }}
290-
GH_TOKEN: ${{ inputs.api-token }}
407+
GH_TOKEN: ${{ inputs.api-token || github.token }}
291408
run: ${{ github.action_path }}/scripts/update-dependency.ps1 -Path $env:DEPENDENCY_PATH -Tag '${{ steps.target.outputs.latestTag }}' -OriginalTag '${{ steps.target.outputs.originalTag }}' -PostUpdateScript $env:POST_UPDATE_SCRIPT
292409

293410
- name: Update Changelog
@@ -297,7 +414,7 @@ runs:
297414
env:
298415
DEPENDENCY_NAME: ${{ inputs.name }}
299416
CHANGELOG_SECTION: ${{ inputs.changelog-section }}
300-
GH_TOKEN: ${{ inputs.api-token }}
417+
GH_TOKEN: ${{ inputs.api-token || github.token }}
301418
run: |
302419
${{ github.action_path }}/scripts/update-changelog.ps1 `
303420
-Name $env:DEPENDENCY_NAME `

0 commit comments

Comments
 (0)