Skip to content
Open
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
76 changes: 76 additions & 0 deletions .mux/actions/github/ensureIssueLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const {
getIssueView,
inputObject,
normalizeIssue,
repositoryFromInput,
requiredIssueNumber,
stringList,
} = require('../../workflow-action-lib/github.cjs');

export const metadata = {
version: 1,
description: 'Idempotently add and remove GitHub issue labels',
effect: 'external',
inputSchema: mux.schema.object(
{
repository: mux.schema.optional(mux.schema.string()),
owner: mux.schema.optional(mux.schema.string()),
repo: mux.schema.optional(mux.schema.string()),
number: mux.schema.integer(),
addLabels: mux.schema.optional(mux.schema.array(mux.schema.string())),
removeLabels: mux.schema.optional(mux.schema.array(mux.schema.string())),
},
{ additionalProperties: false },
),
outputSchema: mux.schema.object(
{
changed: mux.schema.boolean(),
before: mux.schema.array(mux.schema.string()),
after: mux.schema.array(mux.schema.string()),
added: mux.schema.array(mux.schema.string()),
removed: mux.schema.array(mux.schema.string()),
},
{ additionalProperties: false },
),
permissions: [
{ kind: 'command', command: 'gh issue edit' },
{ kind: 'command', command: 'gh issue view' },
],
timeoutMs: 60000,
};

async function getLabelNames(ctx, repository, number) {
const issue = await getIssueView(ctx, repository, number, ['labels']);
return normalizeIssue(issue).labelNames;
}

export async function execute(rawInput, ctx) {
const input = inputObject(rawInput);
const repository = repositoryFromInput(input);
const number = requiredIssueNumber(input.number);
const addLabels = stringList(input.addLabels);
const removeLabels = stringList(input.removeLabels);
const before = await getLabelNames(ctx, repository, number);
const missingAddLabels = addLabels.filter((label) => !before.includes(label));
const presentRemoveLabels = removeLabels.filter((label) =>
before.includes(label),
);
if (missingAddLabels.length === 0 && presentRemoveLabels.length === 0) {
return { changed: false, before, after: before, added: [], removed: [] };
}
const args = ['issue', 'edit', String(number)];
if (repository) args.push('--repo', repository);
for (const label of missingAddLabels) args.push('--add-label', label);
for (const label of presentRemoveLabels) args.push('--remove-label', label);
await ctx.execChecked('gh', args);
const after = await getLabelNames(ctx, repository, number);
return {
changed: true,
before,
after,
added: missingAddLabels,
removed: presentRemoveLabels,
};
}

export const reconcile = execute;
79 changes: 79 additions & 0 deletions .mux/actions/github/findIssueComment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const {
inputObject,
listComments,
requiredIssueNumber,
requiredRepository,
splitRepository,
stringList,
} = require('../../workflow-action-lib/github.cjs');

export const metadata = {
version: 1,
description: 'Find the latest GitHub issue comment containing expected text',
effect: 'read',
inputSchema: mux.schema.object(
{
repository: mux.schema.optional(mux.schema.string()),
owner: mux.schema.optional(mux.schema.string()),
repo: mux.schema.optional(mux.schema.string()),
number: mux.schema.integer(),
requiredBodyIncludes: mux.schema.optional(
mux.schema.array(mux.schema.string()),
),
},
{ additionalProperties: false },
),
outputSchema: mux.schema.object(
{
found: mux.schema.boolean(),
reason: mux.schema.string(),
url: mux.schema.nullable(mux.schema.string()),
commentId: mux.schema.nullable(
mux.schema.union([mux.schema.integer(), mux.schema.string()]),
),
updatedAt: mux.schema.nullable(mux.schema.string()),
},
{ additionalProperties: false },
),
permissions: [{ kind: 'command', command: 'gh api' }],
timeoutMs: 60000,
};

export async function execute(rawInput, ctx) {
const input = inputObject(rawInput);
const repository = requiredRepository(input);
const parts = splitRepository(repository);
const number = requiredIssueNumber(input.number);
const includes = stringList(input.requiredBodyIncludes);
if (includes.length === 0) {
throw new Error('requiredBodyIncludes must include at least one string');
}

const comments = await listComments(ctx, parts.owner, parts.repo, number);
const match = comments
.slice()
.reverse()
.find(
(comment) =>
typeof comment.body === 'string' &&
includes.every((text) => comment.body.includes(text)),
);

if (!match) {
return {
found: false,
reason: 'not-found',
url: null,
commentId: null,
updatedAt: null,
};
}

return {
found: true,
reason: '',
url: match.html_url || null,
commentId: match.id || null,
updatedAt: match.updated_at || null,
};
}
120 changes: 120 additions & 0 deletions .mux/actions/github/getIssueAutomationState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const {
getIssueView,
inputObject,
isMatchingMarker,
listComments,
markerStatus,
normalizeIssue,
optionalString,
requiredIssueNumber,
requiredRepository,
requiredString,
splitRepository,
stringList,
} = require('../../workflow-action-lib/github.cjs');

export const metadata = {
version: 1,
description: 'Read GitHub issue automation marker comments and done labels',
effect: 'read',
inputSchema: mux.schema.object(
{
repository: mux.schema.optional(mux.schema.string()),
owner: mux.schema.optional(mux.schema.string()),
repo: mux.schema.optional(mux.schema.string()),
number: mux.schema.integer(),
doneLabels: mux.schema.optional(mux.schema.array(mux.schema.string())),
ongoingLabels: mux.schema.optional(mux.schema.array(mux.schema.string())),
includeComments: mux.schema.optional(mux.schema.boolean()),
marker: mux.schema.string(),
markerKey: mux.schema.string(),
promptVersion: mux.schema.optional(mux.schema.string()),
},
{ additionalProperties: false },
),
outputSchema: mux.schema.object(
{
done: mux.schema.boolean(),
promptStarted: mux.schema.boolean(),
reportPosted: mux.schema.boolean(),
labelNames: mux.schema.array(mux.schema.string()),
markerComments: mux.schema.array(
mux.schema.object(
{
id: mux.schema.integer(),
url: mux.schema.nullable(mux.schema.string()),
status: mux.schema.string(),
},
{ additionalProperties: false },
),
),
},
{ additionalProperties: false },
),
permissions: [
{ kind: 'command', command: 'gh api' },
{ kind: 'command', command: 'gh issue view' },
],
timeoutMs: 60000,
};

function labelsIncludeAll(labels, labelNames) {
return (
labels.length > 0 && labels.every((label) => labelNames.includes(label))
);
}

function labelsIncludeAny(labels, labelNames) {
return labels.some((label) => labelNames.includes(label));
}

export async function execute(rawInput, ctx) {
const input = inputObject(rawInput);
const repository = requiredRepository(input);
const number = requiredIssueNumber(input.number);
const doneLabels = stringList(input.doneLabels);
const ongoingLabels = stringList(input.ongoingLabels);
const includeComments = input.includeComments !== false;
const issuePromise = getIssueView(ctx, repository, number, ['labels']);

if (!includeComments) {
const issue = await issuePromise;
const labelNames = normalizeIssue(issue).labelNames;
return {
done: labelsIncludeAll(doneLabels, labelNames),
promptStarted: labelsIncludeAny(ongoingLabels, labelNames),
reportPosted: false,
labelNames,
markerComments: [],
};
}

const parts = splitRepository(repository);
const marker = requiredString(input.marker, 'marker');
const markerKey = requiredString(input.markerKey, 'markerKey');
const promptVersion = optionalString(input.promptVersion) || 'v1';
const [issue, comments] = await Promise.all([
issuePromise,
listComments(ctx, parts.owner, parts.repo, number),
]);
const labelNames = normalizeIssue(issue).labelNames;
const matching = comments.filter((comment) =>
isMatchingMarker(comment.body, marker, markerKey, promptVersion),
);
const statuses = matching
.map((comment) => markerStatus(comment.body))
.filter(Boolean);
return {
done: labelsIncludeAll(doneLabels, labelNames),
promptStarted:
labelsIncludeAny(ongoingLabels, labelNames) ||
statuses.includes('prompt-started'),
reportPosted: statuses.includes('report-posted'),
labelNames,
markerComments: matching.map((comment) => ({
id: comment.id,
url: comment.html_url || null,
status: markerStatus(comment.body),
})),
};
}
Loading
Loading