Skip to content

Commit

Permalink
Merge pull request #308 from crazy-max/github-upload-artifact
Browse files Browse the repository at this point in the history
github: upload artifact
  • Loading branch information
crazy-max authored Apr 25, 2024
2 parents 00abdc0 + db0a361 commit 5bbaf90
Show file tree
Hide file tree
Showing 7 changed files with 957 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ jobs:
-
name: Checkout
uses: actions/checkout@v4
-
name: Expose GitHub Runtime
uses: crazy-max/ghaction-github-runtime@v3
-
name: Setup Node
uses: actions/setup-node@v4
Expand Down
41 changes: 41 additions & 0 deletions __tests__/github.test.itg.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@actions/artifact": "^2.1.5",
"@actions/cache": "^3.2.4",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/http-client": "^2.2.1",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.1",
"@azure/storage-blob": "^12.15.0",
"@octokit/core": "^5.1.0",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0",
"async-retry": "^1.3.3",
Expand Down
104 changes: 102 additions & 2 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@
* 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 github from '@actions/github';
import {GitHub as Octokit} from '@actions/github/lib/utils';
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 {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo} from './types/github';
import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo, UploadArtifactOpts, UploadArtifactResponse} from './types/github';

export interface GitHubOpts {
token?: string;
Expand Down Expand Up @@ -90,4 +100,94 @@ export class GitHub {
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
};
}
}
13 changes: 13 additions & 0 deletions src/types/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,16 @@ export interface GitHubActionsRuntimeTokenAC {
Scope: string;
Permission: number;
}

export interface UploadArtifactOpts {
filename: string;
mimeType?: string;
retentionDays?: number;
}

export interface UploadArtifactResponse {
id: number;
filename: string;
size: number;
url: string;
}
5 changes: 2 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"compilerOptions": {
"esModuleInterop": true,
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es6",
"module": "commonjs",
"strict": true,
"declaration": true,
"sourceMap": true,
Expand Down
Loading

0 comments on commit 5bbaf90

Please sign in to comment.