Skip to content
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"compile-exec": "npm run evergreen-release compile",
"compile-all": "npm run compile-compass && npm run compile-exec",
"evergreen-release": "cd packages/build && npm run evergreen-release --",
"release": "cd packages/build && npm run release --",
"report-missing-help": "lerna run --stream --scope @mongosh/shell-api report-missing-help",
"report-supported-api": "lerna run --stream --scope @mongosh/shell-api report-supported-api",
"report-coverage": "nyc report --reporter=text --reporter=html && nyc check-coverage --lines=95",
Expand Down
3 changes: 2 additions & 1 deletion packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"test-ci": "mocha -r \"../../scripts/import-expansions.js\" --timeout 30000 -r ts-node/register \"./src/**/*.spec.ts\"",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"check": "npm run lint",
"evergreen-release": "ts-node -r ../../scripts/import-expansions.js src/index.ts"
"evergreen-release": "ts-node -r ../../scripts/import-expansions.js src/index.ts",
"release": "ts-node src/index.ts trigger-release"
},
"license": "Apache-2.0",
"publishConfig": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { promises as fs } from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import { promisify } from 'util';
import { downloadArtifactFromEvergreen } from './evergreen';
import { downloadArtifactFromEvergreen } from './artifacts';

describe('evergreen', () => {
describe('evergreen artifacts', () => {
describe('downloadArtifactFromEvergreen', () => {
let tmpDir: string;

Expand Down
3 changes: 3 additions & 0 deletions packages/build/src/evergreen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export * from './artifacts';
export * from './rest-api';
108 changes: 108 additions & 0 deletions packages/build/src/evergreen/rest-api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect } from 'chai';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';
import sinon from 'sinon';
import YAML from 'yaml';
import { EvergreenApi, EvergreenTask } from './rest-api';

describe('evergreen rest-api', () => {
describe('from user configuration', () => {
const configData = {
api_server_host: 'host',
user: 'user',
api_key: 'key'
};
const writeEvergreenConfiguration = async(content: string): Promise<string> => {
const configFile = path.join(os.tmpdir(), `evergreen-${new Date().getTime()}-${Math.random()}.yaml`);
await fs.writeFile(configFile, content, { encoding: 'utf-8' });
return configFile;
};

it('parses a configuration file correctly', async() => {
const configFile = await writeEvergreenConfiguration(YAML.stringify(configData));
const api = await EvergreenApi.fromUserConfiguration(configFile);
expect(api.apiBasepath).to.equal('host');
expect(api.apiUser).to.equal('user');
expect(api.apiKey).to.equal('key');
});

it('throws an error when the configuration file does not exist', async() => {
try {
await EvergreenApi.fromUserConfiguration('kasldjflasjk dfalsd jfsdfk');
} catch (e) {
expect(e.message).to.contain('Could not find local evergreen configuration');
return;
}
expect.fail('Expected error');
});

['api_server_host', 'user', 'api_key'].forEach(key => {
it(`throws an error if ${key} is missing`, async() => {
const data: Record<string, string> = {
...configData
};
delete data[key];
const configFile = await writeEvergreenConfiguration(YAML.stringify(data));
try {
await EvergreenApi.fromUserConfiguration(configFile);
} catch (e) {
expect(e.message).to.contain(key);
}
});
});
});

describe('getTasks', () => {
let fetch: sinon.SinonStub;
let api: EvergreenApi;

beforeEach(() => {
fetch = sinon.stub();
api = new EvergreenApi(
'//basePath/api', 'user', 'key', fetch as any
);
});

it('executes a proper GET', async() => {
const task: EvergreenTask = {
task_id: 'task_id',
version_id: 'version',
status: 'success',
display_name: 'Task',
build_variant: 'variant'
};
fetch.resolves({
status: 200,
json: sinon.stub().resolves([task])
});

const tasks = await api.getTasks('mongosh', 'sha');
expect(tasks).to.deep.equal([task]);
expect(fetch).to.have.been.calledWith(
'//basePath/api/rest/v2/projects/mongosh/revisions/sha/tasks',
{
headers: {
'Api-User': 'user',
'Api-Key': 'key'
}
}
);
});

it('fails if there is a non-200 response code', async() => {
fetch.resolves({
status: 404,
text: sinon.stub().resolves('ERR: Not found')
});

try {
await api.getTasks('mongosh', 'sha');
} catch (e) {
expect(e.message).to.equal('Unexpected response status: 404 - ERR: Not found');
return;
}
expect.fail('Expected error');
});
});
});
76 changes: 76 additions & 0 deletions packages/build/src/evergreen/rest-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable camelcase */
import { promises as fs, constants } from 'fs';
import { default as fetchFn } from 'node-fetch';
import os from 'os';
import path from 'path';
import YAML from 'yaml';

export type EvergreenTaskStatus = 'undispatched' | 'scheduled' | 'started' | 'success' | 'failed' | 'aborted';

// For full specification of all fields see: https://github.com/evergreen-ci/evergreen/wiki/REST-V2-Usage#objects
export interface EvergreenTask {
task_id: string;
version_id: string;
display_name: string;
build_variant: string;
status: EvergreenTaskStatus;
}

export class EvergreenApi {
constructor(
public readonly apiBasepath: string,
public readonly apiUser: string,
public readonly apiKey: string,
private readonly fetch: typeof fetchFn = fetchFn
) {}

public static async fromUserConfiguration(
pathToConfiguration = path.join(os.homedir(), '.evergreen.yml')
): Promise<EvergreenApi> {
try {
await fs.access(pathToConfiguration, constants.R_OK);
} catch {
throw new Error(`Could not find local evergreen configuration: ${pathToConfiguration}. Ensure it exists and can be read.`);
}

const configuration = YAML.parse(await fs.readFile(pathToConfiguration, { encoding: 'utf-8' }));
['api_server_host', 'user', 'api_key'].forEach(key => {
if (typeof configuration[key] !== 'string') {
throw new Error(`Evergreen configuration ${pathToConfiguration} misses required key ${key}`);
}
});
return new EvergreenApi(
configuration.api_server_host,
configuration.user,
configuration.api_key,
);
}

public async getTasks(
project: string,
commitSha: string
): Promise<EvergreenTask[]> {
return await this.apiGET<EvergreenTask[]>(
`/projects/${project}/revisions/${commitSha}/tasks`
);
}

private async apiGET<T>(path: string): Promise<T> {
const response = await this.fetch(
`${this.apiBasepath}/rest/v2${path}`,
{ headers: this.getApiHeaders() }
);

if (response.status >= 300) {
throw new Error(`Unexpected response status: ${response.status} - ${await response.text()}`);
}
return await response.json();
}

private getApiHeaders(): Record<string, string> {
return {
'Api-User': this.apiUser,
'Api-Key': this.apiKey,
};
}
}
3 changes: 3 additions & 0 deletions packages/build/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export * from './spawn-sync';
export * from './user-input';
33 changes: 33 additions & 0 deletions packages/build/src/helpers/user-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* istanbul ignore file */
import readline from 'readline';

export async function ask(prompt: string): Promise<string> {
return new Promise(resolve => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(`${prompt} `, (answer) => {
rl.close();
resolve(answer);
});
});
}

export async function confirm(prompt: string): Promise<boolean> {
const answer = await ask(`${prompt} Y/[N]:`);
return !!answer.match(/^[yY]$/);
}

export async function choose(headline: string, options: string[], prompt: string): Promise<string> {
console.info(headline);
options.forEach(o => console.info(` > ${o}`));

let answer: string | undefined;
do {
answer = await ask(prompt);
} while (!options.includes(answer));

return answer;
}
31 changes: 17 additions & 14 deletions packages/build/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@ import path from 'path';
import { ALL_BUILD_VARIANTS } from './config';
import { downloadMongoDb } from './download-mongodb';
import { getArtifactUrl } from './evergreen';
import { triggerRelease } from './local';
import { release, ReleaseCommand } from './release';

export { getArtifactUrl, downloadMongoDb };

if (require.main === module) {
(async() => {
const config = require(path.join(__dirname, '..', '..', '..', 'config', 'build.conf.js'));

const command = process.argv[2];

if (!['bump', 'compile', 'package', 'upload', 'draft', 'publish'].includes(command)) {
throw new Error('USAGE: npm run evergreen-release <bump|compile|package|upload|draft|publish>');
if (!['bump', 'compile', 'package', 'upload', 'draft', 'publish', 'trigger-release'].includes(command)) {
throw new Error('USAGE: npm run evergreen-release <bump|compile|package|upload|draft|publish|trigger-release>');
}

const cliBuildVariant = process.argv
.map((arg) => arg.match(/^--build-variant=(.+)$/))
.filter(Boolean)[0];
if (cliBuildVariant) {
config.buildVariant = cliBuildVariant[1];
if (!ALL_BUILD_VARIANTS.includes(config.buildVariant)) {
throw new Error(`Unknown build variant: ${config.buildVariant} - must be one of: ${ALL_BUILD_VARIANTS}`);
if (command === 'trigger-release') {
await triggerRelease(process.argv.slice(3));
} else {
const config = require(path.join(__dirname, '..', '..', '..', 'config', 'build.conf.js'));
const cliBuildVariant = process.argv
.map((arg) => arg.match(/^--build-variant=(.+)$/))
.filter(Boolean)[0];
if (cliBuildVariant) {
config.buildVariant = cliBuildVariant[1];
if (!ALL_BUILD_VARIANTS.includes(config.buildVariant)) {
throw new Error(`Unknown build variant: ${config.buildVariant} - must be one of: ${ALL_BUILD_VARIANTS}`);
}
}
}

await release(command as ReleaseCommand, config);
await release(command as ReleaseCommand, config);
}
})().then(
() => process.exit(0),
(err) => process.nextTick(() => { throw err; })
Expand Down
71 changes: 71 additions & 0 deletions packages/build/src/local/get-latest-tag.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { getLatestDraftOrReleaseTagFromLog } from './get-latest-tag';

describe('local get-latest-tag', () => {
let spawnSync: sinon.SinonStub;

beforeEach(() => {
spawnSync = sinon.stub();
});

describe('getLatestDraftOrReleaseTagFromLog', () => {
it('extracts the latest draft tag', () => {
spawnSync.onFirstCall().returns({
stdout: [
'v0.7.9',
'v0.8.0-draft.0',
'v0.8.0-draft.1',
'v0.8.0-draft.10',
'v0.8.0-draft.2'
].join('\n')
});
spawnSync.onSecondCall().returns({
stdout: 'tagHash'
});

const result = getLatestDraftOrReleaseTagFromLog(
'somePath',
spawnSync
);
expect(result).to.deep.equal({
commit: 'tagHash',
tag: {
semverName: '0.8.0-draft.10',
releaseVersion: '0.8.0',
draftVersion: 10
}
});
});

it('extracts the latest release tag', () => {
spawnSync.onFirstCall().returns({
stdout: [
'v0.8.0',
'v0.8.0-draft.0',
'v0.8.0-draft.1',
'v0.8.0-draft.10',
'v0.8.1',
'v0.8.0-draft.2',
'v0.8.1-draft.0',
].join('\n')
});
spawnSync.onSecondCall().returns({
stdout: 'tagHash'
});

const result = getLatestDraftOrReleaseTagFromLog(
'somePath',
spawnSync
);
expect(result).to.deep.equal({
commit: 'tagHash',
tag: {
semverName: '0.8.1',
releaseVersion: '0.8.1',
draftVersion: undefined
}
});
});
});
});
Loading