Skip to content
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

Ability to group several build definitions #12

Merged
merged 5 commits into from
Jun 28, 2017
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,14 @@ The extension is enabled by providing the following settings (user or workspace)
}
```

It also allows to add several build definitions grouped into one status bar indicator. It could be helpful if you trigger multiple builds which overall result is success only if all builds are passing (e.g. when running multiple build definitions on the same code base cross-platform). To enable grouped build definitions, add the following configuration in addition to the one at the top:

```json
{
"vsts.definitionsGroup": "1,2,3", // IDs of build definitions to be grouped, separated with a comma
"vsts.definitionsGroupName": "My Grouped Builds", // Name of the grouped build definitions
}
```

## License
MIT, please see LICENSE for details. Copyright (c) 2016 Jeppe Andersen.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
"type": "string",
"default": "",
"description": "Specifies the VSTS project to look for builds in."
},
"vsts.definitionsGroup": {
"type": "string",
"default": "",
"description": "Allows to group status of the several build definitions by IDs (e.g. '1,2,3')."
},
"vsts.definitionsGroupName": {
"type": "string",
"default": "",
"description": "Allows to provide name for grouped build definitions."
}
}
},
Expand Down
53 changes: 41 additions & 12 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export interface Settings {
username: string;
password: string;
project: string;
activeBuildDefinition: BuildDefinition;
activeBuildDefinitions: BuildDefinition[];
definitionsGroup?: BuildDefinition[];
definitionsGroupName?: string;
onDidChangeSettings(handler: () => void): void;

dispose(): void;
Expand All @@ -20,19 +22,21 @@ export class WorkspaceVstsSettings implements Settings {
username: string;
password: string;
project: string;
definitionsGroup?: BuildDefinition[];
definitionsGroupName?: string;

private _activeBuildDefinition: BuildDefinition;
private activeBuildDefinitionStateKey: string = "vsts.active.definition";
private _activeBuildDefinitions: BuildDefinition[] = [];
private activeBuildDefinitionsStateKey: string = "vsts.active.definitions";
private state: Memento;
private workspaceSettingsChangedDisposable: Disposable;
private onDidChangeSettingsHandler: () => any;

constructor(state: Memento) {
this.state = state;

var definition = state.get<BuildDefinition>(this.activeBuildDefinitionStateKey);
if (definition) {
this.activeBuildDefinition = definition;
var definitions = state.get<BuildDefinition[]>(this.activeBuildDefinitionsStateKey);
if (definitions) {
this.activeBuildDefinitions = definitions;
}

this.workspaceSettingsChangedDisposable = workspace.onDidChangeConfiguration(() => {
Expand All @@ -46,21 +50,21 @@ export class WorkspaceVstsSettings implements Settings {
this.reload();
}

get activeBuildDefinition(): BuildDefinition {
return this._activeBuildDefinition;
get activeBuildDefinitions(): BuildDefinition[] {
return this._activeBuildDefinitions;
}

set activeBuildDefinition(definition: BuildDefinition) {
this._activeBuildDefinition = definition;
this.state.update(this.activeBuildDefinitionStateKey, definition);
set activeBuildDefinitions(definitions: BuildDefinition[]) {
this._activeBuildDefinitions = definitions;
this.state.update(this.activeBuildDefinitionsStateKey, definitions);
}

public onDidChangeSettings(handler: () => any): void {
this.onDidChangeSettingsHandler = handler;
}

public isValid(): boolean {
return this.isAccountProvided() && this.isCredentialsProvided() && this.isProjectSpecified();
return this.isAccountProvided() && this.isCredentialsProvided() && this.isProjectSpecified() && this.isBuildDefinitionsNameSpecified();
}

public dispose(): void {
Expand Down Expand Up @@ -93,12 +97,37 @@ export class WorkspaceVstsSettings implements Settings {
return false;
}

private isBuildDefinitionsNameSpecified(): boolean {
if (this.definitionsGroup && !this.definitionsGroupName) {
return false;
}

return true;
}

private reload() {
var configuration = workspace.getConfiguration("vsts");

this.account = configuration.get<string>("account").trim();
this.username = configuration.get<string>("username").trim();
this.password = configuration.get<string>("password").trim();
this.project = configuration.get<string>("project").trim();

const definitionsGroup = configuration.get<string>("definitionsGroup").trim();
this.definitionsGroupName = configuration.get<string>("definitionsGroupName").trim();

if (definitionsGroup) {
const buildIds = definitionsGroup.split(',').map(id => parseInt(id));
let defList: BuildDefinition[] = [];
buildIds.forEach(id => {
defList.push({
id: id,
name: this.definitionsGroupName,
revision: undefined
});
});
this.definitionsGroup = defList;
this.activeBuildDefinitions = defList;
}
}
}
17 changes: 3 additions & 14 deletions src/vstsbuildrestclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ export class VstsBuildRestClientFactoryImpl implements VstsBuildRestClientFactor
}

export interface VstsBuildRestClient {
getLatest(definition: BuildDefinition): Thenable<HttpResponse<Build>>;
getBuilds(definition: BuildDefinition, take: number): Thenable<HttpResponse<Build[]>>;
getBuilds(definitions: BuildDefinition[], take: number): Thenable<HttpResponse<Build[]>>;
getBuild(buildId: number): Thenable<HttpResponse<Build>>;
getLog(build: Build): Thenable<HttpResponse<BuildLog>>;
getDefinitions(): Thenable<HttpResponse<BuildDefinition[]>>;
Expand All @@ -78,18 +77,8 @@ class VstsBuildRestClientImpl implements VstsBuildRestClient {
this.client = new rest.Client();
}

public getLatest(definition: BuildDefinition): Thenable<HttpResponse<Build>> {
return this.getBuilds(definition, 1).then(result => {
if (result.value.length > 0) {
return new HttpResponse(200, result.value[0]);
}

return VstsBuildRestClientImpl.emptyHttpResponse;
});
}

public getBuilds(definition: BuildDefinition, take: number = 5): Thenable<HttpResponse<Build[]>> {
let url = `https://${this.settings.account}.visualstudio.com/DefaultCollection/${this.settings.project}/_apis/build/builds?definitions=${definition.id}&$top=${take}&api-version=2.0`;
public getBuilds(definitions: BuildDefinition[], take: number = 5): Thenable<HttpResponse<Build[]>> {
let url = `https://${this.settings.account}.visualstudio.com/DefaultCollection/${this.settings.project}/_apis/build/builds?definitions=${definitions.map(d => d.id).join(',')}&$top=${take}&api-version=2.0`;

return this.getMany<Build[]>(url).then(response => {
if (response.value && response.value.length > 0) {
Expand Down
89 changes: 60 additions & 29 deletions src/vstsbuildstatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import fs = require("fs");
import openurl = require("openurl");

interface BuildDefinitionQuickPickItem {
id: number;
ids: number[];
label: string;
description: string;
definition: BuildDefinition;
definitions: BuildDefinition[];
}

interface BuildQuickPickItem {
Expand All @@ -26,18 +26,17 @@ export class VstsBuildStatus {
private updateIntervalInSeconds = 15;
private statusBar: VstsBuildStatusBar;

private activeDefinition: BuildDefinition;
private activeDefinitions: BuildDefinition[];
private settings: Settings;
private intervalTimer: NodeJS.Timer;
private restClient: VstsBuildRestClient;
private logStreamHandler: VstsBuildLogStreamHandler;


constructor(settings: Settings, restClientFactory: VstsBuildRestClientFactory) {
this.settings = settings;
this.statusBar = new VstsBuildStatusBar();
this.restClient = restClientFactory.createClient(settings);
this.activeDefinition = settings.activeBuildDefinition;
this.activeDefinitions = settings.activeBuildDefinitions;
this.logStreamHandler = new VstsBuildLogStreamHandler(this.restClient);

this.settings.onDidChangeSettings(() => {
Expand All @@ -54,7 +53,7 @@ export class VstsBuildStatus {

public updateStatus(): void {
// Updates the status bar depending on the state.
// If everything goes well, the method ißs set up to be called periodically.
// If everything goes well, the method is set up to be called periodically.

if (!this.settings.isValid()) {
this.tryCancelPeriodicStatusUpdate();
Expand All @@ -63,26 +62,37 @@ export class VstsBuildStatus {
return;
}

if (!this.activeDefinition) {
this.statusBar.displayInformation("Select build definition", "");

if (!this.activeDefinitions || this.activeDefinitions.length < 1) {
this.statusBar.displayInformation("Select a single build definition or set an aggregated list of IDs in settings");
return;
}

this.restClient.getLatest(this.activeDefinition).then(
this.restClient.getBuilds(this.activeDefinitions, this.activeDefinitions.length).then(
response => {
const definitionName = this.settings.definitionsGroupName && this.activeDefinitions.length > 1 ? this.settings.definitionsGroupName : this.activeDefinitions[0].name;

if (!response.value) {
this.statusBar.displayNoBuilds(this.activeDefinition.name, "No builds found");
this.statusBar.displayNoBuilds(definitionName, "No builds found");
return;
}

if (response.value && response.value.result) {
if (response.value.result === "succeeded") {
this.statusBar.displaySuccess(this.activeDefinition.name, "Last build was completed successfully");
} else {
this.statusBar.displayError(this.activeDefinition.name, "Last build failed");
let succeeded = true;
for (let build of response.value) {
if (build.result) {
if (build.result !== "succeeded") {
this.statusBar.displayError(definitionName, "Last build failed");
succeeded = false;
break;
}
} else if (response.value) {
this.statusBar.displayLoading(definitionName, "Build in progress...");
succeeded = false;
break;
}
} else if (response.value) {
this.statusBar.displayLoading(this.activeDefinition.name, "Build in progress...");
}

if (succeeded) {
this.statusBar.displaySuccess(definitionName, "Last build was completed successfully");
}

this.tryStartPeriodicStatusUpdate();
Expand All @@ -94,8 +104,8 @@ export class VstsBuildStatus {
public openBuildDefinitionSelection(): void {
this.getBuildDefinitionByQuickPick("Select a build definition to monitor").then(result => {
if (result) {
this.activeDefinition = result;
this.settings.activeBuildDefinition = this.activeDefinition;
this.activeDefinitions = result;
this.settings.activeBuildDefinitions = this.activeDefinitions;
this.updateStatus();
}
}, error => {
Expand All @@ -108,8 +118,12 @@ export class VstsBuildStatus {
if (!result) {
return;
}
if (result.length > 1) {
window.showInformationMessage(`Group build definition cannot be opened, please select single one instead.`);
return;
}

return this.getBuildByQuickPick(result, "Select a build to open");
return this.getBuildByQuickPick(result[0], "Select a build to open");
}).then(build => {
if (!build) {
return;
Expand All @@ -124,8 +138,12 @@ export class VstsBuildStatus {
if (!result) {
return;
}
if (result.length > 1) {
window.showInformationMessage(`Viewing group build is not possible, please select single build instead.`);
return;
}

return this.getBuildByQuickPick(result, "Select a build to view");
return this.getBuildByQuickPick(result[0], "Select a build to view");
}).then(build => {
if (!build) {
return;
Expand All @@ -142,14 +160,18 @@ export class VstsBuildStatus {
if (!result) {
return Promise.reject(null);
}
if (result.length > 1) {
window.showInformationMessage(`Queueing group build is not possible, please queue single builds one-by-one instead.`);
return Promise.reject(null);;
}

return window.showInputBox({prompt: "Branch (leave empty to use default) ?"}).then(branch => {
if(branch !== undefined) {
if(branch.length !== 0) {
result.sourceBranch = branch;
result[0].sourceBranch = branch;
}

return this.restClient.queueBuild(result);
return this.restClient.queueBuild(result[0]);
}
else {
// The user has cancel the input box
Expand All @@ -166,7 +188,7 @@ export class VstsBuildStatus {
});
}

private getBuildDefinitionByQuickPick(placeHolder: string): Thenable<BuildDefinition> {
private getBuildDefinitionByQuickPick(placeHolder: string): Thenable<BuildDefinition[]> {
if (!this.settings.isValid()) {
this.showSettingsMissingMessage();

Expand All @@ -179,18 +201,27 @@ export class VstsBuildStatus {
return {
label: definition.name,
description: `Revision ${definition.revision}`,
id: definition.id,
definition: definition
ids: [definition.id],
definitions: [definition]
}
});

if (this.settings.definitionsGroup) {
buildDefinitions.push({
label: this.settings.definitionsGroupName ? this.settings.definitionsGroupName : this.settings.definitionsGroup.map(b => b.id.toString()).join(','),
description: 'Grouped Build Definition',
ids: this.settings.definitionsGroup.map(b => b.id),
definitions: this.settings.definitionsGroup
});
}

let options = {
placeHolder: placeHolder
};

window.showQuickPick(buildDefinitions, options).then(result => {
if (result) {
resolve(result.definition);
resolve(result.definitions);
} else {
resolve(null);
}
Expand All @@ -202,7 +233,7 @@ export class VstsBuildStatus {
}

private getBuildByQuickPick(definition: BuildDefinition, placeHolder: string): Thenable<Build> {
return this.restClient.getBuilds(definition, 10).then(builds => {
return this.restClient.getBuilds([definition], 10).then(builds => {
let buildQuickPickItems: BuildQuickPickItem[] = builds.value.map(build => {
return {
label: new Date(build.queueTime).toLocaleString(),
Expand Down
Loading