Repository navigation
Add: github-app-token, a fail-closed replacement for cross-repo PATs - #30
Merged
Merged
Conversation
CLAUDE.md duplicated docs/pipeline-map.md and docs/architecture.md inline. Replaced with summaries plus links; the docs were already the better source. Verified every fact in the removed text against the two routed docs before cutting it - nothing was dropped, it was already covered there. Adds the gforce-github-actions skill (lockfile) and the gha-workflow-author agent, and AGENTS.md as a symlink for cross-vendor coverage. Also fixes two pre-existing defects in docs/ surfaced by reviewing the new skill, since it now routes every session straight to these files: - docs/consuming-sf-cicd.md still pointed at the frozen v1 release tag and told readers to copy examples/sf-pr-validate.yml and examples/sf-release.yml, filenames that do not exist post-rename (they are reusable-sf-pr-validate.yml and reusable-sf-release.yml now). Its sibling consuming-sf-dispatch.md was updated to v2 during the rename; this one was missed. Fixed both. - docs/claude-actions-reference.md was missing entries for six assets added in PR #27 and PR #28: sf-artifact-build, sf-artifact-deploy, sf-env-config-apply, reusable-sf-org-deploy, smoke-sf-artifact, and smoke-sf-org-login. Added an entry for each, sourced from each asset's own action.yml / workflow file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
069f926's message claimed "nothing was dropped" when CLAUDE.md's TypeScript- actions text was cut. That claim was inaccurate: the removed text named ApexTestSelectionService and package.xml parsing as the substance of libraries/salesforce, and neither appeared anywhere under docs/ - architecture.md described that directory only generically. Restored the detail to docs/architecture.md (the doc that owns code layering), not back into CLAUDE.md, which is already near its 150-line budget. Also: - docs/consuming-sf-cicd.md: the @v1 fix in 069f926 was incomplete. Lines 9, 18, 52 and 55 still used the pre-rename short names sf-pr-validate.yml / sf-release.yml while the real files, examples/README.md, and this doc's own link targets all use reusable-sf-pr-validate.yml / reusable-sf-release.yml - line 52 was self-contradictory within one link. Completed the rename throughout; re-read the file end to end afterward and found no remaining v1 references, no dead paths, and no contradiction with consuming-sf-dispatch.md. - docs/claude-actions-reference.md: sf-artifact-build's head-commit entry claimed a default of GITHUB_SHA; the action.yml declares default: "" with the $GITHUB_SHA fallback applied in the script body at runtime. Corrected. - docs/claude-actions-reference.md: the smoke-sf-artifact.yml entry called reject-validate-without-tests "a fifth job" while conflating it with the workflow's five-property header comment. The workflow has 4 jobs (build-artifact covers properties 1/3/4, unset-replacement-fails covers 2, reject-tampered-artifact covers 5); reject-validate-without-tests is the 4th job, sitting outside the five-property enumeration. Reworded. - docs/claude-actions-reference.md: the note flagging that sf-org-login predates PR #16's credential-source: github-env input lived only at the end of the smoke-sf-org-login.yml entry (~line 396), 280 lines from the stale sf-org-login entry itself (~line 115) where an editor would actually need it. Added a one-line pointer at the sf-org-login entry; left the existing note in place. The underlying staleness is still out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
graphify was uninstalled on 2026-08-14; CLAUDE.md still told every session it was available, and omitted rtk, which is the thing actually compressing Bash output through a PreToolUse hook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`secrets.GITHUB_TOKEN` cannot reach another repository, so every cross-repo job
in the fleet reached for a personal access token instead. Two are in service
today — `E2E_DISPATCH_TOKEN` in sf-docker-images and `CATALOG_SCAN_TOKEN` here.
A PAT is the worst credential available for the job: it belongs to a person, it
has account-wide scope, it never expires on its own, and it dies with the
account.
An App installation token is better on all four counts, but only if it is
actually scoped. `actions/create-github-app-token` is generous in two directions
at once — omit `repositories` and the token reaches every repo in the
installation; omit every `permission-*` and it inherits every permission the App
holds. Do both and the replacement for an over-broad PAT is an over-broad token
that merely expires sooner.
So this action refuses to mint until the caller has named the repositories AND
named the permissions. `allow-broad-scope: true` is the deliberate opt-out, and
it logs a warning saying so.
Also validated here, because each failure is otherwise diagnosed from a bare
401/404 in a later step:
- neither client-id nor app-id set (an unshared org variable arrives as an
empty string, indistinguishable from never passing one)
- a private key without its PEM armour
- an empty private key
`client-id` is preferred; upstream deprecated the numeric `app-id` in v3, and
both are accepted so a caller can migrate without a flag day.
Three CI smoke steps pin the guards. They run with a throwaway key and no App
credentials at all, which is the point — the two scope rejections would
otherwise SUCCEED and hand back a working, over-broad token. Verified locally
across 11 input combinations before wiring: every reject case rejects, every
scoped case passes, and `allow-broad-scope` still opens the door.
Registered with Dependabot per the authoring convention — the action carries a
third-party `uses:`, and composite directories are not covered by "/".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
# Conflicts: # docs/claude-actions-reference.md
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.
Adds
github-app-token, the sanctioned way for any repo in the fleet to reach another one.Why
secrets.GITHUB_TOKENcannot touch another repository, so every cross-repo job reached for a personal access token. Two are in service today —E2E_DISPATCH_TOKENinsf-docker-imagesandCATALOG_SCAN_TOKENhere. A PAT belongs to a person, carries account-wide scope, never expires on its own, and dies with the account.Why it isn't a thin wrapper
actions/create-github-app-tokenis generous in two directions at once:repositories→ the token reaches every repo in the installationpermission-*→ it inherits every permission the App holdsDo both and you have replaced an over-broad PAT with an over-broad token that merely expires sooner. So this action fails closed: it refuses to mint until the caller has named the repositories and named the permissions.
allow-broad-scope: trueis the deliberate opt-out and logs a warning saying so.It also catches three things that would otherwise be diagnosed from a bare 401/404 several steps later: an empty client-id/app-id (an org variable not shared with the calling repo arrives as an empty string, indistinguishable from never passing one), an empty private key, and a key pasted without its PEM armour.
Testing
Three smoke steps in
ci.ymlpin the guards. They run with a throwaway key and no App credentials at all — which is the point: the two scope rejections would otherwise succeed and hand back a working, over-broad token.Verified locally across 11 input combinations before wiring (every reject case rejects, every scoped case passes,
allow-broad-scopestill opens the door).Note on
organization-packagesExposed separately from
packages, because the split is not obvious and gets people: pulling/pushing an image usespackages, but package visibility (PATCH /orgs/{org}/packages/...) and version deletion are org endpoints that 403 withpackagesalone.Follow-ups
sf-docker-imagescan consume it — that PR references@v2, so thev2tag needs to move.CATALOG_SCAN_TOKENincatalog-refresh.ymlcan move to the same App; worth its own change.Setup doc:
docs/github-app-setup.md.🤖 Generated with Claude Code