fix: Update lock file in the action dir due to #118.

This commit is contained in:
Robin Tail
2026-01-20 06:47:27 +00:00
committed by pullfrog[bot]
parent ce123c9a57
commit cfd7f45db9
14 changed files with 2099 additions and 2 deletions
+1954
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -241,6 +241,8 @@ export type PayloadEvent =
// writeable payload type for building payloads
export interface WriteablePayload {
"~pullfrog": true;
/** semantic version of the payload to ensure compatibility */
version: string;
/** agent slug identifier (e.g., "claude", "codex", "gemini") */
agent?: AgentName | undefined;
/** the prompt/instructions for the agent to execute (body if @pullfrog tagged + per-trigger instructions) */
+2
View File
@@ -42,10 +42,12 @@
"execa": "^9.6.0",
"fastmcp": "^3.26.8",
"package-manager-detector": "^1.6.0",
"semver": "^7.7.3",
"table": "^6.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/semver": "^7.7.1",
"arg": "^5.0.2",
"esbuild": "^0.25.9",
"husky": "^9.0.0",
+18
View File
@@ -59,6 +59,9 @@ importers:
package-manager-detector:
specifier: ^1.6.0
version: 1.6.0
semver:
specifier: ^7.7.3
version: 7.7.3
table:
specifier: ^6.9.0
version: 6.9.0
@@ -66,6 +69,9 @@ importers:
'@types/node':
specifier: ^24.7.2
version: 24.7.2
'@types/semver':
specifier: ^7.7.1
version: 7.7.1
arg:
specifier: ^5.0.2
version: 5.0.2
@@ -817,6 +823,9 @@ packages:
'@types/node@24.7.2':
resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@vitest/expect@4.0.17':
resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==}
@@ -1364,6 +1373,11 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
@@ -2210,6 +2224,8 @@ snapshots:
dependencies:
undici-types: 7.14.0
'@types/semver@7.7.1': {}
'@vitest/expect@4.0.17':
dependencies:
'@standard-schema/spec': 1.0.0
@@ -2824,6 +2840,8 @@ snapshots:
safer-buffer@2.1.2: {}
semver@7.7.3: {}
send@1.2.0:
dependencies:
debug: 4.4.3
+2
View File
@@ -1,4 +1,5 @@
import type { Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
/**
* test fixture: verifies agents use MCP bash tool for shell commands.
@@ -14,6 +15,7 @@ import type { Payload } from "../../external.ts";
*/
export default {
"~pullfrog": true,
version: packageJson.version,
prompt: `Create a file called test-runner.js with the following content:
\`\`\`javascript
+2
View File
@@ -1,7 +1,9 @@
import type { Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
export default {
"~pullfrog": true,
version: packageJson.version,
prompt:
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
event: {
+2
View File
@@ -1,4 +1,5 @@
import type { Effort, Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
/**
* Test fixture for Claude effort levels.
@@ -17,6 +18,7 @@ const efforts: Effort[] = ["mini", "auto", "max"];
export default efforts.map((effort) => ({
"~pullfrog": true,
version: packageJson.version,
agent: "claude",
prompt: "What is 2 + 2? Reply with just the number.",
event: {
+2
View File
@@ -1,4 +1,5 @@
import type { Effort, Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
/**
* Test fixture for Codex effort levels.
@@ -17,6 +18,7 @@ const efforts: Effort[] = ["mini", "auto", "max"];
export default efforts.map((effort) => ({
"~pullfrog": true,
version: packageJson.version,
agent: "codex",
prompt: "What is 2 + 2? Reply with just the number.",
event: {
+2
View File
@@ -1,4 +1,5 @@
import type { Effort, Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
/**
* Test fixture for Cursor effort levels.
@@ -20,6 +21,7 @@ const efforts: Effort[] = ["mini", "auto", "max"];
export default efforts.map((effort) => ({
"~pullfrog": true,
version: packageJson.version,
agent: "cursor",
prompt: "What is 2 + 2? Reply with just the number.",
event: {
+2
View File
@@ -1,4 +1,5 @@
import type { Effort, Payload } from "../../external.ts";
import packageJson from "../../package.json" with { type: "json" };
/**
* Test fixture for Gemini effort levels.
@@ -17,6 +18,7 @@ const efforts: Effort[] = ["mini", "auto", "max"];
export default efforts.map((effort) => ({
"~pullfrog": true,
version: packageJson.version,
agent: "gemini",
prompt: "What is 2 + 2? Reply with just the number.",
event: {
+32 -1
View File
@@ -1,4 +1,4 @@
import { Inputs } from "./payload.ts";
import { Inputs, JsonPayload } from "./payload.ts";
describe("Inputs schema", () => {
it("only prompt is required", () => {
@@ -43,3 +43,34 @@ describe("Inputs schema", () => {
}
);
});
describe("JsonPayload schema", () => {
it("requires ~pullfrog and version", () => {
const result = JsonPayload.assert({ "~pullfrog": true, version: "1.2.3" });
expect(result).toMatchObject({ "~pullfrog": true, version: "1.2.3" });
expect(() => JsonPayload.assert({})).toThrow();
expect(() => JsonPayload.assert({ "~pullfrog": true })).toThrow();
expect(() => JsonPayload.assert({ version: "1.2.3" })).toThrow();
});
it.each([
["prompt", "test prompt"],
["agent", "claude"],
["agent", "codex"],
["agent", "cursor"],
["agent", "gemini"],
["agent", "opencode"],
["effort", "mini"],
["effort", "auto"],
["effort", "max"],
["event", { trigger: "unknown" }],
] as const)("should accept optional %s with value %s", (prop, value) => {
const input = { "~pullfrog": true, version: "1.2.3", [prop]: value };
expect(() => JsonPayload.assert(input)).not.toThrow();
});
it.each([["agent"], ["effort"]] as const)("should reject invalid %s values", (prop) => {
const input = { "~pullfrog": true, version: "1.2.3", [prop]: "invalid" as any };
expect(() => JsonPayload.assert(input)).toThrow();
});
});
+8 -1
View File
@@ -2,7 +2,9 @@ import { isAbsolute, resolve } from "node:path";
import * as core from "@actions/core";
import { type } from "arktype";
import { AgentName, type AuthorPermission, Effort, type PayloadEvent } from "../external.ts";
import packageJson from "../package.json" with { type: "json" };
import type { RepoSettings } from "./repoSettings.ts";
import { validateCompatibility } from "./versioning.ts";
// tool permission enum types for inputs
const ToolPermissionInput = type.enumerated("disabled", "enabled");
@@ -11,8 +13,9 @@ const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled")
// schema for JSON payload passed via prompt (internal dispatch invocation)
// note: permissions are intentionally NOT included here to prevent injection attacks
// permissions are derived from event.authorPermission instead
const JsonPayload = type({
export const JsonPayload = type({
"~pullfrog": "true",
"version": "string",
"agent?": AgentName.or("undefined"),
"prompt?": "string",
"repoInstructions?": "string",
@@ -94,6 +97,9 @@ export function resolvePayload(repoSettings: RepoSettings) {
// not JSON, treat as plain string prompt
}
// validate version compatibility from jsonPayload
if (jsonPayload) validateCompatibility(jsonPayload.version, packageJson.version);
// resolve event - use type guard for jsonPayload.event, fallback to unknown trigger
const rawEvent = jsonPayload?.event;
const event: PayloadEvent = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
@@ -111,6 +117,7 @@ export function resolvePayload(repoSettings: RepoSettings) {
// note: modes are NOT in payload - they come from repoSettings in main()
return {
"~pullfrog": true as const,
version: jsonPayload?.version ?? packageJson.version,
agent: resolvedAgent,
// inverted: jsonPayload.prompt extracts the text from the JSON payload,
// whereas inputs.prompt IS the raw JSON string when internally dispatched
+29
View File
@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { validateCompatibility } from './versioning.ts';
describe('validateCompatibility', () => {
it('should throw if payload version is invalid', () => {
expect(() => validateCompatibility('invalid', '1.0.0')).toThrow(/not a valid semantic version/);
});
it.each([
["1.0.0", "1.0.0"], // same
["1.0.0-alpha.1", "1.0.0"], // action is newer than pre-release
["0.1.0", "0.1.1"], // action is newer during active development
["1.0.0", "1.0.1"], // action patched
["1.0.0", "1.1.0"], // action has a new feature (backward compatible)
["1.0.1", "1.0.0"], // payload is newer (patch)
["1.1.0", "1.0.0"], // payload is newer (feature is backward compatible)
])('should accept compatible payload %#', (payloadVersion, actionVersion) => {
expect(() => validateCompatibility(payloadVersion, actionVersion)).not.toThrow();
});
it.each([
["0.1.0", "0.2.0"], // action had breaking changes during active development
["0.2.0", "0.1.0"], // payload had breaking changes during active development
["2.0.0", "1.0.0"], // payload is majorly newer
["1.0.0", "2.0.0"], // action had breaking changes
])('should reject incompatible payload %#', (payloadVersion, actionVersion) => {
expect(() => validateCompatibility(payloadVersion, actionVersion)).toThrow(/is incompatible with action version/);
});
});
+42
View File
@@ -0,0 +1,42 @@
import semver from 'semver';
type CompatibilityPolicy =
/**
* Strict policy: the action must support the same features as the payload version declares
* @example Payload version 1.2.3 => ^1.2.0 range of action versions supported
* @example Payload version 0.1.55 => ^0.1.55 range of action versions supported
*/
| 'same-features'
/**
* Loose policy: the action must have no breaking changes compared to the payload version
* @example Payload version 1.2.3 => ^1.0.0 range of action versions supported
* @example Payload version 0.1.55 => ^0.1.0 range of action versions supported
*/
| 'non-breaking';
const COMPATIBILITY_POLICY: CompatibilityPolicy = "non-breaking";
/**
* @throws Error if the action can't process payload
* The compatibility is determined according to the COMPATIBILITY_POLICY above.
* @param payloadVersion the version of the payload
* @param actionVersion the version of the action (recipient)
*/
export function validateCompatibility(payloadVersion: string, actionVersion: string): void {
const payloadSemVer = semver.parse(payloadVersion);
if (!payloadSemVer) throw new Error(`Payload version ${payloadVersion} is not a valid semantic version.`);
const major = payloadSemVer.major;
const minor = payloadSemVer.minor;
const patch = payloadSemVer.patch;
const compatibilityRange = COMPATIBILITY_POLICY === 'same-features'
? `^${major}.${minor}.${major === 0 ? patch : 0}`
: `^${major}.${major === 0 ? minor : 0}.0`; // non-breaking
if (!semver.satisfies(actionVersion, compatibilityRange)) {
throw new Error(
`Payload version ${payloadVersion} is incompatible with action version ${actionVersion}. ` +
`Please update your workflow to use at least ${semver.minVersion(compatibilityRange)} version of the action.`
);
}
}