diff --git a/__tests__/ghcr-client.test.ts b/__tests__/ghcr-client.test.ts
index 07c7a57..571b8e7 100644
--- a/__tests__/ghcr-client.test.ts
+++ b/__tests__/ghcr-client.test.ts
@@ -25,14 +25,19 @@ const tarFile: fsHelper.FileMetadata = {
const headMockNoExistingBlobs = (): object => {
// Simulate none of the blobs existing currently
return {
- status: 404
+ text() {
+ return '{"errors": [{"code": "NOT_FOUND", "message": "blob not found."}]}'
+ },
+ status: 404,
+ statusText: 'Not Found'
}
}
const headMockAllExistingBlobs = (): object => {
// Simulate all of the blobs existing currently
return {
- status: 200
+ status: 200,
+ statusText: 'OK'
}
}
@@ -42,19 +47,29 @@ const headMockSomeExistingBlobs = (): object => {
// report one as existing
if (count === 1) {
return {
- status: 200
+ status: 200,
+ statusText: 'OK'
}
} else {
// report all others are missing
return {
- status: 404
+ text() {
+ return '{"errors": [{"code": "NOT_FOUND", "message": "blob not found."}]}'
+ },
+ status: 404,
+ statusText: 'Not Found'
}
}
}
const headMockFailure = (): object => {
return {
- status: 503
+ text() {
+ // In this case we'll simulate a response which does not use the expected error format
+ return '503 Service Unavailable'
+ },
+ status: 503,
+ statusText: 'Service Unavailable'
}
}
@@ -75,7 +90,12 @@ const postMockSuccessfulIniationForAllBlobs = (): object => {
const postMockFailure = (): object => {
// Simulate failed initiation of uploads
return {
- status: 503
+ text() {
+ // In this case we'll simulate a response which does not use the expected error format
+ return '503 Service Unavailable'
+ },
+ status: 503,
+ statusText: 'Service Unavailable'
}
}
@@ -110,7 +130,11 @@ const putMockSuccessfulBlobUpload = (url: string): object => {
const putMockFailure = (): object => {
// Simulate fails upload of all blobs & manifest
return {
- status: 500
+ text() {
+ return '{"errors": [{"code": "BAD_REQUEST", "message": "tag already exists."}]}'
+ },
+ status: 400,
+ statusText: 'Bad Request'
}
}
@@ -118,7 +142,11 @@ const putMockFailureManifestUpload = (url: string): object => {
// Simulate unsuccessful upload of all blobs & then the manifest
if (url.includes('manifest')) {
return {
- status: 500
+ text() {
+ return '{"errors": [{"code": "BAD_REQUEST", "message": "tag already exists."}]}'
+ },
+ status: 400,
+ statusText: 'Bad Request'
}
}
return {
@@ -325,7 +353,9 @@ describe('publishOCIArtifact', () => {
tarFile,
testManifest
)
- ).rejects.toThrow(/^Unexpected response from blob check for layer/)
+ ).rejects.toThrow(
+ /^Unexpected 503 Service Unavailable response from check blob/
+ )
})
it('throws an error if initiating layer upload fails', async () => {
@@ -344,7 +374,9 @@ describe('publishOCIArtifact', () => {
tarFile,
testManifest
)
- ).rejects.toThrow('Unexpected response from POST upload 503')
+ ).rejects.toThrow(
+ 'Unexpected 503 Service Unavailable response from initiate layer upload. Response Body: 503 Service Unavailable.'
+ )
})
it('throws an error if the upload endpoint does not return a location', async () => {
@@ -388,7 +420,7 @@ describe('publishOCIArtifact', () => {
tarFile,
testManifest
)
- ).rejects.toThrow(/^Unexpected response from PUT upload 500/)
+ ).rejects.toThrow(/^Unexpected 400 Bad Request response from layer/)
})
it('throws an error if a manifest upload fails', async () => {
@@ -413,7 +445,9 @@ describe('publishOCIArtifact', () => {
tarFile,
testManifest
)
- ).rejects.toThrow(/^Unexpected response from PUT manifest 500/)
+ ).rejects.toThrow(
+ 'Unexpected 400 Bad Request response from manifest upload. Errors: BAD_REQUEST - tag already exists.'
+ )
})
it('throws an error if reading one of the files fails', async () => {
diff --git a/badges/coverage.svg b/badges/coverage.svg
index e9318f9..b75435b 100644
--- a/badges/coverage.svg
+++ b/badges/coverage.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
index 3cc7b96..03f6d02 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -104749,7 +104749,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
return;
}
if (checkExistsResponse.status !== 404) {
- throw new Error(`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status} ${checkExistsResponse.statusText}`);
+ throw new Error(await errorMessageForFailedRequest(`check blob (${layer.digest}) exists`, checkExistsResponse));
}
core.info(`Uploading layer ${layer.digest}.`);
const initiateUploadResponse = await fetchWithDebug(uploadBlobEndpoint, {
@@ -104760,8 +104760,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
body: JSON.stringify(layer)
});
if (initiateUploadResponse.status !== 202) {
- core.error(`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`);
- throw new Error(`Unexpected response from POST upload ${initiateUploadResponse.status}`);
+ throw new Error(await errorMessageForFailedRequest(`initiate layer upload`, initiateUploadResponse));
}
const locationResponseHeader = initiateUploadResponse.headers.get('location');
if (locationResponseHeader === undefined) {
@@ -104788,7 +104787,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
body: data
});
if (putResponse.status !== 201) {
- throw new Error(`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`);
+ throw new Error(await errorMessageForFailedRequest(`layer (${layer.digest}) upload`, putResponse));
}
}
// Uploads the manifest and returns the digest returned by GHCR
@@ -104803,7 +104802,7 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
body: manifestJSON
});
if (putResponse.status !== 201) {
- throw new Error(`Unexpected response from PUT manifest ${putResponse.status}`);
+ throw new Error(await errorMessageForFailedRequest(`manifest upload`, putResponse));
}
const digestResponseHeader = putResponse.headers.get('docker-content-digest');
if (digestResponseHeader === undefined || digestResponseHeader === null) {
@@ -104811,6 +104810,40 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
}
return digestResponseHeader;
}
+// Generate an error message for a failed HTTP request
+async function errorMessageForFailedRequest(requestDescription, response) {
+ const bodyText = await response.text();
+ // Try to parse the body as JSON and extract the expected fields returned from GHCR
+ // Expected format: { "errors": [{"code": "BAD_REQUEST", "message": "Something went wrong."}] }
+ // If the body does not match the expected format, just return the whole response body
+ let errorString = `Response Body: ${bodyText}.`;
+ try {
+ const body = JSON.parse(bodyText);
+ const errors = body.errors;
+ if (Array.isArray(errors) &&
+ errors.length > 0 &&
+ errors.every(isGHCRError)) {
+ const errorMessages = errors.map((error) => {
+ return `${error.code} - ${error.message}`;
+ });
+ errorString = `Errors: ${errorMessages.join(', ')}`;
+ }
+ }
+ catch (error) {
+ // Ignore error
+ }
+ return `Unexpected ${response.status} ${response.statusText} response from ${requestDescription}. ${errorString}`;
+}
+// Runtime checks that parsed JSON object is in the expected format
+// {"code": "BAD_REQUEST", "message": "Something went wrong."}
+function isGHCRError(obj) {
+ return (typeof obj === 'object' &&
+ obj !== null &&
+ 'code' in obj &&
+ typeof obj.code === 'string' &&
+ 'message' in obj &&
+ typeof obj.message === 'string');
+}
const fetchWithDebug = async (url, config = {}) => {
core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`);
try {
diff --git a/src/ghcr-client.ts b/src/ghcr-client.ts
index 6aa4c0e..7aba4ef 100644
--- a/src/ghcr-client.ts
+++ b/src/ghcr-client.ts
@@ -108,7 +108,10 @@ async function uploadLayer(
if (checkExistsResponse.status !== 404) {
throw new Error(
- `Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status} ${checkExistsResponse.statusText}`
+ await errorMessageForFailedRequest(
+ `check blob (${layer.digest}) exists`,
+ checkExistsResponse
+ )
)
}
@@ -123,11 +126,11 @@ async function uploadLayer(
})
if (initiateUploadResponse.status !== 202) {
- core.error(
- `Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`
- )
throw new Error(
- `Unexpected response from POST upload ${initiateUploadResponse.status}`
+ await errorMessageForFailedRequest(
+ `initiate layer upload`,
+ initiateUploadResponse
+ )
)
}
@@ -162,7 +165,10 @@ async function uploadLayer(
if (putResponse.status !== 201) {
throw new Error(
- `Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`
+ await errorMessageForFailedRequest(
+ `layer (${layer.digest}) upload`,
+ putResponse
+ )
)
}
}
@@ -186,7 +192,7 @@ async function uploadManifest(
if (putResponse.status !== 201) {
throw new Error(
- `Unexpected response from PUT manifest ${putResponse.status}`
+ await errorMessageForFailedRequest(`manifest upload`, putResponse)
)
}
@@ -200,6 +206,57 @@ async function uploadManifest(
return digestResponseHeader
}
+interface ghcrError {
+ code: string
+ message: string
+}
+
+// Generate an error message for a failed HTTP request
+async function errorMessageForFailedRequest(
+ requestDescription: string,
+ response: Response
+): Promise {
+ const bodyText = await response.text()
+
+ // Try to parse the body as JSON and extract the expected fields returned from GHCR
+ // Expected format: { "errors": [{"code": "BAD_REQUEST", "message": "Something went wrong."}] }
+ // If the body does not match the expected format, just return the whole response body
+ let errorString = `Response Body: ${bodyText}.`
+
+ try {
+ const body = JSON.parse(bodyText)
+ const errors = body.errors
+
+ if (
+ Array.isArray(errors) &&
+ errors.length > 0 &&
+ errors.every(isGHCRError)
+ ) {
+ const errorMessages = errors.map((error: ghcrError) => {
+ return `${error.code} - ${error.message}`
+ })
+ errorString = `Errors: ${errorMessages.join(', ')}`
+ }
+ } catch (error) {
+ // Ignore error
+ }
+
+ return `Unexpected ${response.status} ${response.statusText} response from ${requestDescription}. ${errorString}`
+}
+
+// Runtime checks that parsed JSON object is in the expected format
+// {"code": "BAD_REQUEST", "message": "Something went wrong."}
+function isGHCRError(obj: unknown): boolean {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'code' in obj &&
+ typeof (obj as { code: unknown }).code === 'string' &&
+ 'message' in obj &&
+ typeof (obj as { message: unknown }).message === 'string'
+ )
+}
+
const fetchWithDebug = async (
url: string,
config: RequestInit = {}