experimental: manually generate and upload all manifests

This commit is contained in:
Conor Sloan
2024-08-22 19:40:17 +01:00
parent bafa38ff94
commit 028b950050
9 changed files with 1432 additions and 2097 deletions
+42 -45
View File
@@ -1,70 +1,67 @@
import * as core from '@actions/core'
import { FileMetadata } from './fs-helper'
import * as ociContainer from './oci-container'
import * as fsHelper from './fs-helper'
// Publish the OCI artifact and return the URL where it can be downloaded
export async function publishOCIArtifact(
export async function uploadOCIImageManifest(
token: string,
registry: URL,
repository: string,
semver: string,
zipFile: FileMetadata,
tarFile: FileMetadata,
manifest: ociContainer.OCIImageManifest
): Promise<{ packageURL: URL; publishedDigest: string }> {
manifest: ociContainer.OCIImageManifest,
blobs: Map<string, Buffer>,
tag?: string
): Promise<string> {
const b64Token = Buffer.from(token).toString('base64')
const manifestSHA = ociContainer.sha256Digest(manifest)
core.info(
`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`
)
if (tag) {
core.info(
`Uploading manifest ${manifestSHA} with tag ${tag} to ${repository}.`
)
} else {
core.info(`Uploading manifest ${manifestSHA} to ${repository}.`)
}
const layerUploads: Promise<void>[] = manifest.layers.map(async layer => {
switch (layer.mediaType) {
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
return uploadLayer(
layer,
fsHelper.readFileContents(zipFile.path),
registry,
repository,
b64Token
)
case 'application/vnd.github.actions.package.layer.v1.zip':
return uploadLayer(
layer,
fsHelper.readFileContents(zipFile.path),
registry,
repository,
b64Token
)
case 'application/vnd.oci.empty.v1+json':
return uploadLayer(
layer,
Buffer.from('{}'),
registry,
repository,
b64Token
)
default:
throw new Error(`Unknown media type ${layer.mediaType}`)
const blob = blobs.get(layer.digest)
if (!blob) {
throw new Error(`Blob for layer ${layer.digest} not found`)
}
return uploadLayer(layer, blob, registry, repository, b64Token)
})
await Promise.all(layerUploads)
const digest = await uploadManifest(
return await uploadManifest(
JSON.stringify(manifest),
manifest.mediaType,
registry,
repository,
semver,
tag || manifestSHA,
b64Token
)
}
return {
packageURL: new URL(`${repository}:${semver}`, registry),
publishedDigest: digest
}
export async function uploadOCIIndexManifest(
token: string,
registry: URL,
repository: string,
manifest: ociContainer.OCIIndexManifest,
tag: string
): Promise<string> {
const b64Token = Buffer.from(token).toString('base64')
const manifestSHA = ociContainer.sha256Digest(manifest)
core.info(
`Uploading index manifest ${manifestSHA} with tag ${tag} to ${repository}.`
)
return await uploadManifest(
JSON.stringify(manifest),
manifest.mediaType,
registry,
repository,
tag,
b64Token
)
}
async function uploadLayer(
+136 -52
View File
@@ -5,7 +5,7 @@ import * as ociContainer from './oci-container'
import * as ghcr from './ghcr-client'
import * as attest from '@actions/attest'
import * as cfg from './config'
import { attachArtifactToImage, Descriptor } from '@sigstore/oci'
import * as crypto from 'crypto'
/**
* The main function for the action.
@@ -53,27 +53,8 @@ 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,
options.nameWithOwner,
const publishedDigest = await publishImmutableActionVersion(
options,
semverTag.raw,
archives.zipFile,
archives.tarFile,
@@ -86,8 +67,48 @@ export async function run(): Promise<void> {
)
}
core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest))
// Attestations are not supported in GHES.
if (!options.isEnterprise) {
const { bundle, bundleDigest } = await generateAttestation(
manifestDigest,
semverTag.raw,
options
)
const attestationCreated = new Date()
const attestationManifest =
ociContainer.createSigstoreAttestationManifest(
bundle.length,
bundleDigest,
ociContainer.sizeInBytes(manifest),
manifestDigest,
attestationCreated
)
const referrerIndexManifest = ociContainer.createReferrerTagManifest(
ociContainer.sha256Digest(attestationManifest),
ociContainer.sizeInBytes(attestationManifest),
attestationCreated
)
const { attestationSHA, referrerIndexSHA } = await publishAttestation(
options,
bundle,
bundleDigest,
manifest,
attestationManifest,
referrerIndexManifest
)
if (attestationSHA !== undefined) {
core.info(`Uploaded attestation ${attestationSHA}`)
core.setOutput('attestation-manifest-sha', attestationSHA)
}
if (referrerIndexSHA !== undefined) {
core.info(`Uploaded referrer index ${referrerIndexSHA}`)
core.setOutput('referrer-index-manifest-sha', referrerIndexSHA)
}
}
core.setOutput('package-manifest-sha', publishedDigest)
} catch (error) {
// Fail the workflow run if an error occurs
@@ -116,17 +137,94 @@ function parseSemverTagFromRef(opts: cfg.PublishActionOptions): semver.SemVer {
return semverTag
}
// Generate an attestation using the actions toolkit
// Subject name will contain the repo/package name and the tag name
async function uploadAttestation(
async function publishImmutableActionVersion(
options: cfg.PublishActionOptions,
semverTag: string,
zipFile: fsHelper.FileMetadata,
tarFile: fsHelper.FileMetadata,
manifest: ociContainer.OCIImageManifest
): Promise<string> {
const manifestDigest = ociContainer.sha256Digest(manifest)
core.info(
`Creating GHCR package ${manifestDigest} for release with semver: ${semver}.`
)
const files = new Map<string, Buffer>()
files.set(zipFile.sha256, fsHelper.readFileContents(zipFile.path))
files.set(tarFile.sha256, fsHelper.readFileContents(tarFile.path))
files.set(ociContainer.emptyConfigSha, Buffer.from('{}'))
return await ghcr.uploadOCIImageManifest(
options.token,
options.containerRegistryUrl,
options.nameWithOwner,
manifest,
files,
semverTag
)
}
async function publishAttestation(
options: cfg.PublishActionOptions,
bundle: Buffer,
bundleDigest: string,
subjectManifest: ociContainer.OCIImageManifest,
attestationManifest: ociContainer.OCIImageManifest,
referrerIndexManifest: ociContainer.OCIIndexManifest
): Promise<{
attestationSHA: string
referrerIndexSHA: string
}> {
const attestationManifestDigest =
ociContainer.sha256Digest(attestationManifest)
const subjectManifestDigest = ociContainer.sha256Digest(subjectManifest)
const referrerIndexManifestDigest = ociContainer.sha256Digest(
referrerIndexManifest
)
core.info(
`Publishing attestation ${attestationManifestDigest} for subject ${subjectManifestDigest}.`
)
const files = new Map<string, Buffer>()
files.set(ociContainer.emptyConfigSha, Buffer.from('{}'))
files.set(bundleDigest, bundle)
const attestationSHA = await ghcr.uploadOCIImageManifest(
options.token,
options.containerRegistryUrl,
options.nameWithOwner,
attestationManifest,
files
)
// The referrer index is tagged with the subject's digest in format sha256-<digest>
const referrerTag = subjectManifestDigest.replace(':', '-')
core.info(
`Publishing referrer index ${referrerIndexManifestDigest} with tag ${referrerTag} for attestation ${attestationManifestDigest} and subject ${subjectManifestDigest}.`
)
const referrerIndexSHA = await ghcr.uploadOCIIndexManifest(
options.token,
options.containerRegistryUrl,
options.nameWithOwner,
referrerIndexManifest,
referrerTag
)
return { attestationSHA, referrerIndexSHA }
}
async function generateAttestation(
manifestDigest: string,
semverTag: string,
options: cfg.PublishActionOptions
): Promise<Descriptor> {
const OCI_TIMEOUT = 30000
const OCI_RETRY = 3
const PREDICATE_TYPE = 'https://slsa.dev/provenance/v1'
): Promise<{
bundle: Buffer
bundleDigest: string
}> {
const subjectName = `${options.nameWithOwner}@${semverTag}`
const subjectDigest = removePrefix(manifestDigest, 'sha256:')
@@ -140,27 +238,13 @@ async function uploadAttestation(
skipWrite: true // We will upload attestations to GHCR
})
// Upload the attestation to the GitHub Container Registry
const credentials = { username: 'token', password: options.token }
const bundleArtifact = Buffer.from(JSON.stringify(attestation.bundle))
return await attachArtifactToImage({
credentials,
imageName: `${options.containerRegistryUrl.host}/${options.nameWithOwner}`,
imageDigest: manifestDigest,
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': PREDICATE_TYPE,
'com.github.package.type': 'actions_oci_pkg_attestation'
},
fetchOpts: {
timeout: OCI_TIMEOUT,
retry: OCI_RETRY,
proxy: undefined,
noProxy: undefined
}
})
const hash = crypto.createHash('sha256')
hash.update(bundleArtifact)
const bundleSHA = hash.digest('hex')
return { bundle: bundleArtifact, bundleDigest: `sha256:${bundleSHA}` }
}
function removePrefix(str: string, prefix: string): string {
+3 -3
View File
@@ -9,14 +9,14 @@ const actionsPackageTarLayerMediaType =
const actionsPackageZipLayerMediaType =
'application/vnd.github.actions.package.layer.v1.zip'
const sigstoreBundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json'
const ociEmptyMediaType = 'application/vnd.oci.empty.v1+json'
const actionPackageAnnotationValue = 'actions_oci_pkg'
const actionPackageAttestationAnnotationValue = 'actions_oci_pkg_attestation'
const actionPackageReferrerTagAnnotationValue = 'actions_oci_pkg_referrer_tag'
const emptyConfigSize = 2
const emptyConfigSha =
export const ociEmptyMediaType = 'application/vnd.oci.empty.v1+json'
export const emptyConfigSize = 2
export const emptyConfigSha =
'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
export interface OCIImageManifest {