/**
* Feature: agentcore-mcp-migration, Property 3: All Third-Party GitHub Actions are SHA-Pinned
*
* Validates: Requirements 8.6
*
* For any `uses:` directive in the GitHub Actions workflow files that references
* a third-party action (not `actions/` org), the version specifier SHALL be a
* full 40-character SHA hash — not a version tag like `v1` or `@main`.
*/
import { describe, it, expect } from 'vitest';
import fc from 'fast-check';
import { readFileSync, readdirSync } from 'fs';
import { resolve, join } from 'path';
const WORKFLOWS_DIR = resolve(import.meta.dirname, '..', '.github', 'workflows');
/**
* Extract all `uses:` directives from a workflow YAML file.
* Returns an array of { action, version } objects.
*/
function extractUsesDirectives(yamlContent) {
const directives = [];
const usesRegex = /uses:\s*([^\s#]+)/g;
let match;
while ((match = usesRegex.exec(yamlContent)) !== null) {
const full = match[1];
const atIndex = full.indexOf('@');
if (atIndex === -1) continue;
const action = full.substring(0, atIndex);
const version = full.substring(atIndex + 1);
directives.push({ action, version });
}
return directives;
}
/**
* Determine if an action is first-party (actions/ org).
* First-party actions are maintained by GitHub and exempt from SHA-pinning requirement.
*/
function isFirstParty(action) {
return action.startsWith('actions/');
}
/** Check if a string is a valid 40-character hex SHA */
function isFullSha(version) {
return /^[0-9a-f]{40}$/.test(version);
}
// Read all workflow files
const workflowFiles = readdirSync(WORKFLOWS_DIR)
.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
.map(f => ({
name: f,
content: readFileSync(join(WORKFLOWS_DIR, f), 'utf-8'),
}));
// Extract all third-party uses directives across all workflow files
const thirdPartyUses = workflowFiles.flatMap(wf =>
extractUsesDirectives(wf.content)
.filter(d => !isFirstParty(d.action))
.map(d => ({ ...d, file: wf.name }))
);
describe('Feature: agentcore-mcp-migration, Property 3: All Third-Party GitHub Actions are SHA-Pinned', () => {
it('should find at least one workflow file', () => {
expect(workflowFiles.length).toBeGreaterThan(0);
});
it('should find at least one third-party action reference', () => {
expect(thirdPartyUses.length).toBeGreaterThan(0);
});
it('all third-party actions should be pinned to a full 40-char SHA', () => {
for (const use of thirdPartyUses) {
expect(
isFullSha(use.version),
`${use.file}: ${use.action}@${use.version} is not SHA-pinned`
).toBe(true);
}
});
it('property: no randomly generated version tag should pass the SHA-pin check for actual actions', () => {
/**
* **Validates: Requirements 8.6**
*
* Generate random version-tag-like strings (v1, v2.3, main, latest, etc.)
* and verify that none of them would be accepted as a valid SHA pin.
* This confirms our SHA validation logic correctly rejects non-SHA versions.
*/
const versionTag = fc.oneof(
// Semver-like tags: v1, v2.3, v1.2.3
fc.tuple(fc.integer({ min: 1, max: 99 }), fc.integer({ min: 0, max: 99 }), fc.integer({ min: 0, max: 99 }))
.map(([a, b, c]) => `v${a}.${b}.${c}`),
fc.integer({ min: 1, max: 99 }).map(n => `v${n}`),
// Branch names
fc.constantFrom('main', 'master', 'develop', 'release', 'latest', 'stable'),
// Short SHAs (not full 40-char)
fc.hexaString({ minLength: 7, maxLength: 12 })
);
fc.assert(
fc.property(versionTag, (tag) => {
// No version tag should pass the full SHA check
return !isFullSha(tag);
}),
{ numRuns: 100 }
);
});
it('property: any valid 40-char hex string should pass the SHA check', () => {
/**
* **Validates: Requirements 8.6**
*
* Generate valid 40-character hex strings and verify they all pass
* the SHA validation. This confirms our check accepts legitimate SHAs.
*/
const validSha = fc.hexaString({ minLength: 40, maxLength: 40 })
.map(s => s.toLowerCase());
fc.assert(
fc.property(validSha, (sha) => {
return isFullSha(sha);
}),
{ numRuns: 100 }
);
});
});