refactor ghcr client for reusable upload functions
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.39%"><title>Coverage: 97.39%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.39%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.39%</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.38%"><title>Coverage: 97.38%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.38%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.38%</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+25
-24
@@ -107430,31 +107430,28 @@ const fsHelper = __importStar(__nccwpck_require__(76642));
|
|||||||
// Publish the OCI artifact and return the URL where it can be downloaded
|
// Publish the OCI artifact and return the URL where it can be downloaded
|
||||||
async function publishOCIArtifact(token, registry, repository, semver, zipFile, tarFile, manifest) {
|
async function publishOCIArtifact(token, registry, repository, semver, zipFile, tarFile, manifest) {
|
||||||
const b64Token = Buffer.from(token).toString('base64');
|
const b64Token = Buffer.from(token).toString('base64');
|
||||||
const checkBlobEndpoint = new URL(`v2/${repository}/blobs/`, registry).toString();
|
|
||||||
const uploadBlobEndpoint = new URL(`v2/${repository}/blobs/uploads/`, registry).toString();
|
|
||||||
const manifestEndpoint = new URL(`v2/${repository}/manifests/${semver}`, registry).toString();
|
|
||||||
core.info(`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`);
|
core.info(`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`);
|
||||||
const layerUploads = manifest.layers.map(async (layer) => {
|
const layerUploads = manifest.layers.map(async (layer) => {
|
||||||
switch (layer.mediaType) {
|
switch (layer.mediaType) {
|
||||||
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
|
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
|
||||||
return uploadLayer(layer, tarFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
return uploadLayer(layer, fsHelper.readFileContents(zipFile.path), registry, repository, b64Token);
|
||||||
case 'application/vnd.github.actions.package.layer.v1.zip':
|
case 'application/vnd.github.actions.package.layer.v1.zip':
|
||||||
return uploadLayer(layer, zipFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
return uploadLayer(layer, fsHelper.readFileContents(zipFile.path), registry, repository, b64Token);
|
||||||
case 'application/vnd.oci.empty.v1+json':
|
case 'application/vnd.oci.empty.v1+json':
|
||||||
return uploadLayer(layer, { path: '', size: 2, sha256: layer.digest }, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
return uploadLayer(layer, Buffer.from('{}'), registry, repository, b64Token);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown media type ${layer.mediaType}`);
|
throw new Error(`Unknown media type ${layer.mediaType}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all(layerUploads);
|
await Promise.all(layerUploads);
|
||||||
const digest = await uploadManifest(JSON.stringify(manifest), manifestEndpoint, b64Token);
|
const digest = await uploadManifest(JSON.stringify(manifest), manifest.mediaType, registry, repository, semver, b64Token);
|
||||||
return {
|
return {
|
||||||
packageURL: new URL(`${repository}:${semver}`, registry),
|
packageURL: new URL(`${repository}:${semver}`, registry),
|
||||||
publishedDigest: digest
|
publishedDigest: digest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBlobEndpoint, b64Token) {
|
async function uploadLayer(layer, data, registryURL, repository, b64Token) {
|
||||||
const checkExistsResponse = await fetchWithDebug(checkBlobEndpoint + layer.digest, {
|
const checkExistsResponse = await fetchWithDebug(checkBlobEndpoint(registryURL, repository, layer.digest), {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${b64Token}`
|
Authorization: `Bearer ${b64Token}`
|
||||||
@@ -107469,7 +107466,8 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
|||||||
throw new Error(await errorMessageForFailedRequest(`check blob (${layer.digest}) exists`, checkExistsResponse));
|
throw new Error(await errorMessageForFailedRequest(`check blob (${layer.digest}) exists`, checkExistsResponse));
|
||||||
}
|
}
|
||||||
core.info(`Uploading layer ${layer.digest}.`);
|
core.info(`Uploading layer ${layer.digest}.`);
|
||||||
const initiateUploadResponse = await fetchWithDebug(uploadBlobEndpoint, {
|
const initiateUploadBlobURL = uploadBlobEndpoint(registryURL, repository);
|
||||||
|
const initiateUploadResponse = await fetchWithDebug(initiateUploadBlobURL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${b64Token}`
|
Authorization: `Bearer ${b64Token}`
|
||||||
@@ -107481,18 +107479,10 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
|||||||
}
|
}
|
||||||
const locationResponseHeader = initiateUploadResponse.headers.get('location');
|
const locationResponseHeader = initiateUploadResponse.headers.get('location');
|
||||||
if (locationResponseHeader === undefined) {
|
if (locationResponseHeader === undefined) {
|
||||||
throw new Error(`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`);
|
throw new Error(`No location header in response from upload post ${initiateUploadBlobURL} for layer ${layer.digest}`);
|
||||||
}
|
}
|
||||||
const pathname = `${locationResponseHeader}?digest=${layer.digest}`;
|
const pathname = `${locationResponseHeader}?digest=${layer.digest}`;
|
||||||
const uploadBlobUrl = new URL(pathname, registryURL).toString();
|
const uploadBlobUrl = new URL(pathname, registryURL).toString();
|
||||||
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
|
|
||||||
let data;
|
|
||||||
if (layer.mediaType === 'application/vnd.oci.empty.v1+json') {
|
|
||||||
data = Buffer.from('{}');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
data = fsHelper.readFileContents(file.path);
|
|
||||||
}
|
|
||||||
const putResponse = await fetchWithDebug(uploadBlobUrl, {
|
const putResponse = await fetchWithDebug(uploadBlobUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -107508,13 +107498,14 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Uploads the manifest and returns the digest returned by GHCR
|
// Uploads the manifest and returns the digest returned by GHCR
|
||||||
async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
|
async function uploadManifest(manifestJSON, manifestMediaType, registry, repository, version, b64Token) {
|
||||||
core.info(`Uploading manifest to ${manifestEndpoint}.`);
|
const manifestUrl = manifestEndpoint(registry, repository, version);
|
||||||
const putResponse = await fetchWithDebug(manifestEndpoint, {
|
core.info(`Uploading manifest to ${manifestUrl}.`);
|
||||||
|
const putResponse = await fetchWithDebug(manifestUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${b64Token}`,
|
Authorization: `Bearer ${b64Token}`,
|
||||||
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
|
'Content-Type': manifestMediaType
|
||||||
},
|
},
|
||||||
body: manifestJSON
|
body: manifestJSON
|
||||||
});
|
});
|
||||||
@@ -107523,7 +107514,7 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
|
|||||||
}
|
}
|
||||||
const digestResponseHeader = putResponse.headers.get('docker-content-digest');
|
const digestResponseHeader = putResponse.headers.get('docker-content-digest');
|
||||||
if (digestResponseHeader === undefined || digestResponseHeader === null) {
|
if (digestResponseHeader === undefined || digestResponseHeader === null) {
|
||||||
throw new Error(`No digest header in response from PUT manifest ${manifestEndpoint}`);
|
throw new Error(`No digest header in response from PUT manifest ${manifestUrl}`);
|
||||||
}
|
}
|
||||||
return digestResponseHeader;
|
return digestResponseHeader;
|
||||||
}
|
}
|
||||||
@@ -107561,6 +107552,16 @@ function isGHCRError(obj) {
|
|||||||
'message' in obj &&
|
'message' in obj &&
|
||||||
typeof obj.message === 'string');
|
typeof obj.message === 'string');
|
||||||
}
|
}
|
||||||
|
function checkBlobEndpoint(registry, repository, digest) {
|
||||||
|
return new URL(`v2/${repository}/blobs/${digest}`, registry).toString();
|
||||||
|
}
|
||||||
|
function uploadBlobEndpoint(registry, repository) {
|
||||||
|
return new URL(`v2/${repository}/blobs/uploads/`, registry).toString();
|
||||||
|
}
|
||||||
|
function manifestEndpoint(registry, repository, version) {
|
||||||
|
return new URL(`v2/${repository}/manifests/${version}`, registry).toString();
|
||||||
|
}
|
||||||
|
// TODO: Add retries with backoff
|
||||||
const fetchWithDebug = async (url, config = {}) => {
|
const fetchWithDebug = async (url, config = {}) => {
|
||||||
core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`);
|
core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`);
|
||||||
try {
|
try {
|
||||||
|
|||||||
+48
-42
@@ -15,19 +15,6 @@ export async function publishOCIArtifact(
|
|||||||
): Promise<{ packageURL: URL; publishedDigest: string }> {
|
): Promise<{ packageURL: URL; publishedDigest: string }> {
|
||||||
const b64Token = Buffer.from(token).toString('base64')
|
const b64Token = Buffer.from(token).toString('base64')
|
||||||
|
|
||||||
const checkBlobEndpoint = new URL(
|
|
||||||
`v2/${repository}/blobs/`,
|
|
||||||
registry
|
|
||||||
).toString()
|
|
||||||
const uploadBlobEndpoint = new URL(
|
|
||||||
`v2/${repository}/blobs/uploads/`,
|
|
||||||
registry
|
|
||||||
).toString()
|
|
||||||
const manifestEndpoint = new URL(
|
|
||||||
`v2/${repository}/manifests/${semver}`,
|
|
||||||
registry
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
core.info(
|
core.info(
|
||||||
`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`
|
`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`
|
||||||
)
|
)
|
||||||
@@ -37,28 +24,25 @@ export async function publishOCIArtifact(
|
|||||||
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
|
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
|
||||||
return uploadLayer(
|
return uploadLayer(
|
||||||
layer,
|
layer,
|
||||||
tarFile,
|
fsHelper.readFileContents(zipFile.path),
|
||||||
registry,
|
registry,
|
||||||
checkBlobEndpoint,
|
repository,
|
||||||
uploadBlobEndpoint,
|
|
||||||
b64Token
|
b64Token
|
||||||
)
|
)
|
||||||
case 'application/vnd.github.actions.package.layer.v1.zip':
|
case 'application/vnd.github.actions.package.layer.v1.zip':
|
||||||
return uploadLayer(
|
return uploadLayer(
|
||||||
layer,
|
layer,
|
||||||
zipFile,
|
fsHelper.readFileContents(zipFile.path),
|
||||||
registry,
|
registry,
|
||||||
checkBlobEndpoint,
|
repository,
|
||||||
uploadBlobEndpoint,
|
|
||||||
b64Token
|
b64Token
|
||||||
)
|
)
|
||||||
case 'application/vnd.oci.empty.v1+json':
|
case 'application/vnd.oci.empty.v1+json':
|
||||||
return uploadLayer(
|
return uploadLayer(
|
||||||
layer,
|
layer,
|
||||||
{ path: '', size: 2, sha256: layer.digest },
|
Buffer.from('{}'),
|
||||||
registry,
|
registry,
|
||||||
checkBlobEndpoint,
|
repository,
|
||||||
uploadBlobEndpoint,
|
|
||||||
b64Token
|
b64Token
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
@@ -70,7 +54,10 @@ export async function publishOCIArtifact(
|
|||||||
|
|
||||||
const digest = await uploadManifest(
|
const digest = await uploadManifest(
|
||||||
JSON.stringify(manifest),
|
JSON.stringify(manifest),
|
||||||
manifestEndpoint,
|
manifest.mediaType,
|
||||||
|
registry,
|
||||||
|
repository,
|
||||||
|
semver,
|
||||||
b64Token
|
b64Token
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,14 +69,13 @@ export async function publishOCIArtifact(
|
|||||||
|
|
||||||
async function uploadLayer(
|
async function uploadLayer(
|
||||||
layer: ociContainer.Descriptor,
|
layer: ociContainer.Descriptor,
|
||||||
file: FileMetadata,
|
data: Buffer,
|
||||||
registryURL: URL,
|
registryURL: URL,
|
||||||
checkBlobEndpoint: string,
|
repository: string,
|
||||||
uploadBlobEndpoint: string,
|
|
||||||
b64Token: string
|
b64Token: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const checkExistsResponse = await fetchWithDebug(
|
const checkExistsResponse = await fetchWithDebug(
|
||||||
checkBlobEndpoint + layer.digest,
|
checkBlobEndpoint(registryURL, repository, layer.digest),
|
||||||
{
|
{
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -117,7 +103,9 @@ async function uploadLayer(
|
|||||||
|
|
||||||
core.info(`Uploading layer ${layer.digest}.`)
|
core.info(`Uploading layer ${layer.digest}.`)
|
||||||
|
|
||||||
const initiateUploadResponse = await fetchWithDebug(uploadBlobEndpoint, {
|
const initiateUploadBlobURL = uploadBlobEndpoint(registryURL, repository)
|
||||||
|
|
||||||
|
const initiateUploadResponse = await fetchWithDebug(initiateUploadBlobURL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${b64Token}`
|
Authorization: `Bearer ${b64Token}`
|
||||||
@@ -137,21 +125,13 @@ async function uploadLayer(
|
|||||||
const locationResponseHeader = initiateUploadResponse.headers.get('location')
|
const locationResponseHeader = initiateUploadResponse.headers.get('location')
|
||||||
if (locationResponseHeader === undefined) {
|
if (locationResponseHeader === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`
|
`No location header in response from upload post ${initiateUploadBlobURL} for layer ${layer.digest}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathname = `${locationResponseHeader}?digest=${layer.digest}`
|
const pathname = `${locationResponseHeader}?digest=${layer.digest}`
|
||||||
const uploadBlobUrl = new URL(pathname, registryURL).toString()
|
const uploadBlobUrl = new URL(pathname, registryURL).toString()
|
||||||
|
|
||||||
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
|
|
||||||
let data: Buffer
|
|
||||||
if (layer.mediaType === 'application/vnd.oci.empty.v1+json') {
|
|
||||||
data = Buffer.from('{}')
|
|
||||||
} else {
|
|
||||||
data = fsHelper.readFileContents(file.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const putResponse = await fetchWithDebug(uploadBlobUrl, {
|
const putResponse = await fetchWithDebug(uploadBlobUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -176,16 +156,21 @@ async function uploadLayer(
|
|||||||
// Uploads the manifest and returns the digest returned by GHCR
|
// Uploads the manifest and returns the digest returned by GHCR
|
||||||
async function uploadManifest(
|
async function uploadManifest(
|
||||||
manifestJSON: string,
|
manifestJSON: string,
|
||||||
manifestEndpoint: string,
|
manifestMediaType: string,
|
||||||
|
registry: URL,
|
||||||
|
repository: string,
|
||||||
|
version: string,
|
||||||
b64Token: string
|
b64Token: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
core.info(`Uploading manifest to ${manifestEndpoint}.`)
|
const manifestUrl = manifestEndpoint(registry, repository, version)
|
||||||
|
|
||||||
const putResponse = await fetchWithDebug(manifestEndpoint, {
|
core.info(`Uploading manifest to ${manifestUrl}.`)
|
||||||
|
|
||||||
|
const putResponse = await fetchWithDebug(manifestUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${b64Token}`,
|
Authorization: `Bearer ${b64Token}`,
|
||||||
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
|
'Content-Type': manifestMediaType
|
||||||
},
|
},
|
||||||
body: manifestJSON
|
body: manifestJSON
|
||||||
})
|
})
|
||||||
@@ -199,7 +184,7 @@ async function uploadManifest(
|
|||||||
const digestResponseHeader = putResponse.headers.get('docker-content-digest')
|
const digestResponseHeader = putResponse.headers.get('docker-content-digest')
|
||||||
if (digestResponseHeader === undefined || digestResponseHeader === null) {
|
if (digestResponseHeader === undefined || digestResponseHeader === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No digest header in response from PUT manifest ${manifestEndpoint}`
|
`No digest header in response from PUT manifest ${manifestUrl}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +242,27 @@ function isGHCRError(obj: unknown): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkBlobEndpoint(
|
||||||
|
registry: URL,
|
||||||
|
repository: string,
|
||||||
|
digest: string
|
||||||
|
): string {
|
||||||
|
return new URL(`v2/${repository}/blobs/${digest}`, registry).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadBlobEndpoint(registry: URL, repository: string): string {
|
||||||
|
return new URL(`v2/${repository}/blobs/uploads/`, registry).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function manifestEndpoint(
|
||||||
|
registry: URL,
|
||||||
|
repository: string,
|
||||||
|
version: string
|
||||||
|
): string {
|
||||||
|
return new URL(`v2/${repository}/manifests/${version}`, registry).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add retries with backoff
|
||||||
const fetchWithDebug = async (
|
const fetchWithDebug = async (
|
||||||
url: string,
|
url: string,
|
||||||
config: RequestInit = {}
|
config: RequestInit = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user