Repository navigation
Conversation
Authenticating as a GitHub App from a shell means signing an RS256 JWT, POSTing it to the installation endpoint and parsing the reply. That gets copied between repositories as a block of openssl and curl, and the copies drift. cmd/githubauth does it in one call. "githubauth token" prints an installation token and nothing else to stdout, so $(githubauth token) drops into curl. "githubauth jwt" prints the App JWT for the endpoints that take one. Built on the standard library's flag package. Cobra would read better, but a CLI dependency lands in go.mod and every library consumer inherits it, which would cost the two-dependency footprint the README advertises. Conventions follow clig.dev: usage errors exit 2 and runtime failures exit 1, errors go to stderr, --json gives machine-readable output, help carries worked examples, and an unknown command suggests the nearest match. Flag defaults are deliberately never printed, because each flag falls back to an environment variable and one of them holds the private key.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #70 +/- ##
==========================================
- Coverage 99.31% 98.88% -0.44%
==========================================
Files 4 5 +1
Lines 294 447 +153
==========================================
+ Hits 292 442 +150
- Misses 2 5 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Rewrites the suite in the table-driven shape the rest of the repo uses: cases are structs with a name, subtests run under t.Run, and pure functions run in parallel. Two fixes the rewrite forced: Every flag falls back to an environment variable, so a developer with GITHUB_APP_CLIENT_ID exported was testing a different program than CI was. Each case now clears all four first. Threading stdin through run() instead of reading os.Stdin inside the commands makes "--key -" reachable from a test; os.Stdin now appears only in main. Coverage of cmd/githubauth goes from 97.9% to 98.5%, with tokenCmd reaching 100% once the --enterprise-url branch and its identifier check had cases. What is left is unreachable: main calls os.Exit, debug.ReadBuildInfo does not fail under test, and a correctly built RSA signer does not fail to sign. Also satisfies errcheck on writes to stdout and stderr, whose failure a CLI cannot act on.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Authenticating as a GitHub App from a shell means signing an RS256 JWT, POSTing it to the
installation endpoint and parsing the reply. That gets copied between repositories as a
block of
opensslandcurl, and the copies drift.cmd/githubauthdoes it in one call:The token is the only thing on stdout, so it composes:
curl -H "Authorization: Bearer $(githubauth token)" \ https://api.github.com/installation/repositoriesgithubauth jwtprints the App JWT for the endpoints that take one. Every flag falls backto an environment variable, and
--keyaccepts a file path, the PEM itself, or-to readstdin, so the key never has to touch disk.
Decisions worth a second look
Standard library
flag, not Cobra. A CLI dependency lands ingo.modand everylibrary consumer inherits it.
go.modis unchanged by this PR, so the two-dependencyfootprint the README advertises still holds. The cost is hand-rolled help and subcommand
dispatch.
Flag defaults are never printed in help. Every flag defaults to an environment
variable and one of them is
GITHUB_APP_PRIVATE_KEY, soflag.PrintDefaultswould printthe private key to anyone with it set.
printFlagsreplaces it and also renders--namerather than the single-dash spelling. There is a regression test for the leak.
Exit codes follow clig.dev: 2 for CLI misuse (unknown flag, missing or conflicting
value), 1 for a well-formed request that failed. An unknown command suggests the nearest
match.
Type of Change
Testing
cmd/githubauthis at 97.9% coverage. The remainder ismain(callsos.Exit) andthe
debug.ReadBuildInfofailure branch. Thetokenpath is exercised against anhttptestserver through--base-url; no test reaches the network.Manually verified: JWT header and claims decode to
{"alg":"RS256"}with the client ID asissand a 10-minute expiry, stdin key piping,--json, typo suggestion, and per-commandhelp.
Documentation
Checklist
go.modand the CI matrixVerified against GitHub
Run end to end against production
api.github.comwith a real GitHub App, not a stub.Token values are not reproduced here.
githubauth jwt→GET /appissGET /app/installationsgithubauth tokenghs_tokenGET /installation/repositories--jsonexpires_atexactly 60 minutes out, matching GitHub's window--repos <name>--key -The JWT was also verified independently of GitHub: the signature checks out against the
App's public key with
openssl dgst -verify, and a tampered payload fails.One observation, unrelated to this PR but confirming the CHANGELOG note on stateless
tokens: GitHub issued a 377-character
ghs_token, the new JWT format rather than theshort opaque string. It passed through unmodified and authenticated a live call, so the
"no action required" note now rests on a production result rather than on reading the
announcement.