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
5 changes: 5 additions & 0 deletions .changeset/damp-dogs-earn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"go-mod-validator": minor
---

feat: repo-sha-exceptions input for allow specific repository commits as exceptions
11 changes: 9 additions & 2 deletions apps/go-mod-validator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ Requirements:
`${{ github.workspace }}`
- `dep-prefix`: Prefix to filter dependencies to check. By default, we use
`github.com/smartcontractkit`
- `repo-branch-exceptions` - Input allowing exceptions for non-default branches
on certain repositories.
- `repo-branch-exceptions` - Long-lived exceptions allowing specific branches as
valid references for certain repositories (e.g. release branches).
- The input is newline delimited, in the format of:
- `<owner>/<repo>:<branch-1>,<optional branches>`
- Example: `smartcontractkit/.github:develop`
- `repo-sha-exceptions` - Short-lived exceptions allowing specific commit SHAs
as valid references for certain repositories (e.g. hotfixes, pre-merge pins).
Only applies to pseudo-version dependencies (commit-pinned). SHA matching is
prefix-based, so a 12-char abbreviated SHA matches a full 40-char SHA.
- The input is newline delimited, in the format of:
- `<owner>/<repo>:<sha-1>,<optional shas>`
- Example: `smartcontractkit/chainlink:abcdefabcdef`

## Outputs

Expand Down
11 changes: 11 additions & 0 deletions apps/go-mod-validator/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ inputs:
smartcontractkit/chainlink:main,release/2.30.1
smartcontractkit/chainlink-ccip:develop
default: ""
repo-sha-exceptions:
description: |
Each line should contain a repository and a comma separated list of commit SHAs.
Only applies to pseudo-version dependencies (commit-pinned, not tag-based).
SHA matching is prefix-based: a 12-char abbreviated SHA matches a full 40-char SHA with that prefix.
Format: <owner>/<repo>:<sha1>,<sha2>,...
Can include the same repo multiple times to append more SHAs.
Example:
smartcontractkit/chainlink:abcdefabcdef
smartcontractkit/chainlink-ccip:deadbeef1234,cafebabe5678
default: ""

runs:
using: "node24"
Expand Down
33 changes: 32 additions & 1 deletion apps/go-mod-validator/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions apps/go-mod-validator/scripts/payload.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
{
"//pull_request": {
"base": { "sha": "1f5fbda7ae76d5494b9864db90a8bfe7183db5fb" },
"head": { "sha": "04c256fe307ced678afbebb5cf87abab747ccb8b" },
"number": 773
},
"issue": {
"number": -1,
"pull_request": {
"number": -1
}
"pull_request": {
"base": { "sha": "5b92e2511d16d1aaafc73ce05c51e6ece0f0363f" },
"head": { "sha": "ee78d55de54240967d8dc405ef6d42d6b4996e6b" },
"number": 22528
}
}
8 changes: 6 additions & 2 deletions apps/go-mod-validator/scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
abspath() { cd "$1" && pwd; }

REPO_ROOT="$(git rev-parse --show-toplevel)"
echo "Repo Root: $REPO_ROOT"

pnpm nx build go-mod-validator

export GITHUB_TOKEN=$(gh auth token)
export GITHUB_ACTOR=$(gh api user --jq .login)
export GITHUB_REPOSITORY="smartcontractkit/billing-platform-service"
export GITHUB_REPOSITORY="smartcontractkit/chainlink"
export GITHUB_EVENT_NAME="push"
export GITHUB_EVENT_PATH="apps/go-mod-validator/scripts/payload.json"

Expand All @@ -18,8 +19,11 @@ tmp_file=$(mktemp)
export GITHUB_STEP_SUMMARY="$tmp_file"

export INPUT_GITHUB_TOKEN="$GITHUB_TOKEN"
export INPUT_GO_MOD_DIR="${INPUT_GO_MOD_DIR:-$(abspath "${REPO_ROOT}/../billing-platform-service")}"
export INPUT_GO_MOD_DIR="${INPUT_GO_MOD_DIR:-$(realpath "${REPO_ROOT}/../chainlink/")}"
echo INPUT_GO_MOD_DIR="$INPUT_GO_MOD_DIR"
export INPUT_DEP_PREFIX="github.com/smartcontractkit"
# export INPUT_REPO_BRANCH_EXCEPTIONS="smartcontractkit/chainlink-aptos:ogt/cherry-pick-revert-txm-duration-config-types-into-2-47-0-branch"
export INPUT_REPO_SHA_EXCEPTIONS="smartcontractkit/chainlink-aptos:c2a8d09e5b46c1c7815700f645a50caf8a466bda"
export CL_LOCAL_DEBUG="true"

node apps/go-mod-validator/dist/index.js
Expand Down
21 changes: 20 additions & 1 deletion apps/go-mod-validator/src/go-mod-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { throttling } from "@octokit/plugin-throttling";
import { getDeps, BaseGoModule, lineForDependencyPathFinder } from "./deps";
import { getChangedGoModFiles } from "./diff";
import { getDefaultBranch, isGoModReferencingBranch } from "./github";
import { getInputs } from "./run-inputs";
import { getInputs, shaMatches } from "./run-inputs";
import { FIXING_ERRORS } from "./strings";

import type { Octokit } from "./github";
Expand Down Expand Up @@ -182,6 +182,25 @@ async function validateDependency(
): Promise<Invalidation | null> {
core.info(`Validating dependency: ${dep.owner}/${dep.repo}@${dep.version}`);

// SHA exception check: only for pseudo-version (commit-pinned) deps.
// Short-circuits before any GitHub API call.
if ("commitSha" in dep) {
const shaExceptions =
inputs.repoShaExceptions.get(`${dep.owner}/${dep.repo}`) || [];
core.debug(
`SHA exceptions for ${dep.owner}/${dep.repo}: ${shaExceptions.join(", ")}`,
);
const matchedSha = shaExceptions.find((exc) =>
shaMatches(dep.commitSha, exc),
);
if (matchedSha) {
core.info(
`[${dep.goModFilePath}] dependency ${dep.name} allowed by SHA exception (matched: ${matchedSha})`,
);
return null;
}
}

const defaultBranch = await getDefaultBranch(octokit, dep);
const exceptions =
inputs.repoBranchExceptions.get(`${dep.owner}/${dep.repo}`) || [];
Expand Down
25 changes: 25 additions & 0 deletions apps/go-mod-validator/src/run-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface RunInputs {
goModDir: string;
depPrefix: string;
repoBranchExceptions: Map<string, string[]>;
repoShaExceptions: Map<string, string[]>;
}

export function getInputs(): RunInputs {
Expand All @@ -23,6 +24,7 @@ export function getInputs(): RunInputs {
repoBranchExceptions: getRunInputRepoBranchExceptions(
"repoBranchExceptions",
),
repoShaExceptions: getRunInputRepoBranchExceptions("repoShaExceptions"),
};

logInputs(inputs);
Expand All @@ -42,6 +44,11 @@ function logInputs(inputs: RunInputs) {
Array.from(inputs.repoBranchExceptions.entries()),
)}`,
);
core.info(
` repoShaExceptions: ${JSON.stringify(
Array.from(inputs.repoShaExceptions.entries()),
)}`,
);
}

interface RunInputConfiguration {
Expand Down Expand Up @@ -80,6 +87,10 @@ const runInputsConfiguration: {
parameter: "repo-branch-exceptions",
localParameter: "REPO_BRANCH_EXCEPTIONS",
},
repoShaExceptions: {
parameter: "repo-sha-exceptions",
localParameter: "REPO_SHA_EXCEPTIONS",
},
};

function getRunInputString(input: keyof RunInputs, defaultValue = "") {
Expand Down Expand Up @@ -140,6 +151,20 @@ export function getRunInputRepoBranchExceptions(
return repoBranchMap;
}

/**
* Note: Exported for testing purposes.
*
* Returns true if depSha and exceptionSha refer to the same commit.
* Prefix-matches to handle abbreviated (12-char) vs full (40-char) SHAs.
* Go pseudo-versions always carry 12-char abbreviated SHAs.
*/
export function shaMatches(depSha: string, exceptionSha: string): boolean {
if (!depSha || !exceptionSha) return false;
const dep = depSha.toLowerCase();
const exc = exceptionSha.toLowerCase();
return dep.length <= exc.length ? exc.startsWith(dep) : dep.startsWith(exc);
}

function parseRepoBranchLine(line: string): [string, string[]] {
const [repo, ...rest] = line.split(":").map((s) => s.trim());
if (!repo) {
Expand Down
37 changes: 36 additions & 1 deletion apps/go-mod-validator/test/run-input.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { getRunInputRepoBranchExceptions } from "../src/run-inputs";
import { getRunInputRepoBranchExceptions, shaMatches } from "../src/run-inputs";
import * as core from "@actions/core";

vi.mock("@actions/core", () => ({
Expand Down Expand Up @@ -162,3 +162,38 @@ smartcontractkit/.github: develop,feature/test`;
}).toThrow("No branch in line: smartcontractkit/.github main");
});
});

describe("shaMatches", () => {
it("returns true for exact 12-char match", () => {
expect(shaMatches("abcdefabcdef", "abcdefabcdef")).toBe(true);
});

it("returns false for exact 12-char mismatch", () => {
expect(shaMatches("abcdefabcdef", "111111111111")).toBe(false);
});

it("returns true when 40-char exception starts with the 12-char dep SHA", () => {
expect(
shaMatches("abcdefabcdef", "abcdefabcdef1234567890abcdefabcdef12345678"),
).toBe(true);
});

it("returns false when 40-char exception does not start with the dep SHA", () => {
expect(
shaMatches("abcdefabcdef", "111111111111abcdefabcdef1234567890abcdef"),
).toBe(false);
});

it("is case-insensitive", () => {
expect(shaMatches("abcdefabcdef", "ABCDEFABCDEF")).toBe(true);
expect(shaMatches("ABCDEFABCDEF", "abcdefabcdef")).toBe(true);
});

it("returns false when depSha is empty", () => {
expect(shaMatches("", "abcdefabcdef")).toBe(false);
});

it("returns false when exceptionSha is empty", () => {
expect(shaMatches("abcdefabcdef", "")).toBe(false);
});
});
Loading