diff --git a/packages/artifact/package-lock.json b/packages/artifact/package-lock.json index 4aa381e2..0933a20c 100644 --- a/packages/artifact/package-lock.json +++ b/packages/artifact/package-lock.json @@ -12,6 +12,7 @@ "@actions/core": "^1.10.0", "@actions/http-client": "^2.1.0", "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "jwt-decode": "^3.1.2", "twirp-ts": "^2.5.0" }, "devDependencies": { @@ -198,6 +199,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", diff --git a/packages/artifact/package.json b/packages/artifact/package.json index d74eddc7..9cd1594b 100644 --- a/packages/artifact/package.json +++ b/packages/artifact/package.json @@ -42,10 +42,11 @@ "@actions/core": "^1.10.0", "@actions/http-client": "^2.1.0", "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "jwt-decode": "^3.1.2", "twirp-ts": "^2.5.0" }, "devDependencies": { "@types/tmp": "^0.2.1", "typescript": "^4.3.0" } -} \ No newline at end of file +} diff --git a/packages/artifact/src/internal/shared/util.ts b/packages/artifact/src/internal/shared/util.ts new file mode 100644 index 00000000..4f378145 --- /dev/null +++ b/packages/artifact/src/internal/shared/util.ts @@ -0,0 +1,73 @@ +import {getRetentionDays, getRuntimeToken} from './config' +import jwt_decode from 'jwt-decode' +import {Timestamp} from 'src/generated' + +export interface BackendIds { + workflowRunBackendId: string + workflowJobRunBackendId: string +} + +interface ActionsToken { + scp: string +} + +// uses the JWT token claims to get the +// workflow run and workflow job run backend ids +export function getBackendIdsFromToken(): BackendIds { + const token = getRuntimeToken() + const decoded = jwt_decode(token) + if (!decoded.scp) { + throw new Error('No scp claim in JWT token') + } + + /* + * example decoded: + * { + * scp: "Actions.ExampleScope Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774" + * } + */ + + const scpParts = decoded.scp.split(' ') + if (scpParts.length === 0) { + throw new Error('No scp parts in JWT token') + } + /* + * example scpParts: + * ["Actions.ExampleScope", "Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774"] + */ + + for (const scopes of scpParts) { + const scopeParts = scopes.split(':') + /* + * example scopeParts: + * ["Actions.Results", "ce7f54c7-61c7-4aae-887f-30da475f5f1a", "ca395085-040a-526b-2ce8-bdc85f692774"] + */ + if (scopeParts.length !== 3) { + // not the Actions.Results scope + continue + } + + if (scopeParts[0] !== 'Actions.Results') { + // not the Actions.Results scope + continue + } + + return { + workflowRunBackendId: scopeParts[1], + workflowJobRunBackendId: scopeParts[2] + } + } + + throw new Error('No valid Actions.Results scope in JWT token') +} + +export function getExpiration(retentionDays?: number): Timestamp | undefined { + if (!retentionDays) { + return undefined + } + + const expirationDate = new Date() + expirationDate.setDate(expirationDate.getDate() + retentionDays) + + return Timestamp.fromDate(expirationDate) +} diff --git a/packages/artifact/src/internal/upload/upload-artifact.ts b/packages/artifact/src/internal/upload/upload-artifact.ts index 07835c99..335936d2 100644 --- a/packages/artifact/src/internal/upload/upload-artifact.ts +++ b/packages/artifact/src/internal/upload/upload-artifact.ts @@ -2,11 +2,18 @@ import * as core from '@actions/core' import {UploadOptions} from './upload-options' import {UploadResponse} from './upload-response' import {validateArtifactName} from './path-and-artifact-name-validation' +import {createArtifactTwirpClient} from '../shared/artifact-twirp-client' import { UploadZipSpecification, getUploadZipSpecification, validateRootDirectory } from './upload-zip-specification' +import {BackendIds, getBackendIdsFromToken, getExpiration} from '../shared/util' +import { + CreateArtifactRequest, + CreateArtifactResponse, + FinalizeArtifactResponse +} from 'src/generated' export async function uploadArtifact( name: string, @@ -14,8 +21,17 @@ export async function uploadArtifact( rootDirectory: string, options?: UploadOptions | undefined // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise { - validateArtifactName(name) - validateRootDirectory(rootDirectory) + try { + validateArtifactName(name) + validateRootDirectory(rootDirectory) + } catch (error) { + core.warning( + `Received error trying to validate artifact name or root directory: ${error}` + ) + return { + success: false + } + } const zipSpecification: UploadZipSpecification[] = getUploadZipSpecification( files, @@ -28,13 +44,97 @@ export async function uploadArtifact( } } + // get the IDs needed for the artifact creation + const backendIds = getBackendIds() + if (!backendIds.workflowRunBackendId || !backendIds.workflowJobRunBackendId) { + core.warning(`Failed to get backend ids`) + return { + success: false + } + } + + // create the artifact client + const artifactClient = createArtifactTwirpClient('upload') + + // create the artifact + const createArtifactReq: CreateArtifactRequest = { + workflowRunBackendId: backendIds.workflowRunBackendId, + workflowJobRunBackendId: backendIds.workflowJobRunBackendId, + name: name, + version: 4 + } + + // if there is a retention period, add it to the request + const expiresAt = getExpiration(options?.retentionDays) + if (expiresAt) { + createArtifactReq.expiresAt = expiresAt + } + const createArtifactResp = await createArtifact(() => + artifactClient.CreateArtifact(createArtifactReq) + ) + if (!createArtifactResp || !createArtifactResp.ok) { + core.warning(`Failed to create artifact`) + return { + success: false + } + } + // TODO - Implement upload functionality + // finalize the artifact + const finalizeArtifactResp = await finalizeArtifact(() => + artifactClient.FinalizeArtifact({ + workflowRunBackendId: backendIds.workflowRunBackendId, + workflowJobRunBackendId: backendIds.workflowJobRunBackendId, + name: name, + size: '0' // TODO - Add size + }) + ) + if (!finalizeArtifactResp || !finalizeArtifactResp.ok) { + core.warning(`Failed to finalize artifact`) + return { + success: false + } + } const uploadResponse: UploadResponse = { success: true, size: 0, - id: 0 + id: parseInt(finalizeArtifactResp.artifactId) // TODO - will this be a problem due to the id being a bigint? } return uploadResponse } + +async function createArtifact( + operation: () => Promise +): Promise { + try { + return await operation() + } catch (error) { + core.warning(`Received error trying to create artifact: ${error}`) + return + } +} + +async function finalizeArtifact( + operation: () => Promise +): Promise { + try { + return await operation() + } catch (error) { + core.warning(`Received error trying to create artifact: ${error}`) + return + } +} + +function getBackendIds(): BackendIds { + try { + return getBackendIdsFromToken() + } catch (error) { + core.warning(`Received error trying to get backend ids: ${error}`) + return { + workflowRunBackendId: '', + workflowJobRunBackendId: '' + } + } +}