Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
52a1164
WIP: define command for triggering workflow run
andrewjensen Apr 29, 2026
7e0a907
Merge branch 'main' of github.com:get-dx/cli into adj/cmd/workflowRun…
andrewjensen May 4, 2026
e91f64e
Add web link, color some items
andrewjensen May 4, 2026
71e312d
WIP: Collect parameter data
andrewjensen May 4, 2026
03e7962
Implement more param prompts, show clearer error on unimplemented use…
andrewjensen May 5, 2026
b617b0f
Extract module for parameters, improve output
andrewjensen May 5, 2026
6ffcdb2
Define info command, move things into its module
andrewjensen May 5, 2026
686d14f
Start handling events
andrewjensen May 5, 2026
7c1b016
Define commands for posting events
andrewjensen May 5, 2026
7ce35f9
Simplify polling
andrewjensen May 5, 2026
d879c66
Make some args into required options
andrewjensen May 5, 2026
07c7e03
Extract new module for rendering events
andrewjensen May 6, 2026
458e179
Render different types of events
andrewjensen May 6, 2026
cbc1995
Render duration, links
andrewjensen May 6, 2026
dfa6047
Show events for info command too
andrewjensen May 6, 2026
4cbeb6a
Confirm before running
andrewjensen May 6, 2026
78f6a41
stdout must also be a TTY to consider the run interactive
andrewjensen May 6, 2026
6e8a6fd
Remove unused arg
andrewjensen May 6, 2026
3e62945
Update tests to match new behavior
andrewjensen May 6, 2026
10a3397
Merge branch 'main' of github.com:get-dx/cli into adj/cmd/workflowRun…
andrewjensen May 6, 2026
bc5a6cc
Add examples to event commands
andrewjensen May 6, 2026
8cdb624
Cleanup items
andrewjensen May 6, 2026
e2618d5
Handle approvals and notifying approvers
andrewjensen May 6, 2026
e555b9c
Delete trigger command and related helpers
andrewjensen May 6, 2026
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
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { scorecardsCommand } from "./commands/scorecards.js";
import { snapshotsCommand } from "./commands/snapshots.js";
import { studioCommand } from "./commands/studio.js";
import { teamsCommand } from "./commands/teams.js";
import { workflowRunsCommand } from "./commands/workflowRuns.js";
import { workflowsCommand } from "./commands/workflows.js";
import { handleError } from "./commandHelpers.js";

Expand Down Expand Up @@ -54,6 +55,7 @@ function createProgram(): Command {
program.addCommand(snapshotsCommand());
program.addCommand(studioCommand());
program.addCommand(teamsCommand());
program.addCommand(workflowRunsCommand());
program.addCommand(workflowsCommand());

applyExitOverride(program);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ async function getTeamInfo(
return response.body;
}

async function listTeams(runtime: Runtime): Promise<ListTeamsResponse> {
export async function listTeams(runtime: Runtime): Promise<ListTeamsResponse> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

const response = await request<ListTeamsResponse>(runtime, "/teams.list", {
method: "GET",
});
Expand Down
19 changes: 19 additions & 0 deletions src/commands/workflowRuns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command } from "commander";

import { addLinkCommand } from "./workflowRuns/addLink.js";
import { changeStatusCommand } from "./workflowRuns/changeStatus.js";
import { infoCommand } from "./workflowRuns/info.js";
import { postMessageCommand } from "./workflowRuns/postMessage.js";

export function workflowRunsCommand(): Command {
const workflowRuns = new Command()
.name("workflowRuns")
.description("Trigger and monitor Self-service workflow runs");

workflowRuns.addCommand(addLinkCommand());
workflowRuns.addCommand(changeStatusCommand());
workflowRuns.addCommand(infoCommand());
workflowRuns.addCommand(postMessageCommand());

return workflowRuns;
}
79 changes: 79 additions & 0 deletions src/commands/workflowRuns/addLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Command } from "commander";

import {
createExampleText,
getContext,
wrapAction,
} from "../../commandHelpers.js";
import { buildRuntime } from "../../runtime.js";
import { renderJson, renderRichText } from "../../renderers.js";
import * as ui from "../../ui.js";
import { request } from "../../http.js";
import { Runtime } from "../../types.js";

export function addLinkCommand() {
return new Command()
.name("addLink")
.description("Add a link to a workflow run")
.argument("<workflow-run-id>", "The ID of the workflow run")
.requiredOption("--url <url>", "The URL of the link")
.requiredOption("--label <label>", "The label of the link")
.addHelpText(
"afterAll",
createExampleText([
{
label: "Add a link to a workflow run",
command:
'dx workflowRuns addLink hvserjgz5lo7 --url https://www.example.com --label "Example Website"',
},
]),
)
.action(
wrapAction(async (workflowRunId: string, options, command) => {
const context = getContext(command);
const runtime = buildRuntime(context);

const response = await addLink(
runtime,
workflowRunId,
options.url,
options.label,
);

if (runtime.context.json) {
renderJson(response);
} else {
renderRichText([
ui.p(
`${ui.success(ui.GLYPHS.CHECK)} Added link to workflow run ${ui.code(workflowRunId)}.`,
),
ui.p(
`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${workflowRunId}`, runtime))}`,
),
]);
}
}),
);
}

type AddLinkResponse = {
ok: true;
};

async function addLink(
runtime: Runtime,
workflowRunId: string,
url: string,
label: string,
): Promise<{ body: AddLinkResponse }> {
return request<AddLinkResponse>(runtime, "/workflowRuns.addLink", {
method: "POST",
body: {
workflow_run_id: workflowRunId,
link: {
url,
label,
},
},
});
}
93 changes: 93 additions & 0 deletions src/commands/workflowRuns/changeStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Command } from "commander";

import {
createExampleText,
getContext,
wrapAction,
} from "../../commandHelpers.js";
import { buildRuntime } from "../../runtime.js";
import { renderJson, renderRichText } from "../../renderers.js";
import * as ui from "../../ui.js";
import { request } from "../../http.js";
import { Runtime } from "../../types.js";
import { CliError, EXIT_CODES } from "../../errors.js";

export function changeStatusCommand() {
return new Command()
.name("changeStatus")
.description("Change the status of a workflow run")
.argument("<workflow-run-id>", "The ID of the workflow run")
.requiredOption("--status <updated-status>", "The status to change to")
.addHelpText(
"afterAll",
createExampleText([
{
label: "Mark a workflow run as succeeded",
command:
"dx workflowRuns changeStatus hvserjgz5lo7 --status SUCCEEDED",
},
{
label: "Mark a workflow run as failed",
command: "dx workflowRuns changeStatus hvserjgz5lo7 --status FAILED",
},
]),
)
.action(
wrapAction(async (workflowRunId: string, options, command) => {
const context = getContext(command);
const runtime = buildRuntime(context);

if (!isValidStatus(options.status)) {
throw new CliError(
`Invalid status: ${options.status}. Must be one of: SUCCEEDED, FAILED.`,
EXIT_CODES.ARGUMENT_ERROR,
);
}

const response = await changeStatus(
runtime,
workflowRunId,
options.status,
);

if (runtime.context.json) {
renderJson(response);
} else {
renderRichText([
ui.p(
`${ui.success(ui.GLYPHS.CHECK)} Changed status of workflow run ${ui.code(workflowRunId)} to ${ui.code(options.status)}.`,
),
ui.p(
`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${workflowRunId}`, runtime))}`,
),
]);
}
}),
);
}

function isValidStatus(
updatedStatus: string,
): updatedStatus is WorkflowRunStatus {
return updatedStatus === "SUCCEEDED" || updatedStatus === "FAILED";
}

type WorkflowRunStatus = "SUCCEEDED" | "FAILED";

type ChangeStatusResponse = {
ok: true;
};

async function changeStatus(
runtime: Runtime,
workflowRunId: string,
status: WorkflowRunStatus,
): Promise<{ body: ChangeStatusResponse }> {
return request<ChangeStatusResponse>(runtime, "/workflowRuns.changeStatus", {
method: "POST",
body: {
workflow_run_id: workflowRunId,
status,
},
});
}
75 changes: 75 additions & 0 deletions src/commands/workflowRuns/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { WorkflowRunEvent } from "./info.js";
import * as ui from "../../ui.js";

export function workflowRunEventContent(event: WorkflowRunEvent): ui.Block {
const ts = ui.dim(event.occurred_at);
const message = formatEventMessage(event);
return ui.p(`${ts} ${message}`, false);
}

function formatEventMessage(event: WorkflowRunEvent): string {
const userName = event.user?.name ?? event.user?.email ?? "API";

switch (event.type) {
case "WORKFLOW_TRIGGERED":
return `Workflow run triggered by ${ui.bold(userName)}`;

case "WORKFLOW_RUN_REQUESTED":
return `Workflow run requested by ${ui.bold(userName)}`;

case "APPROVERS_NOTIFIED":
return "Approvers notified";

case "APPROVAL_PERFORMED":
return `Approved by ${ui.bold(userName)}`;

case "REJECTION_PERFORMED":
return `Rejected by ${ui.bold(userName)}`;

case "HTTP_REQUEST_COMPLETED": {
const method = event.data?.request?.method?.toUpperCase();
const url = event.data?.request?.url;
const status = event.data?.response?.status;
const reqSummary = method && url ? ` ${ui.code(`${method} ${url}`)}` : "";
const statusSummary = status ? ` → ${ui.code(String(status))}` : "";
return `Sent HTTP request${reqSummary}${statusSummary}`;
}

case "POST_MESSAGE": {
const text = event.message ?? "";
const truncated = text.length > 120 ? text.slice(0, 120) + "…" : text;
return truncated;
}

case "ADD_LINK": {
const link = event.data?.link;
if (link) {
const label = link.label ? `${link.label}: ` : "";
return `Added link ${label}${ui.link(link.url)}`;
}
return "Added a link";
}

case "CHANGE_STATUS": {
const status = event.data?.status;
return status
? `Status changed to ${ui.bold(status.toLowerCase())}`
: "Status changed";
}

case "WORKFLOW_SUCCEEDED":
return `${ui.success(ui.GLYPHS.CHECK)} Workflow run succeeded`;

case "WORKFLOW_FAILED":
return `${ui.error(ui.GLYPHS.ERROR)} Workflow run failed`;

case "WORKFLOW_TIMEOUT":
return `${ui.error(ui.GLYPHS.ERROR)} Workflow run timed out`;

case "WORKFLOW_CANCELLED":
return `${ui.warning(ui.GLYPHS.WARNING)} Workflow run cancelled by ${ui.bold(userName)}`;

default:
return `Event: ${(event as WorkflowRunEvent).type}`;
}
}
Loading
Loading