diff --git a/.github/workflows/campaign-generator.lock.yml b/.github/workflows/campaign-generator.lock.yml
index ff29806712f..9e17f015198 100644
--- a/.github/workflows/campaign-generator.lock.yml
+++ b/.github/workflows/campaign-generator.lock.yml
@@ -7522,7 +7522,7 @@ jobs:
};
EOF_4d21ccbd
- cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_006d32d7'
+ cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_60283df2'
// @ts-check
///
@@ -7607,11 +7607,12 @@ jobs:
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -7661,6 +7662,17 @@ jobs:
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -7705,12 +7717,13 @@ jobs:
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -7754,6 +7767,7 @@ jobs:
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
@@ -7875,7 +7889,7 @@ jobs:
createGetSummaryLine,
};
- EOF_006d32d7
+ EOF_60283df2
- name: Assign To Agent
id: assign_to_agent
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent'))
diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml
index a11cbf99991..5778d0a4d89 100644
--- a/.github/workflows/changeset.lock.yml
+++ b/.github/workflows/changeset.lock.yml
@@ -8269,7 +8269,7 @@ jobs:
};
EOF_d0693c3b
- cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_006d32d7'
+ cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_60283df2'
// @ts-check
///
@@ -8354,11 +8354,12 @@ jobs:
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -8408,6 +8409,17 @@ jobs:
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -8452,12 +8464,13 @@ jobs:
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -8501,6 +8514,7 @@ jobs:
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
@@ -8622,7 +8636,7 @@ jobs:
createGetSummaryLine,
};
- EOF_006d32d7
+ EOF_60283df2
- name: Update Pull Request
id: update_pull_request
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'update_pull_request'))
diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index 27f005a58f6..db50ba0729a 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -8404,7 +8404,7 @@ jobs:
};
EOF_4d21ccbd
- cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_006d32d7'
+ cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_60283df2'
// @ts-check
///
@@ -8489,11 +8489,12 @@ jobs:
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -8543,6 +8544,17 @@ jobs:
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -8587,12 +8599,13 @@ jobs:
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -8636,6 +8649,7 @@ jobs:
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
@@ -8757,7 +8771,7 @@ jobs:
createGetSummaryLine,
};
- EOF_006d32d7
+ EOF_60283df2
- name: Update Discussion
id: update_discussion
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'update_discussion'))
@@ -8788,8 +8802,28 @@ jobs:
includeOperation: false,
});
async function executeDiscussionUpdate(github, context, discussionNumber, updateData) {
- const { _operation, _rawBody, ...fieldsToUpdate } = updateData;
- const getDiscussionQuery = `
+ const { _operation, _rawBody, labels, ...fieldsToUpdate } = updateData;
+ const shouldUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true" && labels !== undefined;
+ const getDiscussionQuery = shouldUpdateLabels
+ ? `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
+ id
+ title
+ body
+ url
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ `
+ : `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
discussion(number: $number) {
@@ -8809,9 +8843,11 @@ jobs:
if (!queryResult?.repository?.discussion) {
throw new Error(`Discussion #${discussionNumber} not found`);
}
- const discussionId = queryResult.repository.discussion.id;
- if (fieldsToUpdate.title === undefined && fieldsToUpdate.body === undefined) {
- throw new Error("At least one field (title or body) must be provided for update");
+ const discussion = queryResult.repository.discussion;
+ const discussionId = discussion.id;
+ const currentLabels = shouldUpdateLabels ? discussion.labels?.nodes || [] : [];
+ if (fieldsToUpdate.title === undefined && fieldsToUpdate.body === undefined && !shouldUpdateLabels) {
+ throw new Error("At least one field (title, body, or labels) must be provided for update");
}
if (fieldsToUpdate.body !== undefined) {
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
@@ -8826,22 +8862,130 @@ jobs:
const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
fieldsToUpdate.body = fieldsToUpdate.body + footer;
}
- const mutationFields = [];
- if (fieldsToUpdate.title !== undefined) {
- mutationFields.push("title: $title");
+ if (fieldsToUpdate.title !== undefined || fieldsToUpdate.body !== undefined) {
+ const mutationFields = [];
+ if (fieldsToUpdate.title !== undefined) {
+ mutationFields.push("title: $title");
+ }
+ if (fieldsToUpdate.body !== undefined) {
+ mutationFields.push("body: $body");
+ }
+ const updateDiscussionMutation = `
+ mutation($discussionId: ID!${fieldsToUpdate.title !== undefined ? ", $title: String!" : ""}${fieldsToUpdate.body !== undefined ? ", $body: String!" : ""}) {
+ updateDiscussion(input: {
+ discussionId: $discussionId
+ ${mutationFields.join("\n ")}
+ }) {
+ discussion {
+ id
+ number
+ title
+ body
+ url
+ }
+ }
+ }
+ `;
+ const variables = {
+ discussionId: discussionId,
+ };
+ if (fieldsToUpdate.title !== undefined) {
+ variables.title = fieldsToUpdate.title;
+ }
+ if (fieldsToUpdate.body !== undefined) {
+ variables.body = fieldsToUpdate.body;
+ }
+ const mutationResult = await github.graphql(updateDiscussionMutation, variables);
+ if (!mutationResult?.updateDiscussion?.discussion) {
+ throw new Error("Failed to update discussion");
+ }
}
- if (fieldsToUpdate.body !== undefined) {
- mutationFields.push("body: $body");
- }
- const updateDiscussionMutation = `
- mutation($discussionId: ID!${fieldsToUpdate.title !== undefined ? ", $title: String!" : ""}${fieldsToUpdate.body !== undefined ? ", $body: String!" : ""}) {
- updateDiscussion(input: {
- discussionId: $discussionId
- ${mutationFields.join("\n ")}
- }) {
- discussion {
+ if (shouldUpdateLabels && Array.isArray(labels)) {
+ const repoQuery = `
+ query($owner: String!, $repo: String!) {
+ repository(owner: $owner, name: $repo) {
+ id
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ `;
+ const repoResult = await github.graphql(repoQuery, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ if (!repoResult?.repository) {
+ throw new Error(`Repository ${context.repo.owner}/${context.repo.repo} not found`);
+ }
+ const repoLabels = repoResult.repository.labels?.nodes || [];
+ const labelIds = labels.map(labelName => {
+ const label = repoLabels.find(l => l.name === labelName);
+ if (!label) {
+ throw new Error(`Label "${labelName}" not found in repository`);
+ }
+ return label.id;
+ });
+ if (currentLabels.length > 0) {
+ const removeLabelsMutation = `
+ mutation($labelableId: ID!, $labelIds: [ID!]!) {
+ removeLabelsFromLabelable(input: {
+ labelableId: $labelableId
+ labelIds: $labelIds
+ }) {
+ clientMutationId
+ }
+ }
+ `;
+ await github.graphql(removeLabelsMutation, {
+ labelableId: discussionId,
+ labelIds: currentLabels.map(l => l.id),
+ });
+ }
+ if (labelIds.length > 0) {
+ const addLabelsMutation = `
+ mutation($labelableId: ID!, $labelIds: [ID!]!) {
+ addLabelsToLabelable(input: {
+ labelableId: $labelableId
+ labelIds: $labelIds
+ }) {
+ clientMutationId
+ }
+ }
+ `;
+ await github.graphql(addLabelsMutation, {
+ labelableId: discussionId,
+ labelIds: labelIds,
+ });
+ }
+ }
+ const finalQuery = shouldUpdateLabels
+ ? `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
+ id
+ title
+ body
+ url
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ `
+ : `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
id
- number
title
body
url
@@ -8849,23 +8993,15 @@ jobs:
}
}
`;
- const variables = {
- discussionId: discussionId,
- };
- if (fieldsToUpdate.title !== undefined) {
- variables.title = fieldsToUpdate.title;
- }
- if (fieldsToUpdate.body !== undefined) {
- variables.body = fieldsToUpdate.body;
- }
- const mutationResult = await github.graphql(updateDiscussionMutation, variables);
- if (!mutationResult?.updateDiscussion?.discussion) {
- throw new Error("Failed to update discussion");
- }
- const discussion = mutationResult.updateDiscussion.discussion;
+ const finalQueryResult = await github.graphql(finalQuery, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ number: discussionNumber,
+ });
+ const updatedDiscussion = finalQueryResult.repository.discussion;
return {
- ...discussion,
- html_url: discussion.url,
+ ...updatedDiscussion,
+ html_url: updatedDiscussion.url,
};
}
const getSummaryLine = createGetSummaryLine({
diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index 4e1e5e46a50..2b6a7a4af85 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -10633,7 +10633,7 @@ jobs:
};
EOF_4d21ccbd
- cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_006d32d7'
+ cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_60283df2'
// @ts-check
///
@@ -10718,11 +10718,12 @@ jobs:
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -10772,6 +10773,17 @@ jobs:
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -10816,12 +10828,13 @@ jobs:
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -10865,6 +10878,7 @@ jobs:
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
@@ -10986,7 +11000,7 @@ jobs:
createGetSummaryLine,
};
- EOF_006d32d7
+ EOF_60283df2
- name: Create Issue
id: create_issue
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_issue'))
diff --git a/.github/workflows/sub-issue-closer.lock.yml b/.github/workflows/sub-issue-closer.lock.yml
index 79ff3044722..68936429e28 100644
--- a/.github/workflows/sub-issue-closer.lock.yml
+++ b/.github/workflows/sub-issue-closer.lock.yml
@@ -7428,7 +7428,7 @@ jobs:
};
EOF_4d21ccbd
- cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_006d32d7'
+ cat > /tmp/gh-aw/scripts/update_runner.cjs << 'EOF_60283df2'
// @ts-check
///
@@ -7513,11 +7513,12 @@ jobs:
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -7567,6 +7568,17 @@ jobs:
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -7611,12 +7623,13 @@ jobs:
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -7660,6 +7673,7 @@ jobs:
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
@@ -7781,7 +7795,7 @@ jobs:
createGetSummaryLine,
};
- EOF_006d32d7
+ EOF_60283df2
- name: Add Comment
id: add_comment
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_comment'))
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 242abcbb833..d2f5ac0670e 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -3369,6 +3369,17 @@
"type": "null",
"description": "Allow updating discussion body - presence of key indicates field can be updated"
},
+ "labels": {
+ "type": "null",
+ "description": "Allow updating discussion labels - presence of key indicates field can be updated"
+ },
+ "allowed-labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones)."
+ },
"max": {
"type": "integer",
"description": "Maximum number of discussions to update (default: 1)",
diff --git a/pkg/workflow/compiler_safe_outputs_consolidated.go b/pkg/workflow/compiler_safe_outputs_consolidated.go
index a1cc03341ae..de1f4231edf 100644
--- a/pkg/workflow/compiler_safe_outputs_consolidated.go
+++ b/pkg/workflow/compiler_safe_outputs_consolidated.go
@@ -983,6 +983,9 @@ func (c *Compiler) buildUpdateDiscussionStepConfig(data *WorkflowData, mainJobNa
if cfg.Body != nil {
customEnvVars = append(customEnvVars, " GH_AW_UPDATE_BODY: \"true\"\n")
}
+ if cfg.Labels != nil {
+ customEnvVars = append(customEnvVars, " GH_AW_UPDATE_LABELS: \"true\"\n")
+ }
condition := BuildSafeOutputType("update_discussion")
diff --git a/pkg/workflow/js/update_discussion.cjs b/pkg/workflow/js/update_discussion.cjs
index c8432e9f94f..f1eee2ff90b 100644
--- a/pkg/workflow/js/update_discussion.cjs
+++ b/pkg/workflow/js/update_discussion.cjs
@@ -24,10 +24,32 @@ const renderStagedItem = createRenderStagedItem({
*/
async function executeDiscussionUpdate(github, context, discussionNumber, updateData) {
// Remove internal fields used for operation handling
- const { _operation, _rawBody, ...fieldsToUpdate } = updateData;
+ const { _operation, _rawBody, labels, ...fieldsToUpdate } = updateData;
+
+ // Check if labels should be updated based on environment variable
+ const shouldUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true" && labels !== undefined;
// First, fetch the discussion node ID using its number
- const getDiscussionQuery = `
+ const getDiscussionQuery = shouldUpdateLabels
+ ? `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
+ id
+ title
+ body
+ url
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ `
+ : `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
discussion(number: $number) {
@@ -50,11 +72,13 @@ async function executeDiscussionUpdate(github, context, discussionNumber, update
throw new Error(`Discussion #${discussionNumber} not found`);
}
- const discussionId = queryResult.repository.discussion.id;
+ const discussion = queryResult.repository.discussion;
+ const discussionId = discussion.id;
+ const currentLabels = shouldUpdateLabels ? discussion.labels?.nodes || [] : [];
// Ensure at least one field is being updated
- if (fieldsToUpdate.title === undefined && fieldsToUpdate.body === undefined) {
- throw new Error("At least one field (title or body) must be provided for update");
+ if (fieldsToUpdate.title === undefined && fieldsToUpdate.body === undefined && !shouldUpdateLabels) {
+ throw new Error("At least one field (title, body, or labels) must be provided for update");
}
// Add footer to body if body is being updated
@@ -76,56 +100,174 @@ async function executeDiscussionUpdate(github, context, discussionNumber, update
fieldsToUpdate.body = fieldsToUpdate.body + footer;
}
- // Build the update mutation dynamically based on which fields are being updated
- const mutationFields = [];
- if (fieldsToUpdate.title !== undefined) {
- mutationFields.push("title: $title");
- }
- if (fieldsToUpdate.body !== undefined) {
- mutationFields.push("body: $body");
+ // Update title and/or body if needed
+ if (fieldsToUpdate.title !== undefined || fieldsToUpdate.body !== undefined) {
+ // Build the update mutation dynamically based on which fields are being updated
+ const mutationFields = [];
+ if (fieldsToUpdate.title !== undefined) {
+ mutationFields.push("title: $title");
+ }
+ if (fieldsToUpdate.body !== undefined) {
+ mutationFields.push("body: $body");
+ }
+
+ const updateDiscussionMutation = `
+ mutation($discussionId: ID!${fieldsToUpdate.title !== undefined ? ", $title: String!" : ""}${fieldsToUpdate.body !== undefined ? ", $body: String!" : ""}) {
+ updateDiscussion(input: {
+ discussionId: $discussionId
+ ${mutationFields.join("\n ")}
+ }) {
+ discussion {
+ id
+ number
+ title
+ body
+ url
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ discussionId: discussionId,
+ };
+
+ if (fieldsToUpdate.title !== undefined) {
+ variables.title = fieldsToUpdate.title;
+ }
+
+ if (fieldsToUpdate.body !== undefined) {
+ variables.body = fieldsToUpdate.body;
+ }
+
+ const mutationResult = await github.graphql(updateDiscussionMutation, variables);
+
+ if (!mutationResult?.updateDiscussion?.discussion) {
+ throw new Error("Failed to update discussion");
+ }
}
- const updateDiscussionMutation = `
- mutation($discussionId: ID!${fieldsToUpdate.title !== undefined ? ", $title: String!" : ""}${fieldsToUpdate.body !== undefined ? ", $body: String!" : ""}) {
- updateDiscussion(input: {
- discussionId: $discussionId
- ${mutationFields.join("\n ")}
- }) {
- discussion {
+ // Update labels if provided and enabled
+ if (shouldUpdateLabels && Array.isArray(labels)) {
+ // Get the repository ID to look up label IDs
+ const repoQuery = `
+ query($owner: String!, $repo: String!) {
+ repository(owner: $owner, name: $repo) {
id
- number
- title
- body
- url
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
}
}
+ `;
+
+ const repoResult = await github.graphql(repoQuery, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+
+ if (!repoResult?.repository) {
+ throw new Error(`Repository ${context.repo.owner}/${context.repo.repo} not found`);
}
- `;
- const variables = {
- discussionId: discussionId,
- };
+ const repoLabels = repoResult.repository.labels?.nodes || [];
- if (fieldsToUpdate.title !== undefined) {
- variables.title = fieldsToUpdate.title;
- }
+ // Map label names to IDs
+ const labelIds = labels.map(labelName => {
+ const label = repoLabels.find(l => l.name === labelName);
+ if (!label) {
+ throw new Error(`Label "${labelName}" not found in repository`);
+ }
+ return label.id;
+ });
- if (fieldsToUpdate.body !== undefined) {
- variables.body = fieldsToUpdate.body;
- }
+ // Remove all current labels
+ if (currentLabels.length > 0) {
+ const removeLabelsMutation = `
+ mutation($labelableId: ID!, $labelIds: [ID!]!) {
+ removeLabelsFromLabelable(input: {
+ labelableId: $labelableId
+ labelIds: $labelIds
+ }) {
+ clientMutationId
+ }
+ }
+ `;
- const mutationResult = await github.graphql(updateDiscussionMutation, variables);
+ await github.graphql(removeLabelsMutation, {
+ labelableId: discussionId,
+ labelIds: currentLabels.map(l => l.id),
+ });
+ }
- if (!mutationResult?.updateDiscussion?.discussion) {
- throw new Error("Failed to update discussion");
+ // Add new labels
+ if (labelIds.length > 0) {
+ const addLabelsMutation = `
+ mutation($labelableId: ID!, $labelIds: [ID!]!) {
+ addLabelsToLabelable(input: {
+ labelableId: $labelableId
+ labelIds: $labelIds
+ }) {
+ clientMutationId
+ }
+ }
+ `;
+
+ await github.graphql(addLabelsMutation, {
+ labelableId: discussionId,
+ labelIds: labelIds,
+ });
+ }
}
- const discussion = mutationResult.updateDiscussion.discussion;
+ // Fetch the updated discussion to return
+ const finalQuery = shouldUpdateLabels
+ ? `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
+ id
+ title
+ body
+ url
+ labels(first: 100) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ `
+ : `
+ query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ discussion(number: $number) {
+ id
+ title
+ body
+ url
+ }
+ }
+ }
+ `;
+
+ const finalQueryResult = await github.graphql(finalQuery, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ number: discussionNumber,
+ });
+
+ const updatedDiscussion = finalQueryResult.repository.discussion;
// Return with html_url (which the GraphQL returns as 'url')
return {
- ...discussion,
- html_url: discussion.url,
+ ...updatedDiscussion,
+ html_url: updatedDiscussion.url,
};
}
diff --git a/pkg/workflow/js/update_discussion.test.cjs b/pkg/workflow/js/update_discussion.test.cjs
index 518469fac05..51e24ac69e0 100644
--- a/pkg/workflow/js/update_discussion.test.cjs
+++ b/pkg/workflow/js/update_discussion.test.cjs
@@ -63,9 +63,11 @@ describe("update_discussion.cjs", () => {
delete process.env.GH_AW_AGENT_OUTPUT;
delete process.env.GH_AW_UPDATE_TITLE;
delete process.env.GH_AW_UPDATE_BODY;
+ delete process.env.GH_AW_UPDATE_LABELS;
delete process.env.GH_AW_UPDATE_TARGET;
process.env.GH_AW_UPDATE_TITLE = "false";
process.env.GH_AW_UPDATE_BODY = "false";
+ process.env.GH_AW_UPDATE_LABELS = "false";
const scriptPath = path.join(__dirname, "update_discussion.cjs");
updateDiscussionScript = fs.readFileSync(scriptPath, "utf8");
@@ -134,9 +136,19 @@ describe("update_discussion.cjs", () => {
},
});
+ // Mock the final query to get updated discussion
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ title: "Updated discussion title",
+ },
+ },
+ });
+
await eval(`(async () => { ${updateDiscussionScript} })()`);
- expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
expect(mockCore.setOutput).toHaveBeenCalledWith("discussion_number", 123);
expect(mockCore.setOutput).toHaveBeenCalledWith("discussion_url", mockDiscussion.url);
expect(mockCore.summary.addRaw).toHaveBeenCalled();
@@ -175,9 +187,19 @@ describe("update_discussion.cjs", () => {
},
});
+ // Mock the final query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ body: "New discussion body content",
+ },
+ },
+ });
+
await eval(`(async () => { ${updateDiscussionScript} })()`);
- expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
expect(mockCore.setOutput).toHaveBeenCalledWith("discussion_number", 123);
});
@@ -221,9 +243,20 @@ describe("update_discussion.cjs", () => {
},
});
+ // Mock the final query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ title: "New title",
+ body: "New body content",
+ },
+ },
+ });
+
await eval(`(async () => { ${updateDiscussionScript} })()`);
- expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
});
it('should handle explicit discussion number with target "*"', async () => {
@@ -265,9 +298,19 @@ describe("update_discussion.cjs", () => {
},
});
+ // Mock the final query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ title: "Updated title",
+ },
+ },
+ });
+
await eval(`(async () => { ${updateDiscussionScript} })()`);
- expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
// Should use the explicit discussion number 456
expect(mockGithub.graphql).toHaveBeenNthCalledWith(
1,
@@ -333,11 +376,175 @@ describe("update_discussion.cjs", () => {
});
});
+ // Mock the final query
+ mockGithub.graphql.mockImplementationOnce(() => {
+ return Promise.resolve({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ body: capturedBody,
+ },
+ },
+ });
+ });
+
await eval(`(async () => { ${updateDiscussionScript} })()`);
- expect(mockGithub.graphql).toHaveBeenCalledTimes(2);
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
// Verify the custom footer was used
expect(capturedBody).toContain("Custom footer by");
expect(capturedBody).toContain("Custom Workflow");
});
+
+ it("should update discussion labels successfully", async () => {
+ setAgentOutput({
+ items: [{ type: "update_discussion", labels: ["bug", "enhancement"] }],
+ });
+ process.env.GH_AW_UPDATE_LABELS = "true";
+ global.context.eventName = "discussion";
+
+ const mockDiscussion = {
+ id: "D_kwDOABCD123",
+ number: 123,
+ title: "Test Discussion",
+ body: "Test body",
+ url: "https://github.com/testowner/testrepo/discussions/123",
+ labels: {
+ nodes: [{ id: "L_kwDOABCD001", name: "old-label" }],
+ },
+ };
+
+ const mockLabels = [
+ { id: "L_kwDOABCD002", name: "bug" },
+ { id: "L_kwDOABCD003", name: "enhancement" },
+ ];
+
+ // Mock the first query to get discussion with labels
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: mockDiscussion,
+ },
+ });
+
+ // Mock the repository labels query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ id: "R_kwDOABCD",
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ });
+
+ // Mock remove labels mutation
+ mockGithub.graphql.mockResolvedValueOnce({
+ removeLabelsFromLabelable: {
+ clientMutationId: null,
+ },
+ });
+
+ // Mock add labels mutation
+ mockGithub.graphql.mockResolvedValueOnce({
+ addLabelsToLabelable: {
+ clientMutationId: null,
+ },
+ });
+
+ // Mock the final query to get updated discussion
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ },
+ });
+
+ await eval(`(async () => { ${updateDiscussionScript} })()`);
+
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(5);
+ expect(mockCore.setOutput).toHaveBeenCalledWith("discussion_number", 123);
+ expect(mockCore.setOutput).toHaveBeenCalledWith("discussion_url", mockDiscussion.url);
+ });
+
+ it("should update both title and labels successfully", async () => {
+ setAgentOutput({
+ items: [
+ {
+ type: "update_discussion",
+ title: "New title",
+ labels: ["question"],
+ },
+ ],
+ });
+ process.env.GH_AW_UPDATE_TITLE = "true";
+ process.env.GH_AW_UPDATE_LABELS = "true";
+ global.context.eventName = "discussion";
+
+ const mockDiscussion = {
+ id: "D_kwDOABCD123",
+ number: 123,
+ title: "Old title",
+ body: "Old body",
+ url: "https://github.com/testowner/testrepo/discussions/123",
+ labels: {
+ nodes: [],
+ },
+ };
+
+ const mockLabels = [{ id: "L_kwDOABCD004", name: "question" }];
+
+ // Mock the first query to get discussion
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: mockDiscussion,
+ },
+ });
+
+ // Mock the title update mutation
+ mockGithub.graphql.mockResolvedValueOnce({
+ updateDiscussion: {
+ discussion: {
+ ...mockDiscussion,
+ title: "New title",
+ },
+ },
+ });
+
+ // Mock the repository labels query
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ id: "R_kwDOABCD",
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ });
+
+ // Mock add labels mutation (no need to remove since there are no existing labels)
+ mockGithub.graphql.mockResolvedValueOnce({
+ addLabelsToLabelable: {
+ clientMutationId: null,
+ },
+ });
+
+ // Mock the final query to get updated discussion
+ mockGithub.graphql.mockResolvedValueOnce({
+ repository: {
+ discussion: {
+ ...mockDiscussion,
+ title: "New title",
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ },
+ });
+
+ await eval(`(async () => { ${updateDiscussionScript} })()`);
+
+ expect(mockGithub.graphql).toHaveBeenCalledTimes(5);
+ });
});
diff --git a/pkg/workflow/js/update_runner.cjs b/pkg/workflow/js/update_runner.cjs
index ba20cfa0100..b055b9f2bc2 100644
--- a/pkg/workflow/js/update_runner.cjs
+++ b/pkg/workflow/js/update_runner.cjs
@@ -82,11 +82,12 @@ function resolveTargetNumber(params) {
* @param {boolean} params.canUpdateStatus - Whether status updates are allowed
* @param {boolean} params.canUpdateTitle - Whether title updates are allowed
* @param {boolean} params.canUpdateBody - Whether body updates are allowed
+ * @param {boolean} [params.canUpdateLabels] - Whether label updates are allowed
* @param {boolean} params.supportsStatus - Whether this type supports status
* @returns {{hasUpdates: boolean, updateData: any, logMessages: string[]}}
*/
function buildUpdateData(params) {
- const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, supportsStatus } = params;
+ const { item, canUpdateStatus, canUpdateTitle, canUpdateBody, canUpdateLabels, supportsStatus } = params;
/** @type {any} */
const updateData = {};
@@ -136,6 +137,17 @@ function buildUpdateData(params) {
}
}
+ // Handle labels update
+ if (canUpdateLabels && item.labels !== undefined) {
+ if (Array.isArray(item.labels)) {
+ updateData.labels = item.labels;
+ hasUpdates = true;
+ logMessages.push(`Will update labels to: ${item.labels.join(", ")}`);
+ } else {
+ logMessages.push("Invalid labels value: must be an array");
+ }
+ }
+
return { hasUpdates, updateData, logMessages };
}
@@ -180,12 +192,13 @@ async function runUpdateWorkflow(config) {
const canUpdateStatus = process.env.GH_AW_UPDATE_STATUS === "true";
const canUpdateTitle = process.env.GH_AW_UPDATE_TITLE === "true";
const canUpdateBody = process.env.GH_AW_UPDATE_BODY === "true";
+ const canUpdateLabels = process.env.GH_AW_UPDATE_LABELS === "true";
core.info(`Update target configuration: ${updateTarget}`);
if (supportsStatus) {
- core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update status: ${canUpdateStatus}, title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
} else {
- core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}`);
+ core.info(`Can update title: ${canUpdateTitle}, body: ${canUpdateBody}, labels: ${canUpdateLabels}`);
}
// Check context validity
@@ -229,6 +242,7 @@ async function runUpdateWorkflow(config) {
canUpdateStatus,
canUpdateTitle,
canUpdateBody,
+ canUpdateLabels,
supportsStatus,
});
diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go
index f73d478bd5c..3e05640523d 100644
--- a/pkg/workflow/safe_outputs_config.go
+++ b/pkg/workflow/safe_outputs_config.go
@@ -861,6 +861,9 @@ func generateSafeOutputsConfig(data *WorkflowData) string {
maxValue = data.SafeOutputs.UpdateDiscussions.Max
}
updateDiscussionConfig["max"] = maxValue
+ if len(data.SafeOutputs.UpdateDiscussions.AllowedLabels) > 0 {
+ updateDiscussionConfig["allowed_labels"] = data.SafeOutputs.UpdateDiscussions.AllowedLabels
+ }
safeOutputsConfig["update_discussion"] = updateDiscussionConfig
}
if data.SafeOutputs.UpdatePullRequests != nil {
diff --git a/pkg/workflow/update_discussion.go b/pkg/workflow/update_discussion.go
index 926bcd65ebf..14c9775eff2 100644
--- a/pkg/workflow/update_discussion.go
+++ b/pkg/workflow/update_discussion.go
@@ -9,8 +9,10 @@ var updateDiscussionLog = logger.New("workflow:update_discussion")
// UpdateDiscussionsConfig holds configuration for updating GitHub discussions from agent output
type UpdateDiscussionsConfig struct {
UpdateEntityConfig `yaml:",inline"`
- Title *bool `yaml:"title,omitempty"` // Allow updating discussion title - presence indicates field can be updated
- Body *bool `yaml:"body,omitempty"` // Allow updating discussion body - presence indicates field can be updated
+ Title *bool `yaml:"title,omitempty"` // Allow updating discussion title - presence indicates field can be updated
+ Body *bool `yaml:"body,omitempty"` // Allow updating discussion body - presence indicates field can be updated
+ Labels *bool `yaml:"labels,omitempty"` // Allow updating discussion labels - presence indicates field can be updated
+ AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones).
}
// parseUpdateDiscussionsConfig handles update-discussion configuration
@@ -47,6 +49,21 @@ func (c *Compiler) parseUpdateDiscussionsConfig(outputMap map[string]any) *Updat
if _, exists := configMap["body"]; exists {
updateDiscussionsConfig.Body = new(bool)
}
+
+ // Parse labels - presence of the key (even if nil/empty) indicates field can be updated
+ if _, exists := configMap["labels"]; exists {
+ updateDiscussionsConfig.Labels = new(bool)
+ }
+
+ // Parse allowed-labels using shared helper
+ updateDiscussionsConfig.AllowedLabels = parseAllowedLabelsFromConfig(configMap)
+ if len(updateDiscussionsConfig.AllowedLabels) > 0 {
+ updateDiscussionLog.Printf("Allowed labels configured: %v", updateDiscussionsConfig.AllowedLabels)
+ // If allowed-labels is specified, implicitly enable labels
+ if updateDiscussionsConfig.Labels == nil {
+ updateDiscussionsConfig.Labels = new(bool)
+ }
+ }
}
}
diff --git a/pkg/workflow/update_discussion_test.go b/pkg/workflow/update_discussion_test.go
new file mode 100644
index 00000000000..8dbfa941c11
--- /dev/null
+++ b/pkg/workflow/update_discussion_test.go
@@ -0,0 +1,349 @@
+package workflow
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/githubnext/gh-aw/pkg/testutil"
+)
+
+func TestUpdateDiscussionConfigParsing(t *testing.T) {
+ // Create temporary directory for test files
+ tmpDir := testutil.TempDir(t, "output-update-discussion-test")
+
+ // Test case with basic update-discussion configuration
+ testContent := `---
+on:
+ discussion:
+ types: [created]
+permissions:
+ contents: read
+ discussions: write
+engine: claude
+strict: false
+safe-outputs:
+ update-discussion:
+---
+
+# Test Update Discussion Configuration
+
+This workflow tests the update-discussion configuration parsing.
+`
+
+ testFile := filepath.Join(tmpDir, "test-update-discussion.md")
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ compiler := NewCompiler(false, "", "test")
+
+ // Parse the workflow data
+ workflowData, err := compiler.ParseWorkflowFile(testFile)
+ if err != nil {
+ t.Fatalf("Unexpected error parsing workflow with update-discussion config: %v", err)
+ }
+
+ // Verify output configuration is parsed correctly
+ if workflowData.SafeOutputs == nil {
+ t.Fatal("Expected output configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions == nil {
+ t.Fatal("Expected update-discussion configuration to be parsed")
+ }
+
+ // Check defaults
+ if workflowData.SafeOutputs.UpdateDiscussions.Max != 1 {
+ t.Fatalf("Expected max to be 1, got %d", workflowData.SafeOutputs.UpdateDiscussions.Max)
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Target != "" {
+ t.Fatalf("Expected target to be empty (default), got '%s'", workflowData.SafeOutputs.UpdateDiscussions.Target)
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Title != nil {
+ t.Fatal("Expected title to be nil by default (not updatable)")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Body != nil {
+ t.Fatal("Expected body to be nil by default (not updatable)")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Labels != nil {
+ t.Fatal("Expected labels to be nil by default (not updatable)")
+ }
+
+ if len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels) != 0 {
+ t.Fatal("Expected allowed-labels to be empty by default")
+ }
+}
+
+func TestUpdateDiscussionConfigWithAllOptions(t *testing.T) {
+ // Create temporary directory for test files
+ tmpDir := testutil.TempDir(t, "output-update-discussion-all-test")
+
+ // Test case with all options configured
+ testContent := `---
+on:
+ discussion:
+ types: [created]
+permissions:
+ contents: read
+ discussions: write
+engine: claude
+strict: false
+safe-outputs:
+ update-discussion:
+ max: 3
+ target: "*"
+ title:
+ body:
+ labels:
+ allowed-labels: [bug, enhancement, documentation]
+---
+
+# Test Update Discussion Full Configuration
+
+This workflow tests the update-discussion configuration with all options.
+`
+
+ testFile := filepath.Join(tmpDir, "test-update-discussion-full.md")
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ compiler := NewCompiler(false, "", "test")
+
+ // Parse the workflow data
+ workflowData, err := compiler.ParseWorkflowFile(testFile)
+ if err != nil {
+ t.Fatalf("Unexpected error parsing workflow with full update-discussion config: %v", err)
+ }
+
+ // Verify output configuration is parsed correctly
+ if workflowData.SafeOutputs == nil {
+ t.Fatal("Expected output configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions == nil {
+ t.Fatal("Expected update-discussion configuration to be parsed")
+ }
+
+ // Check all options
+ if workflowData.SafeOutputs.UpdateDiscussions.Max != 3 {
+ t.Fatalf("Expected max to be 3, got %d", workflowData.SafeOutputs.UpdateDiscussions.Max)
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Target != "*" {
+ t.Fatalf("Expected target to be '*', got '%s'", workflowData.SafeOutputs.UpdateDiscussions.Target)
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Title == nil {
+ t.Fatal("Expected title to be non-nil (updatable)")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Body == nil {
+ t.Fatal("Expected body to be non-nil (updatable)")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Labels == nil {
+ t.Fatal("Expected labels to be non-nil (updatable)")
+ }
+
+ // Check allowed-labels
+ expectedAllowedLabels := []string{"bug", "enhancement", "documentation"}
+ if len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels) != len(expectedAllowedLabels) {
+ t.Fatalf("Expected %d allowed-labels, got %d", len(expectedAllowedLabels), len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels))
+ }
+
+ for i, expected := range expectedAllowedLabels {
+ if workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i] != expected {
+ t.Fatalf("Expected allowed-label[%d] to be '%s', got '%s'", i, expected, workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i])
+ }
+ }
+}
+
+func TestUpdateDiscussionConfigTargetParsing(t *testing.T) {
+ // Create temporary directory for test files
+ tmpDir := testutil.TempDir(t, "output-update-discussion-target-test")
+
+ // Test case with specific target number
+ testContent := `---
+on:
+ discussion:
+ types: [created]
+permissions:
+ contents: read
+ discussions: write
+engine: claude
+strict: false
+safe-outputs:
+ update-discussion:
+ target: "123"
+ title:
+---
+
+# Test Update Discussion Target Configuration
+
+This workflow tests the update-discussion target configuration parsing.
+`
+
+ testFile := filepath.Join(tmpDir, "test-update-discussion-target.md")
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ compiler := NewCompiler(false, "", "test")
+
+ // Parse the workflow data
+ workflowData, err := compiler.ParseWorkflowFile(testFile)
+ if err != nil {
+ t.Fatalf("Unexpected error parsing workflow with target update-discussion config: %v", err)
+ }
+
+ // Verify output configuration is parsed correctly
+ if workflowData.SafeOutputs == nil {
+ t.Fatal("Expected output configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions == nil {
+ t.Fatal("Expected update-discussion configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Target != "123" {
+ t.Fatalf("Expected target to be '123', got '%s'", workflowData.SafeOutputs.UpdateDiscussions.Target)
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Title == nil {
+ t.Fatal("Expected title to be non-nil (updatable)")
+ }
+}
+
+func TestUpdateDiscussionConfigLabelsOnly(t *testing.T) {
+ // Create temporary directory for test files
+ tmpDir := testutil.TempDir(t, "output-update-discussion-labels-test")
+
+ // Test case with only labels configuration
+ testContent := `---
+on:
+ discussion:
+ types: [created]
+permissions:
+ contents: read
+ discussions: write
+engine: claude
+strict: false
+safe-outputs:
+ update-discussion:
+ labels:
+ allowed-labels: [question, idea]
+---
+
+# Test Update Discussion Labels Configuration
+
+This workflow tests the update-discussion labels configuration.
+`
+
+ testFile := filepath.Join(tmpDir, "test-update-discussion-labels.md")
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ compiler := NewCompiler(false, "", "test")
+
+ // Parse the workflow data
+ workflowData, err := compiler.ParseWorkflowFile(testFile)
+ if err != nil {
+ t.Fatalf("Unexpected error parsing workflow with labels update-discussion config: %v", err)
+ }
+
+ // Verify output configuration is parsed correctly
+ if workflowData.SafeOutputs == nil {
+ t.Fatal("Expected output configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions == nil {
+ t.Fatal("Expected update-discussion configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions.Labels == nil {
+ t.Fatal("Expected labels to be non-nil (updatable)")
+ }
+
+ // Check allowed-labels
+ expectedAllowedLabels := []string{"question", "idea"}
+ if len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels) != len(expectedAllowedLabels) {
+ t.Fatalf("Expected %d allowed-labels, got %d", len(expectedAllowedLabels), len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels))
+ }
+
+ for i, expected := range expectedAllowedLabels {
+ if workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i] != expected {
+ t.Fatalf("Expected allowed-label[%d] to be '%s', got '%s'", i, expected, workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i])
+ }
+ }
+}
+
+func TestUpdateDiscussionConfigAllowedLabelsImplicitlyEnablesLabels(t *testing.T) {
+ // Create temporary directory for test files
+ tmpDir := testutil.TempDir(t, "output-update-discussion-implicit-labels-test")
+
+ // Test case with only allowed-labels (no explicit labels:)
+ testContent := `---
+on:
+ discussion:
+ types: [created]
+permissions:
+ contents: read
+ discussions: write
+engine: claude
+strict: false
+safe-outputs:
+ update-discussion:
+ allowed-labels: [bug, enhancement]
+---
+
+# Test Update Discussion Implicit Labels Configuration
+
+This workflow tests that allowed-labels implicitly enables labels.
+`
+
+ testFile := filepath.Join(tmpDir, "test-update-discussion-implicit-labels.md")
+ if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ compiler := NewCompiler(false, "", "test")
+
+ // Parse the workflow data
+ workflowData, err := compiler.ParseWorkflowFile(testFile)
+ if err != nil {
+ t.Fatalf("Unexpected error parsing workflow with implicit labels config: %v", err)
+ }
+
+ // Verify output configuration is parsed correctly
+ if workflowData.SafeOutputs == nil {
+ t.Fatal("Expected output configuration to be parsed")
+ }
+
+ if workflowData.SafeOutputs.UpdateDiscussions == nil {
+ t.Fatal("Expected update-discussion configuration to be parsed")
+ }
+
+ // The key test: labels should be implicitly enabled when allowed-labels is present
+ if workflowData.SafeOutputs.UpdateDiscussions.Labels == nil {
+ t.Fatal("Expected labels to be implicitly enabled when allowed-labels is present")
+ }
+
+ // Check allowed-labels
+ expectedAllowedLabels := []string{"bug", "enhancement"}
+ if len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels) != len(expectedAllowedLabels) {
+ t.Fatalf("Expected %d allowed-labels, got %d", len(expectedAllowedLabels), len(workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels))
+ }
+
+ for i, expected := range expectedAllowedLabels {
+ if workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i] != expected {
+ t.Fatalf("Expected allowed-label[%d] to be '%s', got '%s'", i, expected, workflowData.SafeOutputs.UpdateDiscussions.AllowedLabels[i])
+ }
+ }
+}
diff --git a/schemas/agent-output.json b/schemas/agent-output.json
index f4a9df676c6..0dc62831291 100644
--- a/schemas/agent-output.json
+++ b/schemas/agent-output.json
@@ -330,7 +330,7 @@
},
"UpdateDiscussionOutput": {
"title": "Update Discussion Output",
- "description": "Output for updating an existing GitHub discussion's title and/or body. Note: The JavaScript validation ensures at least one of title or body is provided.",
+ "description": "Output for updating an existing GitHub discussion's title, body, and/or labels. Note: The JavaScript validation ensures at least one of title, body, or labels is provided.",
"type": "object",
"properties": {
"type": {
@@ -344,6 +344,13 @@
"type": "string",
"description": "New discussion body to replace the existing content"
},
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Array of label names to set on the discussion (replaces existing labels)"
+ },
"discussion_number": {
"oneOf": [
{"type": "number"},