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
31 changes: 6 additions & 25 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { getTrackerID } = require("./get_tracker_id.cjs");
const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs");
const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { replaceTemporaryIdReferences, isTemporaryId } = require("./temporary_id.cjs");
const { replaceTemporaryIdReferences, getOrGenerateTemporaryId } = require("./temporary_id.cjs");
const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs");
const { addExpirationToFooter } = require("./ephemerals.cjs");
const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
Expand Down Expand Up @@ -247,31 +247,12 @@ async function main(config = {}) {

const pullRequestItem = message;

let temporaryId;
if (pullRequestItem.temporary_id !== undefined && pullRequestItem.temporary_id !== null) {
if (typeof pullRequestItem.temporary_id !== "string") {
core.warning(`Skipping create_pull_request: temporary_id must be a string (got ${typeof pullRequestItem.temporary_id})`);
return {
success: false,
error: `temporary_id must be a string (got ${typeof pullRequestItem.temporary_id})`,
};
}

const rawTemporaryId = pullRequestItem.temporary_id.trim();
const normalized = rawTemporaryId.startsWith("#") ? rawTemporaryId.substring(1).trim() : rawTemporaryId;

if (!isTemporaryId(normalized)) {
core.warning(
`Skipping create_pull_request: Invalid temporary_id format: '${pullRequestItem.temporary_id}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: 'aw_abc' or 'aw_Test123'`
);
return {
success: false,
error: `Invalid temporary_id format: '${pullRequestItem.temporary_id}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: 'aw_abc' or 'aw_Test123'`,
};
}

temporaryId = normalized.toLowerCase();
const tempIdResult = getOrGenerateTemporaryId(pullRequestItem, "pull request");
if (tempIdResult.error) {
core.warning(`Skipping create_pull_request: ${tempIdResult.error}`);
return { success: false, error: tempIdResult.error };
}
const temporaryId = tempIdResult.temporaryId;

core.info(`Processing create_pull_request: title=${pullRequestItem.title || "No title"}, bodyLength=${pullRequestItem.body?.length || 0}`);

Expand Down
36 changes: 23 additions & 13 deletions actions/setup/js/temporary_id.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ const crypto = require("crypto");

/**
* Regex pattern for matching temporary ID references in text
* Format: #aw_XXX to #aw_XXXXXXXXXXXX (aw_ prefix + 3 to 12 alphanumeric characters)
* Format: #aw_XXX to #aw_XXXXXXXXXXXX (aw_ prefix + 3 to 12 alphanumeric or underscore characters)
*/
const TEMPORARY_ID_PATTERN = /#(aw_[A-Za-z0-9]{3,12})\b/gi;
const TEMPORARY_ID_PATTERN = /#(aw_[A-Za-z0-9_]{3,12})\b/gi;

/**
* Regex pattern for detecting candidate #aw_ references (any length, word-boundary delimited)
* Used to identify malformed temporary ID references that don't match TEMPORARY_ID_PATTERN
* Regex pattern for detecting candidate #aw_ references (any alphanumeric, underscore, or hyphen content)
* Used to identify malformed temporary ID references that don't match TEMPORARY_ID_PATTERN.
* Uses a broader character set (including hyphens) than the valid pattern to capture the full token
* and warn about references like #aw_test-id where the hyphen makes the whole token invalid.
*/
const TEMPORARY_ID_CANDIDATE_PATTERN = /#aw_([A-Za-z0-9]+)\b/gi;
const TEMPORARY_ID_CANDIDATE_PATTERN = /#aw_([A-Za-z0-9_-]+)/gi;

/**
* @typedef {Object} RepoIssuePair
Expand All @@ -57,13 +59,13 @@ function generateTemporaryId() {
}

/**
* Check if a value is a valid temporary ID (aw_ prefix + 3 to 12 alphanumeric characters)
* Check if a value is a valid temporary ID (aw_ prefix + 3 to 12 alphanumeric or underscore characters)
* @param {any} value - The value to check
* @returns {boolean} True if the value is a valid temporary ID
*/
function isTemporaryId(value) {
if (typeof value === "string") {
return /^aw_[A-Za-z0-9]{3,12}$/i.test(value);
return /^aw_[A-Za-z0-9_]{3,12}$/i.test(value);
}
return false;
}
Expand Down Expand Up @@ -92,7 +94,7 @@ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
while ((candidate = TEMPORARY_ID_CANDIDATE_PATTERN.exec(text)) !== null) {
const tempId = `aw_${candidate[1]}`;
if (!isTemporaryId(tempId)) {
core.warning(`Malformed temporary ID reference '${candidate[0]}' found in body text. Temporary IDs must be in format '#aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: '#aw_abc' or '#aw_Test123'`);
core.warning(`Malformed temporary ID reference '${candidate[0]}' found in body text. Temporary IDs must be in format '#aw_' followed by 3 to 12 alphanumeric or underscore characters (A-Za-z0-9_). Example: '#aw_abc' or '#aw_pr_fix'`);
}
}

Expand Down Expand Up @@ -132,7 +134,8 @@ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {

/**
* Validate and process a temporary_id from a message
* Auto-generates a temporary ID if not provided, or validates and normalizes if provided
* Auto-generates a temporary ID if not provided, or validates and normalizes if provided.
* If the format is invalid, emits a warning and auto-generates a new ID instead of failing.
*
* @param {Object} message - The message object that may contain a temporary_id field
* @param {string} entityType - Type of entity (e.g., "issue", "discussion", "project") for error messages
Expand Down Expand Up @@ -160,9 +163,16 @@ function getOrGenerateTemporaryId(message, entityType = "item") {
const normalized = rawTemporaryId.startsWith("#") ? rawTemporaryId.substring(1).trim() : rawTemporaryId;

if (!isTemporaryId(normalized)) {
// Warn and auto-generate rather than failing - an invalid temporary_id is a minor issue
const autoGenerated = generateTemporaryId();
if (typeof core !== "undefined") {
core.warning(
`Invalid temporary_id format: '${message.temporary_id}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric or underscore characters (A-Za-z0-9_). Example: 'aw_abc' or 'aw_pr_fix'. Using auto-generated ID: '${autoGenerated}'`
);
}
return {
temporaryId: null,
error: `Invalid temporary_id format: '${message.temporary_id}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: 'aw_abc' or 'aw_Test123'`,
temporaryId: autoGenerated,
error: null,
};
}

Expand Down Expand Up @@ -298,14 +308,14 @@ function resolveIssueNumber(value, temporaryIdMap) {
return {
resolved: null,
wasTemporaryId: false,
errorMessage: `Invalid temporary ID format: '${valueStr}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: 'aw_abc' or 'aw_abc12345'`,
errorMessage: `Invalid temporary ID format: '${valueStr}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric or underscore characters (A-Za-z0-9_). Example: 'aw_abc' or 'aw_pr_fix'`,
};
}

// It's a real issue number - use context repo as default
const issueNumber = typeof value === "number" ? value : parseInt(valueWithoutHash, 10);
if (isNaN(issueNumber) || issueNumber <= 0) {
return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}. Expected either a valid temporary ID (format: aw_ followed by 3-12 alphanumeric characters) or a numeric issue number.` };
return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}. Expected either a valid temporary ID (format: aw_ followed by 3-12 alphanumeric or underscore characters) or a numeric issue number.` };
}

const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
Expand Down
66 changes: 64 additions & 2 deletions actions/setup/js/temporary_id.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ describe("temporary_id.cjs", () => {
expect(isTemporaryId("aw_123456789abc")).toBe(true); // 12 chars - at the limit
});

it("should return true for valid aw_ prefixed strings with underscores", async () => {
const { isTemporaryId } = await import("./temporary_id.cjs");
expect(isTemporaryId("aw_id_123")).toBe(true); // Contains underscore - now valid
expect(isTemporaryId("aw_pr_fix")).toBe(true); // Underscore-separated words
expect(isTemporaryId("aw_pr_testfix")).toBe(true); // From the original issue
});

it("should return false for invalid strings", async () => {
const { isTemporaryId } = await import("./temporary_id.cjs");
expect(isTemporaryId("abc123def456")).toBe(false); // Missing aw_ prefix
expect(isTemporaryId("aw_ab")).toBe(false); // Too short (2 chars)
expect(isTemporaryId("aw_1234567890abc")).toBe(false); // Too long (13 chars)
expect(isTemporaryId("aw_test-id")).toBe(false); // Contains hyphen
expect(isTemporaryId("aw_id_123")).toBe(false); // Contains underscore
expect(isTemporaryId("")).toBe(false);
expect(isTemporaryId("temp_abc123")).toBe(false); // Wrong prefix
});
Expand Down Expand Up @@ -171,6 +177,62 @@ describe("temporary_id.cjs", () => {
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("#aw_ab"));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("#aw_toolongname123"));
});

it("should warn about malformed temporary ID reference containing a hyphen", async () => {
const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs");
const map = new Map();
const text = "Check #aw_test-id for details";
const result = replaceTemporaryIdReferences(text, map, "owner/repo");
expect(result).toBe("Check #aw_test-id for details");
expect(mockCore.warning).toHaveBeenCalledOnce();
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("#aw_test-id"));
});
});

describe("getOrGenerateTemporaryId", () => {
it("should auto-generate a temporary ID when not provided", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ title: "Test" });
expect(result.error).toBeNull();
expect(result.temporaryId).toMatch(/^aw_[A-Za-z0-9]{8}$/);
});

it("should return valid temporary ID when provided", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ temporary_id: "aw_abc123" });
expect(result.error).toBeNull();
expect(result.temporaryId).toBe("aw_abc123");
});

it("should accept temporary ID with underscores", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ temporary_id: "aw_pr_fix" });
expect(result.error).toBeNull();
expect(result.temporaryId).toBe("aw_pr_fix");
});

it("should accept the aw_pr_testfix format from the original issue", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ temporary_id: "aw_pr_testfix" });
expect(result.error).toBeNull();
expect(result.temporaryId).toBe("aw_pr_testfix");
});

it("should warn and auto-generate when format is invalid instead of failing", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ temporary_id: "aw_toolongidentifier123" });
expect(result.error).toBeNull();
expect(result.temporaryId).toMatch(/^aw_[A-Za-z0-9]{8}$/);
expect(mockCore.warning).toHaveBeenCalledOnce();
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("aw_toolongidentifier123"));
});

it("should return error when temporary_id is not a string", async () => {
const { getOrGenerateTemporaryId } = await import("./temporary_id.cjs");
const result = getOrGenerateTemporaryId({ temporary_id: 123 });
expect(result.error).toContain("temporary_id must be a string");
expect(result.temporaryId).toBeNull();
});
});

describe("replaceTemporaryIdReferencesLegacy", () => {
Expand Down Expand Up @@ -323,7 +385,7 @@ describe("temporary_id.cjs", () => {
expect(result.wasTemporaryId).toBe(false);
expect(result.errorMessage).toContain("Invalid temporary ID format");
expect(result.errorMessage).toContain("aw_test-id");
expect(result.errorMessage).toContain("3 to 12 alphanumeric characters");
expect(result.errorMessage).toContain("3 to 12 alphanumeric or underscore characters");
});

it("should return specific error for malformed temporary ID (too short)", async () => {
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -762,11 +762,11 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient =
// Validate IDs used for draft chaining.
// Draft issue chaining must use strict temporary IDs to match the unified handler manager.
if (temporaryId && !isTemporaryId(temporaryId)) {
throw new Error(`${ERR_VALIDATION}: Invalid temporary_id format: "${temporaryId}". Expected format: aw_ followed by 3 to 12 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`);
throw new Error(`${ERR_VALIDATION}: Invalid temporary_id format: "${temporaryId}". Expected format: aw_ followed by 3 to 12 alphanumeric or underscore characters (e.g., "aw_abc", "aw_pr_fix").`);
}

if (draftIssueId && !isTemporaryId(draftIssueId)) {
throw new Error(`${ERR_VALIDATION}: Invalid draft_issue_id format: "${draftIssueId}". Expected format: aw_ followed by 3 to 12 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`);
throw new Error(`${ERR_VALIDATION}: Invalid draft_issue_id format: "${draftIssueId}". Expected format: aw_ followed by 3 to 12 alphanumeric or underscore characters (e.g., "aw_abc", "aw_pr_fix").`);
}

const draftTitle = typeof output.draft_title === "string" ? output.draft_title.trim() : "";
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/update_project.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ describe("updateProject", () => {

queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project-draft")]);

await expect(updateProject(output)).rejects.toThrow(/Invalid temporary_id format.*aw_ followed by 3 to 12 alphanumeric characters/);
await expect(updateProject(output)).rejects.toThrow(/Invalid temporary_id format.*aw_ followed by 3 to 12 alphanumeric or underscore characters/);
});

it("rejects malformed auto-generated draft_issue_id with aw_ prefix", async () => {
Expand All @@ -906,7 +906,7 @@ describe("updateProject", () => {

queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(projectUrl, 60, "project-draft")]);

await expect(updateProject(output, temporaryIdMap)).rejects.toThrow(/Invalid draft_issue_id format.*aw_ followed by 3 to 12 alphanumeric characters/);
await expect(updateProject(output, temporaryIdMap)).rejects.toThrow(/Invalid draft_issue_id format.*aw_ followed by 3 to 12 alphanumeric or underscore characters/);
});

it("rejects draft_issue without title when creating (no draft_issue_id)", async () => {
Expand Down
Loading