diff --git a/__tests__/api-client.test.ts b/__tests__/api-client.test.ts index d10f022..5ed5a00 100644 --- a/__tests__/api-client.test.ts +++ b/__tests__/api-client.test.ts @@ -18,10 +18,20 @@ afterEach(() => { describe('getRepositoryMetadata', () => { it('returns repository metadata when the fetch response is ok', async () => { fetchMock.mockResolvedValueOnce( - new Response(JSON.stringify({ id: '123', owner: { id: '456' } })) + new Response( + JSON.stringify({ + id: '123', + owner: { id: '456' }, + visibility: 'public' + }) + ) ) const result = await getRepositoryMetadata(url, 'repository', 'token') - expect(result).toEqual({ repoId: '123', ownerId: '456' }) + expect(result).toEqual({ + repoId: '123', + ownerId: '456', + visibility: 'public' + }) }) it('throws an error when the fetch errors', async () => { diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 14a293d..a55944b 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -4,6 +4,7 @@ import * as cfg from '../src/config' import * as apiClient from '../src/api-client' let getContainerRegistryURLMock: jest.SpyInstance +let getRepositoryMetadataMock: jest.SpyInstance let getInputMock: jest.SpyInstance const ghcrUrl = new URL('https://ghcr.io') @@ -14,6 +15,10 @@ describe('config.resolvePublishActionOptions', () => { .spyOn(apiClient, 'getContainerRegistryURL') .mockImplementation() + getRepositoryMetadataMock = jest + .spyOn(apiClient, 'getRepositoryMetadata') + .mockImplementation() + getInputMock = jest.spyOn(core, 'getInput').mockImplementation() configureEventContext() @@ -133,6 +138,30 @@ describe('config.resolvePublishActionOptions', () => { ) }) + it('throws an error when getting the repository metadata fails', async () => { + getInputMock.mockReturnValueOnce('token') + getContainerRegistryURLMock.mockResolvedValue(ghcrUrl) + getRepositoryMetadataMock.mockRejectedValue( + new Error('Failed to get repository metadata') + ) + + await expect(cfg.resolvePublishActionOptions()).rejects.toThrow( + 'Failed to get repository metadata' + ) + }) + + it('throws an error when returned repository visibility is empty', async () => { + getInputMock.mockReturnValueOnce('token') + getContainerRegistryURLMock.mockResolvedValue(ghcrUrl) + getRepositoryMetadataMock.mockResolvedValue({ + visibility: '' + }) + + await expect(cfg.resolvePublishActionOptions()).rejects.toThrow( + 'Could not find repository visibility.' + ) + }) + it('returns options when all values are present', async () => { getInputMock.mockImplementation((name: string) => { expect(name).toBe('github-token') @@ -140,6 +169,10 @@ describe('config.resolvePublishActionOptions', () => { }) getContainerRegistryURLMock.mockResolvedValue(ghcrUrl) + getRepositoryMetadataMock.mockResolvedValue({ + visibility: 'public' + }) + const options = await cfg.resolvePublishActionOptions() expect(options).toEqual({ @@ -150,6 +183,7 @@ describe('config.resolvePublishActionOptions', () => { apiBaseUrl: 'apiBaseUrl', runnerTempDir: 'runnerTempDir', sha: 'sha', + repositoryVisibility: 'public', repositoryId: 'repositoryId', repositoryOwnerId: 'repositoryOwnerId', isEnterprise: false, @@ -164,6 +198,9 @@ describe('config.resolvePublishActionOptions', () => { return 'token' }) getContainerRegistryURLMock.mockResolvedValue(ghcrUrl) + getRepositoryMetadataMock.mockResolvedValue({ + visibility: 'public' + }) process.env.GITHUB_SERVER_URL = 'https://github-enterprise.com' @@ -181,7 +218,8 @@ describe('config.resolvePublishActionOptions', () => { repositoryOwnerId: 'repositoryOwnerId', isEnterprise: true, containerRegistryUrl: ghcrUrl, - token: 'token' + token: 'token', + repositoryVisibility: 'public' }) }) }) @@ -200,7 +238,8 @@ describe('config.serializeOptions', () => { repositoryOwnerId: 'repositoryOwnerId', isEnterprise: false, containerRegistryUrl: ghcrUrl, - token: 'token' + token: 'token', + repositoryVisibility: 'public' } const serialized = cfg.serializeOptions(options) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index a56b34a..6e68f36 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -389,6 +389,7 @@ function baseOptions(): cfg.PublishActionOptions { isEnterprise: false, containerRegistryUrl: ghcrUrl, token: 'token', - ref: 'refs/tags/v1.2.3' + ref: 'refs/tags/v1.2.3', + repositoryVisibility: 'public' } } diff --git a/badges/coverage.svg b/badges/coverage.svg index cfc625f..0005240 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 96.52%Coverage96.52% \ No newline at end of file +Coverage: 93.8%Coverage93.8% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index b48e3f4..d7dd128 100644 --- a/dist/index.js +++ b/dist/index.js @@ -99284,7 +99284,7 @@ ZipStream.prototype.finalize = function() { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getContainerRegistryURL = exports.getRepositoryMetadata = void 0; +exports.getRepositoryVisibility = exports.getContainerRegistryURL = exports.getRepositoryMetadata = void 0; async function getRepositoryMetadata(githubAPIURL, repository, token) { const response = await fetch(`${githubAPIURL}/repos/${repository}`, { method: 'GET', @@ -99301,7 +99301,11 @@ async function getRepositoryMetadata(githubAPIURL, repository, token) { if (!data.id || !data.owner.id) { throw new Error(`Failed to fetch repository metadata: unexpected response format`); } - return { repoId: String(data.id), ownerId: String(data.owner.id) }; + return { + repoId: String(data.id), + ownerId: String(data.owner.id), + visibility: String(data.visibility) + }; } exports.getRepositoryMetadata = getRepositoryMetadata; async function getContainerRegistryURL(githubAPIURL) { @@ -99317,6 +99321,18 @@ async function getContainerRegistryURL(githubAPIURL) { return registryURL; } exports.getContainerRegistryURL = getContainerRegistryURL; +async function getRepositoryVisibility(githubAPIURL) { + const response = await fetch(`${githubAPIURL}/`); + if (!response.ok) { + throw new Error(`Failed to fetch repository metadata due to bad status code: ${response.status}`); + } + const data = await response.json(); + if (!data.full_name) { + throw new Error(`Failed to fetch repository metadata: unexpected response format`); + } + return data.full_name; +} +exports.getRepositoryVisibility = getRepositoryVisibility; /***/ }), @@ -99406,6 +99422,11 @@ async function resolvePublishActionOptions() { const containerRegistryUrl = await apiClient.getContainerRegistryURL(apiBaseUrl); const isEnterprise = !githubServerUrl.includes('https://github.com') && !githubServerUrl.endsWith('.ghe.com'); + const repoMetadata = await apiClient.getRepositoryMetadata(apiBaseUrl, nameWithOwner, token); + if (repoMetadata.visibility === '') { + throw new Error(`Could not find repository visibility.`); + } + const repositoryVisibility = repoMetadata.visibility; return { event, ref, @@ -99417,6 +99438,7 @@ async function resolvePublishActionOptions() { sha, containerRegistryUrl, isEnterprise, + repositoryVisibility, repositoryId, repositoryOwnerId }; @@ -99825,12 +99847,13 @@ function parseSemverTagFromRef(ref) { // Generate an attestation using the actions toolkit // Subject name will contain the repo/package name and the tag name async function generateAttestation(manifestDigest, semverTag, options) { - const subjectName = `${options.nameWithOwner}_${semverTag}`; + const subjectName = `${options.nameWithOwner}@${semverTag}`; const subjectDigest = removePrefix(manifestDigest, 'sha256:'); return await attest.attestProvenance({ subjectName, subjectDigest: { sha256: subjectDigest }, token: options.token, + sigstore: 'github', skipWrite: false // TODO: Attestation storage is only supported for public repositories or repositories which belong to a GitHub Enterprise Cloud account }); } diff --git a/src/api-client.ts b/src/api-client.ts index e415992..952f811 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -2,7 +2,7 @@ export async function getRepositoryMetadata( githubAPIURL: string, repository: string, token: string -): Promise<{ repoId: string; ownerId: string }> { +): Promise<{ repoId: string; ownerId: string; visibility: string }> { const response = await fetch(`${githubAPIURL}/repos/${repository}`, { method: 'GET', headers: { @@ -26,7 +26,11 @@ export async function getRepositoryMetadata( ) } - return { repoId: String(data.id), ownerId: String(data.owner.id) } + return { + repoId: String(data.id), + ownerId: String(data.owner.id), + visibility: String(data.visibility) + } } export async function getContainerRegistryURL( @@ -51,3 +55,23 @@ export async function getContainerRegistryURL( const registryURL: URL = new URL(data.url) return registryURL } + +export async function getRepositoryVisibility( + githubAPIURL: string +): Promise { + const response = await fetch(`${githubAPIURL}/`) + if (!response.ok) { + throw new Error( + `Failed to fetch repository metadata due to bad status code: ${response.status}` + ) + } + const data = await response.json() + + if (!data.full_name) { + throw new Error( + `Failed to fetch repository metadata: unexpected response format` + ) + } + + return data.full_name +} diff --git a/src/config.ts b/src/config.ts index 90c74e6..ef307ab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,8 @@ export interface PublishActionOptions { runnerTempDir: string // Whether this action is running in enterprise, determined from the github URL isEnterprise: boolean + // The visibility of the action repository ("public", "internal" or "private") + repositoryVisibility: string // The repository ID of the action repository repositoryId: string // The owner ID of the action repository @@ -97,6 +99,18 @@ export async function resolvePublishActionOptions(): Promise { - const subjectName = `${options.nameWithOwner}_${semverTag}` + const subjectName = `${options.nameWithOwner}@${semverTag}` const subjectDigest = removePrefix(manifestDigest, 'sha256:') return await attest.attestProvenance({ subjectName, subjectDigest: { sha256: subjectDigest }, token: options.token, + sigstore: 'github', skipWrite: false // TODO: Attestation storage is only supported for public repositories or repositories which belong to a GitHub Enterprise Cloud account }) }