diff --git a/__tests__/github/artifact.test.itg.ts b/__tests__/github/artifact.test.itg.ts new file mode 100644 index 0000000..09473c7 --- /dev/null +++ b/__tests__/github/artifact.test.itg.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2026 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 {describe, expect, it} from '@jest/globals'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {GitHubArtifact} from '../../src/github/artifact'; +import {Util} from '../../src/util'; + +const fixturesDir = path.join(__dirname, '..', '.fixtures'); +const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'github-itg-')); + +const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip; + +maybe('upload', () => { + it('uploads an artifact', async () => { + const filename = path.join(tmpDir, `github-repo-${Util.generateRandomString()}.json`); + fs.copyFileSync(path.join(fixturesDir, `github-repo.json`), filename); + const res = await GitHubArtifact.upload({ + filename: filename, + mimeType: 'application/json', + retentionDays: 1 + }); + expect(res).toBeDefined(); + console.log('uploadArtifactResponse', res); + expect(res?.url).toBeDefined(); + }); +}); diff --git a/__tests__/github.test.ts b/__tests__/github/github.test.ts similarity index 92% rename from __tests__/github.test.ts rename to __tests__/github/github.test.ts index 134041d..3df0cf1 100644 --- a/__tests__/github.test.ts +++ b/__tests__/github/github.test.ts @@ -19,10 +19,12 @@ import * as fs from 'fs'; import * as path from 'path'; import * as core from '@actions/core'; -import {GitHub} from '../src/github'; -import {GitHubRepo} from '../src/types/github'; +import {GitHub} from '../../src/github/github'; +import {GitHubRepo} from '../../src/types/github/github'; -import repoFixture from './.fixtures/github-repo.json'; +import repoFixture from '../.fixtures/github-repo.json'; + +const fixturesDir = path.join(__dirname, '..', '.fixtures'); describe('repoData', () => { it('returns GitHub repo data', async () => { @@ -49,7 +51,7 @@ describe('repoData (api)', () => { try { jest.resetModules(); jest.unmock('@actions/github'); - const {GitHub} = await import('../src/github'); + const {GitHub} = await import('../../src/github/github'); const github = new GitHub({token: process.env.GITHUB_TOKEN}); const repo = await github.repoData(); const fullName = repo.full_name ?? `${repo.owner?.login}/${repo.name}`; @@ -172,10 +174,7 @@ describe('actionsRuntimeToken', () => { }).toThrow(); }); it('fixture', async () => { - process.env.ACTIONS_RUNTIME_TOKEN = fs - .readFileSync(path.join(__dirname, '.fixtures', 'runtimeToken.txt')) - .toString() - .trim(); + process.env.ACTIONS_RUNTIME_TOKEN = fs.readFileSync(path.join(fixturesDir, 'runtimeToken.txt')).toString().trim(); const runtimeToken = GitHub.actionsRuntimeToken; expect(runtimeToken?.ac).toEqual('[{"Scope":"refs/heads/master","Permission":3}]'); expect(runtimeToken?.iss).toEqual('vstoken.actions.githubusercontent.com'); @@ -203,10 +202,7 @@ describe('printActionsRuntimeTokenACs', () => { }); it('refs/heads/master', async () => { const infoSpy = jest.spyOn(core, 'info'); - process.env.ACTIONS_RUNTIME_TOKEN = fs - .readFileSync(path.join(__dirname, '.fixtures', 'runtimeToken.txt')) - .toString() - .trim(); + process.env.ACTIONS_RUNTIME_TOKEN = fs.readFileSync(path.join(fixturesDir, 'runtimeToken.txt')).toString().trim(); await GitHub.printActionsRuntimeTokenACs(); expect(infoSpy).toHaveBeenCalledTimes(1); expect(infoSpy).toHaveBeenCalledWith(`refs/heads/master: read/write`); diff --git a/__tests__/github.test.itg.ts b/__tests__/github/summary.test.itg.ts similarity index 87% rename from __tests__/github.test.itg.ts rename to __tests__/github/summary.test.itg.ts index face32b..a3d987e 100644 --- a/__tests__/github.test.itg.ts +++ b/__tests__/github/summary.test.itg.ts @@ -19,34 +19,19 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import {Buildx} from '../src/buildx/buildx'; -import {Bake} from '../src/buildx/bake'; -import {Build} from '../src/buildx/build'; -import {Exec} from '../src/exec'; -import {GitHub} from '../src/github'; -import {History} from '../src/buildx/history'; -import {Util} from '../src/util'; +import {Buildx} from '../../src/buildx/buildx'; +import {Bake} from '../../src/buildx/bake'; +import {Build} from '../../src/buildx/build'; +import {Exec} from '../../src/exec'; +import {GitHubArtifact} from '../../src/github/artifact'; +import {GitHubSummary} from '../../src/github/summary'; +import {History} from '../../src/buildx/history'; -const fixturesDir = path.join(__dirname, '.fixtures'); +const fixturesDir = path.join(__dirname, '..', '.fixtures'); const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'github-itg-')); const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip; -maybe('uploadArtifact', () => { - it('uploads an artifact', async () => { - const filename = path.join(tmpDir, `github-repo-${Util.generateRandomString()}.json`); - fs.copyFileSync(path.join(fixturesDir, `github-repo.json`), filename); - const res = await GitHub.uploadArtifact({ - filename: filename, - mimeType: 'application/json', - retentionDays: 1 - }); - expect(res).toBeDefined(); - console.log('uploadArtifactResponse', res); - expect(res?.url).toBeDefined(); - }); -}); - maybe('writeBuildSummary', () => { // prettier-ignore test.each([ @@ -98,7 +83,7 @@ maybe('writeBuildSummary', () => { expect(exportRes?.dockerbuildSize).toBeDefined(); expect(exportRes?.summaries).toBeDefined(); - const uploadRes = await GitHub.uploadArtifact({ + const uploadRes = await GitHubArtifact.upload({ filename: exportRes?.dockerbuildFilename, mimeType: 'application/gzip', retentionDays: 1 @@ -106,7 +91,7 @@ maybe('writeBuildSummary', () => { expect(uploadRes).toBeDefined(); expect(uploadRes?.url).toBeDefined(); - await GitHub.writeBuildSummary({ + await GitHubSummary.writeBuildSummary({ exportRes: exportRes, uploadRes: uploadRes, inputs: { @@ -178,7 +163,7 @@ maybe('writeBuildSummary', () => { expect(exportRes?.dockerbuildSize).toBeDefined(); expect(exportRes?.summaries).toBeDefined(); - const uploadRes = await GitHub.uploadArtifact({ + const uploadRes = await GitHubArtifact.upload({ filename: exportRes?.dockerbuildFilename, mimeType: 'application/gzip', retentionDays: 1 @@ -186,7 +171,7 @@ maybe('writeBuildSummary', () => { expect(uploadRes).toBeDefined(); expect(uploadRes?.url).toBeDefined(); - await GitHub.writeBuildSummary({ + await GitHubSummary.writeBuildSummary({ exportRes: exportRes, uploadRes: uploadRes, inputs: { @@ -233,7 +218,7 @@ maybe('writeBuildSummary', () => { expect(exportRes?.dockerbuildSize).toBeDefined(); expect(exportRes?.summaries).toBeDefined(); - const uploadRes = await GitHub.uploadArtifact({ + const uploadRes = await GitHubArtifact.upload({ filename: exportRes?.dockerbuildFilename, mimeType: 'application/gzip', retentionDays: 1 @@ -241,7 +226,7 @@ maybe('writeBuildSummary', () => { expect(uploadRes).toBeDefined(); expect(uploadRes?.url).toBeDefined(); - await GitHub.writeBuildSummary({ + await GitHubSummary.writeBuildSummary({ exportRes: exportRes, uploadRes: uploadRes, inputs: { @@ -288,7 +273,7 @@ maybe('writeBuildSummary', () => { expect(exportRes?.dockerbuildSize).toBeDefined(); expect(exportRes?.summaries).toBeDefined(); - await GitHub.writeBuildSummary({ + await GitHubSummary.writeBuildSummary({ exportRes: exportRes, inputs: { context: fixturesDir, diff --git a/src/buildx/build.ts b/src/buildx/build.ts index 5cf3e91..2db3aae 100644 --- a/src/buildx/build.ts +++ b/src/buildx/build.ts @@ -21,7 +21,7 @@ import {parse} from 'csv-parse/sync'; import {Buildx} from './buildx.js'; import {Context} from '../context.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Util} from '../util.js'; import {BuildMetadata} from '../types/buildx/build.js'; diff --git a/src/buildx/buildx.ts b/src/buildx/buildx.ts index 2b26a1b..a4b4b1c 100644 --- a/src/buildx/buildx.ts +++ b/src/buildx/buildx.ts @@ -21,14 +21,14 @@ import * as semver from 'semver'; import {Git} from '../buildkit/git.js'; import {Docker} from '../docker/docker.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Exec} from '../exec.js'; import {Util} from '../util.js'; import {VertexWarning} from '../types/buildkit/client.js'; import {GitURL} from '../types/buildkit/git.js'; import {Cert, LocalRefsOpts, LocalRefsResponse, LocalState} from '../types/buildx/buildx.js'; -import {GitHubAnnotation} from '../types/github.js'; +import {GitHubAnnotation} from '../types/github/github.js'; export interface BuildxOpts { standalone?: boolean; diff --git a/src/buildx/history.ts b/src/buildx/history.ts index c8067ff..b47de35 100644 --- a/src/buildx/history.ts +++ b/src/buildx/history.ts @@ -25,7 +25,7 @@ import {Buildx} from './buildx.js'; import {Context} from '../context.js'; import {Docker} from '../docker/docker.js'; import {Exec} from '../exec.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Util} from '../util.js'; import {ExportOpts, ExportResponse, InspectOpts, InspectResponse, Summaries} from '../types/buildx/history.js'; diff --git a/src/buildx/install.ts b/src/buildx/install.ts index 2b49c1c..85697a5 100644 --- a/src/buildx/install.ts +++ b/src/buildx/install.ts @@ -29,12 +29,12 @@ import {Context} from '../context.js'; import {Exec} from '../exec.js'; import {Docker} from '../docker/docker.js'; import {Git} from '../git.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Sigstore} from '../sigstore/sigstore.js'; import {Util} from '../util.js'; import {DownloadVersion} from '../types/buildx/buildx.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; import {SEARCH_URL} from '../types/sigstore/sigstore.js'; export interface DownloadOpts { diff --git a/src/compose/install.ts b/src/compose/install.ts index de0456e..1eacf83 100644 --- a/src/compose/install.ts +++ b/src/compose/install.ts @@ -25,10 +25,10 @@ import * as util from 'util'; import {Cache} from '../cache.js'; import {Context} from '../context.js'; import {Docker} from '../docker/docker.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {DownloadVersion} from '../types/compose/compose.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; export interface InstallOpts { standalone?: boolean; diff --git a/src/context.ts b/src/context.ts index 0e6c040..85d90a4 100644 --- a/src/context.ts +++ b/src/context.ts @@ -20,7 +20,7 @@ import path from 'path'; import * as tmp from 'tmp'; import * as github from '@actions/github'; -import {GitHub} from './github.js'; +import {GitHub} from './github/github.js'; export class Context { private static readonly _tmpDir = fs.mkdtempSync(path.join(Context.ensureDirExists(process.env.RUNNER_TEMP || os.tmpdir()), 'docker-actions-toolkit-')); diff --git a/src/cosign/install.ts b/src/cosign/install.ts index 5891cba..47f4531 100644 --- a/src/cosign/install.ts +++ b/src/cosign/install.ts @@ -27,12 +27,12 @@ import {Cache} from '../cache.js'; import {Context} from '../context.js'; import {Exec} from '../exec.js'; import {Git} from '../git.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Sigstore} from '../sigstore/sigstore.js'; import {Util} from '../util.js'; import {DownloadVersion} from '../types/cosign/cosign.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; import {dockerfileContent} from './dockerfile.js'; import {SEARCH_URL} from '../types/sigstore/sigstore.js'; diff --git a/src/docker/install.ts b/src/docker/install.ts index 5fcdb83..cfcf616 100644 --- a/src/docker/install.ts +++ b/src/docker/install.ts @@ -28,14 +28,14 @@ import * as tc from '@actions/tool-cache'; import {Context} from '../context.js'; import {Docker} from './docker.js'; import {Exec} from '../exec.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {Regctl} from '../regclient/regctl.js'; import {Undock} from '../undock/undock.js'; import {Util} from '../util.js'; import {limaYamlData, dockerServiceLogsPs1, setupDockerWinPs1} from './assets.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; import {Image} from '../types/oci/config.js'; export interface InstallSourceImage { diff --git a/src/git.ts b/src/git.ts index 7fb9fe6..addedfa 100644 --- a/src/git.ts +++ b/src/git.ts @@ -17,7 +17,7 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; import {Exec} from './exec.js'; -import {GitHub} from './github.js'; +import {GitHub} from './github/github.js'; export type GitContext = typeof github.context; diff --git a/src/github.ts b/src/github.ts deleted file mode 100644 index cea092e..0000000 --- a/src/github.ts +++ /dev/null @@ -1,418 +0,0 @@ -/** - * Copyright 2023 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 crypto from 'crypto'; -import fs from 'fs'; -import he from 'he'; -import {dump as yamldump} from 'js-yaml'; -import os from 'os'; -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 * as httpm from '@actions/http-client'; -import {TransferProgressEvent} from '@azure/core-rest-pipeline'; -import {BlobClient, BlobHTTPHeaders} from '@azure/storage-blob'; -import {jwtDecode, JwtPayload} from 'jwt-decode'; - -import {Util} from './util.js'; - -import {BuildSummaryOpts, GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubContentOpts, GitHubRelease, GitHubRepo, SummaryTableCell, UploadArtifactOpts, UploadArtifactResponse} from './types/github.js'; - -export interface GitHubOpts { - token?: string; -} - -export class GitHub { - private readonly githubToken?: string; - public readonly octokit: ReturnType; - - constructor(opts?: GitHubOpts) { - this.githubToken = opts?.token || process.env.GITHUB_TOKEN; - this.octokit = github.getOctokit(`${this.githubToken}`); - } - - public repoData(): Promise { - return this.octokit.rest.repos.get({...github.context.repo}).then(response => response.data as GitHubRepo); - } - - public async releases(name: string, opts: GitHubContentOpts): Promise> { - let releases: Record; - try { - // try without token first - releases = await this.releasesRaw(name, opts); - } catch (error) { - if (!this.githubToken) { - throw error; - } - // try with token - releases = await this.releasesRaw(name, opts, this.githubToken); - } - return releases; - } - - public async releasesRaw(name: string, opts: GitHubContentOpts, token?: string): Promise> { - const url = `https://raw.githubusercontent.com/${opts.owner}/${opts.repo}/${opts.ref}/${opts.path}`; - const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit'); - // prettier-ignore - const httpResp: httpm.HttpClientResponse = await http.get(url, token ? { - Authorization: `token ${token}` - } : undefined); - const dt = await httpResp.readBody(); - const statusCode = httpResp.message.statusCode || 500; - if (statusCode >= 400) { - throw new Error(`Failed to get ${name} releases from ${url} with status code ${statusCode}: ${dt}`); - } - return >JSON.parse(dt); - } - - static get context(): typeof github.context { - return github.context; - } - - static get serverURL(): string { - return process.env.GITHUB_SERVER_URL || 'https://github.com'; - } - - static get apiURL(): string { - return process.env.GITHUB_API_URL || 'https://api.github.com'; - } - - // Can't use the isGhes() func from @actions/artifact due to @actions/artifact/lib/internal/shared/config - // being internal since ESM-only packages do not support internal exports. - // https://github.com/actions/toolkit/blob/8351a5d84d862813d1bb8bdeef87b215f8a946f9/packages/artifact/src/internal/shared/config.ts#L27 - static get isGHES(): boolean { - const ghURL = new URL(GitHub.serverURL); - const hostname = ghURL.hostname.trimEnd().toUpperCase(); - const isGitHubHost = hostname === 'GITHUB.COM'; - const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM'); - const isLocalHost = hostname.endsWith('.LOCALHOST'); - return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost; - } - - static get repository(): string { - return `${github.context.repo.owner}/${github.context.repo.repo}`; - } - - static get workspace(): string { - return process.env.GITHUB_WORKSPACE || process.cwd(); - } - - static get runId(): number { - return process.env.GITHUB_RUN_ID ? +process.env.GITHUB_RUN_ID : github.context.runId; - } - - static get runAttempt(): number { - // TODO: runAttempt is not yet part of github.context but will be in a - // future release of @actions/github package: https://github.com/actions/toolkit/commit/faa425440f86f9c16587a19dfb59491253a2c92a - return process.env.GITHUB_RUN_ATTEMPT ? +process.env.GITHUB_RUN_ATTEMPT : 1; - } - - public static workflowRunURL(setAttempts?: boolean): string { - return `${GitHub.serverURL}/${GitHub.repository}/actions/runs/${GitHub.runId}${setAttempts ? `/attempts/${GitHub.runAttempt}` : ''}`; - } - - static get actionsRuntimeToken(): GitHubActionsRuntimeToken | undefined { - const token = process.env['ACTIONS_RUNTIME_TOKEN'] || ''; - return token ? (jwtDecode(token) as GitHubActionsRuntimeToken) : undefined; - } - - public static async printActionsRuntimeTokenACs() { - let jwt: GitHubActionsRuntimeToken | undefined; - try { - jwt = GitHub.actionsRuntimeToken; - } catch (e) { - throw new Error(`Cannot parse GitHub Actions Runtime Token: ${e.message}`); - } - if (!jwt) { - throw new Error(`ACTIONS_RUNTIME_TOKEN not set`); - } - try { - >JSON.parse(`${jwt.ac}`).forEach(ac => { - let permission: string; - switch (ac.Permission) { - case 1: - permission = 'read'; - break; - case 2: - permission = 'write'; - break; - case 3: - permission = 'read/write'; - break; - default: - permission = `unimplemented (${ac.Permission})`; - } - core.info(`${ac.Scope}: ${permission}`); - }); - } catch (e) { - throw new Error(`Cannot parse GitHub Actions Runtime Token ACs: ${e.message}`); - } - } - - public static async uploadArtifact(opts: UploadArtifactOpts): Promise { - if (GitHub.isGHES) { - throw new Error('@actions/artifact v2.0.0+ is currently not supported on GHES.'); - } - - 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 - }; - } - - public static async writeBuildSummary(opts: BuildSummaryOpts): Promise { - // can't use original core.summary.addLink due to the need to make - // EOL optional - const addLink = function (text: string, url: string, addEOL = false): string { - return `${text}` + (addEOL ? os.EOL : ''); - }; - - const refsSize = opts.exportRes.refs.length; - const firstRef = refsSize > 0 ? opts.exportRes.refs?.[0] : undefined; - const firstSummary = firstRef ? opts.exportRes.summaries?.[firstRef] : undefined; - const dbcAccount = opts.driver === 'cloud' && opts.endpoint ? opts.endpoint?.replace(/^cloud:\/\//, '').split('/')[0] : undefined; - - const sum = core.summary.addHeading('Docker Build summary', 2); - - if (dbcAccount && refsSize === 1 && firstRef && firstSummary) { - const buildURL = GitHub.formatDBCBuildURL(dbcAccount, firstRef, firstSummary.defaultPlatform); - // prettier-ignore - sum.addRaw(`

`) - .addRaw(`For a detailed look at the build, you can check the results at:`) - .addRaw('

') - .addRaw(`

`) - .addRaw(`:whale: ${addLink(`${buildURL}`, buildURL)}`) - .addRaw(`

`); - } - - if (opts.uploadRes) { - // we just need the last two parts of the URL as they are always relative - // to the workflow run URL otherwise URL could be broken if GitHub - // repository name is part of a secret value used in the workflow. e.g.: - // artifact: https://github.com/docker/actions-toolkit/actions/runs/9552208295/artifacts/1609622746 - // workflow: https://github.com/docker/actions-toolkit/actions/runs/9552208295 - // https://github.com/docker/actions-toolkit/issues/367 - const artifactRelativeURL = `./${GitHub.runId}/${opts.uploadRes.url.split('/').slice(-2).join('/')}`; - - if (dbcAccount && refsSize === 1) { - // prettier-ignore - sum.addRaw(`

`) - .addRaw(`You can also download the following build record archive and import it into Docker Desktop's Builds view. `) - .addBreak() - .addRaw(`Build records include details such as timing, dependencies, results, logs, traces, and other information about a build. `) - .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) - .addRaw('

') - } else { - // prettier-ignore - sum.addRaw(`

`) - .addRaw(`For a detailed look at the build, download the following build record archive and import it into Docker Desktop's Builds view. `) - .addBreak() - .addRaw(`Build records include details such as timing, dependencies, results, logs, traces, and other information about a build. `) - .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) - .addRaw('

') - } - - // prettier-ignore - sum.addRaw(`

`) - .addRaw(`:arrow_down: ${addLink(`${Util.stringToUnicodeEntities(opts.uploadRes.filename)}`, artifactRelativeURL)} (${Util.formatFileSize(opts.uploadRes.size)} - includes ${refsSize} build record${refsSize > 1 ? 's' : ''})`) - .addRaw(`

`); - } else if (opts.exportRes.summaries) { - // prettier-ignore - sum.addRaw(`

`) - .addRaw(`The following table provides a brief summary of your build.`) - .addBreak() - .addRaw(`For a detailed look at the build, including timing, dependencies, results, logs, traces, and other information, consider enabling the export of the build record so you can import it into Docker Desktop's Builds view. `) - .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) - .addRaw(`

`); - } - - // Feedback survey - sum.addRaw(`

`).addRaw(`Find this useful? `).addRaw(addLink('Let us know', 'https://docs.docker.com/feedback/gha-build-summary')).addRaw('

'); - - if (opts.exportRes.summaries) { - // Preview - sum.addRaw('

'); - const summaryTableData: Array> = [ - // prettier-ignore - [ - {header: true, data: 'ID'}, - {header: true, data: 'Name'}, - {header: true, data: 'Status'}, - {header: true, data: 'Cached'}, - {header: true, data: 'Duration'}, - ...(dbcAccount && refsSize > 1 ? [{header: true, data: 'Build result URL'}] : []) - ] - ]; - let buildError: string | undefined; - for (const ref in opts.exportRes.summaries) { - if (Object.prototype.hasOwnProperty.call(opts.exportRes.summaries, ref)) { - const summary = opts.exportRes.summaries[ref]; - // prettier-ignore - summaryTableData.push([ - {data: `${ref.substring(0, 6).toUpperCase()}`}, - {data: `${Util.stringToUnicodeEntities(summary.name)}`}, - {data: `${summary.status === 'completed' ? ':white_check_mark:' : summary.status === 'canceled' ? ':no_entry_sign:' : ':x:'} ${summary.status}`}, - {data: `${summary.numCachedSteps > 0 ? Math.round((summary.numCachedSteps / summary.numTotalSteps) * 100) : 0}%`}, - {data: summary.duration}, - ...(dbcAccount && refsSize > 1 ? [{data: addLink(':whale: Open', GitHub.formatDBCBuildURL(dbcAccount, ref, summary.defaultPlatform))}] : []) - ]); - if (summary.error) { - buildError = summary.error; - } - } - } - sum.addTable([...summaryTableData]); - sum.addRaw(`

`); - - // Build error - if (buildError) { - sum.addRaw(`
`); - if (Util.countLines(buildError) > 10) { - // prettier-ignore - sum - .addRaw(`
Error`) - .addCodeBlock(he.encode(buildError), 'text') - .addRaw(`
`); - } else { - // prettier-ignore - sum - .addRaw(`Error`) - .addBreak() - .addRaw(`

`) - .addCodeBlock(he.encode(buildError), 'text') - .addRaw(`

`); - } - sum.addRaw(`
`); - } - } - - // Build inputs - if (opts.inputs) { - // prettier-ignore - sum.addRaw(`
Build inputs`) - .addCodeBlock( - yamldump(opts.inputs, { - indent: 2, - lineWidth: -1 - }), 'yaml' - ) - .addRaw(`
`); - } - - // Bake definition - if (opts.bakeDefinition) { - // prettier-ignore - sum.addRaw(`
Bake definition`) - .addCodeBlock(JSON.stringify(opts.bakeDefinition, null, 2), 'json') - .addRaw(`
`); - } - - core.info(`Writing summary`); - await sum.addSeparator().write(); - } - - private static formatDBCBuildURL(account: string, ref: string, platform?: string): string { - return `https://app.docker.com/build/accounts/${account}/builds/${(platform ?? 'linux/amd64').replace('/', '-')}/${ref}`; - } -} diff --git a/src/github/artifact.ts b/src/github/artifact.ts new file mode 100644 index 0000000..1dafcf4 --- /dev/null +++ b/src/github/artifact.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2026 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 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 {TransferProgressEvent} from '@azure/core-rest-pipeline'; +import {BlobClient, BlobHTTPHeaders} from '@azure/storage-blob'; + +import {UploadOpts, UploadResponse} from '../types/github/artifact.js'; +import {GitHub} from './github'; + +export class GitHubArtifact { + public static async upload(opts: UploadOpts): Promise { + if (GitHub.isGHES) { + throw new Error('@actions/artifact v2.0.0+ is currently not supported on GHES.'); + } + + 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 + }; + } +} diff --git a/src/github/github.ts b/src/github/github.ts new file mode 100644 index 0000000..c629571 --- /dev/null +++ b/src/github/github.ts @@ -0,0 +1,154 @@ +/** + * Copyright 2023 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 * as core from '@actions/core'; +import * as github from '@actions/github'; +import * as httpm from '@actions/http-client'; +import {jwtDecode, JwtPayload} from 'jwt-decode'; + +import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubContentOpts, GitHubRelease, GitHubRepo} from '../types/github/github.js'; + +export interface GitHubOpts { + token?: string; +} + +export class GitHub { + private readonly githubToken?: string; + public readonly octokit: ReturnType; + + constructor(opts?: GitHubOpts) { + this.githubToken = opts?.token || process.env.GITHUB_TOKEN; + this.octokit = github.getOctokit(`${this.githubToken}`); + } + + public repoData(): Promise { + return this.octokit.rest.repos.get({...github.context.repo}).then(response => response.data as GitHubRepo); + } + + public async releases(name: string, opts: GitHubContentOpts): Promise> { + let releases: Record; + try { + // try without token first + releases = await this.releasesRaw(name, opts); + } catch (error) { + if (!this.githubToken) { + throw error; + } + // try with token + releases = await this.releasesRaw(name, opts, this.githubToken); + } + return releases; + } + + public async releasesRaw(name: string, opts: GitHubContentOpts, token?: string): Promise> { + const url = `https://raw.githubusercontent.com/${opts.owner}/${opts.repo}/${opts.ref}/${opts.path}`; + const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit'); + // prettier-ignore + const httpResp: httpm.HttpClientResponse = await http.get(url, token ? { + Authorization: `token ${token}` + } : undefined); + const dt = await httpResp.readBody(); + const statusCode = httpResp.message.statusCode || 500; + if (statusCode >= 400) { + throw new Error(`Failed to get ${name} releases from ${url} with status code ${statusCode}: ${dt}`); + } + return >JSON.parse(dt); + } + + static get context(): typeof github.context { + return github.context; + } + + static get serverURL(): string { + return process.env.GITHUB_SERVER_URL || 'https://github.com'; + } + + static get apiURL(): string { + return process.env.GITHUB_API_URL || 'https://api.github.com'; + } + + // Can't use the isGhes() func from @actions/artifact due to @actions/artifact/lib/internal/shared/config + // being internal since ESM-only packages do not support internal exports. + // https://github.com/actions/toolkit/blob/8351a5d84d862813d1bb8bdeef87b215f8a946f9/packages/artifact/src/internal/shared/config.ts#L27 + static get isGHES(): boolean { + const ghURL = new URL(GitHub.serverURL); + const hostname = ghURL.hostname.trimEnd().toUpperCase(); + const isGitHubHost = hostname === 'GITHUB.COM'; + const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM'); + const isLocalHost = hostname.endsWith('.LOCALHOST'); + return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost; + } + + static get repository(): string { + return `${github.context.repo.owner}/${github.context.repo.repo}`; + } + + static get workspace(): string { + return process.env.GITHUB_WORKSPACE || process.cwd(); + } + + static get runId(): number { + return process.env.GITHUB_RUN_ID ? +process.env.GITHUB_RUN_ID : github.context.runId; + } + + static get runAttempt(): number { + // TODO: runAttempt is not yet part of github.context but will be in a + // future release of @actions/github package: https://github.com/actions/toolkit/commit/faa425440f86f9c16587a19dfb59491253a2c92a + return process.env.GITHUB_RUN_ATTEMPT ? +process.env.GITHUB_RUN_ATTEMPT : 1; + } + + public static workflowRunURL(setAttempts?: boolean): string { + return `${GitHub.serverURL}/${GitHub.repository}/actions/runs/${GitHub.runId}${setAttempts ? `/attempts/${GitHub.runAttempt}` : ''}`; + } + + static get actionsRuntimeToken(): GitHubActionsRuntimeToken | undefined { + const token = process.env['ACTIONS_RUNTIME_TOKEN'] || ''; + return token ? (jwtDecode(token) as GitHubActionsRuntimeToken) : undefined; + } + + public static async printActionsRuntimeTokenACs() { + let jwt: GitHubActionsRuntimeToken | undefined; + try { + jwt = GitHub.actionsRuntimeToken; + } catch (e) { + throw new Error(`Cannot parse GitHub Actions Runtime Token: ${e.message}`); + } + if (!jwt) { + throw new Error(`ACTIONS_RUNTIME_TOKEN not set`); + } + try { + >JSON.parse(`${jwt.ac}`).forEach(ac => { + let permission: string; + switch (ac.Permission) { + case 1: + permission = 'read'; + break; + case 2: + permission = 'write'; + break; + case 3: + permission = 'read/write'; + break; + default: + permission = `unimplemented (${ac.Permission})`; + } + core.info(`${ac.Scope}: ${permission}`); + }); + } catch (e) { + throw new Error(`Cannot parse GitHub Actions Runtime Token ACs: ${e.message}`); + } + } +} diff --git a/src/github/summary.ts b/src/github/summary.ts new file mode 100644 index 0000000..6a3bc22 --- /dev/null +++ b/src/github/summary.ts @@ -0,0 +1,182 @@ +/** + * Copyright 2026 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 he from 'he'; +import {dump as yamldump} from 'js-yaml'; +import os from 'os'; +import * as core from '@actions/core'; + +import {GitHub} from './github'; +import {Util} from '../util.js'; + +import {BuildSummaryOpts, SummaryTableCell} from '../types/github/summary.js'; + +export class GitHubSummary { + public static async writeBuildSummary(opts: BuildSummaryOpts): Promise { + // can't use original core.summary.addLink due to the need to make + // EOL optional + const addLink = function (text: string, url: string, addEOL = false): string { + return `${text}` + (addEOL ? os.EOL : ''); + }; + + const refsSize = opts.exportRes.refs.length; + const firstRef = refsSize > 0 ? opts.exportRes.refs?.[0] : undefined; + const firstSummary = firstRef ? opts.exportRes.summaries?.[firstRef] : undefined; + const dbcAccount = opts.driver === 'cloud' && opts.endpoint ? opts.endpoint?.replace(/^cloud:\/\//, '').split('/')[0] : undefined; + + const sum = core.summary.addHeading('Docker Build summary', 2); + + if (dbcAccount && refsSize === 1 && firstRef && firstSummary) { + const buildURL = GitHubSummary.formatDBCBuildURL(dbcAccount, firstRef, firstSummary.defaultPlatform); + // prettier-ignore + sum.addRaw(`

`) + .addRaw(`For a detailed look at the build, you can check the results at:`) + .addRaw('

') + .addRaw(`

`) + .addRaw(`:whale: ${addLink(`${buildURL}`, buildURL)}`) + .addRaw(`

`); + } + + if (opts.uploadRes) { + // we just need the last two parts of the URL as they are always relative + // to the workflow run URL otherwise URL could be broken if GitHub + // repository name is part of a secret value used in the workflow. e.g.: + // artifact: https://github.com/docker/actions-toolkit/actions/runs/9552208295/artifacts/1609622746 + // workflow: https://github.com/docker/actions-toolkit/actions/runs/9552208295 + // https://github.com/docker/actions-toolkit/issues/367 + const artifactRelativeURL = `./${GitHub.runId}/${opts.uploadRes.url.split('/').slice(-2).join('/')}`; + + if (dbcAccount && refsSize === 1) { + // prettier-ignore + sum.addRaw(`

`) + .addRaw(`You can also download the following build record archive and import it into Docker Desktop's Builds view. `) + .addBreak() + .addRaw(`Build records include details such as timing, dependencies, results, logs, traces, and other information about a build. `) + .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) + .addRaw('

') + } else { + // prettier-ignore + sum.addRaw(`

`) + .addRaw(`For a detailed look at the build, download the following build record archive and import it into Docker Desktop's Builds view. `) + .addBreak() + .addRaw(`Build records include details such as timing, dependencies, results, logs, traces, and other information about a build. `) + .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) + .addRaw('

') + } + + // prettier-ignore + sum.addRaw(`

`) + .addRaw(`:arrow_down: ${addLink(`${Util.stringToUnicodeEntities(opts.uploadRes.filename)}`, artifactRelativeURL)} (${Util.formatFileSize(opts.uploadRes.size)} - includes ${refsSize} build record${refsSize > 1 ? 's' : ''})`) + .addRaw(`

`); + } else if (opts.exportRes.summaries) { + // prettier-ignore + sum.addRaw(`

`) + .addRaw(`The following table provides a brief summary of your build.`) + .addBreak() + .addRaw(`For a detailed look at the build, including timing, dependencies, results, logs, traces, and other information, consider enabling the export of the build record so you can import it into Docker Desktop's Builds view. `) + .addRaw(addLink('Learn more', 'https://www.docker.com/blog/new-beta-feature-deep-dive-into-github-actions-docker-builds-with-docker-desktop/?utm_source=github&utm_medium=actions')) + .addRaw(`

`); + } + + // Feedback survey + sum.addRaw(`

`).addRaw(`Find this useful? `).addRaw(addLink('Let us know', 'https://docs.docker.com/feedback/gha-build-summary')).addRaw('

'); + + if (opts.exportRes.summaries) { + // Preview + sum.addRaw('

'); + const summaryTableData: Array> = [ + // prettier-ignore + [ + {header: true, data: 'ID'}, + {header: true, data: 'Name'}, + {header: true, data: 'Status'}, + {header: true, data: 'Cached'}, + {header: true, data: 'Duration'}, + ...(dbcAccount && refsSize > 1 ? [{header: true, data: 'Build result URL'}] : []) + ] + ]; + let buildError: string | undefined; + for (const ref in opts.exportRes.summaries) { + if (Object.prototype.hasOwnProperty.call(opts.exportRes.summaries, ref)) { + const summary = opts.exportRes.summaries[ref]; + // prettier-ignore + summaryTableData.push([ + {data: `${ref.substring(0, 6).toUpperCase()}`}, + {data: `${Util.stringToUnicodeEntities(summary.name)}`}, + {data: `${summary.status === 'completed' ? ':white_check_mark:' : summary.status === 'canceled' ? ':no_entry_sign:' : ':x:'} ${summary.status}`}, + {data: `${summary.numCachedSteps > 0 ? Math.round((summary.numCachedSteps / summary.numTotalSteps) * 100) : 0}%`}, + {data: summary.duration}, + ...(dbcAccount && refsSize > 1 ? [{data: addLink(':whale: Open', GitHubSummary.formatDBCBuildURL(dbcAccount, ref, summary.defaultPlatform))}] : []) + ]); + if (summary.error) { + buildError = summary.error; + } + } + } + sum.addTable([...summaryTableData]); + sum.addRaw(`

`); + + // Build error + if (buildError) { + sum.addRaw(`
`); + if (Util.countLines(buildError) > 10) { + // prettier-ignore + sum + .addRaw(`
Error`) + .addCodeBlock(he.encode(buildError), 'text') + .addRaw(`
`); + } else { + // prettier-ignore + sum + .addRaw(`Error`) + .addBreak() + .addRaw(`

`) + .addCodeBlock(he.encode(buildError), 'text') + .addRaw(`

`); + } + sum.addRaw(`
`); + } + } + + // Build inputs + if (opts.inputs) { + // prettier-ignore + sum.addRaw(`
Build inputs`) + .addCodeBlock( + yamldump(opts.inputs, { + indent: 2, + lineWidth: -1 + }), 'yaml' + ) + .addRaw(`
`); + } + + // Bake definition + if (opts.bakeDefinition) { + // prettier-ignore + sum.addRaw(`
Bake definition`) + .addCodeBlock(JSON.stringify(opts.bakeDefinition, null, 2), 'json') + .addRaw(`
`); + } + + core.info(`Writing summary`); + await sum.addSeparator().write(); + } + + private static formatDBCBuildURL(account: string, ref: string, platform?: string): string { + return `https://app.docker.com/build/accounts/${account}/builds/${(platform ?? 'linux/amd64').replace('/', '-')}/${ref}`; + } +} diff --git a/src/regclient/install.ts b/src/regclient/install.ts index 9edb407..ef1a524 100644 --- a/src/regclient/install.ts +++ b/src/regclient/install.ts @@ -24,9 +24,9 @@ import * as util from 'util'; import {Cache} from '../cache.js'; import {Context} from '../context.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; import {DownloadVersion} from '../types/regclient/regclient.js'; export interface InstallOpts { diff --git a/src/sigstore/sigstore.ts b/src/sigstore/sigstore.ts index f48847d..552d82a 100644 --- a/src/sigstore/sigstore.ts +++ b/src/sigstore/sigstore.ts @@ -27,7 +27,7 @@ import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify'; import {Context} from '../context.js'; import {Cosign} from '../cosign/cosign.js'; import {Exec} from '../exec.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; import {ImageTools} from '../buildx/imagetools.js'; import {MEDIATYPE_PAYLOAD as INTOTO_MEDIATYPE_PAYLOAD, Subject} from '../types/intoto/intoto.js'; diff --git a/src/toolkit.ts b/src/toolkit.ts index 1563ce0..d6f9cfe 100644 --- a/src/toolkit.ts +++ b/src/toolkit.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHub} from './github.js'; +import {GitHub} from './github/github.js'; import {Buildx} from './buildx/buildx.js'; import {Build as BuildxBuild} from './buildx/build.js'; import {Bake as BuildxBake} from './buildx/bake.js'; diff --git a/src/types/buildx/buildx.ts b/src/types/buildx/buildx.ts index 0047a19..02d586f 100644 --- a/src/types/buildx/buildx.ts +++ b/src/types/buildx/buildx.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHubContentOpts} from '../github.js'; +import {GitHubContentOpts} from '../github/github.js'; export interface Cert { cacert?: string; diff --git a/src/types/compose/compose.ts b/src/types/compose/compose.ts index 8bb536d..cbd3f73 100644 --- a/src/types/compose/compose.ts +++ b/src/types/compose/compose.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHubContentOpts} from '../github.js'; +import {GitHubContentOpts} from '../github/github.js'; export interface DownloadVersion { key: string; diff --git a/src/types/cosign/cosign.ts b/src/types/cosign/cosign.ts index cda1036..6f816bc 100644 --- a/src/types/cosign/cosign.ts +++ b/src/types/cosign/cosign.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHubContentOpts} from '../github.js'; +import {GitHubContentOpts} from '../github/github.js'; export interface DownloadVersion { version: string; diff --git a/src/types/github/artifact.ts b/src/types/github/artifact.ts new file mode 100644 index 0000000..a8150d2 --- /dev/null +++ b/src/types/github/artifact.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2026 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. + */ + +export interface UploadOpts { + filename: string; + mimeType?: string; + retentionDays?: number; +} + +export interface UploadResponse { + id: number; + filename: string; + size: number; + url: string; +} diff --git a/src/types/github.ts b/src/types/github/github.ts similarity index 63% rename from src/types/github.ts rename to src/types/github/github.ts index 9b4e77d..3c3742c 100644 --- a/src/types/github.ts +++ b/src/types/github/github.ts @@ -14,17 +14,10 @@ * limitations under the License. */ -import * as core from '@actions/core'; import {AnnotationProperties} from '@actions/core'; import type {getOctokit} from '@actions/github'; import {JwtPayload} from 'jwt-decode'; -import {BakeDefinition} from './buildx/bake.js'; -import {ExportResponse} from './buildx/history.js'; - -export type SummaryTableRow = Parameters[0][number]; -export type SummaryTableCell = Exclude; - export interface GitHubRelease { id: number; tag_name: string; @@ -54,27 +47,3 @@ export interface GitHubActionsRuntimeTokenAC { export interface GitHubAnnotation extends AnnotationProperties { message: string; } - -export interface UploadArtifactOpts { - filename: string; - mimeType?: string; - retentionDays?: number; -} - -export interface UploadArtifactResponse { - id: number; - filename: string; - size: number; - url: string; -} - -export interface BuildSummaryOpts { - exportRes: ExportResponse; - uploadRes?: UploadArtifactResponse; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inputs?: any; - bakeDefinition?: BakeDefinition; - // builder options - driver?: string; - endpoint?: string; -} diff --git a/src/types/github/summary.ts b/src/types/github/summary.ts new file mode 100644 index 0000000..d1a9985 --- /dev/null +++ b/src/types/github/summary.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2026 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 * as core from '@actions/core'; + +import {UploadResponse} from './artifact'; +import {BakeDefinition} from '../buildx/bake'; +import {ExportResponse} from '../buildx/history'; + +export type SummaryTableRow = Parameters[0][number]; +export type SummaryTableCell = Exclude; + +export interface BuildSummaryOpts { + exportRes: ExportResponse; + uploadRes?: UploadResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inputs?: any; + bakeDefinition?: BakeDefinition; + // builder options + driver?: string; + endpoint?: string; +} diff --git a/src/types/regclient/regclient.ts b/src/types/regclient/regclient.ts index cda1036..6f816bc 100644 --- a/src/types/regclient/regclient.ts +++ b/src/types/regclient/regclient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHubContentOpts} from '../github.js'; +import {GitHubContentOpts} from '../github/github.js'; export interface DownloadVersion { version: string; diff --git a/src/types/undock/undock.ts b/src/types/undock/undock.ts index 745ca28..3dd760d 100644 --- a/src/types/undock/undock.ts +++ b/src/types/undock/undock.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {GitHubContentOpts} from '../github.js'; +import {GitHubContentOpts} from '../github/github.js'; export interface DownloadVersion { version: string; diff --git a/src/undock/install.ts b/src/undock/install.ts index 887a254..124460b 100644 --- a/src/undock/install.ts +++ b/src/undock/install.ts @@ -24,9 +24,9 @@ import * as util from 'util'; import {Cache} from '../cache.js'; import {Context} from '../context.js'; -import {GitHub} from '../github.js'; +import {GitHub} from '../github/github.js'; -import {GitHubRelease} from '../types/github.js'; +import {GitHubRelease} from '../types/github/github.js'; import {DownloadVersion} from '../types/undock/undock.js'; export interface InstallOpts {