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
50 changes: 46 additions & 4 deletions actions/setup/js/check_workflow_timestamp_api.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,51 @@
*/

const { getErrorMessage } = require("./error_helpers.cjs");
const { computeFrontmatterHash, extractHashFromLockFile, createGitHubFileReader } = require("./frontmatter_hash_pure.cjs");
const { extractHashFromLockFile } = require("./frontmatter_hash_pure.cjs");
const { getFileContent } = require("./github_api_helpers.cjs");

/**
* Compute frontmatter hash using the Go binary (gh aw hash-frontmatter)
* This ensures consistency between compilation and validation
* @param {string} workflowPath - Path to the workflow file
* @returns {Promise<string>} The SHA-256 hash as a lowercase hexadecimal string
*/
async function computeFrontmatterHashViaGo(workflowPath) {
try {
let hashOutput = "";
let errorOutput = "";

const exitCode = await exec.exec("gh", ["aw", "hash-frontmatter", workflowPath], {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: data => {
hashOutput += data.toString();
},
stderr: data => {
errorOutput += data.toString();
},
},
});

if (exitCode !== 0) {
throw new Error(`gh aw hash-frontmatter failed with exit code ${exitCode}: ${errorOutput}`);
}

// Extract the hash from output (remove any ANSI codes and whitespace)
const hash = hashOutput.replace(/\x1b\[[0-9;]*m/g, "").trim();

// Validate hash format (should be 64 hex characters)
if (!/^[a-f0-9]{64}$/.test(hash)) {
throw new Error(`Invalid hash format received: ${hash}`);
}

return hash;
} catch (error) {
throw new Error(`Failed to compute hash via Go binary: ${getErrorMessage(error)}`);
}
}

async function main() {
const workflowFile = process.env.GH_AW_WORKFLOW_FILE;

Expand Down Expand Up @@ -78,9 +120,9 @@ async function main() {
return;
}

// Compute hash from source .md file using GitHub API
const fileReader = createGitHubFileReader(github, owner, repo, ref);
const recomputedHash = await computeFrontmatterHash(workflowMdPath, { fileReader });
// Compute hash using Go binary for consistency with compilation
// Note: This requires the workflow file to exist locally in the checkout
const recomputedHash = await computeFrontmatterHashViaGo(workflowMdPath);

// Log hash comparison
core.info(`Frontmatter hash comparison:`);
Expand Down
68 changes: 50 additions & 18 deletions actions/setup/js/check_workflow_timestamp_api.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ const mockContext = {
sha: "abc123",
};

const mockExec = {
exec: vi.fn(),
};

global.core = mockCore;
global.github = mockGithub;
global.context = mockContext;
global.exec = mockExec;

describe("check_workflow_timestamp_api.cjs", () => {
let main;
Expand Down Expand Up @@ -367,7 +372,8 @@ describe("check_workflow_timestamp_api.cjs", () => {
});

it("should log frontmatter hash comparison when both files exist", async () => {
const lockFileContent = `# frontmatter-hash: abc123def456
const validHash = "cdb5fdf551a14f93f6a8bb32b4f8ee5a6e93a8075052ecd915180be7fbc168ca";
const lockFileContent = `# frontmatter-hash: ${validHash}
name: Test Workflow
on: push
jobs:
Expand Down Expand Up @@ -405,27 +411,30 @@ engine: copilot
],
});

mockGithub.rest.repos.getContent
.mockResolvedValueOnce({
data: {
type: "file",
encoding: "base64",
content: Buffer.from(lockFileContent).toString("base64"),
},
})
.mockResolvedValueOnce({
data: {
type: "file",
encoding: "base64",
content: Buffer.from(mdFileContent).toString("base64"),
},
});
mockGithub.rest.repos.getContent.mockResolvedValueOnce({
data: {
type: "file",
encoding: "base64",
content: Buffer.from(lockFileContent).toString("base64"),
},
});

// Mock the gh aw hash-frontmatter command to return a matching hash
mockExec.exec.mockImplementation((command, args, options) => {
if (command === "gh" && args[0] === "aw" && args[1] === "hash-frontmatter") {
// Call the stdout listener with a hash
options.listeners.stdout(Buffer.from(validHash + "\n"));
return Promise.resolve(0);
}
return Promise.resolve(0);
});

await main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Frontmatter hash comparison"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Lock file hash:"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Recomputed hash:"));
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "hash-frontmatter", ".github/workflows/test.md"], expect.any(Object));
});

it("should handle missing frontmatter hash in lock file", async () => {
Expand Down Expand Up @@ -473,6 +482,14 @@ jobs:
});

it("should handle errors during hash computation gracefully", async () => {
const validHash = "cdb5fdf551a14f93f6a8bb32b4f8ee5a6e93a8075052ecd915180be7fbc168ca";
const lockFileContent = `# frontmatter-hash: ${validHash}
name: Test Workflow
on: push
jobs:
test:
runs-on: ubuntu-latest`;

mockGithub.rest.repos.listCommits
.mockResolvedValueOnce({
data: [
Expand All @@ -497,11 +514,26 @@ jobs:
],
});

mockGithub.rest.repos.getContent.mockRejectedValue(new Error("API error"));
mockGithub.rest.repos.getContent.mockResolvedValueOnce({
data: {
type: "file",
encoding: "base64",
content: Buffer.from(lockFileContent).toString("base64"),
},
});

// Mock the gh aw hash-frontmatter command to fail
mockExec.exec.mockImplementation((command, args, options) => {
if (command === "gh" && args[0] === "aw" && args[1] === "hash-frontmatter") {
options.listeners.stderr(Buffer.from("Command failed"));
return Promise.resolve(1); // Exit code 1 indicates failure
}
return Promise.resolve(0);
});

await main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Could not fetch content"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Could not compute frontmatter hash"));
expect(mockCore.setFailed).not.toHaveBeenCalled(); // Should not fail the workflow
});
});
Expand Down
Loading