-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add workspace governance resources (states, workflows, type governance) + workflow parity #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Rename Line 45 exposes a standard resource deletion method as 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: 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||
| } | ||||||||||
| } | ||||||||||
| 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); | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { BaseResource } from "../BaseResource"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Rename This filename is not kebab-case. Update the import in 🤖 Prompt for AI AgentsSource: 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}/`); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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: renamePins.tstopins.ts.src/api/WorkItemTypeGovernance/ProjectWorkflows.ts#L1-L9: renameProjectWorkflows.tstoproject-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
Source: Coding guidelines