diff --git a/actions/submit-signing-request/connector-url-builder.ts b/actions/submit-signing-request/connector-url-builder.ts new file mode 100644 index 0000000..8acebaf --- /dev/null +++ b/actions/submit-signing-request/connector-url-builder.ts @@ -0,0 +1,28 @@ +export class ConnectorUrlBuilder { + private readonly apiVersion: string = "1.0"; + private readonly baseSigningRequestsRoute: string; + + constructor(private readonly connectorBaseUrl: string, private readonly organizationId: string) { + this.connectorBaseUrl = this.trimSlash(this.connectorBaseUrl); + this.baseSigningRequestsRoute = `${this.connectorBaseUrl}/${encodeURIComponent(this.organizationId)}/SigningRequests` + } + + public buildSubmitSigningRequestUrl(): string { + return `${this.baseSigningRequestsRoute}?api-version=${this.apiVersion}` + } + + public buildGetSigningRequestStatusUrl(signingRequestId: string): string { + return `${this.baseSigningRequestsRoute}/${encodeURIComponent(signingRequestId)}/Status?api-version=${this.apiVersion}` + } + + public buildGetSignedArtifactUrl(signingRequestId: string): string { + return `${this.baseSigningRequestsRoute}/${encodeURIComponent(signingRequestId)}/SignedArtifact?api-version=${this.apiVersion}` + } + + private trimSlash(text: string): string { + if (text && text[text.length - 1] === '/') { + return text.substring(0, text.length - 1); + } + return text; + } +} \ No newline at end of file diff --git a/actions/submit-signing-request/dtos/signing-request-status.ts b/actions/submit-signing-request/dtos/signing-request-status.ts new file mode 100644 index 0000000..0f53820 --- /dev/null +++ b/actions/submit-signing-request/dtos/signing-request-status.ts @@ -0,0 +1,6 @@ +export interface SigningRequestStatusDto { + status: string; + isFinalStatus: boolean; + webLink: string; + hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval: boolean; +} diff --git a/actions/submit-signing-request/dtos/signing-request.ts b/actions/submit-signing-request/dtos/signing-request.ts deleted file mode 100644 index 4d384e9..0000000 --- a/actions/submit-signing-request/dtos/signing-request.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface SigningRequestDto -{ - status: string; - workflowStatus: string; - signedArtifactLink: string; - projectSlug: string; - isFinalStatus: boolean; - unsignedArtifactLink: string; -} diff --git a/actions/submit-signing-request/helper-artifact-download.ts b/actions/submit-signing-request/helper-artifact-download.ts index ce97e6e..7a8cbf8 100644 --- a/actions/submit-signing-request/helper-artifact-download.ts +++ b/actions/submit-signing-request/helper-artifact-download.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as nodeStreamZip from 'node-stream-zip'; import axios, { AxiosError } from 'axios'; import { HelperInputOutput } from "./helper-input-output"; -import { buildSignPathAuthorizationHeader, httpErrorResponseToText } from './utils'; +import { httpErrorResponseToText } from './utils'; import { TimeoutStream } from './timeout-stream'; @@ -19,14 +19,11 @@ export class HelperArtifactDownload { const response = await axios.get(artifactDownloadUrl, { responseType: 'stream', - timeout: timeoutMs, - headers: { - Authorization: buildSignPathAuthorizationHeader(this.helperInputOutput.signPathApiToken) - } + timeout: timeoutMs }) - .catch((e: AxiosError) => { - throw new Error(httpErrorResponseToText(e)); - }); + .catch((e: AxiosError) => { + throw new Error(httpErrorResponseToText(e)); + }); const targetDirectory = this.resolveOrCreateDirectory(this.helperInputOutput.outputArtifactDirectory); @@ -67,8 +64,8 @@ export class HelperArtifactDownload { core.info(`The signed artifact has been successfully downloaded from SignPath and extracted to ${targetDirectory}`); } - public resolveOrCreateDirectory(directoryPath:string): string { - const workingDirectory = process.env.GITHUB_WORKSPACE as string; + public resolveOrCreateDirectory(directoryPath: string): string { + const workingDirectory = process.env.GITHUB_WORKSPACE as string; const absolutePath = path.isAbsolute(directoryPath) ? directoryPath : path.join(workingDirectory as string, directoryPath); diff --git a/actions/submit-signing-request/helper-input-output.ts b/actions/submit-signing-request/helper-input-output.ts index 55f81f2..a4276e2 100644 --- a/actions/submit-signing-request/helper-input-output.ts +++ b/actions/submit-signing-request/helper-input-output.ts @@ -71,8 +71,4 @@ export class HelperInputOutput { setSigningRequestWebUrl(signingRequestUrl: string): void { core.setOutput('signing-request-web-url', signingRequestUrl); } - - setSignPathApiUrl(signingRequestUrl: string): void { - core.setOutput('signpath-api-url', signingRequestUrl); - } } diff --git a/actions/submit-signing-request/signpath-url-builder.ts b/actions/submit-signing-request/signpath-url-builder.ts deleted file mode 100644 index d20feea..0000000 --- a/actions/submit-signing-request/signpath-url-builder.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class SignPathUrlBuilder { - - public signPathBaseUrl: string = 'https://signpath.io'; - - constructor( - private signPathGitHubConnectorBaseUrl: string) { - this.signPathGitHubConnectorBaseUrl = this.trimSlash(this.signPathGitHubConnectorBaseUrl); - } - - buildSubmitSigningRequestUrl(): string { - return this.signPathGitHubConnectorBaseUrl + '/api/sign?api-version=1.0'; - } - - buildGetSigningRequestUrl(organizationId: string, signingRequestId: string): string { - if (!this.signPathBaseUrl) { - throw new Error('SignPath Base Url is not set'); - } - - return this.signPathBaseUrl + `/API/v1/${encodeURIComponent(organizationId)}/SigningRequests/${encodeURIComponent(signingRequestId)}`; - } - - private trimSlash(text: string): string { - if(text && text[text.length - 1] === '/') { - return text.substring(0, text.length - 1); - } - return text; - } - -} \ No newline at end of file diff --git a/actions/submit-signing-request/task.ts b/actions/submit-signing-request/task.ts index c7362bd..a06e321 100644 --- a/actions/submit-signing-request/task.ts +++ b/actions/submit-signing-request/task.ts @@ -2,31 +2,32 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import axiosRetry from 'axios-retry'; import * as core from '@actions/core'; import * as moment from 'moment'; -import url from 'url'; import { LogEntry, LogLevelDebug, LogLevelError, LogLevelInformation, LogLevelWarning, SubmitSigningRequestResult, ValidationResult } from './dtos/submit-signing-request-result'; -import { buildSignPathAuthorizationHeader, executeWithRetries, httpErrorResponseToText } from './utils'; -import { SignPathUrlBuilder } from './signpath-url-builder'; -import { SigningRequestDto } from './dtos/signing-request'; +import { executeWithRetries, httpErrorResponseToText } from './utils'; +import { ConnectorUrlBuilder } from './connector-url-builder'; import { HelperInputOutput } from './helper-input-output'; import { taskVersion } from './version'; import { HelperArtifactDownload } from './helper-artifact-download'; import { Config } from './config'; +import { SigningRequestStatusDto } from './dtos/signing-request-status'; // output variables // signingRequestId - the id of the newly created signing request // signingRequestWebUrl - the url of the signing request in SignPath -// signPathApiUrl - the base API url of the SignPath API -// signingRequestDownloadUrl - the url of the signed artifact in SignPath +// signingRequestDownloadUrl - the url of the signed artifact to retrieve via the connector export class Task { - urlBuilder: SignPathUrlBuilder; - - constructor ( - private helperInputOutput: HelperInputOutput, - private helperArtifactDownload: HelperArtifactDownload, - private config: Config) { - this.urlBuilder = new SignPathUrlBuilder(this.helperInputOutput.signPathConnectorUrl); + private readonly urlBuilder: ConnectorUrlBuilder; + private readonly userAgent: string; + + constructor( + private readonly helperInputOutput: HelperInputOutput, + private readonly helperArtifactDownload: HelperArtifactDownload, + private readonly config: Config + ) { + this.urlBuilder = new ConnectorUrlBuilder(this.helperInputOutput.signPathConnectorUrl, this.helperInputOutput.organizationId); + this.userAgent = `SignPath.SubmitSigningRequestGitHubAction/${taskVersion}(NodeJS/${process.version}; ${process.platform} ${process.arch}})`; } async run() { @@ -37,11 +38,10 @@ export class Task { const signingRequestId = await this.submitSigningRequest(); if (this.helperInputOutput.waitForCompletion) { - const signingRequest = await this.ensureSigningRequestCompleted(signingRequestId); - this.helperInputOutput.setSignedArtifactDownloadUrl(signingRequest.signedArtifactLink); + await this.ensureSigningRequestCompleted(signingRequestId); - if(this.helperInputOutput.outputArtifactDirectory) { - await this.helperArtifactDownload.downloadSignedArtifact(signingRequest.signedArtifactLink); + if (this.helperInputOutput.outputArtifactDirectory) { + await this.helperArtifactDownload.downloadSignedArtifact(this.urlBuilder.buildGetSignedArtifactUrl(signingRequestId)); } } else { @@ -53,25 +53,28 @@ export class Task { } } - private async submitSigningRequest (): Promise { + private async submitSigningRequest(): Promise { - core.info('Submitting the signing request to SignPath CI connector...'); + const submitSigningRequestUrl = this.urlBuilder.buildSubmitSigningRequestUrl(); + core.info('Submitting the signing request to SignPath GitHub Actions connector...'); // prepare the payload const submitRequestPayload = this.buildSigningRequestPayload(); - // call the signPath API to submit the signing request + // call the connector to submit the signing request const response = (await axios - .post(this.urlBuilder.buildSubmitSigningRequestUrl(), - submitRequestPayload, - { responseType: "json" }) + .post(submitSigningRequestUrl, + submitRequestPayload, + { + responseType: "json" + }) .catch((e: AxiosError) => { - if(e.code === AxiosError.ERR_BAD_REQUEST) { + if (e.code === AxiosError.ERR_BAD_REQUEST) { const connectorResponse = e.response as AxiosResponse; - if(connectorResponse.data.error) { + if (connectorResponse.data.error) { this.redirectConnectorLogsToActionLogs(connectorResponse.data.logs); // when an error occurs in the validator the error details are in the validationResult this.checkCiSystemValidationResult(connectorResponse.data.validationResult); @@ -91,16 +94,13 @@ export class Task { this.redirectConnectorLogsToActionLogs(response.logs); this.checkCiSystemValidationResult(response.validationResult); - const signingRequestUrlObj = url.parse(response.signingRequestUrl); - this.urlBuilder.signPathBaseUrl = signingRequestUrlObj.protocol + '//' + signingRequestUrlObj.host; - core.info(`SignPath signing request has been successfully submitted`); core.info(`The signing request id is ${response.signingRequestId}`); core.info(`You can view the signing request here: ${response.signingRequestUrl}`); this.helperInputOutput.setSigningRequestId(response.signingRequestId); this.helperInputOutput.setSigningRequestWebUrl(response.signingRequestUrl); - this.helperInputOutput.setSignPathApiUrl(this.urlBuilder.signPathBaseUrl + '/API'); + this.helperInputOutput.setSignedArtifactDownloadUrl(this.urlBuilder.buildGetSignedArtifactUrl(response.signingRequestId)) return response.signingRequestId; } @@ -115,8 +115,7 @@ export class Task { validationResult.errors.forEach(validationError => { core.error(`${validationError.error}`); - if (validationError.howToFix) - { + if (validationError.howToFix) { core.info(validationError.howToFix); } }); @@ -132,11 +131,11 @@ export class Task { // The token is valid only for the workflow's duration private async ensureSignPathDownloadedUnsignedArtifact(signingRequestId: string): Promise { core.info(`Waiting until SignPath downloaded the unsigned artifact...`); - const requestData = await (executeWithRetries( + const requestData = await (executeWithRetries( async () => { - const signingRequestDto = await (this.getSigningRequest(signingRequestId) + const signingRequestDto = await (this.getSigningRequestStatus(signingRequestId) .then(data => { - if(!data.unsignedArtifactLink && !data.isFinalStatus) { + if (!data.hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval && !data.isFinalStatus) { core.info(`Checking the download status: not yet complete`); // retry artifact download status check return { retry: true }; @@ -149,9 +148,9 @@ export class Task { this.config.CheckArtifactDownloadStatusIntervalInSeconds * 1000, this.config.CheckArtifactDownloadStatusIntervalInSeconds * 1000)); - if (!requestData.unsignedArtifactLink) { + if (!requestData.hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval) { - if(!requestData.isFinalStatus) { + if (!requestData.isFinalStatus) { const maxWaitingTime = moment.utc(this.helperInputOutput.waitForCompletionTimeoutInSeconds * 1000).format("hh:mm"); core.error(`We have exceeded the maximum waiting time, which is ${maxWaitingTime}, and the GitHub artifact is still not downloaded by SignPath`); } else { @@ -166,21 +165,21 @@ export class Task { // artifact already downloaded by SignPath } - private async ensureSigningRequestCompleted(signingRequestId: string): Promise { + private async ensureSigningRequestCompleted(signingRequestId: string): Promise { // check for status update core.info(`Checking the signing request status...`); - const requestData = await (executeWithRetries( + const requestData = await (executeWithRetries( async () => { - const signingRequestDto = await (this.getSigningRequest(signingRequestId) + const signingRequestStatusDto = await (this.getSigningRequestStatus(signingRequestId) .then(data => { - if(data && !data.isFinalStatus) { + if (data && !data.isFinalStatus) { core.info(`The signing request status is ${data.status}, which is not a final status; after a delay, we will check again...`); return { retry: true }; } return { retry: false, result: data }; })); - return signingRequestDto; + return signingRequestStatusDto; }, this.helperInputOutput.waitForCompletionTimeoutInSeconds * 1000, this.config.MinDelayBetweenSigningRequestStatusChecksInSeconds * 1000, @@ -200,18 +199,13 @@ export class Task { return requestData; } - private async getSigningRequest(signingRequestId: string): Promise { - const requestStatusUrl = this.urlBuilder.buildGetSigningRequestUrl( - this.helperInputOutput.organizationId, signingRequestId); - - const signingRequestDto = await axios - .get( + private async getSigningRequestStatus(signingRequestId: string): Promise { + const requestStatusUrl = this.urlBuilder.buildGetSigningRequestStatusUrl(signingRequestId); + const signingRequestStatusDto = await axios + .get( requestStatusUrl, { - responseType: "json", - headers: { - "Authorization": buildSignPathAuthorizationHeader(this.helperInputOutput.signPathApiToken) - } + responseType: "json" } ) .catch((e: AxiosError) => { @@ -220,16 +214,36 @@ export class Task { throw new Error(httpErrorResponseToText(e)); }) .then(response => response.data); - return signingRequestDto; + + return signingRequestStatusDto; } private configureAxios(): void { // set user agent - axios.defaults.headers.common['User-Agent'] = this.buildUserAgent(); + axios.defaults.headers.common['User-Agent'] = this.userAgent; + + // set token for all outgoing requests + axios.defaults.headers.common.Authorization = `Bearer ${this.helperInputOutput.signPathApiToken}` + const timeoutMs = this.helperInputOutput.serviceUnavailableTimeoutInSeconds * 1000 axios.defaults.timeout = timeoutMs; + // log all outgoing requests + axios.interceptors.request.use(request => { + core.debug(`Sending request: ${request.method?.toUpperCase()} ${request.url}`); + return request; + }) + + // log all outgoing responses + axios.interceptors.response.use(response => { + core.debug(`Received response: ${response.status} ${response.statusText} from ${response.request.url}`); + return response; + }, error => { + core.debug(`Received response: ${error.response.status} ${error.response.statusText}`) + return Promise.reject(error); + }) + // original axiosRetry doesn't work for POST requests // thats why we need to override some functions axiosRetry.isNetworkOrIdempotentRequestError = (error: AxiosError) => { @@ -249,22 +263,22 @@ export class Task { axiosRetry.isRetryableError = (error: AxiosError) => { let retryableHttpErrorCode = false; - if(error.response) { - if(error.response.status === 502 + if (error.response) { + if (error.response.status === 502 || error.response.status === 503 || error.response.status === 504) { retryableHttpErrorCode = true; core.info(`SignPath REST API is temporarily unavailable (server responded with ${error.response.status}).`); } - if(error.response.status === 429) { + if (error.response.status === 429) { retryableHttpErrorCode = true; core.info('SignPath REST API encountered too many requests.'); } } return (error.code !== 'ECONNABORTED' && - (!error.response || retryableHttpErrorCode)); + (!error.response || retryableHttpErrorCode)); } // set retries @@ -281,12 +295,6 @@ export class Task { retries: maxRetryCount, retryCondition: axiosRetry.isNetworkOrIdempotentRequestError }); - - } - - private buildUserAgent(): string { - const userAgent = `SignPath.SubmitSigningRequestGitHubAction/${taskVersion}(NodeJS/${process.version}; ${process.platform} ${process.arch}})`; - return userAgent; } private checkResponseStructure(response: SubmitSigningRequestResult): void { @@ -325,12 +333,10 @@ export class Task { private buildSigningRequestPayload(): any { return { - signPathApiToken: this.helperInputOutput.signPathApiToken, artifactId: this.helperInputOutput.githubArtifactId, gitHubWorkflowRunId: process.env.GITHUB_RUN_ID, gitHubRepository: process.env.GITHUB_REPOSITORY, gitHubToken: this.helperInputOutput.gitHubToken, - signPathOrganizationId: this.helperInputOutput.organizationId, signPathProjectSlug: this.helperInputOutput.projectSlug, signPathSigningPolicySlug: this.helperInputOutput.signingPolicySlug, signPathArtifactConfigurationSlug: this.helperInputOutput.artifactConfigurationSlug, diff --git a/actions/submit-signing-request/tests/connector-url-builder.test.ts b/actions/submit-signing-request/tests/connector-url-builder.test.ts new file mode 100644 index 0000000..1c38d20 --- /dev/null +++ b/actions/submit-signing-request/tests/connector-url-builder.test.ts @@ -0,0 +1,32 @@ +import * as uuid from 'uuid'; +import { assert } from "chai"; +import { ConnectorUrlBuilder } from '../connector-url-builder'; + +const connectorUrl = "https://connector.com"; +const apiVersion = "1.0" +const orgId = uuid.v4(); + +const sut = new ConnectorUrlBuilder(connectorUrl, orgId); + +it("Should build submit signing request url correctly", () => { + const expected = `${connectorUrl}/${orgId}/SigningRequests?api-version=${apiVersion}` + const actual = sut.buildSubmitSigningRequestUrl(); + + assert.equal(actual, expected) +}) + +it("Should build get signing request status url correctly", () => { + const srId = uuid.v4(); + const expected = `${connectorUrl}/${orgId}/SigningRequests/${srId}/Status?api-version=${apiVersion}` + const actual = sut.buildGetSigningRequestStatusUrl(srId); + + assert.equal(actual, expected) +}) + +it("Should build get signed artifact url correctly", () => { + const srId = uuid.v4(); + const expected = `${connectorUrl}/${orgId}/SigningRequests/${srId}/SignedArtifact?api-version=${apiVersion}` + const actual = sut.buildGetSignedArtifactUrl(srId); + + assert.equal(actual, expected) +}) \ No newline at end of file diff --git a/actions/submit-signing-request/tests/helper-artifact-download.test.ts b/actions/submit-signing-request/tests/helper-artifact-download.test.ts index a973255..280b31e 100644 --- a/actions/submit-signing-request/tests/helper-artifact-download.test.ts +++ b/actions/submit-signing-request/tests/helper-artifact-download.test.ts @@ -1,7 +1,6 @@ -import { assert, expect } from "chai"; +import { assert } from "chai"; import { HelperArtifactDownload } from "../helper-artifact-download" import * as path from 'path'; -import * as os from 'os'; import * as uuid from 'uuid'; import * as fs from 'fs' import { HelperInputOutput } from "../helper-input-output"; diff --git a/actions/submit-signing-request/tests/task.test.ts b/actions/submit-signing-request/tests/task.test.ts index eb2b977..2a22c76 100644 --- a/actions/submit-signing-request/tests/task.test.ts +++ b/actions/submit-signing-request/tests/task.test.ts @@ -8,16 +8,12 @@ import * as core from '@actions/core'; import { HelperInputOutput } from '../helper-input-output'; import { HelperArtifactDownload } from '../helper-artifact-download'; import axiosRetry from 'axios-retry'; -import { Config } from '../config'; -import { log } from 'console'; +import { SigningRequestStatusDto } from '../dtos/signing-request-status'; const testSignPathApiToken = 'TEST_TOKEN'; const testSigningRequestId = 'TEST_ID'; const testConnectorUrl = 'https://domain'; -const testSignPathUrl = 'https://signpath'; -const testSigningRequestUrl = testSignPathUrl + '/api/SigningRequests'; -const testSignedArtifactLink = testConnectorUrl + '/api/artifactlink'; -const testUnsignedArtifactLink = testConnectorUrl + '/api/unsignedartifactlink'; +const testSigningRequestUrl = testConnectorUrl + '/SigningRequests'; const testGitHubArtifactId = 'TEST_ARTIFACT_ID'; const testArtifactConfigurationSlug = 'TEST_ARTIFACT_CONFIGURATION_SLUG'; const testOrganizationId = 'TEST_ORGANIZATION_ID'; @@ -26,6 +22,9 @@ const testSigningPolicySlug = 'TEST_POLICY_SLUG'; const testGitHubToken = 'TEST_GITHUB_TOKEN'; const testConnectorLogMessage = 'TEST_CONNECTOR_LOG_MESSAGE'; +const testSignedArtifactLink = `${testConnectorUrl}/${testOrganizationId}/SigningRequests/${testSigningRequestId}/SignedArtifact?api-version=1.0` +const submitSigningRequestRouteTemplate = new RegExp(`\/${testOrganizationId}\/SigningRequests.*`) + const defaultTestInputMap = { 'wait-for-completion': 'true', 'connector-url': testConnectorUrl, @@ -54,21 +53,25 @@ let setOutputStub: sinon.SinonStub; let getInputStub: sinon.SinonStub; beforeEach(() => { - const submitSigningRequestResponse = { signingRequestUrl: testSigningRequestUrl, signingRequestId: testSigningRequestId, isFinalStatus: true, status: 'Completed', - unsignedArtifactLink: testUnsignedArtifactLink, - signedArtifactLink: testSignedArtifactLink, - logs: [ { message: testConnectorLogMessage, level: 'Information' } ] + unsignedArtifactLink: "unused", + signedArtifactLink: "unused", + logs: [{ message: testConnectorLogMessage, level: 'Information' }] }; - const getSigningRequestResponse = submitSigningRequestResponse; + const getSigningRequestStatusResponse: SigningRequestStatusDto = { + status: submitSigningRequestResponse.status, + hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval: true, + isFinalStatus: true, + webLink: testSigningRequestUrl + } axiosPostStub = sandbox.stub(axios, 'post').resolves({ data: submitSigningRequestResponse }); - axiosGetStub = sandbox.stub(axios, 'get').resolves({ data: getSigningRequestResponse }); + axiosGetStub = sandbox.stub(axios, 'get').resolves({ data: getSigningRequestStatusResponse }); setOutputStub = sandbox.stub(core, 'setOutput'); // set input stubs to return default values @@ -85,7 +88,6 @@ beforeEach(() => { MaxDelayBetweenSigningRequestStatusChecksInSeconds: 0, CheckArtifactDownloadStatusIntervalInSeconds: 0 }); - }); afterEach(() => { @@ -105,22 +107,21 @@ it('test that the task fails if the signing request submit fails', async () => { it('test that the task fails if the signing request has "Failed" as a final status', async () => { const setFailedStub = sandbox.stub(core, 'setFailed') - .withArgs(sinon.match((value:any) => { + .withArgs(sinon.match((value: any) => { return value.includes('TEST_FAILED') - && value.includes('The signing request is not completed.'); + && value.includes('The signing request is not completed.'); })); const failedStatusSigningRequestResponse = { status: 'TEST_FAILED', isFinalStatus: true, - unsignedArtifactLink: testUnsignedArtifactLink // to go through the unsigned artifact downloading loop }; axiosGetStub.restore(); // we don't need default stub behavior in this test sandbox.stub(axios, 'get').resolves({ data: failedStatusSigningRequestResponse }); await task.run(); assert.equal(setFailedStub.calledOnce, true, 'setFailed should be called once'); - }); +}); it('test that the signing request was not submitted due to validation errors', async () => { const submitSigningRequestValidationErrorResponse = { @@ -137,17 +138,17 @@ it('test that the signing request was not submitted due to validation errors', a sandbox.stub(axios, 'post').resolves({ data: submitSigningRequestValidationErrorResponse }); // check that task was marked as failed, because of validation errors const setFailedStub = sandbox.stub(core, 'setFailed') - .withArgs(sinon.match((value:any) => { + .withArgs(sinon.match((value: any) => { return value.includes('CI system validation failed'); })); // check that error message was logged const errorLogStub = sandbox.stub(core, 'error') - .withArgs(sinon.match((value:any) => { + .withArgs(sinon.match((value: any) => { return value.includes('TEST_ERROR'); })); // check that howToFix message was logged const coreInfoStub = sandbox.stub(core, 'info') - .withArgs(sinon.match((value:any) => { + .withArgs(sinon.match((value: any) => { return value.includes('TEST_FIX'); })); @@ -157,17 +158,9 @@ it('test that the signing request was not submitted due to validation errors', a assert.equal(coreInfoStub.called, true); }); -it('test that the output variables are set correctly', async () => { - await task.run(); - assert.equal(setOutputStub.calledWith('signing-request-id', testSigningRequestId), true); - assert.equal(setOutputStub.calledWith('signing-request-web-url', testSigningRequestUrl), true); - assert.equal(setOutputStub.calledWith('signpath-api-url', testSignPathUrl + '/API'), true); - assert.equal(setOutputStub.calledWith('signed-artifact-download-url', testSignedArtifactLink), true); -}); - it('connector logs logged to the build log', async () => { const coreInfoStub = sandbox.stub(core, 'info') - .withArgs(sinon.match((value:any) => { + .withArgs(sinon.match((value: any) => { return value.includes(testConnectorLogMessage); })); await task.run(); @@ -177,7 +170,7 @@ it('connector logs logged to the build log', async () => { it('test that the connectors url has api version', async () => { await task.run(); assert.equal(axiosPostStub.calledWith( - sinon.match((value:any) => { + sinon.match((value: any) => { return value.indexOf('api-version') !== -1; })), true); }); @@ -186,10 +179,8 @@ it('test if input variables are passed through', async () => { await task.run(); assert.equal(axiosPostStub.calledWith( sinon.match.any, - sinon.match((value:any) => { - return value.signPathApiToken === testSignPathApiToken - && value.signPathOrganizationId === testOrganizationId - && value.artifactId === testGitHubArtifactId + sinon.match((value: any) => { + return value.artifactId === testGitHubArtifactId && value.signPathProjectSlug === testProjectSlug && value.signPathSigningPolicySlug === testSigningPolicySlug && value.gitHubToken === testGitHubToken @@ -207,21 +198,20 @@ it('task fails if the submit request connector fails', async () => { throw { response: { data: httpCallError } }; }); const setFailedStub = sandbox.stub(core, 'setFailed') - .withArgs(sinon.match((value:string) => { + .withArgs(sinon.match((value: string) => { return value.indexOf(httpCallError) !== -1; })); await task.run(); assert.equal(setFailedStub.calledOnce, true); }); - it('if submit signing request fails with 429,502,503,504 the task retries', async () => { // use real *POST* axios for this test, because retries are implemented in axios axiosPostStub.restore(); const retryTestId = 'RETRY_TEST_ID'; const addErrorResponse = (httpCode: number) => { - nock(testConnectorUrl).post(/\/api\/sign.*/).once().reply(httpCode, 'Server Error'); + nock(testConnectorUrl).post(submitSigningRequestRouteTemplate).once().reply(httpCode, 'Server Error'); } addErrorResponse(429); @@ -230,7 +220,7 @@ it('if submit signing request fails with 429,502,503,504 the task retries', asyn addErrorResponse(504); nock(testConnectorUrl) - .post(/\/api\/sign.*/) + .post(submitSigningRequestRouteTemplate) .reply(200, { signingRequestUrl: testSigningRequestUrl, signingRequestId: retryTestId @@ -242,53 +232,52 @@ it('if submit signing request fails with 429,502,503,504 the task retries', asyn await task.run(); // signing request id should be set in the output - assert.equal(setOutputStub.calledWith('signing-request-id', retryTestId), true); + assert.equal(setOutputStub.calledWith('signing-request-id', retryTestId), true); }); it('no retries for http code 500', async () => { // use real *POST* axios for this test, because retries are implemented in axios axiosPostStub.restore(); - nock(testConnectorUrl).post(/\/api\/sign.*/).reply(500, 'Server Error'); - + nock(testConnectorUrl).post(submitSigningRequestRouteTemplate).reply(500, 'Server Error'); const setFailedStub = sandbox.stub(core, 'setFailed'); await task.run(); assert.equal(setFailedStub.calledOnce, true); }); -it('task waits for artifact being downloaded before completing', async () => { +it('task waits for unsigned artifact being downloaded by SignPath before completing', async () => { - // use non stubbed axius, define responses sequence suing nock + // use non stubbed axios, define responses sequence suing nock axiosGetStub.restore(); // non-default input map, with 'wait-for-completion' set to 'false' getInputStub.restore(); - const input = Object.assign({ }, defaultTestInputMap); + const input = Object.assign({}, defaultTestInputMap); input['wait-for-completion'] = 'false'; getInputStub = sandbox.stub(core, 'getInput').callsFake((paramName) => { return input[paramName as keyof typeof input] || 'test'; }); - const addGetRequestDataResponse = (link: string | null) => { - return nock(testSignPathUrl).get(uri => uri.includes('SigningRequests')).once().reply(200, { - unsignedArtifactLink: link + const addGetRequestDataResponse = (hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval: boolean) => { + return nock(testConnectorUrl).get(uri => uri.includes('SigningRequests')).once().reply(200, { + hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval }); } const nockScopes = []; // artifact is not downloaded for the first 4 calls - nockScopes.push(addGetRequestDataResponse(null)); - nockScopes.push(addGetRequestDataResponse(null)); - nockScopes.push(addGetRequestDataResponse(null)); - nockScopes.push(addGetRequestDataResponse(null)); + nockScopes.push(addGetRequestDataResponse(false)); + nockScopes.push(addGetRequestDataResponse(false)); + nockScopes.push(addGetRequestDataResponse(false)); + nockScopes.push(addGetRequestDataResponse(false)); // artifact is downloaded when the 5th call happens - nockScopes.push(addGetRequestDataResponse(testUnsignedArtifactLink)); + nockScopes.push(addGetRequestDataResponse(true)); // this request should not happen // because it should stop checking after the previous request - const notDoneScope = addGetRequestDataResponse(null); + const notDoneScope = addGetRequestDataResponse(false); const setFailedStub = sandbox.stub(core, 'setFailed'); await task.run(); @@ -302,12 +291,12 @@ it('task waits for artifact being downloaded before completing', async () => { it('if the signing request status is final, the task stops checking for artifact download status and reports an error', async () => { - // use non stubbed axius, define responses sequence suing nock + // use non stubbed axios, define responses sequence using nock axiosGetStub.restore(); // non-default input map, with 'wait-for-completion' set to 'false' getInputStub.restore(); - const input = Object.assign({ }, defaultTestInputMap); + const input = Object.assign({}, defaultTestInputMap); input['wait-for-completion'] = 'false'; getInputStub = sandbox.stub(core, 'getInput').callsFake((paramName) => { return input[paramName as keyof typeof input] || 'test'; @@ -315,8 +304,8 @@ it('if the signing request status is final, the task stops checking for artifact // signing request status is final and artifact is not downloaded // something went wrong, the sining request cannot be completed - nock(testSignPathUrl).get(uri => uri.includes('SigningRequests')).once().reply(200, { - unsignedArtifactLink: null, + nock(testConnectorUrl).get(uri => uri.includes('SigningRequests')).once().reply(200, { + hasArtifactBeenDownloadedBySignPathInCaseOfArtifactRetrieval: false, isFinalStatus: true }); @@ -325,4 +314,12 @@ it('if the signing request status is final, the task stops checking for artifact // and successfully completed assert.equal(setFailedStub.called, true); +}); + +it('test that the output variables are set correctly', async () => { + await task.run(); + + assert.equal(setOutputStub.calledWith('signing-request-id', testSigningRequestId), true); + assert.equal(setOutputStub.calledWith('signing-request-web-url', testSigningRequestUrl), true); + assert.equal(setOutputStub.calledWith('signed-artifact-download-url', testSignedArtifactLink), true); }); \ No newline at end of file diff --git a/actions/submit-signing-request/utils.ts b/actions/submit-signing-request/utils.ts index c615f64..a0761ff 100644 --- a/actions/submit-signing-request/utils.ts +++ b/actions/submit-signing-request/utils.ts @@ -49,10 +49,6 @@ export function getInputNumber(name: string, options?: core.InputOptions): numbe return result; } -export function buildSignPathAuthorizationHeader(apiToken: string): string { - return `Bearer ${apiToken}`; -} - export function httpErrorResponseToText(err: AxiosError): string { const response = err.response as AxiosResponse; diff --git a/actions/submit-signing-request/version.ts b/actions/submit-signing-request/version.ts index 314f1ec..9d7606a 100644 --- a/actions/submit-signing-request/version.ts +++ b/actions/submit-signing-request/version.ts @@ -1,2 +1,2 @@ -const taskVersion = '1.1'; +const taskVersion = '2.0'; export { taskVersion }; diff --git a/make.js b/make.js index 2675aec..11ef017 100644 --- a/make.js +++ b/make.js @@ -5,7 +5,6 @@ var fs = require('fs'); var argv = require('minimist')(process.argv.slice(2)); - var run = util.run; var CLI = {}; diff --git a/package.json b/package.json index a859697..3eab851 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "tslint --project tsconfig.json" }, "name": "signpath.connectors.githubactions.actions", - "version": "1.0.0", + "version": "2.0.0", "description": "Use SignPath to sign your build artifacts.", "devDependencies": { "@types/chai": "^4.3.5",