From 8c70197cbf557c3debb0fe43143c40cc2ffb1859 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:02:38 +0000 Subject: [PATCH 1/5] fix(rules): accept gh actions-lock format v0.0.3 under the v0.0.2 grammar Dependabot #926 regenerated .github/workflows/actions.lock as `version: 'v0.0.3'`. Hypatia.Rules.ActionsLock accepted only v0.0.2, so the repository's own lock stopped parsing: {:line, 4, {:unsupported_version, "v0.0.3"}}. That failed "parses the repository's authoritative generated lockfile" and turned E2E and Integration red on main from 5173ca7 on. The v0.0.3 file has exactly the v0.0.2 line shapes; only the version line and the pins changed. So v0.0.3 is accepted under the same grammar, and the parser stays fail-closed: any line shape v0.0.3 might add is still refused until the grammar is extended with a fixture. The parsed struct now records which version it read. Tests: v0.0.3 accepted and associated per workflow; a malformed or unknown line in a v0.0.3 lock still fails closed; v0.0.4 is refused (negative control); a second version line is refused even when both are supported. With the old parser, 4 of these 17 tests fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0165rGu3tsKmJtevmrdMVyfJ --- lib/rules/actions_lock.ex | 32 ++++++++++++++++++--------- test/rules/actions_lock_test.exs | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/lib/rules/actions_lock.ex b/lib/rules/actions_lock.ex index fce24544..d02497c0 100644 --- a/lib/rules/actions_lock.ex +++ b/lib/rules/actions_lock.ex @@ -3,7 +3,7 @@ defmodule Hypatia.Rules.ActionsLock do @moduledoc """ - Strict local validation for machine-generated `gh actions-lock` v0.0.2 files. + Strict local validation for machine-generated `gh actions-lock` files (v0.0.2, v0.0.3). This module validates lockfile *shape and association*. It does not replace `gh actions-lock --verify`, which remains authoritative for resolving refs @@ -13,11 +13,19 @@ defmodule Hypatia.Rules.ActionsLock do The parser is deliberately line-oriented. `actions.lock` is generated by a single tool with a versioned stable format, and Hypatia must not add a general - YAML dependency merely to consume that format. Unknown or malformed v0.0.2 - lines fail closed rather than being guessed at. + YAML dependency merely to consume that format. Unknown or malformed lines + fail closed rather than being guessed at. + + Format v0.0.3 is accepted under the v0.0.2 line grammar, unchanged. Measured + 2026-10-19 on the first v0.0.3 lock `gh actions-lock` generated for this + repository (#926): every line has a v0.0.2 shape; only the version line and + the pins differ. Accepting the version therefore admits no new syntax: if a + v0.0.3 file ever carries a line shape v0.0.2 lacks, it still fails closed + here, and the grammar must be extended deliberately, with a fixture, before + it is trusted. """ - @supported_version "v0.0.2" + @supported_versions ["v0.0.2", "v0.0.3"] @action_ref ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+@[^\s':]+$/ @dependency_key ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[^\s':]+$/ @sha_ref ~r/^[0-9A-Fa-f]{40}$/ @@ -38,7 +46,7 @@ defmodule Hypatia.Rules.ActionsLock do } @doc """ - Parse and validate a complete v0.0.2 lockfile. + Parse and validate a complete lockfile in a supported format version. Returns `{:error, reason}` for missing, unsupported, malformed, incomplete, duplicate, or contradictory input. One bad dependency invalidates the whole @@ -135,18 +143,20 @@ defmodule Hypatia.Rules.ActionsLock do defp consume_line(line, line_number, %{section: :preamble} = state) do cond do match = Regex.run(~r/^version: '([^']+)'$/, line) -> - case match do - [_, @supported_version] when is_nil(state.version) -> - {:ok, %{state | version: @supported_version}} + [_, version] = match - [_, @supported_version] -> + cond do + not is_nil(state.version) -> line_error(line_number, :duplicate_version) - [_, version] -> + version in @supported_versions -> + {:ok, %{state | version: version}} + + true -> line_error(line_number, {:unsupported_version, version}) end - line == "workflows:" and state.version == @supported_version -> + line == "workflows:" and not is_nil(state.version) -> {:ok, %{state | section: :workflows}} true -> diff --git a/test/rules/actions_lock_test.exs b/test/rules/actions_lock_test.exs index e7e9c9eb..b18c9c1a 100644 --- a/test/rules/actions_lock_test.exs +++ b/test/rules/actions_lock_test.exs @@ -101,6 +101,44 @@ defmodule Hypatia.Rules.ActionsLockTest do ActionsLock.parse(missing_dependency) end + # #926: `gh actions-lock` began writing v0.0.3 with v0.0.2's line grammar. + describe "format v0.0.3" do + @v3_lock String.replace(@valid_lock, "version: 'v0.0.2'", "version: 'v0.0.3'") + + test "is accepted, and records the version it was read as" do + assert {:ok, lock} = ActionsLock.parse(@v3_lock) + assert lock.version == "v0.0.3" + assert ActionsLock.pinned?(lock, "ci.yml", "actions/checkout@v7.0.1") + refute ActionsLock.pinned?(lock, "other.yml", "actions/checkout@v7.0.1") + + assert {:ok, %{version: "v0.0.2"}} = ActionsLock.parse(@valid_lock) + end + + test "is held to the same grammar: a malformed line still fails closed" do + short = String.replace(@v3_lock, ~r/sha1-[0-9a-f]{40}/, "sha1-1234") + assert {:error, {:line, _, :malformed_dependency_commit}} = ActionsLock.parse(short) + + unknown = + String.replace(@v3_lock, " repo_id:", " integrity: 'x'\n repo_id:") + + assert {:error, {:line, _, :malformed_dependency_entry}} = ActionsLock.parse(unknown) + end + + test "negative control: a version past v0.0.3 is still refused" do + assert {:error, {:line, 2, {:unsupported_version, "v0.0.4"}}} = + @valid_lock + |> String.replace("v0.0.2", "v0.0.4") + |> ActionsLock.parse() + end + + test "a second version line is refused, even when both are supported" do + twice = + String.replace(@v3_lock, "version: 'v0.0.3'\n", "version: 'v0.0.3'\nversion: 'v0.0.2'\n") + + assert {:error, {:line, 3, :duplicate_version}} = ActionsLock.parse(twice) + end + end + test "rejects symbolic-ref and SHA-key contradictions" do symbolic = String.replace(@valid_lock, "ref: 'v7.0.1'", "ref: 'v6.0.0'") From 22e9b49f92a2fa44e70eda165cd35bfd8fbbed60 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:10:14 +0000 Subject: [PATCH 2/5] Fix 21 failing CI check(s): GitHub Actions: ABI codegen drift / 0_abi-codegen-drift.txt, GitHub Actions: Hypatia Security Scan / 0_hypatia _ Hypatia Neurosymbolic Analysis.txt, GitHub Actions: Secret Scanner / 0_scan _ shell-secrets.txt, GitHub Actions: Secret Scanner / 1_scan _ gitleaks.txt, GitHub Actions: Secret Scanner / 2_scan _ rust-secrets.txt, GitHub Actions: Security / 3_Rust License & Ban Check.txt, GitHub Actions: Security / 5_Rust Dependency Audit.txt, GitHub Actions: Governance / 1_governance _ Trusted-base reduction policy.txt, GitHub Actions: Governance / 2_governance _ Debt ratchet.txt, GitHub Actions: Governance / 3_governance _ Actions lockfile verify.txt, GitHub Actions: Governance / 4_governance _ Licence consistency.txt, GitHub Actions: Governance / 5_governance _ Live Actions policy (credentialed advisory).txt, GitHub Actions: Governance / 6_governance _ Exemption ratchet.txt, GitHub Actions: Governance / 7_governance _ Workflow security linter.txt, GitHub Actions: Governance / 8_governance _ Well-Known (RFC 9116 + RSR).txt, GitHub Actions: Governance / 9_governance _ Allowlist Preflight.txt, GitHub Actions: Governance / 10_governance _ Code quality + docs.txt, GitHub Actions: Governance / 11_governance _ Security policy checks.txt, GitHub Actions: Governance / 12_governance _ Guix packaging policy (Nix retired).txt, GitHub Actions: Governance / 13_governance _ Language _ package anti-pattern policy.txt, GitHub Actions: Governance / 14_governance _ Check Workflow Staleness.txt Co-Authored-By: CodeRabbit --- deny.toml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 deny.toml diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..ac05e287 --- /dev/null +++ b/deny.toml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MPL-2.0 +# cargo-deny configuration for hypatia +# +# Checked in so the "Rust License & Ban Check" job stops generating its +# fallback config. That fallback uses the pre-0.16 schema +# (`vulnerability`/`unmaintained = "warn"`/`notice`/`unlicensed`/`copyleft`/ +# `default`), which current cargo-deny refuses to deserialize. Under the v2 +# schema vulnerabilities are always denied; `unmaintained`/`yanked` keep the +# old advisory-only intent. + +[graph] +all-features = false + +[advisories] +version = 2 +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +unmaintained = "none" +yanked = "warn" +ignore = [] + +[licenses] +version = 2 +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "CC0-1.0", + "MPL-2.0", + "Unicode-DFS-2016", + "Unicode-3.0", +] +confidence-threshold = 0.8 + +[bans] +multiple-versions = "warn" +wildcards = "warn" +highlight = "all" +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "warn" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] From cce739da3c890a889214bdf3a9bb471290af3e6c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:14:04 +0000 Subject: [PATCH 3/5] fix(rules): accept the v0.0.3 dependency hostname field; drop bot-added deny.toml Codex review of #927 was right: the published v0.0.3 schema (github/actions-lockfile, schema/lockfile-v0.0.3.json) is the v0.0.2 shape plus an optional dependency-level `hostname`, written for pins that are not on the lock's home host (GHE.com data residency). I had inferred the grammar from one lockfile that happens not to use it, so a valid generated lock with a hostname line was refused as malformed. The parser now accepts `hostname: ''` in v0.0.3 only, and only as github.com or a lowercase .ghe.com (the schema's pattern); a malformed, repeated, or v0.0.2 hostname still fails closed. The value is recorded on the dependency. pinned?/3 and the identity checks still ignore the host, which the moduledoc states. Also removes deny.toml, which CodeRabbit's autofix added to this PR. It is outside this change, and its `unmaintained = "none"` silences unmaintained advisories entirely instead of keeping them advisory-only (Codex's second finding). The cargo-deny job fix belongs in its own change. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0165rGu3tsKmJtevmrdMVyfJ --- deny.toml | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 deny.toml diff --git a/deny.toml b/deny.toml deleted file mode 100644 index ac05e287..00000000 --- a/deny.toml +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# cargo-deny configuration for hypatia -# -# Checked in so the "Rust License & Ban Check" job stops generating its -# fallback config. That fallback uses the pre-0.16 schema -# (`vulnerability`/`unmaintained = "warn"`/`notice`/`unlicensed`/`copyleft`/ -# `default`), which current cargo-deny refuses to deserialize. Under the v2 -# schema vulnerabilities are always denied; `unmaintained`/`yanked` keep the -# old advisory-only intent. - -[graph] -all-features = false - -[advisories] -version = 2 -db-path = "~/.cargo/advisory-db" -db-urls = ["https://github.com/rustsec/advisory-db"] -unmaintained = "none" -yanked = "warn" -ignore = [] - -[licenses] -version = 2 -allow = [ - "MIT", - "Apache-2.0", - "Apache-2.0 WITH LLVM-exception", - "BSD-2-Clause", - "BSD-3-Clause", - "ISC", - "Zlib", - "CC0-1.0", - "MPL-2.0", - "Unicode-DFS-2016", - "Unicode-3.0", -] -confidence-threshold = 0.8 - -[bans] -multiple-versions = "warn" -wildcards = "warn" -highlight = "all" -skip = [] -skip-tree = [] - -[sources] -unknown-registry = "deny" -unknown-git = "warn" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] From e1b7f514f42e75163431f2216b6a2849f72e154b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:15:15 +0000 Subject: [PATCH 4/5] fix(rules): accept the v0.0.3 dependency hostname field Codex review of #927 was right: the published v0.0.3 schema (github/actions-lockfile, schema/lockfile-v0.0.3.json) is the v0.0.2 shape plus an optional dependency-level `hostname`, written for pins that are not on the lock's home host (GHE.com data residency). I had inferred the grammar from one lockfile that happens not to use it, so a valid generated lock with a hostname line was refused as malformed. The parser now accepts `hostname: ''` in v0.0.3 only, and only as github.com or a lowercase .ghe.com (the schema's pattern); a malformed, repeated, or v0.0.2 hostname still fails closed. The value is recorded on the dependency. pinned?/3 and the identity checks still ignore the host, which the moduledoc states. (The previous commit, cce739d, only removed the bot-added deny.toml; the parser change was left unstaged by mistake and is in this commit.) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0165rGu3tsKmJtevmrdMVyfJ --- lib/rules/actions_lock.ex | 37 ++++++++++++++++---- test/rules/actions_lock_test.exs | 58 +++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/lib/rules/actions_lock.ex b/lib/rules/actions_lock.ex index d02497c0..6f94ac6a 100644 --- a/lib/rules/actions_lock.ex +++ b/lib/rules/actions_lock.ex @@ -16,19 +16,23 @@ defmodule Hypatia.Rules.ActionsLock do YAML dependency merely to consume that format. Unknown or malformed lines fail closed rather than being guessed at. - Format v0.0.3 is accepted under the v0.0.2 line grammar, unchanged. Measured - 2026-10-19 on the first v0.0.3 lock `gh actions-lock` generated for this - repository (#926): every line has a v0.0.2 shape; only the version line and - the pins differ. Accepting the version therefore admits no new syntax: if a - v0.0.3 file ever carries a line shape v0.0.2 lacks, it still fails closed - here, and the grammar must be extended deliberately, with a fixture, before - it is trusted. + Format v0.0.3 is the v0.0.2 line grammar plus one optional dependency field, + `hostname` (schema: github/actions-lockfile `schema/lockfile-v0.0.3.json`). + It names the GitHub instance that owns a pin and is written only for pins + that are not on the lock's home host (GHE.com data residency). It is + accepted in v0.0.3 only, and only as `github.com` or a lowercase + `.ghe.com`. Any other new line shape still fails closed. + + Limits: `pinned?/3` and the repository-identity checks key on + `owner/repo@ref` and ignore the host, so two pins that differ only by host + are not told apart. """ @supported_versions ["v0.0.2", "v0.0.3"] @action_ref ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+@[^\s':]+$/ @dependency_key ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[^\s':]+$/ @sha_ref ~r/^[0-9A-Fa-f]{40}$/ + @hostname ~r/^(github\.com|[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.ghe\.com)$/ defstruct version: nil, workflows: %{}, dependencies: %{} @@ -37,6 +41,7 @@ defmodule Hypatia.Rules.ActionsLock do commit: String.t(), owner_id: pos_integer(), repo_id: pos_integer(), + hostname: String.t() | nil, uses: MapSet.t(String.t()) } @type t :: %__MODULE__{ @@ -223,6 +228,23 @@ defmodule Hypatia.Rules.ActionsLock do Regex.match?(~r/^ (?:owner_id|repo_id):/, line) -> line_error(line_number, :malformed_dependency_identity) + match = Regex.run(~r/^ hostname: '([^']*)'$/, line) -> + [_, hostname] = match + + cond do + state.version != "v0.0.3" -> + line_error(line_number, {:hostname_requires_v0_0_3, state.version}) + + Regex.match?(@hostname, hostname) -> + put_dependency_field(state, :hostname, hostname, line_number) + + true -> + line_error(line_number, {:malformed_dependency_hostname, hostname}) + end + + Regex.match?(~r/^ hostname:/, line) -> + line_error(line_number, :malformed_dependency_hostname) + line == " uses:" -> put_dependency_field(state, :uses, MapSet.new(), line_number) @@ -340,6 +362,7 @@ defmodule Hypatia.Rules.ActionsLock do commit: commit, owner_id: owner_id, repo_id: repo_id, + hostname: Map.get(dependency, :hostname), uses: Map.get(dependency, :uses, MapSet.new()) } diff --git a/test/rules/actions_lock_test.exs b/test/rules/actions_lock_test.exs index b18c9c1a..27c24ee6 100644 --- a/test/rules/actions_lock_test.exs +++ b/test/rules/actions_lock_test.exs @@ -101,7 +101,8 @@ defmodule Hypatia.Rules.ActionsLockTest do ActionsLock.parse(missing_dependency) end - # #926: `gh actions-lock` began writing v0.0.3 with v0.0.2's line grammar. + # #926: `gh actions-lock` began writing v0.0.3: the v0.0.2 line grammar plus + # an optional dependency `hostname` (github/actions-lockfile lockfile-v0.0.3.json). describe "format v0.0.3" do @v3_lock String.replace(@valid_lock, "version: 'v0.0.2'", "version: 'v0.0.3'") @@ -124,6 +125,61 @@ defmodule Hypatia.Rules.ActionsLockTest do assert {:error, {:line, _, :malformed_dependency_entry}} = ActionsLock.parse(unknown) end + test "accepts and records the optional dependency hostname (GHE.com pins)" do + lock = + String.replace( + @v3_lock, + " owner_id:", + " hostname: 'github.com'\n owner_id:" + ) + + assert {:ok, parsed} = ActionsLock.parse(lock) + assert parsed.dependencies["actions/checkout@v7.0.1"].hostname == "github.com" + assert ActionsLock.pinned?(parsed, "ci.yml", "actions/checkout@v7.0.1") + + tenant = String.replace(lock, "github.com", "octocorp.ghe.com") + assert {:ok, parsed} = ActionsLock.parse(tenant) + assert parsed.dependencies["actions/checkout@v7.0.1"].hostname == "octocorp.ghe.com" + + assert {:ok, parsed} = ActionsLock.parse(@v3_lock) + assert parsed.dependencies["actions/checkout@v7.0.1"].hostname == nil + end + + test "refuses a malformed, repeated, or v0.0.2 hostname" do + with_host = fn host -> + String.replace( + @v3_lock, + " owner_id:", + " hostname: '#{host}'\n owner_id:" + ) + end + + for bad <- [ + "Octocorp.ghe.com", + "https://github.com", + "github.com:443", + "evil.example.com", + "", + "a.ghe.com/x" + ] do + assert {:error, {:line, _, reason}} = ActionsLock.parse(with_host.(bad)) + assert reason in [:malformed_dependency_hostname, {:malformed_dependency_hostname, bad}] + end + + twice = + String.replace( + with_host.("github.com"), + " owner_id:", + " hostname: 'github.com'\n owner_id:" + ) + + assert {:error, {:line, _, {:duplicate_dependency_field, _, :hostname}}} = + ActionsLock.parse(twice) + + v2 = String.replace(with_host.("github.com"), "v0.0.3", "v0.0.2") + assert {:error, {:line, _, {:hostname_requires_v0_0_3, "v0.0.2"}}} = ActionsLock.parse(v2) + end + test "negative control: a version past v0.0.3 is still refused" do assert {:error, {:line, 2, {:unsupported_version, "v0.0.4"}}} = @valid_lock From 2ee756e3e734c40c5d27975171fc4bfd7ded1e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:21:21 +0000 Subject: [PATCH 5/5] fix(rules): scope lockfile owner/repo identity checks to the dependency host Codex review of #927: the v0.0.3 schema defines owner_id and repo_id "on the recorded hostname", but the identity checks compared every dependency regardless of host. A valid GHE lock with the same owner name on a tenant and on github.com (different IDs, or coincident repo IDs) was rejected as an identity mismatch, invalidating the whole lock. The checks now compare only among dependencies recorded on the same host; an omitted hostname is its own scope. Test: the same owner with different IDs is a mismatch on one host and accepted across hosts (fails without the change). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0165rGu3tsKmJtevmrdMVyfJ --- lib/rules/actions_lock.ex | 19 ++++++++++++++---- test/rules/actions_lock_test.exs | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/lib/rules/actions_lock.ex b/lib/rules/actions_lock.ex index 6f94ac6a..41a1ad01 100644 --- a/lib/rules/actions_lock.ex +++ b/lib/rules/actions_lock.ex @@ -23,9 +23,10 @@ defmodule Hypatia.Rules.ActionsLock do accepted in v0.0.3 only, and only as `github.com` or a lowercase `.ghe.com`. Any other new line shape still fails closed. - Limits: `pinned?/3` and the repository-identity checks key on - `owner/repo@ref` and ignore the host, so two pins that differ only by host - are not told apart. + Owner/repository ID consistency is checked per host (IDs are numbered per + GitHub instance). Limit: `pinned?/3` keys on `owner/repo@ref` and ignores the + host, and an omitted hostname is a scope of its own, distinct from an + explicit `github.com`. """ @supported_versions ["v0.0.2", "v0.0.3"] @@ -350,7 +351,7 @@ defmodule Hypatia.Rules.ActionsLock do :ok <- validate_dependency_consistency(key, ref, commit, header_line), :ok <- validate_repository_identity( - state.dependencies, + scope_to_host(state.dependencies, Map.get(dependency, :hostname)), normalized, owner_id, repo_id, @@ -378,6 +379,16 @@ defmodule Hypatia.Rules.ActionsLock do end end + # Owner and repository IDs are numbered per GitHub instance (the v0.0.3 + # schema defines them "on the recorded hostname"), so identity is compared + # only among pins recorded on the same host. An omitted hostname is its own + # scope (the lock's home host). + defp scope_to_host(dependencies, hostname) do + dependencies + |> Enum.filter(fn {_ref, dependency} -> dependency.hostname == hostname end) + |> Map.new() + end + defp validate_repository_identity(dependencies, normalized, owner_id, repo_id, line_number) do [repository, _ref] = String.split(normalized, "@", parts: 2) [owner, _repo] = String.split(repository, "/", parts: 2) diff --git a/test/rules/actions_lock_test.exs b/test/rules/actions_lock_test.exs index 27c24ee6..82d471e4 100644 --- a/test/rules/actions_lock_test.exs +++ b/test/rules/actions_lock_test.exs @@ -145,6 +145,39 @@ defmodule Hypatia.Rules.ActionsLockTest do assert parsed.dependencies["actions/checkout@v7.0.1"].hostname == nil end + test "scopes owner/repo identity checks to the dependency's host" do + # Same owner name and repo, different numeric IDs: a conflict within one + # host, but legitimate across hosts (IDs are numbered per instance). + two_pins = """ + version: 'v0.0.3' + workflows: + '.github/workflows/ci.yml': + - 'actions/checkout@v7.0.1' + - 'actions/setup-node@v7.0.0' + dependencies: + 'actions/checkout@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'actions/setup-node@v7.0.0': + HOST + ref: 'v7.0.0' + commit: 'sha1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + owner_id: 99 + repo_id: 12345 + """ + + same_host = Regex.replace(~r/^ *HOST\n/m, two_pins, "") + + assert {:error, {:line, _, {:owner_identity_mismatch, "actions", 44_036_562, 99}}} = + ActionsLock.parse(same_host) + + cross_host = String.replace(two_pins, "HOST", "hostname: 'octocorp.ghe.com'") + assert {:ok, parsed} = ActionsLock.parse(cross_host) + assert parsed.dependencies["actions/setup-node@v7.0.0"].hostname == "octocorp.ghe.com" + end + test "refuses a malformed, repeated, or v0.0.2 hostname" do with_host = fn host -> String.replace(