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
59 changes: 47 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71878,6 +71878,8 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.classifyError = classifyError;
exports.runPreflightChecks = runPreflightChecks;
exports.verifyCommits = verifyCommits;
exports.processGpgResults = processGpgResults;
exports.isGpgSignedCommit = isGpgSignedCommit;
exports.classifyArtifactError = classifyArtifactError;
exports.verifyArtifact = verifyArtifact;
exports.getCommitsInRange = getCommitsInRange;
Expand Down Expand Up @@ -72005,10 +72007,10 @@ async function verifyCommits(commitRange, options) {
try {
const parsed = JSON.parse(stdout.trim());
if (Array.isArray(parsed)) {
verifyResults = processGpgResults(parsed);
verifyResults = await processGpgResults(parsed);
}
else {
verifyResults = processGpgResults([parsed]);
verifyResults = await processGpgResults([parsed]);
}
}
catch (e) {
Expand All @@ -72029,22 +72031,55 @@ async function verifyCommits(commitRange, options) {
* Process results to handle GPG-signed commits gracefully.
* GPG-signed commits are marked as skipped rather than failed.
*/
function processGpgResults(results) {
return results.map(result => {
if (!result.valid && result.error &&
result.error.toLowerCase().includes('gpg')) {
return {
async function processGpgResults(results) {
const processed = [];
for (const result of results) {
if (!result.valid && (await isGpgSignedCommit(result.commit))) {
processed.push({
...result,
valid: true,
skipped: true,
skipReason: 'GPG signature (not SSH)'
};
});
continue;
}
if (!result.valid && result.error && !result.failureType) {
return { ...result, failureType: classifyError(result.error) };
processed.push({ ...result, failureType: classifyError(result.error) });
continue;
}
return result;
});
processed.push(result);
}
return processed;
}
/**
* Determine whether a commit carries a PGP (GPG) signature rather than an SSH one.
*
* Reads the raw commit object so the decision rests on the actual signature type,
* never on substring-matching a verification error. Only PGP-signed commits are the
* intentionally-unsupported kind that this action skips; SSH/KERI signatures are the
* ones it verifies, so a failure on those must stay a failure.
*
* Args:
* * `commit`: The commit SHA to inspect.
*
* Usage:
* ```ignore
* if (await isGpgSignedCommit(sha)) { /* skip — out of scope *\/ }
* ```
*/
async function isGpgSignedCommit(commit) {
let raw = '';
try {
await exec.exec('git', ['cat-file', '-p', commit], {
listeners: { stdout: (data) => { raw += data.toString(); } },
ignoreReturnCode: true,
silent: true
});
}
catch {
return false;
}
return /^gpgsig /m.test(raw) && raw.includes('-----BEGIN PGP SIGNATURE-----');
}
/**
* Verify commits one by one (fallback method)
Expand Down Expand Up @@ -72077,7 +72112,7 @@ async function verifyCommitsOneByOne(authsPath, commits, options) {
if (stdout.trim()) {
try {
const result = JSON.parse(stdout.trim());
results.push(...processGpgResults([result]));
results.push(...(await processGpgResults([result])));
continue;
}
catch {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

51 changes: 50 additions & 1 deletion src/__tests__/verifier.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAuthsDownloadUrl, getBinaryName, getCommitsInRange, verifyChecksum, ensureAuthsInstalled, verifyArtifact, classifyArtifactError, ArtifactVerificationResult } from '../verifier';
import { getAuthsDownloadUrl, getBinaryName, getCommitsInRange, verifyChecksum, ensureAuthsInstalled, verifyArtifact, classifyArtifactError, ArtifactVerificationResult, processGpgResults, VerificationResult } from '../verifier';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -271,6 +271,55 @@ describe('verifyChecksum', () => {
});
});

describe('processGpgResults', () => {
beforeEach(() => {
jest.resetAllMocks();
});

const gitReturning = (raw: string) =>
mockExec.exec.mockImplementation(async (_cmd: string, _args: string[], opts: any) => {
opts?.listeners?.stdout?.(Buffer.from(raw));
return 0;
});

it('does not flip a real failure to valid just because the error text contains "gpg"', async () => {
gitReturning('tree 0000\nauthor a <a@x> 1 +0000\n\nmsg\n'); // not pgp-signed
const results: VerificationResult[] = [
{ commit: 'deadbeef', valid: false, error: 'invalid signature (gpg parser noise in a real failure)' }
];

const [out] = await processGpgResults(results);

expect(out.valid).toBe(false);
expect(out.skipped).toBeFalsy();
});

it('skips a commit positively identified as PGP-signed', async () => {
gitReturning('tree 0000\ngpgsig -----BEGIN PGP SIGNATURE-----\n abc\n -----END PGP SIGNATURE-----\n\nmsg\n');
const results: VerificationResult[] = [
{ commit: 'cafebabe', valid: false, error: 'no auths trailer' }
];

const [out] = await processGpgResults(results);

expect(out.valid).toBe(true);
expect(out.skipped).toBe(true);
expect(out.skipReason).toMatch(/gpg/i);
});

it('does not skip a failing SSH-signed commit (in scope, must stay failed)', async () => {
gitReturning('tree 0000\ngpgsig -----BEGIN SSH SIGNATURE-----\n abc\n -----END SSH SIGNATURE-----\n\nmsg\n');
const results: VerificationResult[] = [
{ commit: 'feedface', valid: false, error: 'signature verification failed' }
];

const [out] = await processGpgResults(results);

expect(out.valid).toBe(false);
expect(out.skipped).toBeFalsy();
});
});

describe('ensureAuthsInstalled - cross-run caching', () => {
const realTmpdir = require('os').tmpdir();
const cachePath = path.join(realTmpdir, 'auths-cache');
Expand Down
57 changes: 45 additions & 12 deletions src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ export async function verifyCommits(
try {
const parsed = JSON.parse(stdout.trim());
if (Array.isArray(parsed)) {
verifyResults = processGpgResults(parsed);
verifyResults = await processGpgResults(parsed);
} else {
verifyResults = processGpgResults([parsed]);
verifyResults = await processGpgResults([parsed]);
}
} catch (e) {
core.warning(`Failed to parse auths output: ${stdout}`);
Expand All @@ -190,22 +190,55 @@ export async function verifyCommits(
* Process results to handle GPG-signed commits gracefully.
* GPG-signed commits are marked as skipped rather than failed.
*/
function processGpgResults(results: VerificationResult[]): VerificationResult[] {
return results.map(result => {
if (!result.valid && result.error &&
result.error.toLowerCase().includes('gpg')) {
return {
export async function processGpgResults(results: VerificationResult[]): Promise<VerificationResult[]> {
const processed: VerificationResult[] = [];
for (const result of results) {
if (!result.valid && (await isGpgSignedCommit(result.commit))) {
processed.push({
...result,
valid: true,
skipped: true,
skipReason: 'GPG signature (not SSH)'
};
});
continue;
}
if (!result.valid && result.error && !result.failureType) {
return { ...result, failureType: classifyError(result.error) };
processed.push({ ...result, failureType: classifyError(result.error) });
continue;
}
return result;
});
processed.push(result);
}
return processed;
}

/**
* Determine whether a commit carries a PGP (GPG) signature rather than an SSH one.
*
* Reads the raw commit object so the decision rests on the actual signature type,
* never on substring-matching a verification error. Only PGP-signed commits are the
* intentionally-unsupported kind that this action skips; SSH/KERI signatures are the
* ones it verifies, so a failure on those must stay a failure.
*
* Args:
* * `commit`: The commit SHA to inspect.
*
* Usage:
* ```ignore
* if (await isGpgSignedCommit(sha)) { /* skip — out of scope *\/ }
* ```
*/
export async function isGpgSignedCommit(commit: string): Promise<boolean> {
let raw = '';
try {
await exec.exec('git', ['cat-file', '-p', commit], {
listeners: { stdout: (data: Buffer) => { raw += data.toString(); } },
ignoreReturnCode: true,
silent: true
});
} catch {
return false;
}
return /^gpgsig /m.test(raw) && raw.includes('-----BEGIN PGP SIGNATURE-----');
}

/**
Expand Down Expand Up @@ -246,7 +279,7 @@ async function verifyCommitsOneByOne(
if (stdout.trim()) {
try {
const result = JSON.parse(stdout.trim());
results.push(...processGpgResults([result]));
results.push(...(await processGpgResults([result])));
continue;
} catch {
// Fall through to default result
Expand Down
Loading