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 )