Skip to content
Draft
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full release history.
### 🏗️ **Build & Integration**
- **Build Summaries** — Lightweight build status cards in PR and work item detail panels
- **Pipelines View** — Browse recent Azure Pipelines runs across selected scopes; filter/group runs, inspect timeline details, open step logs from the tree or details timeline in VS Code, and open artifacts
- **Deployments View** — Read-only visibility into classic Releases environments plus recent pipeline runs across selected scopes
- **MCP Server** — Foundry integration for AI-powered workflows and automations
- **Azure Boards Integration** — Full WIQL query support for advanced filtering and bulk operations

Expand Down Expand Up @@ -124,7 +125,8 @@ code --install-extension ./adoext-<version>.vsix
- **Backlog** — Hierarchical view of all work
- **Sprints** — Current and future sprint planning
- **Boards** — Kanban-style board view
- **Pipelines** — Recent CI/CD runs across your selected scopes
- **Pipelines** — Recent CI/CD runs across your selected scopes
- **Deployments** — Classic release environment status plus recent pipeline runs (read-only)

---

Expand Down
61 changes: 59 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
"name": "Pipelines",
"icon": "media/icons/ado.svg",
"contextualTitle": "Azure DevOps Pipelines"
},
{
"id": "adoext.deployments",
"name": "Deployments",
"icon": "media/icons/ado.svg",
"contextualTitle": "Azure DevOps Deployments"
}
]
},
Expand Down Expand Up @@ -232,6 +238,12 @@
"category": "ADOExt",
"icon": "$(refresh)"
},
{
"command": "adoext.refreshDeployments",
"title": "Refresh Deployments",
"category": "ADOExt",
"icon": "$(refresh)"
},
{
"command": "adoext.setPipelineRunsFilter",
"title": "Filter Pipeline Runs",
Expand All @@ -250,6 +262,24 @@
"category": "ADOExt",
"icon": "$(eye)"
},
{
"command": "adoext.viewReleaseDetails",
"title": "View Release Details",
"category": "ADOExt",
"icon": "$(eye)"
},
{
"command": "adoext.openClassicRelease",
"title": "Open Release in Browser",
"category": "ADOExt",
"icon": "$(link-external)"
},
{
"command": "adoext.openClassicReleaseEnvironment",
"title": "Open Release Environment in Browser",
"category": "ADOExt",
"icon": "$(link-external)"
},
{
"command": "adoext.openPipelineRun",
"title": "Open Pipeline Run in Browser",
Expand Down Expand Up @@ -579,12 +609,12 @@
},
{
"command": "adoext.detectRepoContext",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines || view == adoext.deployments",
"group": "navigation"
},
{
"command": "adoext.signIn",
"when": "(view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines) && !adoext.isSignedIn",
"when": "(view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines || view == adoext.deployments) && !adoext.isSignedIn",
"group": "navigation"
},
{
Expand Down Expand Up @@ -617,6 +647,11 @@
"when": "view == adoext.pipelines",
"group": "navigation"
},
{
"command": "adoext.refreshDeployments",
"when": "view == adoext.deployments",
"group": "navigation"
},
{
"command": "adoext.setPipelineRunsFilter",
"when": "view == adoext.pipelines",
Expand Down Expand Up @@ -729,11 +764,26 @@
"when": "viewItem == pipelineRun || viewItem == pipelineRunRunning",
"group": "inline"
},
{
"command": "adoext.viewReleaseDetails",
"when": "viewItem == classicRelease || viewItem == classicReleaseEnvironment",
"group": "inline"
},
{
"command": "adoext.openPipelineRun",
"when": "viewItem == pipelineRun || viewItem == pipelineRunRunning",
"group": "navigation"
},
{
"command": "adoext.openClassicRelease",
"when": "viewItem == classicRelease || viewItem == classicReleaseEnvironment",
"group": "navigation"
},
{
"command": "adoext.openClassicReleaseEnvironment",
"when": "viewItem == classicReleaseEnvironment",
"group": "navigation"
},
{
"command": "adoext.openPipelineRunLogs",
"when": "viewItem == pipelineRun || viewItem == pipelineRunRunning",
Expand Down Expand Up @@ -1087,6 +1137,13 @@
"maximum": 100,
"description": "Maximum number of pipeline runs fetched per project scope for the ADOExt Pipelines view."
},
"adoext.classicReleasesTop": {
"type": "number",
"default": 10,
"minimum": 1,
"maximum": 50,
"description": "Maximum number of classic releases fetched per project scope for the ADOExt Deployments view."
},
"adoext.showResolvedPullRequestThreads": {
"type": "boolean",
"default": true,
Expand Down
80 changes: 80 additions & 0 deletions src/api/adoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type { IGitApi } from 'azure-devops-node-api/GitApi';
import type { ICoreApi } from 'azure-devops-node-api/CoreApi';
import type { IPolicyApi } from 'azure-devops-node-api/PolicyApi';
import type { IBuildApi } from 'azure-devops-node-api/BuildApi';
import type { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
import { CommentExpandOptions, QueryExpand, TreeStructureGroup, WorkItemExpand } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
import { GitVersionType, VersionControlChangeType, GitStatusState, PullRequestAsyncStatus, PullRequestMergeFailureType, PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { BuildReason, BuildResult, BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Operation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { ReleaseExpands, ReleaseQueryOrder } from 'azure-devops-node-api/interfaces/ReleaseInterfaces';
import { normalizeWorkItemTypeName, workItemTypeScopeKey } from '../utils/workItemTypeIcons';
import type {
WorkItem,
Expand Down Expand Up @@ -35,6 +37,7 @@ import type { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterface
import type { IdentityRef, JsonPatchDocument, JsonPatchOperation, ResourceRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import type { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { PolicyEvaluationStatus } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import type { Release, ReleaseEnvironment } from 'azure-devops-node-api/interfaces/ReleaseInterfaces';

export type {
WorkItem,
Expand All @@ -54,6 +57,8 @@ export type {
BuildArtifact,
BuildLog,
Timeline,
Release,
ReleaseEnvironment,
PolicyEvaluationRecord
};
export { GitStatusState, PolicyEvaluationStatus, PullRequestAsyncStatus, PullRequestMergeFailureType, PullRequestStatus, BuildReason, BuildResult, BuildStatus };
Expand Down Expand Up @@ -1297,6 +1302,81 @@ export class AdoClient {
);
}

/**
* List classic Releases (Release Management / "Releases" UI) for a project.
*
* This API can be unavailable or permission-gated; callers should catch
* errors and degrade gracefully.
*/
async listClassicReleases(
project: string,
organization?: string,
options?: {
top?: number;
}
): Promise<Release[]> {
const releaseApi: IReleaseApi = await this.getConnectionFor(organization).getReleaseApi();
const top = options?.top ?? 10;
const expand = ReleaseExpands.Environments | ReleaseExpands.Artifacts | ReleaseExpands.Approvals;

return releaseApi.getReleases(
project,
undefined, // definitionId
undefined, // definitionEnvironmentId
undefined, // searchText
undefined, // createdBy
undefined, // statusFilter
undefined, // environmentStatusFilter
undefined, // minCreatedTime
undefined, // maxCreatedTime
ReleaseQueryOrder.Descending,
top,
undefined, // continuationToken
expand
);
}

/**
* Fetch a single classic release with expanded environments/artifacts.
*
* `getRelease()` does not support expanding environments/artifacts, so we
* query `getReleases()` with a releaseId filter.
*/
async getClassicRelease(
project: string,
releaseId: number,
organization?: string
): Promise<Release | undefined> {
const releaseApi: IReleaseApi = await this.getConnectionFor(organization).getReleaseApi();
const expand = ReleaseExpands.Environments | ReleaseExpands.Artifacts | ReleaseExpands.Approvals;

const releases = await releaseApi.getReleases(
project,
undefined, // definitionId
undefined, // definitionEnvironmentId
undefined, // searchText
undefined, // createdBy
undefined, // statusFilter
undefined, // environmentStatusFilter
undefined, // minCreatedTime
undefined, // maxCreatedTime
ReleaseQueryOrder.Descending,
1,
undefined, // continuationToken
expand,
undefined, // artifactTypeId
undefined, // sourceId
undefined, // artifactVersionId
undefined, // sourceBranchFilter
undefined, // isDeleted
undefined, // tagFilter
undefined, // propertyFilters
[releaseId] // releaseIdFilter
);

return releases?.[0];
}

get organization(): string | undefined {
return this._organization;
}
Expand Down
74 changes: 74 additions & 0 deletions src/commands/deploymentsCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as vscode from 'vscode';
import type { AdoClient } from '../api/adoClient';
import type { ConfigManager } from '../config/configManager';
import { ClassicReleaseEnvironmentNode, type ClassicReleaseNode } from '../providers/deploymentsProvider';
import { ReleaseDetailsPanel } from '../views/releaseDetailsPanel';
import { showErrorMessage, showInformationMessage } from '../utils/notifications';
import { classicReleaseUrl } from '../utils/releaseUrls';

export async function viewReleaseDetails(
context: vscode.ExtensionContext,
node: ClassicReleaseNode | ClassicReleaseEnvironmentNode | undefined,
client: AdoClient,
config: ConfigManager
): Promise<void> {
const releaseId = node instanceof ClassicReleaseEnvironmentNode ? node.releaseId : node?.releaseId;
if (!releaseId) {
showInformationMessage('Select a release first.');
return;
}
if (!node) {
return;
}

await ReleaseDetailsPanel.show(context, client, config, releaseId, {
organization: node.organization,
project: node.project
});
}

export async function openClassicReleaseInBrowser(
node: ClassicReleaseNode | ClassicReleaseEnvironmentNode | undefined,
client: AdoClient,
config: ConfigManager
): Promise<void> {
const organization = node?.organization ?? client.organization ?? config.organization;
const project = node?.project ?? config.project;
const releaseId = node instanceof ClassicReleaseEnvironmentNode ? node.releaseId : node?.releaseId;
if (!organization || !project || !releaseId) {
showInformationMessage('Select an organization, project, and release first.');
return;
}

try {
await vscode.env.openExternal(vscode.Uri.parse(classicReleaseUrl(organization, project, releaseId)));
} catch (err) {
showErrorMessage(`Failed to open release: ${err}`);
}
}

export async function openClassicReleaseEnvironmentInBrowser(
node: ClassicReleaseEnvironmentNode | undefined,
client: AdoClient,
config: ConfigManager
): Promise<void> {
if (!node) {
showInformationMessage('Select a release environment first.');
return;
}

const organization = node.organization ?? client.organization ?? config.organization;
const project = node.project ?? config.project;
const releaseId = node.releaseId;
const environmentId = node.environmentId;
if (!organization || !project || !releaseId || !environmentId) {
showInformationMessage('Select an organization and project first.');
return;
}

try {
await vscode.env.openExternal(vscode.Uri.parse(classicReleaseUrl(organization, project, releaseId, { environmentId })));
} catch (err) {
showErrorMessage(`Failed to open release environment: ${err}`);
}
}
7 changes: 7 additions & 0 deletions src/config/configManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ export class ConfigManager {
return Math.max(1, Math.min(100, top));
}

/** Max classic releases fetched per scope for the Deployments view (1-50). */
get classicReleasesTop(): number {
const raw = this.config.get<number>('classicReleasesTop', 10);
const top = Math.floor(raw);
return Math.max(1, Math.min(50, top));
}

/** Returns true if both organization and project are configured. */
get isConfigured(): boolean {
const organizations = this.selectedOrganizations;
Expand Down
Loading