diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts index 5e5735f4723..efc8a5f1301 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts @@ -4,28 +4,72 @@ import fetch, {RequestInit} from 'node-fetch'; import {assertNotMissingOrEmpty} from './utils'; // Constants -const CIRCLE_CI_API_URL = 'https://circleci.com/api/v2/project/gh'; +const CIRCLE_CI_BASE_API_URL = 'https://circleci.com/api/v2'; +const CIRCLE_CI_BUILD_API_URL = `${CIRCLE_CI_BASE_API_URL}/project/gh`; +const CIRCLE_CI_PIPELINE_API_URL = `${CIRCLE_CI_BASE_API_URL}/pipeline`; // Interfaces - Types + +// API docs: https://circleci.com/docs/api/v2#operation/getJobArtifacts +// Example: https://circleci.com/api/v2/project/gh/angular/angular/1163941/artifacts export interface ArtifactInfo { + /** The path of this build artifacts. */ path: string; - pretty_path: string; - node_index: number; + + /** The full URL where this artifact can be downloaded from. */ url: string; + + // There are other fields but they are not used in this code. } -export type ArtifactResponse = ArtifactInfo[]; +export type ArtifactResponse = {items: ArtifactInfo[]}; +// API docs: https://circleci.com/docs/api/v2#operation/getJobDetails +// Example: https://circleci.com/api/v2/project/gh/angular/angular/job/1163941 export interface BuildInfo { - reponame: string; - failed: boolean; - branch: string; - username: string; - build_num: number; - has_artifacts: boolean; - outcome: string; // e.g. 'success' - vcs_revision: string; // HEAD SHA - // there are other fields but they are not used in this code + /** The job number. */ + number: number; + + /** The job name (e.g. `'aio_preview'`). */ + name: string; + + /** The job status (e.g. `'success'` or `'failed'`). */ + status: string; + + /** Info about the organization which the project related to this job belongs to. */ + organization: { + name: string; + }; + + /** Info about the project related to this job. */ + project: { + name: string; + }; + + /** Info about the [pipeline](https://circleci.com/docs/2.0/pipelines/) that this job is part of. */ + pipeline: { + id: string; + }; + + // There are other fields but they are not used in this code. +} + +// API docs: https://circleci.com/docs/api/v2#operation/getPipelineById +// https://circleci.com/api/v2/pipeline/356227c0-32f6-4f99-bfc2-3938db90a147 +export interface PipelineInfo { + /** The pipeline ID. */ + id: string; + + /** Info related to/retrieved from the version control system provider (e.g. GitHub). */ + vcs: { + /** The PR number. */ + review_id: string; + + /** The HEAD SHA. */ + revision: string; + }; + + // There are other fields but they are not used in this code. } /** @@ -59,13 +103,13 @@ export class CircleCiApi { } /** - * Get the info for a build from the CircleCI API + * Get the info for a build (aka job) from the CircleCI API. * @param buildNumber The CircleCI build number that generated the artifact. - * @returns A promise to the info about the build + * @returns A promise to the info about the build. */ public async getBuildInfo(buildNumber: number): Promise { try { - const url = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/job/${buildNumber}`; + const url = `${CIRCLE_CI_BUILD_API_URL}/${this.githubOrg}/${this.githubRepo}/job/${buildNumber}`; const response = await this.fetchFromCircleCi(url); return response.json(); } catch (error) { @@ -79,10 +123,10 @@ export class CircleCiApi { * @returns A promise to the URL that can be requested to download the actual build artifact file. */ public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise { - const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`; + const baseUrl = `${CIRCLE_CI_BUILD_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`; try { const response = await this.fetchFromCircleCi(`${baseUrl}/artifacts`); - const artifacts = await response.json() as {items: ArtifactResponse}; + const artifacts = await response.json() as ArtifactResponse; const artifact = artifacts.items.find(item => item.path === artifactPath); if (!artifact) { throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`); @@ -92,4 +136,19 @@ export class CircleCiApi { throw new Error(`CircleCI artifact URL request failed (${(error as Error).message})`); } } + + /** + * Get the info for a [pipeline](https://circleci.com/docs/2.0/pipelines/) from the CircleCI API. + * @param pipelineId The CircleCI pipeline ID that generated the artifact. + * @returns A promise to the info about the pipeline. + */ + public async getPipelineInfo(pipelineId: string): Promise { + try { + const url = `${CIRCLE_CI_PIPELINE_API_URL}/${pipelineId}`; + const response = await this.fetchFromCircleCi(url); + return response.json(); + } catch (error) { + throw new Error(`CircleCI pipeline info request failed (${(error as Error).message})`); + } + } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts index 97f3954c388..19c154dbce6 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts @@ -36,12 +36,13 @@ export class BuildRetriever { */ public async getGithubInfo(buildNum: number): Promise { const buildInfo = await this.api.getBuildInfo(buildNum); + const pipelineInfo = await this.api.getPipelineInfo(buildInfo.pipeline.id); const githubInfo: GithubInfo = { - org: buildInfo.username, - pr: getPrFromBranch(buildInfo.branch), - repo: buildInfo.reponame, - sha: buildInfo.vcs_revision, - success: !buildInfo.failed, + org: buildInfo.organization.name, + pr: +pipelineInfo.vcs.review_id, + repo: buildInfo.project.name, + sha: pipelineInfo.vcs.revision, + success: buildInfo.status === 'success', }; return githubInfo; } @@ -76,12 +77,3 @@ export class BuildRetriever { } } } - -function getPrFromBranch(branch: string): number { - // CircleCI only exposes PR numbers via the `branch` field :-( - const match = /^pull\/(\d+)$/.exec(branch); - if (!match) { - throw new Error(`No PR found in branch field: ${branch}`); - } - return +match[1]; -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts index 19ce0938790..dfa47863761 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts @@ -4,6 +4,8 @@ export const enum BuildNums { BUILD_INFO_BUILD_FAILED, BUILD_INFO_INVALID_GH_ORG, BUILD_INFO_INVALID_GH_REPO, + PIPELINE_INFO_ERROR, + PIPELINE_INFO_404, CHANGED_FILES_ERROR, CHANGED_FILES_404, CHANGED_FILES_NONE, @@ -21,6 +23,17 @@ export const enum BuildNums { TRUST_CHECK_INACTIVE_TRUSTED_USER, } +export const enum PipelineIds { + PIPELINE_INFO_ERROR = 'pipeline-error', + PIPELINE_INFO_404 = 'pipeline-404', + CHANGED_FILES_ERROR = 'pipeline-cfe', + CHANGED_FILES_404 = 'pipeline-cf404', + CHANGED_FILES_NONE = 'pipeline-cfn', + TRUST_CHECK_ERROR = 'pipeline-tce', + TRUST_CHECK_UNTRUSTED = 'pipeline-tcu', + PIPELINE_INFO_OK = 'pipeline-ok', +} + export const enum PrNums { CHANGED_FILES_ERROR = 1, CHANGED_FILES_404, diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts index 9e9ab784f75..9ff9ac312cb 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts @@ -3,7 +3,7 @@ import * as nock from 'nock'; import * as tar from 'tar-stream'; import {gzipSync} from 'zlib'; import {getEnvVar, Logger} from '../common/utils'; -import {BuildNums, PrNums, SHA} from './constants'; +import {BuildNums, PipelineIds, PrNums, SHA} from './constants'; // We are using the `nock` library to fake responses from REST requests, when testing. // This is necessary, because the test preview-server runs as a separate node process to @@ -30,11 +30,18 @@ const INACTIVE_TRUSTED_USER = 'inactive-trusted-user'; const UNTRUSTED_USER = 'untrusted-user'; const BASIC_BUILD_INFO = { - branch: `pull/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`, - failed: false, - reponame: AIO_GITHUB_REPO, - username: AIO_GITHUB_ORGANIZATION, - vcs_revision: SHA, + name: 'test_job', + status: 'success', + organization: {name: AIO_GITHUB_ORGANIZATION}, + project: {name: AIO_GITHUB_REPO}, + pipeline: {id: PipelineIds.PIPELINE_INFO_OK}, +}; +const BASIC_PIPELINE_INFO = { + id: PipelineIds.PIPELINE_INFO_OK, + vcs: { + review_id: `${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`, + revision: SHA, + }, }; const ISSUE_INFO_TRUSTED_LABEL = { labels: [{ name: AIO_TRUSTED_PR_LABEL }], user: { login: UNTRUSTED_USER } }; @@ -57,10 +64,12 @@ const ARTIFACT_VALID_TRUSTED_LABEL = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_C const ARTIFACT_VALID_UNTRUSTED = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/untrusted`, _urlPath: '/artifacts/valid/untrusted' }; const CIRCLE_CI_BUILD_INFO_URL = `/api/v2/project/gh/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}`; +const CIRCLE_CI_PIPELINE_INFO_URL = '/api/v2/pipeline'; const buildInfoUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/job/${buildNum}`; const buildArtifactsUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}/artifacts`; -const buildInfo = (prNum: number) => ({ ...BASIC_BUILD_INFO, branch: `pull/${prNum}` }); +const pipelineInfoUrl = (pipelineId: string) => `${CIRCLE_CI_PIPELINE_INFO_URL}/${pipelineId}`; +const buildInfo = (pipelineId: string) => ({ ...BASIC_BUILD_INFO, pipeline: { id: pipelineId } }); const GITHUB_API_HOST = 'https://api.github.com'; const GITHUB_ISSUES_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/issues`; @@ -72,6 +81,15 @@ const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`; const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`; +const setUpCircleCiApiForBuild = + (buildNum: number, pipelineId: string, prNum = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, sha = SHA) => { + circleCiApi.get(buildInfoUrl(buildNum)).reply(200, buildInfo(pipelineId)); + circleCiApi.get(pipelineInfoUrl(pipelineId)).reply(200, { + id: pipelineId, + vcs: { review_id: `${prNum}`, revision: sha }, + }); + }; + const createArchive = (buildNum: number, prNum: number, sha: string) => { logger.log('createArchive', buildNum, prNum, sha); const pack = tar.pack(); @@ -91,42 +109,50 @@ const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', ` // GENERAL responses githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO); githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200); +circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_OK)).reply(200, BASIC_PIPELINE_INFO); -// BUILD_INFO errors +// BUILD INFO errors circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_ERROR)).replyWithError('BUILD_INFO_ERROR'); circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_404)).reply(404, 'BUILD_INFO_404'); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_BUILD_FAILED)).reply(200, { ...BASIC_BUILD_INFO, failed: true }); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, username: 'bad' }); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, reponame: 'bad' }); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_BUILD_FAILED)).reply(200, { ...BASIC_BUILD_INFO, status: 'failed' }); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, organization: { name: 'bad' } }); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, project: { name: 'bad' } }); + +// PIPELINE INFO errors +circleCiApi.get(buildInfoUrl(BuildNums.PIPELINE_INFO_ERROR)).reply(200, { ...BASIC_BUILD_INFO, pipeline: { id: PipelineIds.PIPELINE_INFO_ERROR } }); +circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_ERROR)).replyWithError('PIPELINE_INFO_ERROR'); + +circleCiApi.get(buildInfoUrl(BuildNums.PIPELINE_INFO_404)).reply(200, { ...BASIC_BUILD_INFO, pipeline: { id: PipelineIds.PIPELINE_INFO_404 } }); +circleCiApi.get(pipelineInfoUrl(PipelineIds.PIPELINE_INFO_ERROR)).reply(404, 'PIPELINE_INFO_404'); // CHANGED FILE errors -circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_ERROR)).reply(200, buildInfo(PrNums.CHANGED_FILES_ERROR)); +setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_ERROR, PipelineIds.CHANGED_FILES_ERROR, PrNums.CHANGED_FILES_ERROR); githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_ERROR)).replyWithError('CHANGED_FILES_ERROR'); -circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_404)).reply(200, buildInfo(PrNums.CHANGED_FILES_404)); +setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_404, PipelineIds.CHANGED_FILES_404, PrNums.CHANGED_FILES_404); githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_404)).reply(404, 'CHANGED_FILES_404'); -circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_NONE)).reply(200, buildInfo(PrNums.CHANGED_FILES_NONE)); +setUpCircleCiApiForBuild(BuildNums.CHANGED_FILES_NONE, PipelineIds.CHANGED_FILES_NONE, PrNums.CHANGED_FILES_NONE); githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_NONE)).reply(200, []); // ARTIFACT URL errors -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).replyWithError('BUILD_ARTIFACTS_ERROR'); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(404, 'BUILD_ARTIFACTS_ERROR'); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, { items: [] }); -circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, { items: [ARTIFACT_1, ARTIFACT_2, ARTIFACT_3] }); // ARTIFACT DOWNLOAD errors -circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, { items: [ARTIFACT_ERROR] }); circleCiApi.get(ARTIFACT_ERROR._urlPath).replyWithError(ARTIFACT_ERROR._urlPath); -circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, BASIC_BUILD_INFO); circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, [ARTIFACT_404]); circleCiApi.get(ARTIFACT_ERROR._urlPath).reply(404, ARTIFACT_ERROR._urlPath); // TRUST CHECK errors -circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ERROR)); +setUpCircleCiApiForBuild(BuildNums.TRUST_CHECK_ERROR, PipelineIds.TRUST_CHECK_ERROR, PrNums.TRUST_CHECK_ERROR); githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ERROR)).reply(200, [{ filename: 'aio/a' }]); circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, { items: [ARTIFACT_VALID_TRUSTED_USER] }); githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ERROR)).replyWithError('TRUST_CHECK_ERROR'); @@ -155,7 +181,7 @@ githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, githubApi.get(getTeamMembershipUrl(0, INACTIVE_TRUSTED_USER)).reply(200, INACTIVE_STATE); // UNTRUSTED reponse -circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, buildInfo(PrNums.TRUST_CHECK_UNTRUSTED)); +setUpCircleCiApiForBuild(BuildNums.TRUST_CHECK_UNTRUSTED, PipelineIds.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED); githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, [{ filename: 'aio/a' }]); circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, { items: [ARTIFACT_VALID_UNTRUSTED] }); circleCiApi.get(ARTIFACT_VALID_UNTRUSTED._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED, SHA)); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts index 72a9cc32648..148dec5893f 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts @@ -145,9 +145,11 @@ describe('preview-server', () => { ]); }); - it('should respond with 500 if the CircleCI API request errors', async () => { + it('should respond with 500 if any of the CircleCI API requests errors', async () => { await curl(payload(BuildNums.BUILD_INFO_ERROR)).then(h.verifyResponse(500)); await curl(payload(BuildNums.BUILD_INFO_404)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.PIPELINE_INFO_ERROR)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.PIPELINE_INFO_404)).then(h.verifyResponse(500)); }); it('should respond with 204 if the build on CircleCI failed', async () => { diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts index 831b88ef066..ff337a4d13d 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts @@ -4,7 +4,8 @@ import {CircleCiApi} from '../../lib/common/circle-ci-api'; const ORG = 'testorg'; const REPO = 'testrepo'; const TOKEN = 'xxxx'; -const BASE_URL = `https://circleci.com/api/v2/project/gh/${ORG}/${REPO}`; +const BASE_BUILD_URL = `https://circleci.com/api/v2/project/gh/${ORG}/${REPO}`; +const BASE_PIPELINE_URL = 'https://circleci.com/api/v2/pipeline'; describe('CircleCIApi', () => { describe('constructor()', () => { @@ -27,11 +28,11 @@ describe('CircleCIApi', () => { describe('fetchFromCircleCI', () => { it('should include the authentication token in the headers on every request', async () => { const api = new CircleCiApi(ORG, REPO, TOKEN); - const request = nock(BASE_URL) + const request = nock(BASE_BUILD_URL) .get('/') .matchHeader('Circle-Token', TOKEN) .reply(200); - await api.fetchFromCircleCi(`${BASE_URL}/`); + await api.fetchFromCircleCi(`${BASE_BUILD_URL}/`); request.done(); }) }) @@ -42,7 +43,7 @@ describe('CircleCIApi', () => { const buildNum = 12345; const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum }; - const request = nock(BASE_URL) + const request = nock(BASE_BUILD_URL) .get(`/job/${buildNum}`) .reply(200, expectedBuildInfo); @@ -55,17 +56,17 @@ describe('CircleCIApi', () => { const api = new CircleCiApi(ORG, REPO, TOKEN); const buildNum = 12345; const errorMessage = 'Invalid request'; - const request = nock(BASE_URL).get(`/job/${buildNum}`); + const request = nock(BASE_BUILD_URL).get(`/job/${buildNum}`); request.replyWithError(errorMessage); await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError( `CircleCI build info request failed ` + - `(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`); + `(request to ${BASE_BUILD_URL}/job/${buildNum} failed, reason: ${errorMessage})`); request.reply(404, errorMessage); await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError( `CircleCI build info request failed ` + - `(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`); + `(request to ${BASE_BUILD_URL}/job/${buildNum} failed, reason: ${errorMessage})`); }); }); @@ -76,7 +77,7 @@ describe('CircleCIApi', () => { const artifact0: any = { path: 'some/path/0', url: 'https://url/0' }; const artifact1: any = { path: 'some/path/1', url: 'https://url/1' }; const artifact2: any = { path: 'some/path/2', url: 'https://url/2' }; - const request = nock(BASE_URL) + const request = nock(BASE_BUILD_URL) .get(`/${buildNum}/artifacts`) .reply(200, {items: [artifact0, artifact1, artifact2]}); @@ -89,17 +90,17 @@ describe('CircleCIApi', () => { const api = new CircleCiApi(ORG, REPO, TOKEN); const buildNum = 12345; const errorMessage = 'Invalid request'; - const request = nock(BASE_URL).get(`/${buildNum}/artifacts`); + const request = nock(BASE_BUILD_URL).get(`/${buildNum}/artifacts`); request.replyWithError(errorMessage); await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError( `CircleCI artifact URL request failed ` + - `(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`); + `(request to ${BASE_BUILD_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`); request.reply(404, errorMessage); await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError( `CircleCI artifact URL request failed ` + - `(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`); + `(request to ${BASE_BUILD_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`); }); it('should throw an error if the response does not contain the specified artifact', async () => { @@ -108,7 +109,7 @@ describe('CircleCIApi', () => { const artifact0: any = { path: 'some/path/0', url: 'https://url/0' }; const artifact1: any = { path: 'some/path/1', url: 'https://url/1' }; const artifact2: any = { path: 'some/path/2', url: 'https://url/2' }; - nock(BASE_URL) + nock(BASE_BUILD_URL) .get(`/${buildNum}/artifacts`) .reply(200, {items: [artifact0, artifact1, artifact2]}); @@ -117,4 +118,37 @@ describe('CircleCIApi', () => { `(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`); }); }); + + describe('getPipelineInfo', () => { + it('should make a request to the CircleCI API for the given pipeline ID', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const pipelineId = 'a1-b2-c3'; + const expectedPipelineInfo: any = { org: ORG, repo: REPO, pipeline_id: pipelineId }; + + const request = nock(BASE_PIPELINE_URL) + .get(`/${pipelineId}`) + .reply(200, expectedPipelineInfo); + + const pipelineInfo = await api.getPipelineInfo(pipelineId); + expect(pipelineInfo).toEqual(expectedPipelineInfo); + request.done(); + }); + + it('should throw an error if the request fails', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const pipelineId = 'a1-b2-c3'; + const errorMessage = 'Invalid request'; + const request = nock(BASE_PIPELINE_URL).get(`/${pipelineId}`); + + request.replyWithError(errorMessage); + await expectAsync(api.getPipelineInfo(pipelineId)).toBeRejectedWithError( + `CircleCI pipeline info request failed ` + + `(request to ${BASE_PIPELINE_URL}/${pipelineId} failed, reason: ${errorMessage})`); + + request.reply(404, errorMessage); + await expectAsync(api.getPipelineInfo(pipelineId)).toBeRejectedWithError( + `CircleCI pipeline info request failed ` + + `(request to ${BASE_PIPELINE_URL}/${pipelineId} failed, reason: ${errorMessage})`); + }); + }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts index d5586419355..9377abb9d4f 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as nock from 'nock'; import {resolve as resolvePath} from 'path'; -import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api'; +import {BuildInfo, CircleCiApi, PipelineInfo} from '../../lib/common/circle-ci-api'; import {Logger} from '../../lib/common/utils'; import {BuildRetriever} from '../../lib/preview-server/build-retriever'; @@ -13,6 +13,7 @@ describe('BuildRetriever', () => { let api: CircleCiApi; let BUILD_INFO: BuildInfo; + let PIPELINE_INFO: PipelineInfo; let WRITEFILE_RESULT: any; let writeFileSpy: jasmine.Spy; let EXISTS_RESULT: boolean; @@ -21,18 +22,24 @@ describe('BuildRetriever', () => { beforeEach(() => { BUILD_INFO = { - branch: 'pull/777', - build_num: 12345, - failed: false, - has_artifacts: true, - outcome: 'success', - reponame: 'REPO', - username: 'ORG', - vcs_revision: 'COMMIT', + number: 12345, + name: 'test_job', + status: 'success', + organization: {name: 'ORG'}, + project: {name: 'REPO'}, + pipeline: {id: 'test_pipeline'}, + }; + PIPELINE_INFO = { + id: 'test_pipeline', + vcs: { + review_id: '777', + revision: 'COMMIT', + }, }; api = new CircleCiApi('ORG', 'REPO', 'TOKEN'); spyOn(api, 'getBuildInfo').and.resolveTo(BUILD_INFO); + spyOn(api, 'getPipelineInfo').and.resolveTo(PIPELINE_INFO); getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl').and.resolveTo(BASE_URL + ARTIFACT_PATH); WRITEFILE_RESULT = undefined; @@ -65,19 +72,13 @@ describe('BuildRetriever', () => { describe('getGithubInfo', () => { - it('should request the info from CircleCI', async () => { + it('should request the build and pipeline info from CircleCI', async () => { const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); const info = await retriever.getGithubInfo(12345); expect(api.getBuildInfo).toHaveBeenCalledWith(12345); + expect(api.getPipelineInfo).toHaveBeenCalledWith('test_pipeline'); expect(info).toEqual({org: 'ORG', pr: 777, repo: 'REPO', sha: 'COMMIT', success: true}); }); - - it('should error if it is not possible to extract the PR number from the branch', async () => { - BUILD_INFO.branch = 'master'; - const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); - - await expectAsync(retriever.getGithubInfo(12345)).toBeRejectedWithError('No PR found in branch field: master'); - }); });