Jobs without a permissions block inherit the repository default token
scope. Scopes are derived from what each workflow actually does with
the token:
- the fourteen package build and test workflows plus the code linter
never write through the token, they drop to contents: read.
- lint-pr-title.yml gets pull-requests: read, which is what the
semantic PR title action documents needing.
- comment-icon-preview.yml runs on workflow_run, a privileged trigger
processing artifacts produced by fork PRs, so an exact scope matters
most there: actions: read to download the artifact, issues: write
plus pull-requests: write to post the preview comment, contents:
read for the rest.
The workflows that already declared scoped permissions (labeler,
stale, linting-icons, release and friends) are untouched.
Version tags are movable pointers: whoever controls an action's repo
can re-point them, and the previous commit's CVE is the proof it
happens in practice, to this very repository's dependency set. These
workflows hold real credentials: release.yml publishes the whole
lucide npm family with OIDC trusted publishing, ci.yml creates the
GitHub releases, and the comment and review workflows carry
pull-request write tokens.
Pinning by full commit SHA makes the reviewed code the code that runs.
Every pin keeps a version comment, resolved from the upstream repo and
cross-checked against its release tag (softprops v1 has no patch tag,
the comment names the floating tag it replaced). Dependabot understands
SHA pins and keeps bumping them with the comment in sync; the repo has
no dependabot.yml yet, adding one with the github-actions ecosystem is
a one-block follow-up.