GitHub Actions
Basics
Unlike the language-package ecosystems above, GitHub Actions is a CI/CD supply-chain pinning
ecosystem: deps-lsp does not track library dependencies, it tracks which commit of a
third-party Action each workflow step trusts. Two manifest shapes are recognized:
.github/workflows/*.yml/*.yaml— ordinary workflow files, matched via a directory-pattern rule (any file in that directory with a.yml/.yamlextension), not an exact filename.action.yml/action.yaml— a composite/reusable Action’s own metadata file, whoseruns.stepscan itself reference other Actions.
Every uses: step is a dependency:
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v4.1.1
A tag reference (@v4) resolves against GitHub’s own tags API for that owner/repo (no
separate package registry exists for Actions) and is flagged by the mutable-ref-pin diagnostic,
since a tag can be force-moved by the repository owner to point at different code without the
version string in your workflow ever changing — see CI/CD
Pinning for why this matters and how the SHA-pinning code
action/code lens works. A full 40-character commit SHA is the only pin GitHub itself cannot
silently repoint.
Non-Semver Tag Handling (issue #550)
When a GitHub Action repository has only tags that don’t parse as full semantic versions — such as
dtolnay/rust-toolchain with its sole tag v1, or literal-named tags like cargo-deny — the
hover and diagnostics handle these gracefully instead of showing a false “Unknown package”
diagnostic.
- Hover: shows the package as resolvable (not unknown), but with an empty “Recent versions” list since no tag matches the standard semver filter. The mutable-ref-pin diagnostic still fires for the tag ref even though no update-to-latest is available.
- Diagnostics: the package is recognized as resolvable, not reported as “Unknown package” — this is a real action, just not one with a conventional semver release train.
- Literal-named tags (non-version-like names): are now recognized as actual tags (when confirmed by the registry) and qualify for the mutable-ref-pin diagnostic, even though they don’t follow the
major.minor.patchorv\d+patterns the parser heuristic would normally detect.
SHA-Pin Comment Tag Freshness (issue #907)
A SHA-pinned step commonly carries a human-readable trailing comment naming the
tag it was pinned from (uses: actions/checkout@<sha> # v4). This comment is
accepted at partial precision too — # v6, # v2.9 — not just a full
major.minor.patch tag; is_partial_semver_shaped (deps-core’s git_ref
module) is the acceptance gate, stricter than the git-ref-oriented
is_tag_shaped so free-text comments (a date, an issue number) aren’t mistaken
for a tag.
The comment is treated as a hint, not ground truth: hover, diagnostics, inlay
hints, and the bulk “update outdated” code lens all resolve the pin’s actual
freshness against TagIndex.sha_to_tag (which SHA the tag really points at
today), so a comment that has drifted from the pinned SHA no longer produces a
false “up to date” result just because the comment text looked current.
A full-SHA pin that no release tag points at (with no comment, or a non-version one like
# cargo-deny) is reported as outdated once the repository’s tags are loaded; a pin to a
non-release commit is therefore offered the latest release. This includes a commentless pin on a
non-release commit newer than the latest release: it is reported outdated and update-all re-pins
it to the latest release (effectively a downgrade).
A pin whose SHA is absent from the loaded tag index is reported outdated even when it carries a
version comment, since the comment cannot make a non-release commit the latest release. The
“absent” verdict is only drawn from a complete tag list: a repository with more tags than the
fetch cap (30 pages of 100) yields a truncated index, and a SHA missing from it is never called
outdated nor proven current: the comment may still stand in for its status, but only short of up to
date (an up-to-date comment, whatever its shape, reads unresolved), and no mismatch diagnostic is
raised. The one exception is a comment naming a full version (# v4.2.0) that the truncated index
maps to a different commit: that comment is provably wrong, so it is not trusted, the status is
unresolved and the sha-comment-mismatch diagnostic fires. Tags are matched by version, so
# 4.2.0, # V4.2.0 and # v4.2.0+build are contradicted by the tag v4.2.0 as well. A comment
naming a version the truncated index does not list stays unverifiable, with the same cap on its
status. An exact tag pin (@v4.8.0) missing from a truncated index is capped the same way,
and so is a floating partial pin (@v1) the list does not contain: a truncated list proves a tag
is present, never that it is absent, so such a pin reads unresolved where a complete index would
read it by the usual moving-line rule.
A SHA pin whose commit is tagged only by a floating tag below the latest release (v1 or 1.1
while the latest is v1.1.0 on another commit) is reported outdated (issue #1730). A tag at or
above the latest release, including v3.0.0-rc1, counts as up to date.
Trade-off. A non-release commit newer than the latest release that carries only a floating tag is also reported outdated, and update-all re-pins it to the latest release. Making that case distinguishable is tracked in #1725.
When the update target is not version-shaped (a release named stable), the SHA is rewritten
without a # tag comment; any words after the old tag stay in the comment
(# v1.0.0 pinned for CVE becomes # pinned for CVE).
Peer-document rescan (issue #1716)
When a tags fetch adds or changes a repository’s tag index, every other open workflow that uses that repository is rescanned: its vulnerability check and diagnostics are refreshed without an edit or reopen. Events are coalesced over 250 ms and the rescan runs for at most 4 documents at a time. The vulnerability rescan applies only while vulnerability checking is enabled and the server is online, but diagnostics are republished (and inlay hints and code lenses refreshed once per batch when a document did not already request its own) regardless, since the tag index also drives the status and comment checks. A document that is still loading is skipped and publishes after its own load. GitLab CI/CD does not emit refresh events.
Comment mismatch diagnostic (issue #1722)
When the trailing comment names a tag that is provably not the pinned commit’s tag, the
sha-comment-mismatch diagnostic flags the step (an imposter-commit or stale-comment signal) and
hover adds a **Warning** line, either comment says v2.87.20, but SHA is v2.87.22 or
SHA is not the commit of any release tag. A partial-precision comment (# v4) agrees with a
SHA whose most specific tag extends it. Nothing is reported while the tag index is cold or when
the SHA is absent from a truncated index, unless the comment names a full version that index maps
to another commit. Severity defaults to warning and is set with
diagnostics.sha_comment_mismatch_severity; there is no on/off toggle.
The Correct version comment to <tag> quickfix rewrites only the comment’s tag token to the
tag the SHA actually carries, leaving the SHA, its casing and any closing quote or } untouched.
It is withheld when the registry tag is not a plain version, contains control or bidirectional
characters, is overly long, or when the comment token ends in punctuation.
Tag pins and release ordering
A tag pin is compared with the latest release by SemVer precedence. A pre-release pin
(@v2-beta, @v3.0.0-rc.1) is reported outdated once a newer release exists, while a partial pin
(@v4) stays current as long as the latest release extends it. A pin that is ahead of the latest
release is reported up to date, unless the tag index is complete and has no such tag (@v40, a
typo or a deleted tag): that pin is unresolved, never outdated, so no downgrade is offered. A
branch named like a version and ahead of the latest release reads the same way. Refs that are not on a version line (@v1.x, @v3-node20) are
never reported outdated. SHA pins are matched case-insensitively and shown in lowercase.
Unpublished refs and the unknown-ref diagnostic (issue #1766)
Tag refs are exact, so @4.3.1 names nothing in a repository that only tags v4.3.1. When the
repository’s tag list is complete, a tag pin whose exact text matches no tag is an unpublished
ref, and what it means depends on its shape:
- A full release (
@4.3.1,@v4.3.10,@v5.0.0-rc.1: a plainmajor.minor.patchcore, or one with a recognized pre-release label such asrcorbeta) cannot plausibly be a branch. It is unresolved instead of up to date at any position, and theunknown-refdiagnostic reports`4.3.1` is not a published tag of actions/checkout. Severity defaults to warning and is set withdiagnostics.unknown_ref_severity; there is no on/off toggle. An outdated full release stays outdated, since the update target is valid either way. - Any other tag-shaped ref (
@v1,@v5.x,@v3-node20,@v40,@v3.4.0-working) may be a branch:ruby/setup-ruby@v1,pnpm/action-setup@v3,aws-actions/configure-aws-credentials@v3-node20andcodecov/codecov-action@v5.xare documented branch pins. They keep the rule above (unresolved only when ahead of the latest release) and are never reported byunknown-ref, so a ref such as@v40gets an unresolved status but no diagnostic.
Nothing is reported while the tag index is cold, empty or truncated. deps-cli check --fail-on unknown-ref fails on the diagnostic regardless of its severity; it is not part of the
default --fail-on set.
When exactly one published tag matches the written ref after normalization (4.3.1 beside tag
v4.3.1), the Change ref to published tag <tag> quickfix rewrites the ref to that spelling
(issue #1781). It is withheld when no tag or several differently-targeted tags match.
In the editor the verdict is only as fresh as the last tags fetch of that repository. Every
document open or edit that uses it re-requests the tags (a conditional request, so an unchanged
list is cheap) and refreshes the diagnostics, so a pin bumped to a tag published after the last
fetch can show the warning until the next open or edit of a workflow using that repository.
deps-cli always fetches fresh.
Updating quoted and flow-style pins (issue #1724)
Update-all and the update quickfix rewrite a plain scalar SHA pin to <new sha> # <tag>. For a
quoted or flow-style pin (uses: 'owner/repo@<sha>', {uses: owner/repo@<sha>, with: {...}})
without a comment only the 40-hex SHA is replaced, so the quoting and flow structure stay intact.
The same holds for a tag that does not read as a version (@stable): the pin is converted
and rewritten to the bare SHA with no # <tag> comment, since a comment the next parse could not
read back would pile up on every update.
Comments after quotes and flow mappings (issue #1732)
The trailing # vX comment is also read, mismatch-checked and rewritten when only the closing
quote and/or the flow mapping’s } sit between the SHA and the comment: uses: "owner/repo@<sha>" # v4,
uses: 'owner/repo@<sha>' # v4, {uses: owner/repo@<sha>} # v4, {uses: "owner/repo@<sha>"} # v4.
Blanks before the } ({ uses: owner/repo@<sha> } # v4) are accepted too. The rewrite keeps the
delimiters (<new sha>" # <new tag>). A comment after a flow mapping that
has sibling keys after uses ({uses: owner/repo@<sha>, name: x} # v4) is ambiguous and not
attributed to the pin.
Mutable-Ref Pinning
GitHub Actions shares its mutable-ref-pin diagnostic and bulk “Pin All to SHA” code lens with GitLab CI/CD — see CI/CD Pinning for the full detail.
Release-Freshness Coverage (shared with Swift)
GitHub Actions sources release dates from the same deps_core::github::ReleaseDatesCache Swift
uses — see Swift for the full
detail, including the four ways this coverage is partial and the GITHUB_TOKEN requirement.
Vulnerability Scanning
OSV.dev does not version-match the GitHub Actions ecosystem server-side (a versioned query
returns nothing even for an affected version), so deps-lsp fetches a package’s advisories
without a version and matches their affected ranges locally against the pinned version.
- A full SemVer pin (
@v4.1.2, or a SHA pin whose tag resolves to one) is checked; an advisory whose range contains it produces the usual vulnerability hover and diagnostic. - A floating tag (
@v4,@v4.1) is resolved through the commit the tag currently points at, using the same tags fetch as SHA pins: the most specific release tag on that commit that extends the written tag (v4.2.2for@v4) is the version that is checked, and hover shows it asResolved. This is a snapshot taken at scan time: if the tag later moves to another commit, the result is refreshed only when the tags are next fetched. If the commit carries no such release (for example onlyv4, or an unrelatedv5.0.0), the tag index is not loaded yet, or the pin is a bare major (@4), a SHA pin with no resolvable tag, or any other non-SemVer pin, it is shown as “not checked”, never as clean. - A commit often carries several release tags (
v4.8.0andv4.9.0). Every one of them is checked, so an advisory that affects only a sibling tag is still reported, and hover and the diagnostic name it (matched release tag v4.9.0). For a SHA pin all release tags on the commit count; for an exact tag pin (@v4.8.0) only the releases of the same major version do, and for a floating tag (@v4) only the releases that extend the written tag. Pre-release tags are never checked as siblings. When a tag list was truncated (more than 3000 tags), or has not been fetched yet (cold cache) or failed to fetch, the sibling list may be incomplete: a clean answer for such a pin is shown as not fully checked in hover rather than as clean (no diagnostic is raised), anddeps-cli updaterefuses alatestit cannot verify the same way. A sibling-only advisory whose fix is not newer than the pinned version is not offered as a fix. The same sibling check applies to the latest version, to upgrade candidates and to a recommended fix target; while a candidate’s sibling tags are unknown (tag index not loaded yet) it is shown as “not checked”, never as clean. - An affected range that has only an
introducedevent and nofixedevent is treated as open-ended unless the advisory’sdatabase_specific.last_known_affected_version_rangegives a parsable upper bound (< Xor<= X); versions above that bound are not reported as affected, whether the range or an explicit versions list matched them. An unparsable bound makes the range unevaluable rather than affecting every later version. - An advisory exists for the package but its affected range cannot be evaluated: a diagnostic
notes that vulnerability data was not checked (
UnevaluableAdvisoryRange). - A package with more than 50 advisories is reported as truncated rather than partially matched.
OSV.dev package names are case-sensitive, so the queried name is the repository’s canonical
owner/repo casing taken from the GitHub tags response (a lowercase uses: value still
matches). Vulnerability checking therefore depends on the GitHub tags fetch and its API quota
(60 requests/hour unauthenticated; set GITHUB_TOKEN to raise it).
Until the casing is confirmed (or when the tags fetch fails or the repository is private), the
name as written in the manifest is queried instead, and only a positive result is trusted: an
advisory found under the written name is reported, while an empty or non-matching answer stays
“not checked” (CanonicalNameUnconfirmed), never clean. Such a result is re-checked under the
canonical casing once the tags arrive, and a recommended fix is not offered as verified for it.
Because of this fallback, the written owner/repo of a private repository is sent to osv.dev
whenever its canonical name cannot be confirmed, as it was before canonical-name resolution.
Known limitations: a renamed or transferred repository is queried only under its current GitHub name once confirmed, so an advisory OSV.dev still files under the old name is not matched.