Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions test/fixtures/actions_lock/closure/closed-case-variant.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
# Closure corpus fixture: closed: owner/repo spelled in another case than its record (GitHub canonicalises owner/repo). Contract: expected.tsv in this directory.
version: 'v0.0.3'
workflows:
'.github/workflows/rust.yml':
- 'Example/Rust-Cache@5555555555555555555555555555555555555555'
dependencies:
'example/rust-cache@5555555555555555555555555555555555555555':
ref: 'v2.9.2'
commit: 'sha1-5555555555555555555555555555555555555555'
owner_id: 101
repo_id: 205
21 changes: 21 additions & 0 deletions test/fixtures/actions_lock/closure/closed-v0.0.2.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
# Closure corpus fixture: closed, format v0.0.2. Contract: expected.tsv in this directory.
version: 'v0.0.2'
workflows:
'.github/workflows/ci.yml':
- 'example/build@1111111111111111111111111111111111111111'
'.github/workflows/empty.yml': []
dependencies:
'example/build@1111111111111111111111111111111111111111':
ref: 'v1.0.0'
commit: 'sha1-1111111111111111111111111111111111111111'
owner_id: 101
repo_id: 201
uses:
- 'example/setup@2222222222222222222222222222222222222222'
'example/setup@2222222222222222222222222222222222222222':
ref: 'v2.0.0'
commit: 'sha1-2222222222222222222222222222222222222222'
owner_id: 101
repo_id: 202
21 changes: 21 additions & 0 deletions test/fixtures/actions_lock/closure/closed-v0.0.3.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
# Closure corpus fixture: closed, format v0.0.3 with the optional hostname field. Contract: expected.tsv in this directory.
version: 'v0.0.3'
workflows:
'.github/workflows/ci.yml':
- 'example/build@1111111111111111111111111111111111111111'
dependencies:
'example/build@1111111111111111111111111111111111111111':
ref: 'v1.0.0'
commit: 'sha1-1111111111111111111111111111111111111111'
owner_id: 101
repo_id: 201
hostname: 'github.com'
uses:
- 'example/setup@2222222222222222222222222222222222222222'
'example/setup@2222222222222222222222222222222222222222':
ref: 'v2.0.0'
commit: 'sha1-2222222222222222222222222222222222222222'
owner_id: 101
repo_id: 202
15 changes: 15 additions & 0 deletions test/fixtures/actions_lock/closure/dangling-nested.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
# Closure corpus fixture: dangling: a record's nested uses: names a ref with no record (the standards#963 shape). Contract: expected.tsv in this directory.
version: 'v0.0.2'
workflows:
'.github/workflows/push.yml':
- 'example/push-signed@3333333333333333333333333333333333333333'
dependencies:
'example/push-signed@3333333333333333333333333333333333333333':
ref: 'v1.3'
commit: 'sha1-3333333333333333333333333333333333333333'
owner_id: 101
repo_id: 203
uses:
- 'example/setup-python@v2'
14 changes: 14 additions & 0 deletions test/fixtures/actions_lock/closure/dangling-workflow-ref.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
# Closure corpus fixture: dangling: a ref under workflows: with no record. Contract: expected.tsv in this directory.
version: 'v0.0.3'
workflows:
'.github/workflows/release.yml':
- 'example/build@1111111111111111111111111111111111111111'
- 'example/generator@4444444444444444444444444444444444444444'
dependencies:
'example/build@1111111111111111111111111111111111111111':
ref: 'v1.0.0'
commit: 'sha1-1111111111111111111111111111111111111111'
owner_id: 101
repo_id: 201
23 changes: 23 additions & 0 deletions test/fixtures/actions_lock/closure/expected.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Closure corpus for actions.lock (standards#963, criterion 5). Each lock is
# CLOSED when every ref under workflows: and every ref in a record's nested
# uses: has a top-level dependency record, comparing owner/repo
# case-insensitively and the ref exactly (GitHub's lockfile parser
# canonicalises owner/repo case; see #1294 and #1314). Otherwise it is
# DANGLING, and the third column lists the dangling refs as the lock spells
# them, space-separated.
#
# Two implementations must agree with this table:
# * scripts/close-lock.sh --check (scripts/tests/close-lock-test.sh)
# * Hypatia.Rules.ActionsLock.parse/1 in hyperpolymath/hypatia, which
# vendors a byte-identical copy of this directory.
# A third, GitHub's runner, is the reason the table exists.
#
# file verdict dangling
closed-v0.0.2.lock closed -
closed-v0.0.3.lock closed -
closed-case-variant.lock closed -
dangling-nested.lock dangling example/setup-python@v2
dangling-workflow-ref.lock dangling example/generator@4444444444444444444444444444444444444444
67 changes: 67 additions & 0 deletions test/rules/actions_lock_closure_corpus_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: MPL-2.0

defmodule Hypatia.Rules.ActionsLockClosureCorpusTest do
@moduledoc """
Contract test for actions.lock closure, shared with hyperpolymath/standards
(standards#963, criterion 5).

A lock is closed when every ref under `workflows:` and in a record's nested
`uses:` has a top-level record of its own, with owner/repo compared
case-insensitively and the ref exactly. `test/fixtures/actions_lock/closure/`
is a byte-identical copy of standards'
`scripts/tests/fixtures/actions-lock-closure/`. Its `expected.tsv` gives each
lock's verdict and, for a dangling lock, the refs that dangle.

standards' `close-lock.sh --check` runs against the same table, so the two
implementations cannot drift apart without one of these suites going red.
Change both copies together.
"""
use ExUnit.Case, async: true

alias Hypatia.Rules.ActionsLock

@dir "test/fixtures/actions_lock/closure"
@table Path.join(@dir, "expected.tsv")
@external_resource @table

@rows (for line <- String.split(File.read!(@table), "\n"),
line != "",
not String.starts_with?(line, "#") do
[file, verdict, dangling] = String.split(line, "\t")
refs = if dangling == "-", do: [], else: String.split(dangling, " ")
{file, verdict, refs}
end)

@dangling_kinds [:workflow_dependencies_missing, :transitive_dependencies_missing]

test "expected.tsv has exactly one row for each lock in the corpus" do
locks =
@dir
|> Path.join("*.lock")
|> Path.wildcard()
|> Enum.map(&Path.basename/1)
|> Enum.sort()

assert locks == @rows |> Enum.map(&elem(&1, 0)) |> Enum.sort()
assert length(locks) >= 5
end

for {file, verdict, refs} <- @rows do
test "#{file} is #{verdict}" do
result = ActionsLock.parse(File.read!(Path.join(@dir, unquote(file))))
assert_verdict(result, unquote(verdict), unquote(refs))
end
end

defp assert_verdict(result, "closed", []) do
assert {:ok, %ActionsLock{}} = result
end

defp assert_verdict(result, "dangling", refs) do
assert {:error, {kind, entries}} = result
assert kind in @dangling_kinds

got = entries |> Enum.map(&elem(&1, 1)) |> Enum.map(&ActionsLock.normalise_ref/1)
assert Enum.sort(got) == refs |> Enum.map(&ActionsLock.normalise_ref/1) |> Enum.sort()
end
end
Loading