upload attestation and referrer index before artifact

This avoids race conditions when the artifact is read but its attestation doesn't exist
This commit is contained in:
Conor Sloan
2024-08-22 16:10:09 +01:00
parent 1f725c56d6
commit c11354f432
3 changed files with 154 additions and 143 deletions
+131 -120
View File
@@ -207,6 +207,112 @@ describe('run', () => {
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async () => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if uploading attestation to GHCR fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: 'application/vnd.cncf.notary.v2+jwt',
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
}
}
}
})
attachArtifactToImageMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if publishing OCI artifact fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
@@ -254,6 +360,15 @@ describe('run', () => {
}
})
attachArtifactToImageMock.mockImplementation(() => {
return {
digest: 'sha256:my-test-attestation-digest',
urls: [
'ghcr.io/v2/test-org/test-package/manifests/sha256:my-test-attestation-digest'
]
}
})
publishOCIArtifactMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
@@ -312,6 +427,15 @@ describe('run', () => {
}
})
attachArtifactToImageMock.mockImplementation(() => {
return {
digest: 'sha256:some-other-digest',
urls: [
'ghcr.io/v2/test-org/test-package/manifests/sha256:some-other-digest'
]
}
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
@@ -328,119 +452,6 @@ describe('run', () => {
)
})
it('fails if uploading attestation to GHCR fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: 'application/vnd.cncf.notary.v2+jwt',
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
}
}
}
})
attachArtifactToImageMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async () => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('uploads the artifact, returns package metadata from GHCR, and skips writing attestation in enterprise', async () => {
const options = baseOptions()
options.isEnterprise = true
@@ -535,13 +546,6 @@ describe('run', () => {
return 'sha256:my-test-digest'
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
@@ -568,6 +572,13 @@ describe('run', () => {
}
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
// Run the action
await main.run()
Generated Vendored
+6 -6
View File
@@ -107636,13 +107636,9 @@ async function run() {
const archives = await fsHelper.createArchives(stagedActionFilesDir, archiveDir);
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, options.nameWithOwner, options.repositoryId, options.repositoryOwnerId, options.sha, semverTag.raw, new Date());
const manifestDigest = ociContainer.sha256Digest(manifest);
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(options.token, options.containerRegistryUrl, options.nameWithOwner, semverTag.raw, archives.zipFile, archives.tarFile, manifest);
if (manifestDigest !== publishedDigest) {
throw new Error(`Unexpected digest returned for manifest. Expected ${manifestDigest}, got ${publishedDigest}`);
}
// Attestations are not currently supported in GHES.
// Attestations are not supported in GHES.
if (!options.isEnterprise) {
const attestation = await uploadAttestation(publishedDigest, semverTag.raw, options);
const attestation = await uploadAttestation(manifestDigest, semverTag.raw, options);
if (attestation.digest !== undefined) {
core.info(`Uploaded attestation ${attestation.digest}`);
core.setOutput('attestation-manifest-sha', attestation.digest);
@@ -107652,6 +107648,10 @@ async function run() {
core.setOutput('attestation-url', attestation.urls[0]);
}
}
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(options.token, options.containerRegistryUrl, options.nameWithOwner, semverTag.raw, archives.zipFile, archives.tarFile, manifest);
if (manifestDigest !== publishedDigest) {
throw new Error(`Unexpected digest returned for manifest. Expected ${manifestDigest}, got ${publishedDigest}`);
}
core.setOutput('package-url', packageURL.toString());
core.setOutput('package-manifest', JSON.stringify(manifest));
core.setOutput('package-manifest-sha', publishedDigest);
+17 -17
View File
@@ -53,6 +53,23 @@ export async function run(): Promise<void> {
const manifestDigest = ociContainer.sha256Digest(manifest)
// Attestations are not supported in GHES.
if (!options.isEnterprise) {
const attestation = await uploadAttestation(
manifestDigest,
semverTag.raw,
options
)
if (attestation.digest !== undefined) {
core.info(`Uploaded attestation ${attestation.digest}`)
core.setOutput('attestation-manifest-sha', attestation.digest)
}
if (attestation.urls !== undefined && attestation.urls.length > 0) {
core.info(`Attestation URL: ${attestation.digest}`)
core.setOutput('attestation-url', attestation.urls[0])
}
}
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(
options.token,
options.containerRegistryUrl,
@@ -69,23 +86,6 @@ export async function run(): Promise<void> {
)
}
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
const attestation = await uploadAttestation(
publishedDigest,
semverTag.raw,
options
)
if (attestation.digest !== undefined) {
core.info(`Uploaded attestation ${attestation.digest}`)
core.setOutput('attestation-manifest-sha', attestation.digest)
}
if (attestation.urls !== undefined && attestation.urls.length > 0) {
core.info(`Attestation URL: ${attestation.digest}`)
core.setOutput('attestation-url', attestation.urls[0])
}
}
core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', publishedDigest)