Compare commits

..

5 Commits

Author SHA1 Message Date
James M. Greene
c7b5c1034e Update distributables 2023-06-15 15:14:49 -05:00
James M. Greene
b4d15f6490 Merge branch 'main' into error-count 2023-06-15 15:11:35 -05:00
James M. Greene
c883148031 Separate tests for Deployment##setOptionalUserInput to limit scope 2023-06-15 15:09:03 -05:00
Greta Parks
b1a18fc1bd little nits
Co-authored-by: James M. Greene <JamesMGreene@github.com>
2023-06-14 17:02:05 +00:00
Greta Parks
a378718509 adds a check for error_count variable 2023-05-22 22:59:30 +00:00
13 changed files with 1817 additions and 1429 deletions

View File

@@ -11,6 +11,6 @@ jobs:
draft-release:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@65c5fb495d1e69aa8c08a3317bc44ff8aabe9772 # v5.24.0
- uses: release-drafter/release-drafter@569eb7ee3a85817ab916c8f8ff03a5bd96c9c83e # v5.23.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,11 +6,11 @@ This action is used to deploy [Actions artifacts][artifacts] to [GitHub Pages](h
## Usage
See [action.yml](action.yml) for the various `inputs` this action supports (or [below](#inputs-📥)).
See [action.yml](action.yml) for the various `inputs` this action supports.
For examples that make use of this action, check out our [starter-workflows][starter-workflows] in a variety of frameworks.
This action deploys a Pages site previously uploaded as an artifact (e.g. using [`actions/upload-pages-artifact`][upload-pages-artifact]).
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].
We recommend this action to be used in a dedicated job:
@@ -67,11 +67,15 @@ jobs:
| -------- | ----------- |
| `GITHUB_PAGES` | This environment variable is created and set to the string value `"true"` so that framework build tools may choose to differentiate their output based on the intended target hosting platform. |
## Scope
⚠️ Official support for building Pages with Actions is in public beta at the moment.
## Security Considerations
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. See [`actions/upload-pages-artifact`][upload-pages-artifact] for more information about the format of the artifact we expect.
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.
2. The job that executes the deployment must at minimum have the following permissions:
- `pages: write`
@@ -89,11 +93,12 @@ This action is primarily design for use with GitHub.com's Actions workflows and
| Release | GHES Compatibility |
|:---|:---|
| [`v2`](https://github.com/actions/deploy-pages/releases/tag/v2) | `>= 3.9` |
| `v2.x.x` | `>= 3.9` |
| [`v2`](https://github.com/actions/deploy-pages/releases/tag/v2) | 🛑 Incompatible. Anticipating compatibility with `>= 3.9`. |
| [`v2.0.1`](https://github.com/actions/deploy-pages/releases/tag/v2.0.1) | 🛑 Incompatible. Anticipating compatibility with `>= 3.9`. |
| [`v2.0.0`](https://github.com/actions/deploy-pages/releases/tag/v2.0.0) | 🛑 Incompatible. Anticipating compatibility with `>= 3.9`. |
| [`v1`](https://github.com/actions/deploy-pages/releases/tag/v1) | `>= 3.7` |
| [`v1.2.8`](https://github.com/actions/deploy-pages/releases/tag/v1.2.8) | `>= 3.7` |
| [`v1.2.7`](https://github.com/actions/deploy-pages/releases/tag/v1.2.7) | :warning: `>= 3.9` [Incompatible with prior versions!](https://github.com/actions/deploy-pages/issues/137) |
| [`v1.2.7`](https://github.com/actions/deploy-pages/releases/tag/v1.2.7) | :warning: [Incompatible](https://github.com/actions/deploy-pages/issues/137). Anticipating compatibility with `>= 3.9`. |
| [`v1.2.6`](https://github.com/actions/deploy-pages/releases/tag/v1.2.6) | `>= 3.7` |
| `v1.x.x` | `>= 3.7` |

View File

@@ -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: 79.53%"><title>Coverage: 79.53%</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="#e05d44"/><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">79.53%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">79.53%</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: 80.18%"><title>Coverage: 80.18%</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="#dfb317"/><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">80.18%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">80.18%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

389
dist/index.js generated vendored
View File

@@ -4090,6 +4090,104 @@ exports.restEndpointMethods = restEndpointMethods;
//# sourceMappingURL=index.js.map
/***/ }),
/***/ 537:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// pkg/dist-src/index.js
var dist_src_exports = {};
__export(dist_src_exports, {
RequestError: () => RequestError
});
module.exports = __toCommonJS(dist_src_exports);
var import_deprecation = __nccwpck_require__(8932);
var import_once = __toESM(__nccwpck_require__(1223));
var logOnceCode = (0, import_once.default)((deprecation) => console.warn(deprecation));
var logOnceHeaders = (0, import_once.default)((deprecation) => console.warn(deprecation));
var RequestError = class extends Error {
constructor(message, statusCode, options) {
super(message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.name = "HttpError";
this.status = statusCode;
let headers;
if ("headers" in options && typeof options.headers !== "undefined") {
headers = options.headers;
}
if ("response" in options) {
this.response = options.response;
headers = options.response.headers;
}
const requestCopy = Object.assign({}, options.request);
if (options.request.headers.authorization) {
requestCopy.headers = Object.assign({}, options.request.headers, {
authorization: options.request.headers.authorization.replace(
/ .*$/,
" [REDACTED]"
)
});
}
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
this.request = requestCopy;
Object.defineProperty(this, "code", {
get() {
logOnceCode(
new import_deprecation.Deprecation(
"[@octokit/request-error] `error.code` is deprecated, use `error.status`."
)
);
return statusCode;
}
});
Object.defineProperty(this, "headers", {
get() {
logOnceHeaders(
new import_deprecation.Deprecation(
"[@octokit/request-error] `error.headers` is deprecated, use `error.response.headers`."
)
);
return headers || {};
}
});
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (0);
/***/ }),
/***/ 6234:
@@ -4566,6 +4664,102 @@ class Deprecation extends Error {
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"
};
/***/ }),
/***/ 3287:
@@ -9617,62 +9811,120 @@ function wrappy (fn, cb) {
const core = __nccwpck_require__(2186)
const github = __nccwpck_require__(5438)
const hc = __nccwpck_require__(6255)
const { RequestError } = __nccwpck_require__(537)
const HttpStatusMessages = __nccwpck_require__(3703)
async function getArtifactMetadata({ githubToken, runId, artifactName }) {
const octokit = github.getOctokit(githubToken)
// All variables we need from the runtime are loaded here
const getContext = __nccwpck_require__(8454)
async function processRuntimeResponse(res, requestOptions) {
// Parse the response body as JSON
let obj = null
try {
core.info(`Fetching artifact metadata for ${artifactName} in run ${runId}`)
const response = await octokit.request(
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts?name={artifactName}',
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
run_id: runId,
artifactName: artifactName
}
)
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}.`
)
}
const artifact = response.data.artifacts[0]
core.debug(`Artifact: ${JSON.stringify(artifact)}`)
const artifactSize = artifact.size_in_bytes
if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
return {
id: artifact.id,
size: artifactSize
const contents = await res.readBody()
if (contents && contents.length > 0) {
obj = JSON.parse(contents)
}
} catch (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
)
// 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 {
const requestHeaders = {
accept: 'application/json',
authorization: `Bearer ${runtimeToken}`
}
const requestOptions = {
method: 'GET',
url: artifactExchangeUrl,
headers: {
...requestHeaders
},
body: null
}
core.info(`Artifact exchange URL: ${artifactExchangeUrl}`)
const res = await httpClient.get(artifactExchangeUrl, requestHeaders)
// May throw a RequestError (HttpError)
const response = await processRuntimeResponse(res, requestOptions)
data = response.data
core.debug(JSON.stringify(data))
} catch (error) {
core.error('Getting signed artifact URL failed', 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, artifactId, buildVersion, idToken, isPreview = false }) {
async function createPagesDeployment({ githubToken, artifactUrl, buildVersion, idToken, isPreview = false }) {
const octokit = github.getOctokit(githubToken)
const payload = {
artifact_id: artifactId,
artifact_url: artifactUrl,
pages_build_version: buildVersion,
oidc_token: idToken
}
@@ -9732,7 +9984,7 @@ async function cancelPagesDeployment({ githubToken, deploymentId }) {
}
module.exports = {
getArtifactMetadata,
getSignedArtifactMetadata,
createPagesDeployment,
getPagesDeploymentStatus,
cancelPagesDeployment
@@ -9749,7 +10001,9 @@ const core = __nccwpck_require__(2186)
// Load variables from Actions runtime
function getRequiredVars() {
return {
runTimeUrl: process.env.ACTIONS_RUNTIME_URL,
workflowRun: process.env.GITHUB_RUN_ID,
runTimeToken: process.env.ACTIONS_RUNTIME_TOKEN,
repositoryNwo: process.env.GITHUB_REPOSITORY,
buildVersion: process.env.GITHUB_SHA,
buildActor: process.env.GITHUB_ACTOR,
@@ -9784,7 +10038,7 @@ const core = __nccwpck_require__(2186)
// All variables we need from the runtime are loaded here
const getContext = __nccwpck_require__(8454)
const {
getArtifactMetadata,
getSignedArtifactMetadata,
createPagesDeployment,
getPagesDeploymentStatus,
cancelPagesDeployment
@@ -9798,7 +10052,6 @@ const temporaryErrorStatus = {
const finalErrorStatus = {
deployment_failed: 'Deployment failed, try again later.',
deployment_perms_error: 'Deployment failed, Please ensure that the file permissions are correct.',
deployment_content_failed:
'Artifact could not be deployed. Please ensure the content does not contain any hard links, symlinks and total size is less than 10GB.',
deployment_cancelled: 'Deployment cancelled.',
@@ -9812,7 +10065,9 @@ const SIZE_LIMIT_DESCRIPTION = '1 GB'
class Deployment {
constructor() {
const context = getContext()
this.runTimeUrl = context.runTimeUrl
this.repositoryNwo = context.repositoryNwo
this.runTimeToken = context.runTimeToken
this.buildVersion = context.buildVersion
this.buildActor = context.buildActor
this.actionsId = context.actionsId
@@ -9825,28 +10080,39 @@ class Deployment {
this.isPreview = context.isPreview === true
this.timeout = MAX_TIMEOUT
this.startTime = null
this.maxErrorCount = null
}
// Call GitHub api to fetch artifacts matching the provided name and deploy to GitHub Pages
// by creating a deployment with that artifact id
async create(idToken) {
if (Number(core.getInput('timeout')) > MAX_TIMEOUT) {
setOptionalUserInput() {
const timeoutInput = Number(core.getInput('timeout'))
if (timeoutInput > MAX_TIMEOUT) {
core.warning(
`Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.`
)
}
const timeoutInput = Number(core.getInput('timeout'))
this.timeout = !timeoutInput || timeoutInput <= 0 ? MAX_TIMEOUT : Math.min(timeoutInput, MAX_TIMEOUT)
const maxErrorCountInput = Number(core.getInput('error_count'))
if (!maxErrorCountInput || maxErrorCountInput <= 0) {
core.warning('Invalid error_count value will be ignored. Please ensure the value is a positive integer.')
} else {
this.maxErrorCount = maxErrorCountInput
}
}
// Ask the runtime for the unsigned artifact URL and deploy to GitHub Pages
// by creating a deployment with that artifact
async create(idToken) {
try {
this.setOptionalUserInput()
core.debug(`Actor: ${this.buildActor}`)
core.debug(`Action ID: ${this.actionsId}`)
core.debug(`Actions Workflow Run ID: ${this.workflowRun}`)
const artifactData = await getArtifactMetadata({
githubToken: this.githubToken,
runId: this.workflowRun,
const artifactData = await getSignedArtifactMetadata({
runtimeToken: this.runTimeToken,
workflowRunId: this.workflowRun,
artifactName: this.artifactName
})
@@ -9858,7 +10124,7 @@ class Deployment {
const deployment = await createPagesDeployment({
githubToken: this.githubToken,
artifactId: artifactData.id,
artifactUrl: artifactData.url,
buildVersion: this.buildVersion,
idToken,
isPreview: this.isPreview
@@ -9881,6 +10147,9 @@ class Deployment {
} catch (error) {
core.error(error.stack)
// output raw error in debug mode.
core.debug(JSON.stringify(error))
// build customized error message based on server response
if (error.response) {
let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}. `
@@ -9922,7 +10191,6 @@ class Deployment {
const deploymentId = this.deploymentInfo.id || this.buildVersion
const reportingInterval = Number(core.getInput('reporting_interval'))
const maxErrorCount = Number(core.getInput('error_count'))
let errorCount = 0
@@ -9965,6 +10233,9 @@ class Deployment {
} catch (error) {
core.error(error.stack)
// output raw error in debug mode.
core.debug(JSON.stringify(error))
// build customized error message based on server response
if (error.response) {
errorStatus = error.status || error.response.status
@@ -9978,7 +10249,7 @@ class Deployment {
}
}
if (errorCount >= maxErrorCount) {
if (errorCount >= this.maxErrorCount) {
core.error('Too many errors, aborting!')
core.setFailed('Failed with status code: ' + errorStatus)

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

@@ -453,6 +453,8 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
http-status-messages
is-plain-object
MIT
The MIT License (MIT)

2163
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,17 +5,20 @@
"main": "./dist/index.js",
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1"
"@actions/github": "^5.1.1",
"@actions/http-client": "^2.1.0",
"@octokit/request-error": "^4.0.1",
"http-status-messages": "^1.1.0"
},
"devDependencies": {
"@vercel/ncc": "^0.36.1",
"eslint": "^8.44.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-github": "^4.8.0",
"jest": "^29.6.1",
"make-coverage-badge": "^1.2.0",
"jest": "^29.5.0",
"nock": "^13.3.1",
"prettier": "^3.0.0"
"prettier": "^2.8.8",
"make-coverage-badge": "^1.2.0"
},
"scripts": {
"all": "npm run format && npm run lint && npm run prepare && npm run test && npm run coverage-badge",

View File

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

View File

@@ -9,7 +9,9 @@ const fakeJwt =
describe('Deployment', () => {
beforeEach(() => {
jest.clearAllMocks()
process.env.ACTIONS_RUNTIME_URL = 'http://my-url/'
process.env.GITHUB_RUN_ID = '123'
process.env.ACTIONS_RUNTIME_TOKEN = 'a-token'
process.env.GITHUB_REPOSITORY = 'actions/is-awesome'
process.env.GITHUB_TOKEN = 'gha-token'
process.env.GITHUB_SHA = '123abc'
@@ -42,6 +44,101 @@ describe('Deployment', () => {
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
})
describe('#setOptionalUserInput', () => {
it('warns when the timeout is greater than the maximum allowed', async () => {
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'timeout':
return MAX_TIMEOUT + 1
default:
return process.env[`INPUT_${param.toUpperCase()}`] || ''
}
})
const deployment = new Deployment()
deployment.setOptionalUserInput()
expect(deployment.timeout).toBe(MAX_TIMEOUT)
expect(core.warning).toBeCalledWith(
`Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.`
)
})
it('sets the error_count input when valid', async () => {
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'error_count':
return '1'
default:
return process.env[`INPUT_${param.toUpperCase()}`] || ''
}
})
// Create the deployment
const deployment = new Deployment()
deployment.setOptionalUserInput()
expect(deployment.maxErrorCount).toBe(1)
})
it('sets the error_count input to null if zero and warns user', async () => {
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'error_count':
return '0'
default:
return process.env[`INPUT_${param.toUpperCase()}`] || ''
}
})
const deployment = new Deployment()
deployment.setOptionalUserInput()
expect(deployment.maxErrorCount).toBe(null)
expect(core.warning).toHaveBeenCalledWith(
'Invalid error_count value will be ignored. Please ensure the value is a positive integer.'
)
})
it('sets the error_count input to null if negative and warns user', async () => {
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'error_count':
return '-1'
default:
return process.env[`INPUT_${param.toUpperCase()}`] || ''
}
})
const deployment = new Deployment()
deployment.setOptionalUserInput()
expect(deployment.maxErrorCount).toBe(null)
expect(core.warning).toHaveBeenCalledWith(
'Invalid error_count value will be ignored. Please ensure the value is a positive integer.'
)
})
it('sets the error_count input to null if not a number and warns user', async () => {
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'error_count':
return 'not a number'
default:
return process.env[`INPUT_${param.toUpperCase()}`] || ''
}
})
const deployment = new Deployment()
deployment.setOptionalUserInput()
expect(deployment.maxErrorCount).toBe(null)
expect(core.warning).toHaveBeenCalledWith(
'Invalid error_count value will be ignored. Please ensure the value is a positive integer.'
)
})
})
describe('#create', () => {
afterEach(() => {
// Remove mock for `core.getInput('preview')`
@@ -51,18 +148,18 @@ describe('Deployment', () => {
it('can successfully create a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -82,25 +179,61 @@ describe('Deployment', () => {
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('invokes #setOptionalUserInput', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
.reply(200, {
status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`,
page_url: 'https://actions.github.io/is-awesome'
})
core.getIDToken = jest.fn().mockResolvedValue(fakeJwt)
// Create the deployment
const deployment = new Deployment()
deployment.setOptionalUserInput = jest.fn()
await deployment.create(fakeJwt)
expect(deployment.setOptionalUserInput).toHaveBeenCalled()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('can successfully create a preview deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt,
preview: true
@@ -125,44 +258,36 @@ describe('Deployment', () => {
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('reports errors with failed artifact metadata exchange', async () => {
it('reports errors with failed artifact exchange', async () => {
process.env.GITHUB_SHA = 'invalid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(400, { message: 'Bad request' })
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(400, {})
// Create the deployment
const deployment = new Deployment()
await expect(deployment.create()).rejects.toEqual(
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`
)
)
artifactMetadataScope.done()
artifactExchangeScope.done()
})
it('reports errors with a failed 500 in a deployment', async () => {
process.env.GITHUB_SHA = 'build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
})
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] })
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://invalid-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA
})
.reply(500, { message: 'oh no' })
@@ -175,24 +300,19 @@ describe('Deployment', () => {
)
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('reports errors with an unexpected 403 during deployment', async () => {
process.env.GITHUB_SHA = 'build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
})
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] })
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://invalid-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA
})
.reply(403, { message: 'You are forbidden' })
@@ -205,24 +325,19 @@ describe('Deployment', () => {
)
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('reports errors with an unexpected 404 during deployment', async () => {
process.env.GITHUB_SHA = 'build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
})
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] })
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://invalid-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA
})
.reply(404, { message: 'Not found' })
@@ -235,24 +350,19 @@ describe('Deployment', () => {
)
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('reports errors with failed deployments', async () => {
process.env.GITHUB_SHA = 'invalid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
})
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, { value: [{ url: 'https://invalid-artifact.com', name: 'github-pages' }] })
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://invalid-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA
})
.reply(400, { message: 'Bad request' })
@@ -265,100 +375,26 @@ describe('Deployment', () => {
)
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('fails if there are multiple artifacts with the same name', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 2,
artifacts: [
{
id: 13,
name: `github-pages`,
size_in_bytes: 1400
},
{
id: 14,
name: `github-pages`,
size_in_bytes: 1620
}
]
})
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.`
)
artifactMetadataScope.done()
})
it('fails if there are no artifacts found', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(200, {
total_count: 0,
artifacts: []
})
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.`
)
artifactMetadataScope.done()
})
it('fails with error message if list artifact endpoint returns 500', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
.reply(500, { message: 'oh no' })
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.`
)
artifactMetadataScope.done()
})
it('warns if the artifact size is bigger than maximum', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactSize = ONE_GIGABYTE + 1
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [
{
id: 12,
name: `github-pages`,
size_in_bytes: `${artifactSize}`
}
value: [
{ url: 'https://fake-artifact.com', name: 'github-pages', size: `${artifactSize}` },
{ url: 'https://another-artifact.com', name: 'another-artifact' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 12,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -378,25 +414,25 @@ describe('Deployment', () => {
expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`))
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('warns when the timeout is greater than the maximum allowed', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -427,7 +463,7 @@ describe('Deployment', () => {
`Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.`
)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
})
@@ -436,18 +472,18 @@ describe('Deployment', () => {
it('sets output to success when deployment is successful', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -472,7 +508,7 @@ describe('Deployment', () => {
expect(core.setOutput).toBeCalledWith('status', 'succeed')
expect(core.info).toHaveBeenLastCalledWith('Reported success!')
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
deploymentStatusScope.done()
})
@@ -487,18 +523,18 @@ describe('Deployment', () => {
it('exits early when deployment is not in progress', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -515,25 +551,25 @@ describe('Deployment', () => {
await deployment.check()
expect(core.setFailed).toBeCalledWith('Unable to get deployment status.')
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
})
it('enforces max timeout', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -580,7 +616,7 @@ describe('Deployment', () => {
expect(core.error).toBeCalledWith('Timeout reached, aborting!')
expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!')
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
cancelDeploymentScope.done()
})
@@ -588,18 +624,18 @@ describe('Deployment', () => {
it('sets timeout to user timeout if user timeout is less than max timeout', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -646,7 +682,7 @@ describe('Deployment', () => {
expect(core.error).toBeCalledWith('Timeout reached, aborting!')
expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!')
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
cancelDeploymentScope.done()
})
@@ -654,18 +690,18 @@ describe('Deployment', () => {
it('sets output to success when timeout is set but not reached', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -682,7 +718,6 @@ describe('Deployment', () => {
core.getIDToken = jest.fn().mockResolvedValue(fakeJwt)
// Set timeout to great than max
jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) {
case 'artifact_name':
@@ -715,7 +750,7 @@ describe('Deployment', () => {
expect(core.setOutput).toBeCalledWith('status', 'succeed')
expect(core.info).toHaveBeenLastCalledWith('Reported success!')
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
deploymentStatusScope.done()
})
@@ -725,18 +760,18 @@ describe('Deployment', () => {
it('can successfully cancel a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -760,7 +795,7 @@ describe('Deployment', () => {
expect(core.info).toHaveBeenLastCalledWith(`Canceled deployment with ID ${process.env.GITHUB_SHA}`)
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
cancelDeploymentScope.done()
})
@@ -781,18 +816,18 @@ describe('Deployment', () => {
it('catches an error when trying to cancel a deployment', async () => {
process.env.GITHUB_SHA = 'valid-build-version'
const artifactMetadataScope = nock(`https://api.github.com`)
.get(
`/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`
)
const artifactExchangeScope = nock(`http://my-url`)
.get('/_apis/pipelines/workflows/123/artifacts?api-version=6.0-preview')
.reply(200, {
total_count: 1,
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
value: [
{ url: 'https://another-artifact.com', name: 'another-artifact' },
{ url: 'https://fake-artifact.com', name: 'github-pages' }
]
})
const createDeploymentScope = nock('https://api.github.com')
.post(`/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, {
artifact_id: 11,
artifact_url: 'https://fake-artifact.com&%24expand=SignedContent',
pages_build_version: process.env.GITHUB_SHA,
oidc_token: fakeJwt
})
@@ -817,7 +852,7 @@ describe('Deployment', () => {
expect(core.error).toHaveBeenCalledWith(`Canceling Pages deployment failed`, expect.anything())
artifactMetadataScope.done()
artifactExchangeScope.done()
createDeploymentScope.done()
cancelDeploymentScope.done()
})

View File

@@ -1,61 +1,119 @@
const core = require('@actions/core')
const github = require('@actions/github')
const hc = require('@actions/http-client')
const { RequestError } = require('@octokit/request-error')
const HttpStatusMessages = require('http-status-messages')
async function getArtifactMetadata({ githubToken, runId, artifactName }) {
const octokit = github.getOctokit(githubToken)
// All variables we need from the runtime are loaded here
const getContext = require('./context')
async function processRuntimeResponse(res, requestOptions) {
// Parse the response body as JSON
let obj = null
try {
core.info(`Fetching artifact metadata for ${artifactName} in run ${runId}`)
const response = await octokit.request(
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts?name={artifactName}',
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
run_id: runId,
artifactName: artifactName
}
)
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}.`
)
}
const artifact = response.data.artifacts[0]
core.debug(`Artifact: ${JSON.stringify(artifact)}`)
const artifactSize = artifact.size_in_bytes
if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
}
return {
id: artifact.id,
size: artifactSize
const contents = await res.readBody()
if (contents && contents.length > 0) {
obj = JSON.parse(contents)
}
} catch (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
)
// 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 {
const requestHeaders = {
accept: 'application/json',
authorization: `Bearer ${runtimeToken}`
}
const requestOptions = {
method: 'GET',
url: artifactExchangeUrl,
headers: {
...requestHeaders
},
body: null
}
core.info(`Artifact exchange URL: ${artifactExchangeUrl}`)
const res = await httpClient.get(artifactExchangeUrl, requestHeaders)
// May throw a RequestError (HttpError)
const response = await processRuntimeResponse(res, requestOptions)
data = response.data
core.debug(JSON.stringify(data))
} catch (error) {
core.error('Getting signed artifact URL failed', 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, artifactId, buildVersion, idToken, isPreview = false }) {
async function createPagesDeployment({ githubToken, artifactUrl, buildVersion, idToken, isPreview = false }) {
const octokit = github.getOctokit(githubToken)
const payload = {
artifact_id: artifactId,
artifact_url: artifactUrl,
pages_build_version: buildVersion,
oidc_token: idToken
}
@@ -115,7 +173,7 @@ async function cancelPagesDeployment({ githubToken, deploymentId }) {
}
module.exports = {
getArtifactMetadata,
getSignedArtifactMetadata,
createPagesDeployment,
getPagesDeploymentStatus,
cancelPagesDeployment

View File

@@ -3,7 +3,9 @@ const core = require('@actions/core')
// Load variables from Actions runtime
function getRequiredVars() {
return {
runTimeUrl: process.env.ACTIONS_RUNTIME_URL,
workflowRun: process.env.GITHUB_RUN_ID,
runTimeToken: process.env.ACTIONS_RUNTIME_TOKEN,
repositoryNwo: process.env.GITHUB_REPOSITORY,
buildVersion: process.env.GITHUB_SHA,
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
const getContext = require('./context')
const {
getArtifactMetadata,
getSignedArtifactMetadata,
createPagesDeployment,
getPagesDeploymentStatus,
cancelPagesDeployment
@@ -17,7 +17,6 @@ const temporaryErrorStatus = {
const finalErrorStatus = {
deployment_failed: 'Deployment failed, try again later.',
deployment_perms_error: 'Deployment failed, Please ensure that the file permissions are correct.',
deployment_content_failed:
'Artifact could not be deployed. Please ensure the content does not contain any hard links, symlinks and total size is less than 10GB.',
deployment_cancelled: 'Deployment cancelled.',
@@ -31,7 +30,9 @@ const SIZE_LIMIT_DESCRIPTION = '1 GB'
class Deployment {
constructor() {
const context = getContext()
this.runTimeUrl = context.runTimeUrl
this.repositoryNwo = context.repositoryNwo
this.runTimeToken = context.runTimeToken
this.buildVersion = context.buildVersion
this.buildActor = context.buildActor
this.actionsId = context.actionsId
@@ -44,28 +45,39 @@ class Deployment {
this.isPreview = context.isPreview === true
this.timeout = MAX_TIMEOUT
this.startTime = null
this.maxErrorCount = null
}
// Call GitHub api to fetch artifacts matching the provided name and deploy to GitHub Pages
// by creating a deployment with that artifact id
async create(idToken) {
if (Number(core.getInput('timeout')) > MAX_TIMEOUT) {
setOptionalUserInput() {
const timeoutInput = Number(core.getInput('timeout'))
if (timeoutInput > MAX_TIMEOUT) {
core.warning(
`Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.`
)
}
const timeoutInput = Number(core.getInput('timeout'))
this.timeout = !timeoutInput || timeoutInput <= 0 ? MAX_TIMEOUT : Math.min(timeoutInput, MAX_TIMEOUT)
const maxErrorCountInput = Number(core.getInput('error_count'))
if (!maxErrorCountInput || maxErrorCountInput <= 0) {
core.warning('Invalid error_count value will be ignored. Please ensure the value is a positive integer.')
} else {
this.maxErrorCount = maxErrorCountInput
}
}
// Ask the runtime for the unsigned artifact URL and deploy to GitHub Pages
// by creating a deployment with that artifact
async create(idToken) {
try {
this.setOptionalUserInput()
core.debug(`Actor: ${this.buildActor}`)
core.debug(`Action ID: ${this.actionsId}`)
core.debug(`Actions Workflow Run ID: ${this.workflowRun}`)
const artifactData = await getArtifactMetadata({
githubToken: this.githubToken,
runId: this.workflowRun,
const artifactData = await getSignedArtifactMetadata({
runtimeToken: this.runTimeToken,
workflowRunId: this.workflowRun,
artifactName: this.artifactName
})
@@ -77,7 +89,7 @@ class Deployment {
const deployment = await createPagesDeployment({
githubToken: this.githubToken,
artifactId: artifactData.id,
artifactUrl: artifactData.url,
buildVersion: this.buildVersion,
idToken,
isPreview: this.isPreview
@@ -100,6 +112,9 @@ class Deployment {
} catch (error) {
core.error(error.stack)
// output raw error in debug mode.
core.debug(JSON.stringify(error))
// build customized error message based on server response
if (error.response) {
let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}. `
@@ -141,7 +156,6 @@ class Deployment {
const deploymentId = this.deploymentInfo.id || this.buildVersion
const reportingInterval = Number(core.getInput('reporting_interval'))
const maxErrorCount = Number(core.getInput('error_count'))
let errorCount = 0
@@ -184,6 +198,9 @@ class Deployment {
} catch (error) {
core.error(error.stack)
// output raw error in debug mode.
core.debug(JSON.stringify(error))
// build customized error message based on server response
if (error.response) {
errorStatus = error.status || error.response.status
@@ -197,7 +214,7 @@ class Deployment {
}
}
if (errorCount >= maxErrorCount) {
if (errorCount >= this.maxErrorCount) {
core.error('Too many errors, aborting!')
core.setFailed('Failed with status code: ' + errorStatus)