github: upload artifact

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2024-04-18 16:33:56 +02:00
parent 8d4d79eeb7
commit db0a36131e
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
6 changed files with 955 additions and 27 deletions

View File

@ -97,6 +97,9 @@ jobs:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
-
name: Expose GitHub Runtime
uses: crazy-max/ghaction-github-runtime@v3
- -
name: Setup Node name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -0,0 +1,41 @@
/**
* Copyright 2024 actions-toolkit authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {beforeEach, describe, expect, it, jest} from '@jest/globals';
import * as path from 'path';
import {GitHub} from '../src/github';
const fixturesDir = path.join(__dirname, 'fixtures');
const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip;
beforeEach(() => {
jest.clearAllMocks();
});
maybe('uploadArtifact', () => {
it('uploads an artifact', async () => {
const res = await GitHub.uploadArtifact({
filename: path.join(fixturesDir, 'github-repo.json'),
mimeType: 'application/json',
retentionDays: 1
});
expect(res).toBeDefined();
console.log('uploadArtifactResponse', res);
expect(res?.url).toBeDefined();
});
});

View File

@ -45,6 +45,7 @@
"registry": "https://registry.npmjs.org/" "registry": "https://registry.npmjs.org/"
}, },
"dependencies": { "dependencies": {
"@actions/artifact": "^2.1.5",
"@actions/cache": "^3.2.4", "@actions/cache": "^3.2.4",
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
@ -52,6 +53,7 @@
"@actions/http-client": "^2.2.1", "@actions/http-client": "^2.2.1",
"@actions/io": "^1.1.3", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.1", "@actions/tool-cache": "^2.0.1",
"@azure/storage-blob": "^12.15.0",
"@octokit/core": "^5.1.0", "@octokit/core": "^5.1.0",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/plugin-rest-endpoint-methods": "^10.4.0",
"async-retry": "^1.3.3", "async-retry": "^1.3.3",

View File

@ -14,13 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
import {GitHub as Octokit} from '@actions/github/lib/utils'; import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import {CreateArtifactRequest, FinalizeArtifactRequest, StringValue} from '@actions/artifact/lib/generated';
import {internalArtifactTwirpClient} from '@actions/artifact/lib/internal/shared/artifact-twirp-client';
import {getBackendIdsFromToken} from '@actions/artifact/lib/internal/shared/util';
import {getExpiration} from '@actions/artifact/lib/internal/upload/retention';
import {InvalidResponseError, NetworkError} from '@actions/artifact';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as github from '@actions/github'; import * as github from '@actions/github';
import {GitHub as Octokit} from '@actions/github/lib/utils';
import {Context} from '@actions/github/lib/context'; import {Context} from '@actions/github/lib/context';
import {TransferProgressEvent} from '@azure/core-http';
import {BlobClient, BlobHTTPHeaders} from '@azure/storage-blob';
import {jwtDecode, JwtPayload} from 'jwt-decode'; import {jwtDecode, JwtPayload} from 'jwt-decode';
import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo} from './types/github'; import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo, UploadArtifactOpts, UploadArtifactResponse} from './types/github';
export interface GitHubOpts { export interface GitHubOpts {
token?: string; token?: string;
@ -90,4 +100,94 @@ export class GitHub {
throw new Error(`Cannot parse GitHub Actions Runtime Token ACs: ${e.message}`); throw new Error(`Cannot parse GitHub Actions Runtime Token ACs: ${e.message}`);
} }
} }
public static async uploadArtifact(opts: UploadArtifactOpts): Promise<UploadArtifactResponse> {
const artifactName = path.basename(opts.filename);
const backendIds = getBackendIdsFromToken();
const artifactClient = internalArtifactTwirpClient();
core.info(`Uploading ${artifactName} to blob storage`);
const createArtifactReq: CreateArtifactRequest = {
workflowRunBackendId: backendIds.workflowRunBackendId,
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
name: artifactName,
version: 4
};
const expiresAt = getExpiration(opts?.retentionDays);
if (expiresAt) {
createArtifactReq.expiresAt = expiresAt;
}
const createArtifactResp = await artifactClient.CreateArtifact(createArtifactReq);
if (!createArtifactResp.ok) {
throw new InvalidResponseError('cannot create artifact client');
}
let uploadByteCount = 0;
const blobClient = new BlobClient(createArtifactResp.signedUploadUrl);
const blockBlobClient = blobClient.getBlockBlobClient();
const headers: BlobHTTPHeaders = {
blobContentDisposition: `attachment; filename="${artifactName}"`
};
if (opts.mimeType) {
headers.blobContentType = opts.mimeType;
}
core.debug(`Upload headers: ${JSON.stringify(headers)}`);
try {
core.info('Beginning upload of artifact content to blob storage');
await blockBlobClient.uploadFile(opts.filename, {
blobHTTPHeaders: headers,
onProgress: (progress: TransferProgressEvent): void => {
core.info(`Uploaded bytes ${progress.loadedBytes}`);
uploadByteCount = progress.loadedBytes;
}
});
} catch (error) {
if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code);
}
throw error;
}
core.info('Finished uploading artifact content to blob storage!');
const sha256Hash = crypto.createHash('sha256').update(fs.readFileSync(opts.filename)).digest('hex');
core.info(`SHA256 hash of uploaded artifact is ${sha256Hash}`);
const finalizeArtifactReq: FinalizeArtifactRequest = {
workflowRunBackendId: backendIds.workflowRunBackendId,
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
name: artifactName,
size: uploadByteCount ? uploadByteCount.toString() : '0'
};
if (sha256Hash) {
finalizeArtifactReq.hash = StringValue.create({
value: `sha256:${sha256Hash}`
});
}
core.info(`Finalizing artifact upload`);
const finalizeArtifactResp = await artifactClient.FinalizeArtifact(finalizeArtifactReq);
if (!finalizeArtifactResp.ok) {
throw new InvalidResponseError('Cannot finalize artifact upload');
}
const artifactId = BigInt(finalizeArtifactResp.artifactId);
core.info(`Artifact successfully finalized (${artifactId})`);
const artifactURL = `${GitHub.workflowRunURL}/artifacts/${artifactId}`;
core.info(`Artifact download URL: ${artifactURL}`);
return {
id: Number(artifactId),
filename: artifactName,
size: uploadByteCount,
url: artifactURL
};
}
} }

View File

@ -34,3 +34,16 @@ export interface GitHubActionsRuntimeTokenAC {
Scope: string; Scope: string;
Permission: number; Permission: number;
} }
export interface UploadArtifactOpts {
filename: string;
mimeType?: string;
retentionDays?: number;
}
export interface UploadArtifactResponse {
id: number;
filename: string;
size: number;
url: string;
}

819
yarn.lock

File diff suppressed because it is too large Load Diff