mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build(docs-infra): move to circleci v2 api for aio-builds (#45349)
Move to the CircleCI v2 api as the authentication fails for downloading artifacts using the v1 methods. CircleCI v2 api now requires authentication to occur view the headers instead of being done in a query parameter, all of the CircleCI interactions are now performed through one fetchFromCircleCi method which ensures the token is provided in the headers as expected. PR Close #45349
This commit is contained in:
committed by
Alex Rickabaugh
parent
2a75754ee8
commit
a049840e0d
@@ -1,10 +1,10 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import fetch from 'node-fetch';
|
import fetch, {RequestInit} from 'node-fetch';
|
||||||
|
|
||||||
import {assertNotMissingOrEmpty} from './utils';
|
import {assertNotMissingOrEmpty} from './utils';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v1.1/project/github';
|
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v2/project/gh';
|
||||||
|
|
||||||
// Interfaces - Types
|
// Interfaces - Types
|
||||||
export interface ArtifactInfo {
|
export interface ArtifactInfo {
|
||||||
@@ -32,8 +32,6 @@ export interface BuildInfo {
|
|||||||
* A Helper that can interact with the CircleCI API.
|
* A Helper that can interact with the CircleCI API.
|
||||||
*/
|
*/
|
||||||
export class CircleCiApi {
|
export class CircleCiApi {
|
||||||
private tokenParam = `circle-token=${this.circleCiToken}`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a helper that can interact with the CircleCI REST API.
|
* Construct a helper that can interact with the CircleCI REST API.
|
||||||
* @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g.
|
* @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g.
|
||||||
@@ -51,6 +49,15 @@ export class CircleCiApi {
|
|||||||
assertNotMissingOrEmpty('circleCiToken', circleCiToken);
|
assertNotMissingOrEmpty('circleCiToken', circleCiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async fetchFromCircleCi(url: string, params: RequestInit = {}) {
|
||||||
|
params.headers = {...params.headers, 'Circle-Token': this.circleCiToken};
|
||||||
|
const response = await fetch(url, params);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`${url}: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the info for a build from the CircleCI API
|
* Get the info for a build from the CircleCI API
|
||||||
* @param buildNumber The CircleCI build number that generated the artifact.
|
* @param buildNumber The CircleCI build number that generated the artifact.
|
||||||
@@ -58,11 +65,8 @@ export class CircleCiApi {
|
|||||||
*/
|
*/
|
||||||
public async getBuildInfo(buildNumber: number): Promise<BuildInfo> {
|
public async getBuildInfo(buildNumber: number): Promise<BuildInfo> {
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
const url = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/job/${buildNumber}`;
|
||||||
const response = await fetch(`${baseUrl}?${this.tokenParam}`);
|
const response = await this.fetchFromCircleCi(url);
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`CircleCI build info request failed (${(error as Error).message})`);
|
throw new Error(`CircleCI build info request failed (${(error as Error).message})`);
|
||||||
@@ -77,9 +81,9 @@ export class CircleCiApi {
|
|||||||
public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise<string> {
|
public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise<string> {
|
||||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
const response = await this.fetchFromCircleCi(`${baseUrl}/artifacts`);
|
||||||
const artifacts = await response.json() as ArtifactResponse;
|
const artifacts = await response.json() as {items: ArtifactResponse};
|
||||||
const artifact = artifacts.find(item => item.path === artifactPath);
|
const artifact = artifacts.items.find(item => item.path === artifactPath);
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import {dirname} from 'path';
|
import {dirname} from 'path';
|
||||||
import {mkdir} from 'shelljs';
|
import {mkdir} from 'shelljs';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
@@ -63,11 +62,7 @@ export class BuildRetriever {
|
|||||||
await new Promise(resolve => fs.exists(outPath, exists => resolve(exists)));
|
await new Promise(resolve => fs.exists(outPath, exists => resolve(exists)));
|
||||||
if (!downloadExists) {
|
if (!downloadExists) {
|
||||||
const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath);
|
const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath);
|
||||||
const response = await fetch(url, {size: this.downloadSizeLimit});
|
const response = await this.api.fetchFromCircleCi(url, {size: this.downloadSizeLimit});
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new PreviewServerError(
|
|
||||||
response.status, `Error ${response.status} - ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const buffer = await response.buffer();
|
const buffer = await response.buffer();
|
||||||
mkdir('-p', dirname(outPath));
|
mkdir('-p', dirname(outPath));
|
||||||
await promisify(fs.writeFile)(outPath, buffer);
|
await promisify(fs.writeFile)(outPath, buffer);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
|||||||
const ORG = 'testorg';
|
const ORG = 'testorg';
|
||||||
const REPO = 'testrepo';
|
const REPO = 'testrepo';
|
||||||
const TOKEN = 'xxxx';
|
const TOKEN = 'xxxx';
|
||||||
const BASE_URL = `https://circleci.com/api/v1.1/project/github/${ORG}/${REPO}`;
|
const BASE_URL = `https://circleci.com/api/v2/project/gh/${ORG}/${REPO}`;
|
||||||
|
|
||||||
describe('CircleCIApi', () => {
|
describe('CircleCIApi', () => {
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
@@ -24,6 +24,18 @@ 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)
|
||||||
|
.get('/')
|
||||||
|
.matchHeader('Circle-Token', TOKEN)
|
||||||
|
.reply(200);
|
||||||
|
await api.fetchFromCircleCi(`${BASE_URL}/`);
|
||||||
|
request.done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('getBuildInfo', () => {
|
describe('getBuildInfo', () => {
|
||||||
it('should make a request to the CircleCI API for the given build number', async () => {
|
it('should make a request to the CircleCI API for the given build number', async () => {
|
||||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
@@ -31,7 +43,7 @@ describe('CircleCIApi', () => {
|
|||||||
const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum };
|
const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum };
|
||||||
|
|
||||||
const request = nock(BASE_URL)
|
const request = nock(BASE_URL)
|
||||||
.get(`/${buildNum}?circle-token=${TOKEN}`)
|
.get(`/job/${buildNum}`)
|
||||||
.reply(200, expectedBuildInfo);
|
.reply(200, expectedBuildInfo);
|
||||||
|
|
||||||
const buildInfo = await api.getBuildInfo(buildNum);
|
const buildInfo = await api.getBuildInfo(buildNum);
|
||||||
@@ -43,17 +55,17 @@ describe('CircleCIApi', () => {
|
|||||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
const buildNum = 12345;
|
const buildNum = 12345;
|
||||||
const errorMessage = 'Invalid request';
|
const errorMessage = 'Invalid request';
|
||||||
const request = nock(BASE_URL).get(`/${buildNum}?circle-token=${TOKEN}`);
|
const request = nock(BASE_URL).get(`/job/${buildNum}`);
|
||||||
|
|
||||||
request.replyWithError(errorMessage);
|
request.replyWithError(errorMessage);
|
||||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||||
`CircleCI build info request failed ` +
|
`CircleCI build info request failed ` +
|
||||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
`(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
|
||||||
|
|
||||||
request.reply(404, errorMessage);
|
request.reply(404, errorMessage);
|
||||||
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
await expectAsync(api.getBuildInfo(buildNum)).toBeRejectedWithError(
|
||||||
`CircleCI build info request failed ` +
|
`CircleCI build info request failed ` +
|
||||||
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
`(request to ${BASE_URL}/job/${buildNum} failed, reason: ${errorMessage})`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,8 +77,8 @@ describe('CircleCIApi', () => {
|
|||||||
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||||
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||||
const request = nock(BASE_URL)
|
const request = nock(BASE_URL)
|
||||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
.get(`/${buildNum}/artifacts`)
|
||||||
.reply(200, [artifact0, artifact1, artifact2]);
|
.reply(200, {items: [artifact0, artifact1, artifact2]});
|
||||||
|
|
||||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeResolvedTo('https://url/1');
|
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeResolvedTo('https://url/1');
|
||||||
request.done();
|
request.done();
|
||||||
@@ -77,17 +89,17 @@ describe('CircleCIApi', () => {
|
|||||||
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
const buildNum = 12345;
|
const buildNum = 12345;
|
||||||
const errorMessage = 'Invalid request';
|
const errorMessage = 'Invalid request';
|
||||||
const request = nock(BASE_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`);
|
const request = nock(BASE_URL).get(`/${buildNum}/artifacts`);
|
||||||
|
|
||||||
request.replyWithError(errorMessage);
|
request.replyWithError(errorMessage);
|
||||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||||
`CircleCI artifact URL request failed ` +
|
`CircleCI artifact URL request failed ` +
|
||||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
`(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
|
||||||
|
|
||||||
request.reply(404, errorMessage);
|
request.reply(404, errorMessage);
|
||||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/1')).toBeRejectedWithError(
|
||||||
`CircleCI artifact URL request failed ` +
|
`CircleCI artifact URL request failed ` +
|
||||||
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
`(request to ${BASE_URL}/${buildNum}/artifacts failed, reason: ${errorMessage})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the response does not contain the specified artifact', async () => {
|
it('should throw an error if the response does not contain the specified artifact', async () => {
|
||||||
@@ -97,8 +109,8 @@ describe('CircleCIApi', () => {
|
|||||||
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||||
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||||
nock(BASE_URL)
|
nock(BASE_URL)
|
||||||
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
.get(`/${buildNum}/artifacts`)
|
||||||
.reply(200, [artifact0, artifact1, artifact2]);
|
.reply(200, {items: [artifact0, artifact1, artifact2]});
|
||||||
|
|
||||||
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/3')).toBeRejectedWithError(
|
await expectAsync(api.getBuildArtifactUrl(buildNum, 'some/path/3')).toBeRejectedWithError(
|
||||||
`CircleCI artifact URL request failed ` +
|
`CircleCI artifact URL request failed ` +
|
||||||
|
|||||||
+1
-1
@@ -160,7 +160,7 @@ describe('BuildRetriever', () => {
|
|||||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
|
||||||
|
|
||||||
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
await expectAsync(retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH)).
|
||||||
toBeRejectedWithError('CircleCI artifact download failed (Error 404 - Not Found)');
|
toBeRejectedWithError(`CircleCI artifact download failed (${BASE_URL}/some/path/build.zip: 404 - Not Found)`);
|
||||||
|
|
||||||
artifactRequest.done();
|
artifactRequest.done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ Necessary secrets:
|
|||||||
- Generate new token with the `public_repo` scope.
|
- Generate new token with the `public_repo` scope.
|
||||||
|
|
||||||
2. `CIRCLE_CI_TOKEN`
|
2. `CIRCLE_CI_TOKEN`
|
||||||
- Visit https://circleci.com/gh/angular/angular/edit#api.
|
- Visit https://app.circleci.com/settings/user/tokens.
|
||||||
- Create an API token with `Build Artifacts` scope.
|
- Create a user API token.
|
||||||
|
|
||||||
|
|
||||||
## Save secrets on the VM
|
## Save secrets on the VM
|
||||||
|
|||||||
Reference in New Issue
Block a user