Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/.yaml extension), not an exact filename.
  • action.yml/action.yaml — a composite/reusable Action’s own metadata file, whose runs.steps can 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.patch or v\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 plain major.minor.patch core, or one with a recognized pre-release label such as rc or beta) cannot plausibly be a branch. It is unresolved instead of up to date at any position, and the unknown-ref diagnostic reports `4.3.1` is not a published tag of actions/checkout. Severity defaults to warning and is set with diagnostics.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-node20 and codecov/codecov-action@v5.x are documented branch pins. They keep the rule above (unresolved only when ahead of the latest release) and are never reported by unknown-ref, so a ref such as @v40 gets 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.2 for @v4) is the version that is checked, and hover shows it as Resolved. 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 only v4, or an unrelated v5.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.0 and v4.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), and deps-cli update refuses a latest it 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 introduced event and no fixed event is treated as open-ended unless the advisory’s database_specific.last_known_affected_version_range gives a parsable upper bound (< X or <= 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.