diff --git a/__tests__/github/artifact.test.itg.ts b/__tests__/github/artifact.test.itg.ts new file mode 100644 index 00000000..09473c76 --- /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 134041df..3df0cf16 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 face32b9..a3d987e0 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 5cf3e919..2db3aaee 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 2b26a1b0..a4b4b1cc 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 c8067ffd..b47de351 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 2b49c1c4..85697a58 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 de0456e8..1eacf838 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 0e6c0409..85d90a48 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 5891cba9..47f45316 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 5fcdb837..cfcf6168 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 7fb9fe64..addedfa1 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 cea092eb..00000000 --- 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 00000000..1dafcf4b --- /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 00000000..c629571a --- /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 00000000..6a3bc228 --- /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 9edb407b..ef1a5244 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 f48847d0..552d82ad 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 1563ce0b..d6f9cfed 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 0047a190..02d586f5 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 8bb536d4..cbd3f73e 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 cda1036b..6f816bce 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 00000000..a8150d2f --- /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 9b4e77dc..3c3742c5 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 00000000..d1a9985f --- /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 cda1036b..6f816bce 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 745ca280..3dd760d7 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 887a254b..124460b6 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 {