mirror of
https://github.com/docker/actions-toolkit.git
synced 2024-11-23 11:36:10 +08:00
github: upload artifact
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
parent
8d4d79eeb7
commit
db0a36131e
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@ -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
|
||||||
|
41
__tests__/github.test.itg.ts
Normal file
41
__tests__/github.test.itg.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
@ -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",
|
||||||
|
104
src/github.ts
104
src/github.ts
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user