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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ const project = await client.projects.create("workspace-slug", {
- **Releases**: Release management with tags, labels, item labels, changelog, comments, links, and work items
- **Collections**: Folders that group workspace pages, with member and page management
- **AgentRuns**: AI agent run orchestration and activity tracking
- **Workflows**: Project workflow management with state attachments and transitions
- **Workflows**: Project workflow management with state attachments, transitions, transition hooks, activities, and work item approvals
- **ProjectTemplates**: Work item and page template management per project
- **Features**: Workspace and project features management
- **WorkspaceStates**: Workspace-level (catalog) work-item states under workspace governance — dual-mode reads, governed-only writes
- **WorkspaceWorkflows**: Workspace-level workflow catalog under workspace governance, with chain (states), transitions, usage, activities, and transition hooks
- **WorkItemTypeGovernance**: Governs which workflows a workspace-level work item type may use (any/constrained/required modes), with per-project pins and the project-side pick/fallback-preview endpoints

## Development

Expand Down
48 changes: 48 additions & 0 deletions src/api/WorkItemTypeGovernance/Pins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import { CreateWorkItemTypeWorkflowPins, WorkItemTypeWorkflowPin } from "../../models/WorkItemTypeGovernance";
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use kebab-case filenames for these API modules.

Rename the files and update their imports.

  • src/api/WorkItemTypeGovernance/Pins.ts#L1-L3: rename Pins.ts to pins.ts.
  • src/api/WorkItemTypeGovernance/ProjectWorkflows.ts#L1-L9: rename ProjectWorkflows.ts to project-workflows.ts.

As per coding guidelines: src/**/*.ts: Use kebab-case for file names.

📍 Affects 2 files
  • src/api/WorkItemTypeGovernance/Pins.ts#L1-L3 (this comment)
  • src/api/WorkItemTypeGovernance/ProjectWorkflows.ts#L1-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/WorkItemTypeGovernance/Pins.ts` around lines 1 - 3, Rename
src/api/WorkItemTypeGovernance/Pins.ts to pins.ts and update every import or
export referencing it; rename src/api/WorkItemTypeGovernance/ProjectWorkflows.ts
to project-workflows.ts and update every corresponding import or export,
preserving the existing module symbols and behavior.

Source: Coding guidelines


/**
* WorkItemTypeGovernance.pins sub-resource
*
* A pin forces one project to resolve a type to a specific workflow,
* overriding the workspace default and the constrained allowlist.
*/
export class Pins extends BaseResource {
constructor(config: Configuration) {
super(config);
}

/**
* List a type's project-to-workflow pins
*/
async list(workspaceSlug: string, typeId: string): Promise<WorkItemTypeWorkflowPin[]> {
const data = await this.get<WorkItemTypeWorkflowPin[] | { results: WorkItemTypeWorkflowPin[] }>(
`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/pins/`
);
return Array.isArray(data) ? data : data.results;
}

/**
* Pin a workflow for this type across one or more projects.
* Returns the type's pins after the change.
*/
async create(
workspaceSlug: string,
typeId: string,
data: CreateWorkItemTypeWorkflowPins
): Promise<WorkItemTypeWorkflowPin[]> {
const response = await this.post<WorkItemTypeWorkflowPin[] | { results: WorkItemTypeWorkflowPin[] }>(
`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/pins/`,
data
);
return Array.isArray(response) ? response : response.results;
}

/**
* Remove a pin
*/
async delete(workspaceSlug: string, typeId: string, pinId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/pins/${pinId}/`);
Comment on lines +45 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename delete to del.

Line 45 exposes a standard resource deletion method as delete. Use del to match the SDK resource contract.

Proposed fix
-  async delete(workspaceSlug: string, typeId: string, pinId: string): Promise<void> {
+  async del(workspaceSlug: string, typeId: string, pinId: string): Promise<void> {

As per coding guidelines: src/api/**/*.ts: Standard resource methods should be named: list, create, retrieve, update, del.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async delete(workspaceSlug: string, typeId: string, pinId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/pins/${pinId}/`);
async del(workspaceSlug: string, typeId: string, pinId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/pins/${pinId}/`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/WorkItemTypeGovernance/Pins.ts` around lines 45 - 46, Rename the
public deletion method in the Pins resource from delete to del, preserving its
parameters, return type, and existing httpDelete request path so it conforms to
the standard resource method contract.

Source: Coding guidelines

}
}
89 changes: 89 additions & 0 deletions src/api/WorkItemTypeGovernance/ProjectWorkflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import {
GovernancePreview,
ProjectTypeWorkflow,
ProjectWorkflowPickResult,
SetProjectWorkflowPick,
WorkflowFallbackPreviewRequest,
} from "../../models/WorkItemTypeGovernance";

type GovernancePreviewResponse = { preview?: GovernancePreview } | GovernancePreview;

function unwrapPreview(response: GovernancePreviewResponse): GovernancePreview {
return "preview" in response && response.preview ? response.preview : (response as GovernancePreview);
}

/**
* WorkItemTypeGovernance.projectWorkflows sub-resource
*
* Reports each active type's governance mode and the workflow it effectively
* resolves to within a project, and manages the project's own workflow pick
* for a type.
*/
export class ProjectWorkflows extends BaseResource {
constructor(config: Configuration) {
super(config);
}

/**
* List every active type's governance mode, effective workflow, and
* pickable options for a project
*/
async list(workspaceSlug: string, projectId: string): Promise<ProjectTypeWorkflow[]> {
const data = await this.get<ProjectTypeWorkflow[] | { results: ProjectTypeWorkflow[] }>(
`/workspaces/${workspaceSlug}/projects/${projectId}/work-item-types/workflows/`
);
return Array.isArray(data) ? data : data.results;
}

/**
* Retrieve one type's governance mode and effective workflow in a project
*/
async retrieve(workspaceSlug: string, projectId: string, typeId: string): Promise<ProjectTypeWorkflow> {
return this.get<ProjectTypeWorkflow>(
`/workspaces/${workspaceSlug}/projects/${projectId}/work-item-types/${typeId}/workflows/`
);
}

/**
* Retrieve the project's current workflow pick context for a type
*/
async retrievePick(workspaceSlug: string, projectId: string, typeId: string): Promise<ProjectTypeWorkflow> {
return this.get<ProjectTypeWorkflow>(
`/workspaces/${workspaceSlug}/projects/${projectId}/work-item-types/${typeId}/workflow/`
);
}

/**
* Set the project's workflow pick for a type.
* Runs the workflow fallback for stranded work items; every orphan must be
* covered by `data.state_mapping` (400 with an orphan report otherwise).
*/
async updatePick(
workspaceSlug: string,
projectId: string,
typeId: string,
data: SetProjectWorkflowPick
): Promise<ProjectWorkflowPickResult> {
return this.put<ProjectWorkflowPickResult>(
`/workspaces/${workspaceSlug}/projects/${projectId}/work-item-types/${typeId}/workflow/`,
data
);
}

/**
* Dry-run the project's workflow fallback (re-type / switch dialogs)
*/
async previewFallback(
workspaceSlug: string,
projectId: string,
data: WorkflowFallbackPreviewRequest
): Promise<GovernancePreview> {
const response = await this.post<GovernancePreviewResponse>(
`/workspaces/${workspaceSlug}/projects/${projectId}/workflow-fallback-preview/`,
data
);
return unwrapPreview(response);
}
}
65 changes: 65 additions & 0 deletions src/api/WorkItemTypeGovernance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import {
GovernancePreview,
TypeGovernance,
TypeGovernancePreviewRequest,
UpdateTypeGovernance,
} from "../../models/WorkItemTypeGovernance";
import { Pins } from "./Pins";
import { ProjectWorkflows } from "./ProjectWorkflows";

type GovernancePreviewResponse = { preview?: GovernancePreview } | GovernancePreview;

function unwrapPreview(response: GovernancePreviewResponse): GovernancePreview {
return "preview" in response && response.preview ? response.preview : (response as GovernancePreview);
}

/**
* WorkItemTypeGovernance API resource (workspace governance only)
*
* Governs which workflows a workspace-level work item type may use
* (`any` / `constrained` / `required` modes and allowlists). Per-project pins
* live on `.pins`; the project-side view of effective workflows and picks
* lives on `.projectWorkflows`. Every endpoint requires the workspace to own
* states and workflows — otherwise the API responds 400 with code
* `workspace_not_managed`.
*/
export class WorkItemTypeGovernance extends BaseResource {
public pins: Pins;
public projectWorkflows: ProjectWorkflows;

constructor(config: Configuration) {
super(config);
this.pins = new Pins(config);
this.projectWorkflows = new ProjectWorkflows(config);
}

/**
* Retrieve a type's governance settings (mode, required workflow, allowlist)
*/
async retrieve(workspaceSlug: string, typeId: string): Promise<TypeGovernance> {
return this.get<TypeGovernance>(`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/`);
}

/**
* Update a type's governance mode / allowlist / required workflow.
* Destructive changes (dropping in-use workflows, mandating one) require
* `data.acknowledge` and may need a `data.state_mapping` for orphaned work
* items.
*/
async update(workspaceSlug: string, typeId: string, data: UpdateTypeGovernance): Promise<TypeGovernance> {
return this.patch<TypeGovernance>(`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/`, data);
}

/**
* Dry-run a governance change and report affected work items (no writes)
*/
async preview(workspaceSlug: string, typeId: string, data: TypeGovernancePreviewRequest): Promise<GovernancePreview> {
const response = await this.post<GovernancePreviewResponse>(
`/workspaces/${workspaceSlug}/work-item-types/${typeId}/governance/preview/`,
data
);
return unwrapPreview(response);
}
}
102 changes: 102 additions & 0 deletions src/api/Workflows/Hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { BaseResource } from "../BaseResource";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Rename Hooks.ts to hooks.ts.

This filename is not kebab-case. Update the import in src/api/Workflows/index.ts after the rename. As per coding guidelines, “Use kebab-case for file names.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/Workflows/Hooks.ts` at line 1, Rename the Hooks.ts module to
kebab-case as hooks.ts, and update the import in the Workflows index entrypoint
to point to the new filename. Keep the existing exported symbols such as
BaseResource unchanged; only adjust the file name and the corresponding import
reference so the workflow API continues to resolve correctly.

Source: Coding guidelines

import { Configuration } from "../../Configuration";
import {
CreateWorkflowTransitionHook,
UpdateWorkflowTransitionHook,
WorkflowTransitionHook,
} from "../../models/Workflow";

/**
* WorkflowTransitionHooks sub-resource
* Manages hooks attached to project workflow transitions
*/
export class Hooks extends BaseResource {
constructor(config: Configuration) {
super(config);
}

private basePath(workspaceSlug: string, projectId: string, workflowId: string, transitionId: string): string {
return (
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}` +
`/state-transitions/${transitionId}/hooks`
);
}

/**
* List hooks on a workflow transition
*/
async list(
workspaceSlug: string,
projectId: string,
workflowId: string,
transitionId: string
): Promise<WorkflowTransitionHook[]> {
const data = await this.get<WorkflowTransitionHook[] | { results: WorkflowTransitionHook[] }>(
`${this.basePath(workspaceSlug, projectId, workflowId, transitionId)}/`
);
return Array.isArray(data) ? data : data.results;
}

/**
* Create a hook on a workflow transition.
* For send_webhook handlers the one-shot `secret_plaintext` is included in
* the response of this call only.
*/
async create(
workspaceSlug: string,
projectId: string,
workflowId: string,
transitionId: string,
data: CreateWorkflowTransitionHook
): Promise<WorkflowTransitionHook> {
return this.post<WorkflowTransitionHook>(
`${this.basePath(workspaceSlug, projectId, workflowId, transitionId)}/`,
data
);
}

/**
* Retrieve a hook by ID
*/
async retrieve(
workspaceSlug: string,
projectId: string,
workflowId: string,
transitionId: string,
hookId: string
): Promise<WorkflowTransitionHook> {
return this.get<WorkflowTransitionHook>(
`${this.basePath(workspaceSlug, projectId, workflowId, transitionId)}/${hookId}/`
);
}

/**
* Update a hook (`phase` and `handler_name` are immutable)
*/
async update(
workspaceSlug: string,
projectId: string,
workflowId: string,
transitionId: string,
hookId: string,
data: UpdateWorkflowTransitionHook
): Promise<WorkflowTransitionHook> {
return this.patch<WorkflowTransitionHook>(
`${this.basePath(workspaceSlug, projectId, workflowId, transitionId)}/${hookId}/`,
data
);
}

/**
* Delete a hook
*/
async del(
workspaceSlug: string,
projectId: string,
workflowId: string,
transitionId: string,
hookId: string
): Promise<void> {
return this.httpDelete(`${this.basePath(workspaceSlug, projectId, workflowId, transitionId)}/${hookId}/`);
}
}
45 changes: 44 additions & 1 deletion src/api/Workflows/States.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseResource } from "../BaseResource";
import { Configuration } from "../../Configuration";
import { AttachWorkflowStates } from "../../models/Workflow";
import { AttachWorkflowStates, UpdateWorkflowState, WorkflowState } from "../../models/Workflow";

/**
* WorkflowStates sub-resource
Expand All @@ -11,6 +11,16 @@ export class States extends BaseResource {
super(config);
}

/**
* List the states attached to a workflow
*/
async list(workspaceSlug: string, projectId: string, workflowId: string): Promise<WorkflowState[]> {
const data = await this.get<WorkflowState[] | { results: WorkflowState[] }>(
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/`
);
return Array.isArray(data) ? data : data.results;
}

/**
* Attach states to a workflow
*/
Expand All @@ -23,6 +33,23 @@ export class States extends BaseResource {
return this.post<void>(`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/`, data);
}

/**
* Update a state's membership row (type, allow_issue_creation, is_default)
*/
async update(
workspaceSlug: string,
projectId: string,
workflowId: string,
stateId: string,
data: UpdateWorkflowState
): Promise<WorkflowState | null> {
const response = await this.patch<WorkflowState | null>(
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/${stateId}/`,
data
);
return response ?? null;
}

/**
* Detach a state from a workflow
*/
Expand All @@ -31,4 +58,20 @@ export class States extends BaseResource {
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/${stateId}/`
);
}

/**
* Transfer work items off a state and remove it from the workflow
*/
async transfer(
workspaceSlug: string,
projectId: string,
workflowId: string,
stateId: string,
newStateId: string
): Promise<void> {
return this.post<void>(
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/${stateId}/transfer/`,
{ new_state_id: newStateId }
);
}
}
Loading
Loading