Merge pull request #279 from actions/artifacts-next-ga

Use artifacts v4
This commit is contained in:
Jess Bees
2023-12-18 20:42:41 -05:00
committed by GitHub
11 changed files with 1025 additions and 5952 deletions

View File

@@ -6,11 +6,11 @@ This action is used to deploy [Actions artifacts][artifacts] to [GitHub Pages](h
## Usage ## Usage
See [action.yml](action.yml) for the various `inputs` this action supports. See [action.yml](action.yml) for the various `inputs` this action supports (or [below](#inputs-📥)).
For examples that make use of this action, check out our [starter-workflows][starter-workflows] in a variety of frameworks. For examples that make use of this action, check out our [starter-workflows][starter-workflows] in a variety of frameworks.
This action expects an artifact named `github-pages` to have been created prior to execution. This is done automatically when using [`actions/upload-pages-artifact`][upload-pages-artifact]. This action deploys a Pages site previously uploaded as an artifact (e.g. using [`actions/upload-pages-artifact`][upload-pages-artifact]).
We recommend this action to be used in a dedicated job: We recommend this action to be used in a dedicated job:
@@ -71,7 +71,7 @@ jobs:
There are a few important considerations to be aware of: There are a few important considerations to be aware of:
1. The artifact being deployed must have been uploaded in a previous step, either in the same job or a separate job that doesn't execute until the upload is complete. 1. The artifact being deployed must have been uploaded in a previous step, either in the same job or a separate job that doesn't execute until the upload is complete. See [`actions/upload-pages-artifact`][upload-pages-artifact] for more information about the format of the artifact we expect.
2. The job that executes the deployment must at minimum have the following permissions: 2. The job that executes the deployment must at minimum have the following permissions:
- `pages: write` - `pages: write`

351
dist/index.js generated vendored
View File

@@ -24859,7 +24859,7 @@ __export(dist_src_exports, {
module.exports = __toCommonJS(dist_src_exports); module.exports = __toCommonJS(dist_src_exports);
// pkg/dist-src/version.js // pkg/dist-src/version.js
var VERSION = "9.1.4"; var VERSION = "9.1.5";
// pkg/dist-src/normalize-paginated-list-response.js // pkg/dist-src/normalize-paginated-list-response.js
function normalizePaginatedListResponse(response) { function normalizePaginatedListResponse(response) {
@@ -27863,102 +27863,6 @@ class Deprecation extends Error {
exports.Deprecation = Deprecation; exports.Deprecation = Deprecation;
/***/ }),
/***/ 3703:
/***/ ((module) => {
// Source: 2014-06-11: http://en.wikipedia.org/wiki/HTTP_status_codes
module.exports = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
306: "Switch Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
419: "Authentication Timeout",
420: "Method Failure",
420: "Enhance Your Calm",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
440: "Login Timeout",
444: "No Response",
449: "Retry With",
450: "Blocked by Windows Parental Controls",
451: "Unavailable For Legal Reasons",
451: "Redirect",
494: "Request Header Too Large",
495: "Cert Error",
496: "No Cert",
497: "HTTP to HTTPS",
499: "Client Closed Request",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
509: "Bandwidth Limit Exceeded",
510: "Not Extended",
511: "Network Authentication Required",
520: "Origin Error",
521: "Web server is down",
522: "Connection timed out",
523: "Proxy Declined Request",
524: "A timeout occurred",
598: "Network read timeout error",
599: "Network connect timeout error"
};
/***/ }), /***/ }),
/***/ 1223: /***/ 1223:
@@ -29659,6 +29563,18 @@ module.exports = class BodyReadable extends Readable {
return super.destroy(err) return super.destroy(err)
} }
_destroy (err, callback) {
// Workaround for Node "bug". If the stream is destroyed in same
// tick as it is created, then a user who is waiting for a
// promise (i.e micro tick) for installing a 'error' listener will
// never get a chance and will always encounter an unhandled exception.
// - tick => process.nextTick(fn)
// - micro tick => queueMicrotask(fn)
queueMicrotask(() => {
callback(err)
})
}
emit (ev, ...args) { emit (ev, ...args) {
if (ev === 'data') { if (ev === 'data') {
// Node < 16.7 // Node < 16.7
@@ -29763,7 +29679,7 @@ module.exports = class BodyReadable extends Readable {
} }
} }
if (this.closed) { if (this._readableState.closeEmitted) {
return Promise.resolve(null) return Promise.resolve(null)
} }
@@ -29807,33 +29723,44 @@ function isUnusable (self) {
} }
async function consume (stream, type) { async function consume (stream, type) {
if (isUnusable(stream)) {
throw new TypeError('unusable')
}
assert(!stream[kConsume]) assert(!stream[kConsume])
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stream[kConsume] = { if (isUnusable(stream)) {
type, const rState = stream._readableState
stream, if (rState.destroyed && rState.closeEmitted === false) {
resolve, stream
reject, .on('error', err => {
length: 0, reject(err)
body: [] })
.on('close', () => {
reject(new TypeError('unusable'))
})
} else {
reject(rState.errored ?? new TypeError('unusable'))
}
} else {
stream[kConsume] = {
type,
stream,
resolve,
reject,
length: 0,
body: []
}
stream
.on('error', function (err) {
consumeFinish(this[kConsume], err)
})
.on('close', function () {
if (this[kConsume].body !== null) {
consumeFinish(this[kConsume], new RequestAbortedError())
}
})
queueMicrotask(() => consumeStart(stream[kConsume]))
} }
stream
.on('error', function (err) {
consumeFinish(this[kConsume], err)
})
.on('close', function () {
if (this[kConsume].body !== null) {
consumeFinish(this[kConsume], new RequestAbortedError())
}
})
process.nextTick(consumeStart, stream[kConsume])
}) })
} }
@@ -33209,12 +33136,19 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
body.resume() body.resume()
} }
} }
const onAbort = function () { const onClose = function () {
if (finished) { // 'close' might be emitted *before* 'error' for
return // broken streams. Wait a tick to avoid this case.
queueMicrotask(() => {
// It's only safe to remove 'error' listener after
// 'close'.
body.removeListener('error', onFinished)
})
if (!finished) {
const err = new RequestAbortedError()
queueMicrotask(() => onFinished(err))
} }
const err = new RequestAbortedError()
queueMicrotask(() => onFinished(err))
} }
const onFinished = function (err) { const onFinished = function (err) {
if (finished) { if (finished) {
@@ -33232,8 +33166,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
body body
.removeListener('data', onData) .removeListener('data', onData)
.removeListener('end', onFinished) .removeListener('end', onFinished)
.removeListener('error', onFinished) .removeListener('close', onClose)
.removeListener('close', onAbort)
if (!err) { if (!err) {
try { try {
@@ -33256,7 +33189,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
.on('data', onData) .on('data', onData)
.on('end', onFinished) .on('end', onFinished)
.on('error', onFinished) .on('error', onFinished)
.on('close', onAbort) .on('close', onClose)
if (body.resume) { if (body.resume) {
body.resume() body.resume()
@@ -51320,120 +51253,62 @@ function wrappy (fn, cb) {
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
const github = __nccwpck_require__(5438) const github = __nccwpck_require__(5438)
const hc = __nccwpck_require__(6255)
const { RequestError } = __nccwpck_require__(537)
const HttpStatusMessages = __nccwpck_require__(3703)
// All variables we need from the runtime are loaded here async function getArtifactMetadata({ githubToken, runId, artifactName }) {
const getContext = __nccwpck_require__(8454) const octokit = github.getOctokit(githubToken)
async function processRuntimeResponse(res, requestOptions) {
// Parse the response body as JSON
let obj = null
try {
const contents = await res.readBody()
if (contents && contents.length > 0) {
obj = JSON.parse(contents)
}
} catch (error) {
// Invalid resource (contents not json); leaving resulting obj as null
}
// Specific response shape aligned with Octokit
const response = {
url: res.message?.url || requestOptions.url,
status: res.message?.statusCode || 0,
headers: {
...res.message?.headers
},
data: obj
}
// Forcibly throw errors for negative HTTP status codes!
// @actions/http-client doesn't do this by default.
// Mimic the errors thrown by Octokit for consistency.
if (response.status >= 400) {
// Try to get an error message from the response body
const errorMsg =
(typeof response.data === 'string' && response.data) ||
response.data?.error ||
response.data?.message ||
// Try the Node HTTP IncomingMessage's statusMessage property
res.message?.statusMessage ||
// Fallback to the HTTP status message based on the status code
HttpStatusMessages[response.status] ||
// Or if the status code is unexpected...
`Unknown error (${response.status})`
throw new RequestError(errorMsg, response.status, {
response,
request: requestOptions
})
}
return response
}
async function getSignedArtifactMetadata({ runtimeToken, workflowRunId, artifactName }) {
const { runTimeUrl: RUNTIME_URL } = getContext()
const artifactExchangeUrl = `${RUNTIME_URL}_apis/pipelines/workflows/${workflowRunId}/artifacts?api-version=6.0-preview`
const httpClient = new hc.HttpClient()
let data = null
try { try {
const requestHeaders = { core.info(`Fetching artifact metadata for ${artifactName} in run ${runId}`)
accept: 'application/json',
authorization: `Bearer ${runtimeToken}` const response = await octokit.request(
} 'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts?name={artifactName}',
const requestOptions = { {
method: 'GET', owner: github.context.repo.owner,
url: artifactExchangeUrl, repo: github.context.repo.repo,
headers: { run_id: runId,
...requestHeaders artifactName: artifactName
}, }
body: null )
const artifactCount = response.data.total_count
core.debug(`List artifact count: ${artifactCount}`)
if (artifactCount === 0) {
throw new Error(
`No artifacts found for workflow run ${runId}. Ensure artifacts are uploaded with actions/artifact@v4 or later.`
)
} else if (artifactCount > 1) {
throw new Error(
`Multiple artifact unexpectedly found for workflow run ${runId}. Artifact count is ${artifactCount}.`
)
} }
core.info(`Artifact exchange URL: ${artifactExchangeUrl}`) const artifact = response.data.artifacts[0]
const res = await httpClient.get(artifactExchangeUrl, requestHeaders) core.debug(`Artifact: ${JSON.stringify(artifact)}`)
// May throw a RequestError (HttpError) const artifactSize = artifact.size_in_bytes
const response = await processRuntimeResponse(res, requestOptions) if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
data = response.data return {
core.debug(JSON.stringify(data)) id: artifact.id,
size: artifactSize
}
} catch (error) { } catch (error) {
core.error('Getting signed artifact URL failed', error) core.error(
'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages or Actions? Please re-run the deployment at a later time.',
error
)
throw error throw error
} }
const artifact = data?.value?.find(artifact => artifact.name === artifactName)
const artifactRawUrl = artifact?.url
if (!artifactRawUrl) {
throw new Error(
'No uploaded artifact was found! Please check if there are any errors at build step, or uploaded artifact name is correct.'
)
}
const signedArtifactUrl = `${artifactRawUrl}&%24expand=SignedContent`
const artifactSize = artifact?.size
if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
return {
url: signedArtifactUrl,
size: artifactSize
}
} }
async function createPagesDeployment({ githubToken, artifactUrl, buildVersion, idToken, isPreview = false }) { async function createPagesDeployment({ githubToken, artifactId, buildVersion, idToken, isPreview = false }) {
const octokit = github.getOctokit(githubToken) const octokit = github.getOctokit(githubToken)
const payload = { const payload = {
artifact_url: artifactUrl, artifact_id: artifactId,
pages_build_version: buildVersion, pages_build_version: buildVersion,
oidc_token: idToken oidc_token: idToken
} }
@@ -51493,7 +51368,7 @@ async function cancelPagesDeployment({ githubToken, deploymentId }) {
} }
module.exports = { module.exports = {
getSignedArtifactMetadata, getArtifactMetadata,
createPagesDeployment, createPagesDeployment,
getPagesDeploymentStatus, getPagesDeploymentStatus,
cancelPagesDeployment cancelPagesDeployment
@@ -51510,9 +51385,7 @@ const core = __nccwpck_require__(2186)
// Load variables from Actions runtime // Load variables from Actions runtime
function getRequiredVars() { function getRequiredVars() {
return { return {
runTimeUrl: process.env.ACTIONS_RUNTIME_URL,
workflowRun: process.env.GITHUB_RUN_ID, workflowRun: process.env.GITHUB_RUN_ID,
runTimeToken: process.env.ACTIONS_RUNTIME_TOKEN,
repositoryNwo: process.env.GITHUB_REPOSITORY, repositoryNwo: process.env.GITHUB_REPOSITORY,
buildVersion: process.env.GITHUB_SHA, buildVersion: process.env.GITHUB_SHA,
buildActor: process.env.GITHUB_ACTOR, buildActor: process.env.GITHUB_ACTOR,
@@ -51547,7 +51420,7 @@ const core = __nccwpck_require__(2186)
// All variables we need from the runtime are loaded here // All variables we need from the runtime are loaded here
const getContext = __nccwpck_require__(8454) const getContext = __nccwpck_require__(8454)
const { const {
getSignedArtifactMetadata, getArtifactMetadata,
createPagesDeployment, createPagesDeployment,
getPagesDeploymentStatus, getPagesDeploymentStatus,
cancelPagesDeployment cancelPagesDeployment
@@ -51575,9 +51448,7 @@ const SIZE_LIMIT_DESCRIPTION = '1 GB'
class Deployment { class Deployment {
constructor() { constructor() {
const context = getContext() const context = getContext()
this.runTimeUrl = context.runTimeUrl
this.repositoryNwo = context.repositoryNwo this.repositoryNwo = context.repositoryNwo
this.runTimeToken = context.runTimeToken
this.buildVersion = context.buildVersion this.buildVersion = context.buildVersion
this.buildActor = context.buildActor this.buildActor = context.buildActor
this.actionsId = context.actionsId this.actionsId = context.actionsId
@@ -51592,8 +51463,8 @@ class Deployment {
this.startTime = null this.startTime = null
} }
// Ask the runtime for the unsigned artifact URL and deploy to GitHub Pages // Call GitHub api to fetch artifacts matching the provided name and deploy to GitHub Pages
// by creating a deployment with that artifact // by creating a deployment with that artifact id
async create(idToken) { async create(idToken) {
if (Number(core.getInput('timeout')) > MAX_TIMEOUT) { if (Number(core.getInput('timeout')) > MAX_TIMEOUT) {
core.warning( core.warning(
@@ -51609,9 +51480,9 @@ class Deployment {
core.debug(`Action ID: ${this.actionsId}`) core.debug(`Action ID: ${this.actionsId}`)
core.debug(`Actions Workflow Run ID: ${this.workflowRun}`) core.debug(`Actions Workflow Run ID: ${this.workflowRun}`)
const artifactData = await getSignedArtifactMetadata({ const artifactData = await getArtifactMetadata({
runtimeToken: this.runTimeToken, githubToken: this.githubToken,
workflowRunId: this.workflowRun, runId: this.workflowRun,
artifactName: this.artifactName artifactName: this.artifactName
}) })
@@ -51623,7 +51494,7 @@ class Deployment {
const deployment = await createPagesDeployment({ const deployment = await createPagesDeployment({
githubToken: this.githubToken, githubToken: this.githubToken,
artifactUrl: artifactData.url, artifactId: artifactData.id,
buildVersion: this.buildVersion, buildVersion: this.buildVersion,
idToken, idToken,
isPreview: this.isPreview isPreview: this.isPreview

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

2
dist/licenses.txt generated vendored
View File

@@ -465,8 +465,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
http-status-messages
once once
ISC ISC
The ISC License The ISC License

5993
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,7 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0"
"@actions/http-client": "^2.2.0",
"@octokit/request-error": "^5.0.1",
"http-status-messages": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",

View File

@@ -4,9 +4,7 @@ const path = require('path')
describe('with all environment variables set', () => { describe('with all environment variables set', () => {
beforeEach(() => { beforeEach(() => {
process.env.ACTIONS_RUNTIME_URL = 'http://my-url'
process.env.GITHUB_RUN_ID = '123' process.env.GITHUB_RUN_ID = '123'
process.env.ACTIONS_RUNTIME_TOKEN = 'a-token'
process.env.GITHUB_REPOSITORY = 'actions/is-awesome' process.env.GITHUB_REPOSITORY = 'actions/is-awesome'
process.env.GITHUB_TOKEN = 'gha-token' process.env.GITHUB_TOKEN = 'gha-token'
process.env.GITHUB_SHA = '123abc' process.env.GITHUB_SHA = '123abc'
@@ -26,7 +24,7 @@ describe('with all environment variables set', () => {
describe('with variables missing', () => { describe('with variables missing', () => {
it('execution fails if there are missing variables', done => { it('execution fails if there are missing variables', done => {
delete process.env.ACTIONS_RUNTIME_URL delete process.env.GITHUB_RUN_ID
const ip = path.join(__dirname, '../index.js') const ip = path.join(__dirname, '../index.js')
cp.exec(`node ${ip}`, { env: process.env }, (err, stdout) => { cp.exec(`node ${ip}`, { env: process.env }, (err, stdout) => {
expect(stdout).toBe('') expect(stdout).toBe('')

View File

@@ -1,6 +1,4 @@
const core = require('@actions/core') const core = require('@actions/core')
// For mocking network calls with core http (http-client)
const nock = require('nock')
// For mocking network calls with native Fetch (octokit) // For mocking network calls with native Fetch (octokit)
const { MockAgent, setGlobalDispatcher } = require('undici') const { MockAgent, setGlobalDispatcher } = require('undici')
@@ -14,9 +12,7 @@ describe('Deployment', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
process.env.ACTIONS_RUNTIME_URL = 'http://my-url/'
process.env.GITHUB_RUN_ID = '123' process.env.GITHUB_RUN_ID = '123'
process.env.ACTIONS_RUNTIME_TOKEN = 'a-token'
process.env.GITHUB_REPOSITORY = 'actions/is-awesome' process.env.GITHUB_REPOSITORY = 'actions/is-awesome'
process.env.GITHUB_TOKEN = 'gha-token' process.env.GITHUB_TOKEN = 'gha-token'
process.env.GITHUB_SHA = '123abc' process.env.GITHUB_SHA = '123abc'
@@ -67,14 +63,19 @@ describe('Deployment', () => {
it('can successfully create a deployment', async () => { it('can successfully create a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -85,10 +86,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA && body.pages_build_version === process.env.GITHUB_SHA &&
body.oidc_token === fakeJwt body.oidc_token === fakeJwt
) )
@@ -113,21 +114,24 @@ describe('Deployment', () => {
expect(core.info).toHaveBeenLastCalledWith( expect(core.info).toHaveBeenLastCalledWith(
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
) )
artifactExchangeScope.done()
}) })
it('can successfully create a preview deployment', async () => { it('can successfully create a preview deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -138,11 +142,11 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 4 && keys.length === 4 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
keys[3] === 'preview' && keys[3] === 'preview' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA && body.pages_build_version === process.env.GITHUB_SHA &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.preview === true body.preview === true
@@ -172,32 +176,42 @@ describe('Deployment', () => {
expect(core.info).toHaveBeenLastCalledWith( expect(core.info).toHaveBeenLastCalledWith(
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
) )
artifactExchangeScope.done()
}) })
it('reports errors with failed artifact exchange', async () => { it('reports errors with failed artifact metadata exchange', async () => {
process.env.GITHUB_SHA = 'invalid-build-version' process.env.GITHUB_SHA = 'invalid-build-version'
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') mockPool
.reply(400, {}) .intercept({
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(400, { message: 'Bad request' }, { headers: { 'content-type': 'application/json' } })
// Create the deployment // Create the deployment
const deployment = new Deployment() const deployment = new Deployment()
await expect(deployment.create()).rejects.toEqual( await expect(deployment.create()).rejects.toEqual(
new Error( new Error(
`Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad Request` `Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad request`
) )
) )
artifactExchangeScope.done()
}) })
it('reports errors with a failed 500 in a deployment', async () => { it('reports errors with a failed 500 in a deployment', async () => {
process.env.GITHUB_SHA = 'build-version' process.env.GITHUB_SHA = 'build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] }) path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -208,9 +222,9 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 2 && keys.length === 2 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'pages_build_version' && keys[1] === 'pages_build_version' &&
body.artifact_url === 'https://invalid-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
} }
@@ -224,15 +238,23 @@ describe('Deployment', () => {
`Failed to create deployment (status: 500) with build version ${process.env.GITHUB_SHA}. Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.` `Failed to create deployment (status: 500) with build version ${process.env.GITHUB_SHA}. Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.`
) )
) )
artifactExchangeScope.done()
}) })
it('reports errors with an unexpected 403 during deployment', async () => { it('reports errors with an unexpected 403 during deployment', async () => {
process.env.GITHUB_SHA = 'build-version' process.env.GITHUB_SHA = 'build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] }) path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -243,9 +265,9 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 2 && keys.length === 2 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'pages_build_version' && keys[1] === 'pages_build_version' &&
body.artifact_url === 'https://invalid-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
} }
@@ -259,15 +281,23 @@ describe('Deployment', () => {
`Failed to create deployment (status: 403) with build version ${process.env.GITHUB_SHA}. Ensure GITHUB_TOKEN has permission "pages: write".` `Failed to create deployment (status: 403) with build version ${process.env.GITHUB_SHA}. Ensure GITHUB_TOKEN has permission "pages: write".`
) )
) )
artifactExchangeScope.done()
}) })
it('reports errors with an unexpected 404 during deployment', async () => { it('reports errors with an unexpected 404 during deployment', async () => {
process.env.GITHUB_SHA = 'build-version' process.env.GITHUB_SHA = 'build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] }) path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -278,9 +308,9 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 2 && keys.length === 2 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'pages_build_version' && keys[1] === 'pages_build_version' &&
body.artifact_url === 'https://invalid-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
} }
@@ -294,15 +324,23 @@ describe('Deployment', () => {
`Failed to create deployment (status: 404) with build version ${process.env.GITHUB_SHA}. Ensure GitHub Pages has been enabled: https://github.com/actions/is-awesome/settings/pages` `Failed to create deployment (status: 404) with build version ${process.env.GITHUB_SHA}. Ensure GitHub Pages has been enabled: https://github.com/actions/is-awesome/settings/pages`
) )
) )
artifactExchangeScope.done()
}) })
it('reports errors with failed deployments', async () => { it('reports errors with failed deployments', async () => {
process.env.GITHUB_SHA = 'invalid-build-version' process.env.GITHUB_SHA = 'invalid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] }) path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -313,9 +351,9 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 2 && keys.length === 2 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'pages_build_version' && keys[1] === 'pages_build_version' &&
body.artifact_url === 'https://invalid-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
} }
@@ -329,22 +367,98 @@ describe('Deployment', () => {
`Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad request` `Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad request`
) )
) )
})
artifactExchangeScope.done() it('fails if there are multiple artifacts with the same name', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
mockPool
.intercept({
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 2,
artifacts: [
{
id: 13,
name: `github-pages`,
size_in_bytes: 1400
},
{
id: 14,
name: `github-pages`,
size_in_bytes: 1620
}
]
},
{ headers: { 'content-type': 'application/json' } }
)
const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow(
`Multiple artifact unexpectedly found for workflow run ${process.env.GITHUB_RUN_ID}. Artifact count is 2.`
)
})
it('fails if there are no artifacts found', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
mockPool
.intercept({
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(
200,
{
total_count: 0,
artifacts: []
},
{ headers: { 'content-type': 'application/json' } }
)
const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow(
`No artifacts found for workflow run ${process.env.GITHUB_RUN_ID}. Ensure artifacts are uploaded with actions/artifact@v4 or later.`
)
})
it('fails with error message if list artifact endpoint returns 500', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
mockPool
.intercept({
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply(500, { message: 'oh no' }, { headers: { 'content-type': 'application/json' } })
const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow(
`Failed to create deployment (status: 500) with build version valid-build-version. Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.`
)
}) })
it('warns if the artifact size is bigger than maximum', async () => { it('warns if the artifact size is bigger than maximum', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactSize = ONE_GIGABYTE + 1 const artifactSize = ONE_GIGABYTE + 1
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://fake-artifact.com', name: 'github-pages', size: `${artifactSize}` },
{ url: 'https://another-artifact.com', name: 'another-artifact' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 12, name: `github-pages`, size_in_bytes: artifactSize }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -355,10 +469,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 12 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -383,21 +497,24 @@ describe('Deployment', () => {
expect(core.info).toHaveBeenLastCalledWith( expect(core.info).toHaveBeenLastCalledWith(
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
) )
artifactExchangeScope.done()
}) })
it('warns when the timeout is greater than the maximum allowed', async () => { it('warns when the timeout is greater than the maximum allowed', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -408,10 +525,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -450,8 +567,6 @@ describe('Deployment', () => {
expect(core.warning).toBeCalledWith( expect(core.warning).toBeCalledWith(
`Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.` `Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.`
) )
artifactExchangeScope.done()
}) })
}) })
@@ -459,14 +574,19 @@ describe('Deployment', () => {
it('sets output to success when deployment is successful', async () => { it('sets output to success when deployment is successful', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -477,10 +597,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -511,8 +631,6 @@ describe('Deployment', () => {
expect(core.setOutput).toBeCalledWith('status', 'succeed') expect(core.setOutput).toBeCalledWith('status', 'succeed')
expect(core.info).toHaveBeenLastCalledWith('Reported success!') expect(core.info).toHaveBeenLastCalledWith('Reported success!')
artifactExchangeScope.done()
}) })
it('fails check when no deployment is found', async () => { it('fails check when no deployment is found', async () => {
@@ -525,14 +643,19 @@ describe('Deployment', () => {
it('exits early when deployment is not in progress', async () => { it('exits early when deployment is not in progress', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -543,10 +666,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -568,21 +691,24 @@ describe('Deployment', () => {
deployment.deploymentInfo.pending = false deployment.deploymentInfo.pending = false
await deployment.check() await deployment.check()
expect(core.setFailed).toBeCalledWith('Unable to get deployment status.') expect(core.setFailed).toBeCalledWith('Unable to get deployment status.')
artifactExchangeScope.done()
}) })
it('enforces max timeout', async () => { it('enforces max timeout', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -593,10 +719,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -664,21 +790,24 @@ describe('Deployment', () => {
expect(deployment.timeout).toEqual(MAX_TIMEOUT) expect(deployment.timeout).toEqual(MAX_TIMEOUT)
expect(core.error).toBeCalledWith('Timeout reached, aborting!') expect(core.error).toBeCalledWith('Timeout reached, aborting!')
expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!') expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!')
artifactExchangeScope.done()
}) })
it('sets timeout to user timeout if user timeout is less than max timeout', async () => { it('sets timeout to user timeout if user timeout is less than max timeout', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -689,10 +818,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -749,21 +878,24 @@ describe('Deployment', () => {
expect(deployment.timeout).toEqual(42) expect(deployment.timeout).toEqual(42)
expect(core.error).toBeCalledWith('Timeout reached, aborting!') expect(core.error).toBeCalledWith('Timeout reached, aborting!')
expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!') expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!')
artifactExchangeScope.done()
}) })
it('sets output to success when timeout is set but not reached', async () => { it('sets output to success when timeout is set but not reached', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -774,10 +906,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -835,8 +967,6 @@ describe('Deployment', () => {
expect(core.error).not.toBeCalled() expect(core.error).not.toBeCalled()
expect(core.setOutput).toBeCalledWith('status', 'succeed') expect(core.setOutput).toBeCalledWith('status', 'succeed')
expect(core.info).toHaveBeenLastCalledWith('Reported success!') expect(core.info).toHaveBeenLastCalledWith('Reported success!')
artifactExchangeScope.done()
}) })
}) })
@@ -844,14 +974,19 @@ describe('Deployment', () => {
it('can successfully cancel a deployment', async () => { it('can successfully cancel a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -862,10 +997,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -897,8 +1032,6 @@ describe('Deployment', () => {
await deployment.cancel() await deployment.cancel()
expect(core.info).toHaveBeenLastCalledWith(`Canceled deployment with ID ${process.env.GITHUB_SHA}`) expect(core.info).toHaveBeenLastCalledWith(`Canceled deployment with ID ${process.env.GITHUB_SHA}`)
artifactExchangeScope.done()
}) })
it('can exit if a pages deployment was not created and none need to be cancelled', async () => { it('can exit if a pages deployment was not created and none need to be cancelled', async () => {
@@ -917,14 +1050,19 @@ describe('Deployment', () => {
it('catches an error when trying to cancel a deployment', async () => { it('catches an error when trying to cancel a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`) mockPool
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview') .intercept({
.reply(200, { path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
value: [ method: 'GET'
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
}) })
.reply(
200,
{
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
},
{ headers: { 'content-type': 'application/json' } }
)
mockPool mockPool
.intercept({ .intercept({
@@ -935,10 +1073,10 @@ describe('Deployment', () => {
const keys = Object.keys(body).sort() const keys = Object.keys(body).sort()
return ( return (
keys.length === 3 && keys.length === 3 &&
keys[0] === 'artifact_url' && keys[0] === 'artifact_id' &&
keys[1] === 'oidc_token' && keys[1] === 'oidc_token' &&
keys[2] === 'pages_build_version' && keys[2] === 'pages_build_version' &&
body.artifact_url === 'https://fake-artifact.com&%24expand=SignedContent' && body.artifact_id === 11 &&
body.oidc_token === fakeJwt && body.oidc_token === fakeJwt &&
body.pages_build_version === process.env.GITHUB_SHA body.pages_build_version === process.env.GITHUB_SHA
) )
@@ -970,8 +1108,6 @@ describe('Deployment', () => {
await deployment.cancel() await deployment.cancel()
expect(core.error).toHaveBeenCalledWith(`Canceling Pages deployment failed`, expect.anything()) expect(core.error).toHaveBeenCalledWith(`Canceling Pages deployment failed`, expect.anything())
artifactExchangeScope.done()
}) })
}) })
}) })

View File

@@ -1,119 +1,61 @@
const core = require('@actions/core') const core = require('@actions/core')
const github = require('@actions/github') const github = require('@actions/github')
const hc = require('@actions/http-client')
const { RequestError } = require('@octokit/request-error')
const HttpStatusMessages = require('http-status-messages')
// All variables we need from the runtime are loaded here async function getArtifactMetadata({ githubToken, runId, artifactName }) {
const getContext = require('./context') const octokit = github.getOctokit(githubToken)
async function processRuntimeResponse(res, requestOptions) {
// Parse the response body as JSON
let obj = null
try {
const contents = await res.readBody()
if (contents && contents.length > 0) {
obj = JSON.parse(contents)
}
} catch (error) {
// Invalid resource (contents not json); leaving resulting obj as null
}
// Specific response shape aligned with Octokit
const response = {
url: res.message?.url || requestOptions.url,
status: res.message?.statusCode || 0,
headers: {
...res.message?.headers
},
data: obj
}
// Forcibly throw errors for negative HTTP status codes!
// @actions/http-client doesn't do this by default.
// Mimic the errors thrown by Octokit for consistency.
if (response.status >= 400) {
// Try to get an error message from the response body
const errorMsg =
(typeof response.data === 'string' && response.data) ||
response.data?.error ||
response.data?.message ||
// Try the Node HTTP IncomingMessage's statusMessage property
res.message?.statusMessage ||
// Fallback to the HTTP status message based on the status code
HttpStatusMessages[response.status] ||
// Or if the status code is unexpected...
`Unknown error (${response.status})`
throw new RequestError(errorMsg, response.status, {
response,
request: requestOptions
})
}
return response
}
async function getSignedArtifactMetadata({ runtimeToken, workflowRunId, artifactName }) {
const { runTimeUrl: RUNTIME_URL } = getContext()
const artifactExchangeUrl = `${RUNTIME_URL}_apis/pipelines/workflows/${workflowRunId}/artifacts?api-version=6.0-preview`
const httpClient = new hc.HttpClient()
let data = null
try { try {
const requestHeaders = { core.info(`Fetching artifact metadata for ${artifactName} in run ${runId}`)
accept: 'application/json',
authorization: `Bearer ${runtimeToken}` const response = await octokit.request(
} 'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts?name={artifactName}',
const requestOptions = { {
method: 'GET', owner: github.context.repo.owner,
url: artifactExchangeUrl, repo: github.context.repo.repo,
headers: { run_id: runId,
...requestHeaders artifactName: artifactName
}, }
body: null )
const artifactCount = response.data.total_count
core.debug(`List artifact count: ${artifactCount}`)
if (artifactCount === 0) {
throw new Error(
`No artifacts found for workflow run ${runId}. Ensure artifacts are uploaded with actions/artifact@v4 or later.`
)
} else if (artifactCount > 1) {
throw new Error(
`Multiple artifact unexpectedly found for workflow run ${runId}. Artifact count is ${artifactCount}.`
)
} }
core.info(`Artifact exchange URL: ${artifactExchangeUrl}`) const artifact = response.data.artifacts[0]
const res = await httpClient.get(artifactExchangeUrl, requestHeaders) core.debug(`Artifact: ${JSON.stringify(artifact)}`)
// May throw a RequestError (HttpError) const artifactSize = artifact.size_in_bytes
const response = await processRuntimeResponse(res, requestOptions) if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
data = response.data return {
core.debug(JSON.stringify(data)) id: artifact.id,
size: artifactSize
}
} catch (error) { } catch (error) {
core.error('Getting signed artifact URL failed', error) core.error(
'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages or Actions? Please re-run the deployment at a later time.',
error
)
throw error throw error
} }
const artifact = data?.value?.find(artifact => artifact.name === artifactName)
const artifactRawUrl = artifact?.url
if (!artifactRawUrl) {
throw new Error(
'No uploaded artifact was found! Please check if there are any errors at build step, or uploaded artifact name is correct.'
)
}
const signedArtifactUrl = `${artifactRawUrl}&%24expand=SignedContent`
const artifactSize = artifact?.size
if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
return {
url: signedArtifactUrl,
size: artifactSize
}
} }
async function createPagesDeployment({ githubToken, artifactUrl, buildVersion, idToken, isPreview = false }) { async function createPagesDeployment({ githubToken, artifactId, buildVersion, idToken, isPreview = false }) {
const octokit = github.getOctokit(githubToken) const octokit = github.getOctokit(githubToken)
const payload = { const payload = {
artifact_url: artifactUrl, artifact_id: artifactId,
pages_build_version: buildVersion, pages_build_version: buildVersion,
oidc_token: idToken oidc_token: idToken
} }
@@ -173,7 +115,7 @@ async function cancelPagesDeployment({ githubToken, deploymentId }) {
} }
module.exports = { module.exports = {
getSignedArtifactMetadata, getArtifactMetadata,
createPagesDeployment, createPagesDeployment,
getPagesDeploymentStatus, getPagesDeploymentStatus,
cancelPagesDeployment cancelPagesDeployment

View File

@@ -3,9 +3,7 @@ const core = require('@actions/core')
// Load variables from Actions runtime // Load variables from Actions runtime
function getRequiredVars() { function getRequiredVars() {
return { return {
runTimeUrl: process.env.ACTIONS_RUNTIME_URL,
workflowRun: process.env.GITHUB_RUN_ID, workflowRun: process.env.GITHUB_RUN_ID,
runTimeToken: process.env.ACTIONS_RUNTIME_TOKEN,
repositoryNwo: process.env.GITHUB_REPOSITORY, repositoryNwo: process.env.GITHUB_REPOSITORY,
buildVersion: process.env.GITHUB_SHA, buildVersion: process.env.GITHUB_SHA,
buildActor: process.env.GITHUB_ACTOR, buildActor: process.env.GITHUB_ACTOR,

View File

@@ -3,7 +3,7 @@ const core = require('@actions/core')
// All variables we need from the runtime are loaded here // All variables we need from the runtime are loaded here
const getContext = require('./context') const getContext = require('./context')
const { const {
getSignedArtifactMetadata, getArtifactMetadata,
createPagesDeployment, createPagesDeployment,
getPagesDeploymentStatus, getPagesDeploymentStatus,
cancelPagesDeployment cancelPagesDeployment
@@ -31,9 +31,7 @@ const SIZE_LIMIT_DESCRIPTION = '1 GB'
class Deployment { class Deployment {
constructor() { constructor() {
const context = getContext() const context = getContext()
this.runTimeUrl = context.runTimeUrl
this.repositoryNwo = context.repositoryNwo this.repositoryNwo = context.repositoryNwo
this.runTimeToken = context.runTimeToken
this.buildVersion = context.buildVersion this.buildVersion = context.buildVersion
this.buildActor = context.buildActor this.buildActor = context.buildActor
this.actionsId = context.actionsId this.actionsId = context.actionsId
@@ -48,8 +46,8 @@ class Deployment {
this.startTime = null this.startTime = null
} }
// Ask the runtime for the unsigned artifact URL and deploy to GitHub Pages // Call GitHub api to fetch artifacts matching the provided name and deploy to GitHub Pages
// by creating a deployment with that artifact // by creating a deployment with that artifact id
async create(idToken) { async create(idToken) {
if (Number(core.getInput('timeout')) > MAX_TIMEOUT) { if (Number(core.getInput('timeout')) > MAX_TIMEOUT) {
core.warning( core.warning(
@@ -65,9 +63,9 @@ class Deployment {
core.debug(`Action ID: ${this.actionsId}`) core.debug(`Action ID: ${this.actionsId}`)
core.debug(`Actions Workflow Run ID: ${this.workflowRun}`) core.debug(`Actions Workflow Run ID: ${this.workflowRun}`)
const artifactData = await getSignedArtifactMetadata({ const artifactData = await getArtifactMetadata({
runtimeToken: this.runTimeToken, githubToken: this.githubToken,
workflowRunId: this.workflowRun, runId: this.workflowRun,
artifactName: this.artifactName artifactName: this.artifactName
}) })
@@ -79,7 +77,7 @@ class Deployment {
const deployment = await createPagesDeployment({ const deployment = await createPagesDeployment({
githubToken: this.githubToken, githubToken: this.githubToken,
artifactUrl: artifactData.url, artifactId: artifactData.id,
buildVersion: this.buildVersion, buildVersion: this.buildVersion,
idToken, idToken,
isPreview: this.isPreview isPreview: this.isPreview