Developer-Dashboard
view release on metacpan or search on metacpan
t/142-action-pin-provenance.t view on Meta::CPAN
like( $err, qr/no SHA-pinned actions/, 'the gate reports an empty pin set as unusable, not clean' );
}
# The clean case.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'actions/checkout', $SHA_PINNED, 'v7.0.1' ) },
contents => { "actions/checkout|action.yml|$SHA_PINNED" => "runs:\n using: node24\n" },
tags => { 'actions/checkout|v7.0.1' => $SHA_PINNED },
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 0, 'a pin whose comment resolves to its own SHA on a node24 runtime passes' );
like( $out, qr/examined 1 SHA-pinned action\b/, 'the gate reports how many pins it examined' );
like( $out, qr/FIXTURE MODE/, 'a fixture-backed run announces that it is not network-verified' );
}
# The runtime floor - the defect that broke CI for ten days. A node20 action is
# force-run on node24 by GitHub and may or may not survive it, so it is a latent
# failure rather than a warning.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'shogo82148/actions-setup-perl', $SHA_PINNED, 'v1.31.3' ) },
contents => { "shogo82148/actions-setup-perl|action.yml|$SHA_PINNED" => qq{runs:\n using: "node20"\n} },
tags => { 'shogo82148/actions-setup-perl|v1.31.3' => $SHA_PINNED },
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 1, 'a pin declaring node20 is a finding' );
like( $err, qr/declares node20, below the node24 floor/, 'the gate names the runtime it rejected' );
}
# The provenance defect itself: the comment is trusted by t/34's version floors,
# so a comment naming a tag that resolves to a different commit is exactly the
# failure this gate exists to catch, and it must NOT be reported as unusable.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'actions/checkout', $SHA_PINNED, 'v5.2.2' ) },
contents => { "actions/checkout|action.yml|$SHA_PINNED" => "runs:\n using: node24\n" },
tags => { 'actions/checkout|v5.2.2' => $SHA_OTHER },
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 1, 'a comment naming a tag that resolves to another commit is a finding' );
like( $err, qr/does not describe the pinned commit/,
'the gate explains that the comment and the SHA disagree' );
}
# An unresolvable tag is "could not audit", not "clean" and not "wrong".
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'actions/checkout', $SHA_PINNED, 'v9.9.9' ) },
contents => { "actions/checkout|action.yml|$SHA_PINNED" => "runs:\n using: node24\n" },
tags => {},
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 3, 'a tag that cannot be resolved is reported as unusable' );
like( $err, qr/could not resolve upstream tag v9\.9\.9/, 'the gate names the tag it could not resolve' );
}
# An unreadable manifest is likewise unusable. This is the case the round that
# wrote this gate hit for real: /repos/.../commits/{sha} answers "No commit
# found" for github/codeql-action even for a current release tag, so an
# endpoint saying no is not proof that a pin is bad.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'actions/checkout', $SHA_PINNED, undef ) },
contents => {},
tags => {},
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 3, 'a pin whose action.yml cannot be read is reported as unusable' );
like( $err, qr/could not read action\.yml at the pinned SHA/,
'the gate distinguishes "could not look" from "looked and it is wrong"' );
}
# Docker and composite actions declare no node runtime at all, so the floor must
# not invent one for them.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => _uses( 'some/docker-action', $SHA_PINNED, undef ) },
contents => { "some/docker-action|action.yml|$SHA_PINNED" => "runs:\n using: docker\n image: Dockerfile\n" },
tags => {},
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 0, 'an action with no node runtime is not judged against the node floor' );
}
# A monorepo action lives at a subdirectory, so the manifest lookup has to use
# the sub-path rather than the repository root.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'codeql.yml' => _uses( 'github/codeql-action/init', $SHA_PINNED, 'v4.37.6' ) },
contents => { "github/codeql-action|init/action.yml|$SHA_PINNED" => "runs:\n using: node24\n" },
tags => { 'github/codeql-action|v4.37.6' => $SHA_PINNED },
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 0, 'an action published from a monorepo subdirectory resolves through its sub-path' );
}
# checkout is pinned in five workflows here, so the report has to collapse the
# repeats into one audited pin while still naming every file that carries it -
# otherwise a five-file pin produces five identical findings.
{
my ( $dir, $fixture ) = _scenario(
workflows => {
'test.yml' => _uses( 'actions/checkout', $SHA_PINNED, 'v7.0.1' ),
'codeql.yml' => _uses( 'actions/checkout', $SHA_PINNED, 'v7.0.1' ),
},
contents => { "actions/checkout|action.yml|$SHA_PINNED" => qq{runs:\n using: 'node20'\n} },
tags => { 'actions/checkout|v7.0.1' => $SHA_PINNED },
);
my ( $rc, $out, $err ) = _run_gate( [$dir], $fixture );
is( $rc, 1, 'a repeated pin still fails once its runtime is below the floor' );
like( $out, qr/examined 1 SHA-pinned action\b/, 'a pin repeated across workflows is audited once' );
like( $err, qr/codeql\.yml, test\.yml/, 'the finding names every workflow carrying the pin' );
}
# A floating tag carries no 40-hex SHA and so is not a pin at all; t/34 is what
# forbids those. This gate must simply not claim to have audited one.
{
my ( $dir, $fixture ) = _scenario(
workflows => { 'test.yml' => "jobs:\n a:\n steps:\n - uses: actions/checkout\@v4\n" },
contents => {},
( run in 0.814 second using v1.01-cache-2.11-cpan-9789f410c06 )