chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:11 +08:00
commit 70bf21e064
5213 changed files with 731895 additions and 0 deletions
@@ -0,0 +1,246 @@
import {
mkdirSync,
mkdtempSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import dedent from "ts-dedent";
import { afterEach, beforeEach, describe, it } from "vitest";
import { validateActionPinning } from "../validate-action-pinning";
describe("validateActionPinning()", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), "action-pinning-")));
mkdirSync(join(tmpDir, ".github", "workflows"), { recursive: true });
});
afterEach(() => {
// eslint-disable-next-line workers-sdk/no-direct-recursive-rm -- test cleanup
rmSync(tmpDir, { recursive: true, force: true });
});
function writeWorkflow(filename: string, content: string) {
writeFileSync(
join(tmpDir, ".github", "workflows", filename),
content,
"utf-8"
);
}
function writeCompositeAction(actionDir: string, content: string) {
const dir = join(tmpDir, ".github", "actions", actionDir);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "action.yml"), content, "utf-8");
}
it("should pass for pinned third-party actions", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
`
);
expect(validateActionPinning(tmpDir)).toEqual([]);
});
it("should pass for pinned actions with a version comment", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
`
);
expect(validateActionPinning(tmpDir)).toEqual([]);
});
it("should fail for third-party actions pinned to a tag", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: dorny/paths-filter@v3
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("dorny/paths-filter@v3");
expect(errors[0]).toContain("not pinned to a commit SHA");
});
it("should fail for third-party actions pinned to a semver tag", ({
expect,
}) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: Ana06/get-changed-files@v2.3.0
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("Ana06/get-changed-files@v2.3.0");
});
it("should fail for third-party actions pinned to a branch", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: changesets/action@main
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("changesets/action@main");
});
it("should skip first-party actions/* regardless of pinning", ({
expect,
}) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/upload-artifact@v4
`
);
expect(validateActionPinning(tmpDir)).toEqual([]);
});
it("should skip local actions", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: ./.github/actions/install-dependencies
`
);
expect(validateActionPinning(tmpDir)).toEqual([]);
});
it("should report multiple errors across multiple files", ({ expect }) => {
writeWorkflow(
"a.yml",
dedent`
jobs:
build:
steps:
- uses: foo/bar@v1
`
);
writeWorkflow(
"b.yml",
dedent`
jobs:
build:
steps:
- uses: baz/qux@v2
- uses: quux/corge@latest
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(3);
const joined = errors.join("\n");
expect(joined).toContain("foo/bar@v1");
expect(joined).toContain("baz/qux@v2");
expect(joined).toContain("quux/corge@latest");
});
it("should validate composite actions in .github/actions/", ({ expect }) => {
writeCompositeAction(
"my-action",
dedent`
runs:
using: composite
steps:
- uses: unpinned/action@v1
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("unpinned/action@v1");
expect(errors[0]).toContain(".github/actions/my-action/action.yml");
});
it("should include the correct line number in errors", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
name: Test
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: unpinned/action@v3
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain(":8");
});
it("should handle uses without a hyphen prefix", ({ expect }) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- name: Do something
uses: unpinned/action@v1
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("unpinned/action@v1");
});
it("should fail for third-party actions with no version reference at all", ({
expect,
}) => {
writeWorkflow(
"test.yml",
dedent`
jobs:
build:
steps:
- uses: some-org/some-action
`
);
const errors = validateActionPinning(tmpDir);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain("some-org/some-action");
expect(errors[0]).toContain("has no version reference at all");
});
it("should pass when there are no workflow files", ({ expect }) => {
// tmpDir has the .github/workflows/ directory but no files
expect(validateActionPinning(tmpDir)).toEqual([]);
});
});
@@ -0,0 +1,106 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { globSync } from "tinyglobby";
/**
* Matches `uses:` directives in GitHub Actions workflow YAML files.
* Captures the action reference (everything after `uses:`), trimmed.
*
* Examples that match:
* - uses: actions/checkout@v4
* uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
*/
const USES_RE = /^\s*-?\s*uses:\s*(.+)$/;
/**
* A valid SHA pin is exactly 40 lowercase hex characters.
*/
const SHA_RE = /^[0-9a-f]{40}$/;
/**
* First-party GitHub action prefixes that are trusted and do not
* need to be pinned to a commit SHA.
*/
const TRUSTED_PREFIXES = ["actions/"];
/**
* Checks that all third-party GitHub Actions in workflow and composite
* action files are pinned to a full commit SHA rather than a mutable
* tag or branch reference.
*
* @param repoRoot - Absolute path to the repository root directory.
* @returns An array of human-readable error strings (empty = all good).
*/
export function validateActionPinning(repoRoot: string): string[] {
const patterns = [
".github/workflows/*.yml",
".github/workflows/*.yaml",
".github/actions/*/action.yml",
".github/actions/*/action.yaml",
];
const files = patterns.flatMap((pattern) =>
globSync(pattern, { cwd: repoRoot })
);
const errors: string[] = [];
for (const relPath of files) {
const absPath = resolve(repoRoot, relPath);
const content = readFileSync(absPath, "utf-8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(USES_RE);
if (!match) {
continue;
}
const raw = match[1].trim();
// Local actions (e.g. ./.github/actions/foo) — skip
if (raw.startsWith("./") || raw.startsWith("../")) {
continue;
}
// Trusted first-party actions — skip
if (TRUSTED_PREFIXES.some((prefix) => raw.startsWith(prefix))) {
continue;
}
// At this point it's a third-party action.
// Expected format: owner/repo@<ref> (possibly with a trailing comment)
const atIndex = raw.indexOf("@");
if (atIndex === -1) {
errors.push(
`${relPath}:${i + 1}${raw.split(/\s/)[0]} has no version reference at all`
);
continue;
}
// The ref is everything after @ up to the first space (comments follow a space)
const afterAt = raw.slice(atIndex + 1).split(/\s/)[0];
if (!SHA_RE.test(afterAt)) {
errors.push(
`${relPath}:${i + 1}${raw.split(/\s/)[0]} is not pinned to a commit SHA`
);
}
}
}
return errors;
}
if (require.main === module) {
console.log("::group::Checking GitHub Action pinning");
const repoRoot = resolve(__dirname, "../..");
const errors = validateActionPinning(repoRoot);
if (errors.length > 0) {
console.error(
"::error::Action pinning checks:" + errors.map((e) => `\n- ${e}`).join("")
);
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
}