Merge pull request #288 from actions/use-artifacts-client

Use the Actions artifacts client to avoid needing the `actions: read` permission
This commit is contained in:
James M. Greene
2023-12-22 13:31:06 -06:00
committed by GitHub
10 changed files with 100594 additions and 1271 deletions

View File

@@ -30,7 +30,6 @@ jobs:
permissions: permissions:
pages: write # to deploy to Pages pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source id-token: write # to verify the deployment originates from an appropriate source
actions: read # to download an artifact uploaded by `actions/upload-pages-artifact@v3`
# Deploy to the github-pages environment # Deploy to the github-pages environment
environment: environment:
@@ -77,7 +76,6 @@ There are a few important considerations to be aware of:
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`
- `id-token: write` - `id-token: write`
- `actions: read`
3. The deployment should target the `github-pages` environment (you may use a different environment name if needed, but this is not recommended.) 3. The deployment should target the `github-pages` environment (you may use a different environment name if needed, but this is not recommended.)

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: 81.39%"><title>Coverage: 81.39%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#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">81.39%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">81.39%</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 81.13%"><title>Coverage: 81.13%</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">81.13%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">81.13%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

97523
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

2766
dist/licenses.txt generated vendored

File diff suppressed because it is too large Load Diff

1186
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,11 @@
"description": "Deploy an actions artifact to GitHub Pages", "description": "Deploy an actions artifact to GitHub Pages",
"main": "./dist/index.js", "main": "./dist/index.js",
"dependencies": { "dependencies": {
"@actions/artifact": "^2.0.0",
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.0" "@actions/github": "^6.0.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

@@ -1,4 +1,6 @@
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')
@@ -7,6 +9,8 @@ const { Deployment, MAX_TIMEOUT, ONE_GIGABYTE, SIZE_LIMIT_DESCRIPTION } = requir
const fakeJwt = const fakeJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjllMWIxOC1jOGFiLTRhZGQtOGYxOC03MzVlMzVjZGJhZjAiLCJzdWIiOiJyZXBvOnBhcGVyLXNwYS9taW55aTplbnZpcm9ubWVudDpQcm9kdWN0aW9uIiwiYXVkIjoiaHR0cHM6Ly9naXRodWIuY29tL3BhcGVyLXNwYSIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInNoYSI6ImEyODU1MWJmODdiZDk3NTFiMzdiMmM0YjM3M2MxZjU3NjFmYWM2MjYiLCJyZXBvc2l0b3J5IjoicGFwZXItc3BhL21pbnlpIiwicmVwb3NpdG9yeV9vd25lciI6InBhcGVyLXNwYSIsInJ1bl9pZCI6IjE1NDY0NTkzNjQiLCJydW5fbnVtYmVyIjoiMzQiLCJydW5fYXR0ZW1wdCI6IjIiLCJhY3RvciI6IllpTXlzdHkiLCJ3b3JrZmxvdyI6IkNJIiwiaGVhZF9yZWYiOiIiLCJiYXNlX3JlZiI6IiIsImV2ZW50X25hbWUiOiJwdXNoIiwicmVmX3R5cGUiOiJicmFuY2giLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJqb2Jfd29ya2Zsb3dfcmVmIjoicGFwZXItc3BhL21pbnlpLy5naXRodWIvd29ya2Zsb3dzL2JsYW5rLnltbEByZWZzL2hlYWRzL21haW4iLCJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwibmJmIjoxNjM4ODI4MDI4LCJleHAiOjE2Mzg4Mjg5MjgsImlhdCI6MTYzODgyODYyOH0.1wyupfxu1HGoTyIqatYg0hIxy2-0bMO-yVlmLSMuu2w' 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjllMWIxOC1jOGFiLTRhZGQtOGYxOC03MzVlMzVjZGJhZjAiLCJzdWIiOiJyZXBvOnBhcGVyLXNwYS9taW55aTplbnZpcm9ubWVudDpQcm9kdWN0aW9uIiwiYXVkIjoiaHR0cHM6Ly9naXRodWIuY29tL3BhcGVyLXNwYSIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInNoYSI6ImEyODU1MWJmODdiZDk3NTFiMzdiMmM0YjM3M2MxZjU3NjFmYWM2MjYiLCJyZXBvc2l0b3J5IjoicGFwZXItc3BhL21pbnlpIiwicmVwb3NpdG9yeV9vd25lciI6InBhcGVyLXNwYSIsInJ1bl9pZCI6IjE1NDY0NTkzNjQiLCJydW5fbnVtYmVyIjoiMzQiLCJydW5fYXR0ZW1wdCI6IjIiLCJhY3RvciI6IllpTXlzdHkiLCJ3b3JrZmxvdyI6IkNJIiwiaGVhZF9yZWYiOiIiLCJiYXNlX3JlZiI6IiIsImV2ZW50X25hbWUiOiJwdXNoIiwicmVmX3R5cGUiOiJicmFuY2giLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJqb2Jfd29ya2Zsb3dfcmVmIjoicGFwZXItc3BhL21pbnlpLy5naXRodWIvd29ya2Zsb3dzL2JsYW5rLnltbEByZWZzL2hlYWRzL21haW4iLCJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwibmJmIjoxNjM4ODI4MDI4LCJleHAiOjE2Mzg4Mjg5MjgsImlhdCI6MTYzODgyODYyOH0.1wyupfxu1HGoTyIqatYg0hIxy2-0bMO-yVlmLSMuu2w'
const LIST_ARTIFACTS_TWIRP_PATH = '/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts'
describe('Deployment', () => { describe('Deployment', () => {
let mockPool let mockPool
@@ -19,6 +23,10 @@ describe('Deployment', () => {
process.env.GITHUB_ACTOR = 'monalisa' process.env.GITHUB_ACTOR = 'monalisa'
process.env.GITHUB_ACTION = '__monalisa/octocat' process.env.GITHUB_ACTION = '__monalisa/octocat'
process.env.GITHUB_ACTION_PATH = 'something' process.env.GITHUB_ACTION_PATH = 'something'
// A valid actions token must have an 'scp' field whose value is a space-delimited list of strings
process.env.ACTIONS_RUNTIME_TOKEN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY3AiOiJBY3Rpb25zLkV4YW1wbGVTY29wZSBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhOmNhMzk1MDg1LTA0MGEtNTI2Yi0yY2U4LWJkYzg1ZjY5Mjc3NCJ9.l-VcBU1PeNk_lWpOhjWehQlYyjCcY2dp_EMt7Rf06io'
process.env.ACTIONS_RESULTS_URL = 'https://actions-results-url.biz'
jest.spyOn(core, 'getInput').mockImplementation(param => { jest.spyOn(core, 'getInput').mockImplementation(param => {
switch (param) { switch (param) {
@@ -48,7 +56,7 @@ describe('Deployment', () => {
jest.spyOn(core, 'debug').mockImplementation(jest.fn()) jest.spyOn(core, 'debug').mockImplementation(jest.fn())
// Set up Fetch mocking // Set up Fetch mocking
const mockAgent = new MockAgent() let mockAgent = new MockAgent()
mockAgent.disableNetConnect() mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent) setGlobalDispatcher(mockAgent)
mockPool = mockAgent.get('https://api.github.com') mockPool = mockAgent.get('https://api.github.com')
@@ -63,16 +71,12 @@ 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -114,21 +118,18 @@ 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}`))
) )
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -176,39 +177,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}`))
) )
twirpScope.done()
}) })
it('reports errors with failed artifact metadata 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`, .reply(400, { msg: 'yikes!' }, { 'content-type': 'application/json' })
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.toThrow(
new Error( `Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}.`
`Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad request`
)
) )
expect(core.error).toHaveBeenNthCalledWith(
1,
'Listing artifact metadata failed',
new Error('Failed to ListArtifacts: Received non-retryable error: Failed request: (400) null: yikes!')
)
expect(core.error).toHaveBeenNthCalledWith(
2,
'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages, or Actions? Please re-run the deployment at a later time.',
expect.any(Error)
)
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -238,20 +242,17 @@ 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.`
) )
) )
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -281,20 +282,17 @@ 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".`
) )
) )
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -324,20 +322,17 @@ 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`
) )
) )
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -367,31 +362,20 @@ 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`
) )
) )
twirpScope.done()
}) })
it('fails if there are multiple artifacts with the same name', async () => { it('fails if there are multiple artifacts with the same name', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 2,
artifacts: [ artifacts: [
{ { databaseId: 13, name: 'github-pages', size: 1400 },
id: 13, { databaseId: 14, name: 'github-pages', size: 1620 }
name: `github-pages`,
size_in_bytes: 1400
},
{
id: 14,
name: `github-pages`,
size_in_bytes: 1620
}
] ]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
@@ -399,22 +383,19 @@ describe('Deployment', () => {
const deployment = new Deployment() const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow( await expect(deployment.create(fakeJwt)).rejects.toThrow(
`Multiple artifact unexpectedly found for workflow run ${process.env.GITHUB_RUN_ID}. Artifact count is 2.` `Multiple artifacts named "github-pages" were unexpectedly found for this workflow run. Artifact count is 2.`
) )
twirpScope.done()
}) })
it('fails if there are no artifacts found', async () => { it('fails if there are no artifacts found', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 0,
artifacts: [] artifacts: []
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
@@ -422,40 +403,46 @@ describe('Deployment', () => {
const deployment = new Deployment() const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow( 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.` `No artifacts named "github-pages" were found for this workflow run. Ensure artifacts are uploaded with actions/artifact@v4 or later.`
) )
twirpScope.done()
}) })
it('fails with error message if list artifact endpoint returns 500', async () => { it('fails with error message if list artifact endpoint returns 501', async () => {
process.env.GITHUB_SHA = 'valid-build-version' process.env.GITHUB_SHA = 'valid-build-version'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`, .reply(501, { msg: 'oh no' }, { headers: { 'content-type': 'application/json' } })
method: 'GET'
})
.reply(500, { message: 'oh no' }, { headers: { 'content-type': 'application/json' } })
const deployment = new Deployment() const deployment = new Deployment()
await expect(deployment.create(fakeJwt)).rejects.toThrow( 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.` `Failed to create deployment (status: 501) 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.`
) )
expect(core.error).toHaveBeenNthCalledWith(
1,
'Listing artifact metadata failed',
new Error('Failed to ListArtifacts: Received non-retryable error: Failed request: (501) null: oh no')
)
expect(core.error).toHaveBeenNthCalledWith(
2,
'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages, or Actions? Please re-run the deployment at a later time.',
expect.any(Error)
)
twirpScope.done()
}) })
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
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 12, name: 'github-pages', size: artifactSize }]
artifacts: [{ id: 12, name: `github-pages`, size_in_bytes: artifactSize }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -497,21 +484,18 @@ 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}`))
) )
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -567,6 +551,7 @@ 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.`
) )
twirpScope.done()
}) })
}) })
@@ -574,16 +559,12 @@ 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -631,6 +612,7 @@ 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!')
twirpScope.done()
}) })
it('fails check when no deployment is found', async () => { it('fails check when no deployment is found', async () => {
@@ -643,16 +625,12 @@ 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -691,21 +669,18 @@ 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.')
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -790,21 +765,18 @@ 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!')
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -878,21 +850,18 @@ 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!')
twirpScope.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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -967,6 +936,7 @@ 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!')
twirpScope.done()
}) })
}) })
@@ -974,16 +944,12 @@ 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -1032,6 +998,7 @@ 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}`)
twirpScope.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 () => {
@@ -1050,16 +1017,12 @@ 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'
mockPool const twirpScope = nock(process.env.ACTIONS_RESULTS_URL)
.intercept({ .post(LIST_ARTIFACTS_TWIRP_PATH)
path: `/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/artifacts?name=github-pages`,
method: 'GET'
})
.reply( .reply(
200, 200,
{ {
total_count: 1, artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }]
artifacts: [{ id: 11, name: `github-pages`, size_in_bytes: 221 }]
}, },
{ headers: { 'content-type': 'application/json' } } { headers: { 'content-type': 'application/json' } }
) )
@@ -1108,6 +1071,7 @@ 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())
twirpScope.done()
}) })
}) })
}) })

View File

@@ -1,50 +1,107 @@
const core = require('@actions/core') const core = require('@actions/core')
const github = require('@actions/github') const github = require('@actions/github')
const { DefaultArtifactClient } = require('@actions/artifact')
const { RequestError } = require('@octokit/request-error')
const HttpStatusMessages = require('http-status-messages')
async function getArtifactMetadata({ githubToken, runId, artifactName }) { function wrapTwirpResponseLikeOctokit(twirpResponse, requestOptions) {
const octokit = github.getOctokit(githubToken) // Specific response shape aligned with Octokit
const response = {
url: requestOptions.url,
status: 200,
headers: {
...requestOptions.headers
},
data: twirpResponse
}
return response
}
// Mimic the errors thrown by Octokit for consistency.
function wrapTwirpErrorLikeOctokit(twirpError, requestOptions) {
const rawErrorMsg = twirpError?.message || twirpError?.toString() || ''
const statusCodeMatch = rawErrorMsg.match(/Failed request: \((?<statusCode>\d+)\)/)
const statusCode = statusCodeMatch?.groups?.statusCode ?? 500
// Try to provide the best error message
const errorMsg =
rawErrorMsg ||
// Fallback to the HTTP status message based on the status code
HttpStatusMessages[statusCode] ||
// Or if the status code is unexpected...
`Unknown error (${statusCode})`
// RequestError is an Octokit-specific class
return new RequestError(errorMsg, statusCode, {
response: {
url: requestOptions.url,
status: statusCode,
headers: {
...requestOptions.headers
},
data: rawErrorMsg ? { message: rawErrorMsg } : ''
},
request: requestOptions
})
}
function getArtifactsServiceOrigin() {
const resultsUrl = process.env.ACTIONS_RESULTS_URL
return resultsUrl ? new URL(resultsUrl).origin : ''
}
async function getArtifactMetadata({ artifactName }) {
const artifactClient = new DefaultArtifactClient()
// Primarily for debugging purposes, accuracy is not critical
const requestOptions = {
method: 'POST',
url: `${getArtifactsServiceOrigin()}/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts`,
headers: {
'content-type': 'application/json'
},
body: {}
}
try { try {
core.info(`Fetching artifact metadata for ${artifactName} in run ${runId}`) core.info(`Fetching artifact metadata for "${artifactName}" in this workflow run`)
const response = await octokit.request( let response
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts?name={artifactName}', try {
{ const twirpResponse = await artifactClient.listArtifacts()
owner: github.context.repo.owner, response = wrapTwirpResponseLikeOctokit(twirpResponse, requestOptions)
repo: github.context.repo.repo, } catch (twirpError) {
run_id: runId, core.error('Listing artifact metadata failed', twirpError)
artifactName: artifactName const octokitError = wrapTwirpErrorLikeOctokit(twirpError, requestOptions)
} throw octokitError
) }
const artifactCount = response.data.total_count const filteredArtifacts = response.data.artifacts.filter(artifact => artifact.name === artifactName)
const artifactCount = filteredArtifacts.length
core.debug(`List artifact count: ${artifactCount}`) core.debug(`List artifact count: ${artifactCount}`)
if (artifactCount === 0) { if (artifactCount === 0) {
throw new Error( throw new Error(
`No artifacts found for workflow run ${runId}. Ensure artifacts are uploaded with actions/artifact@v4 or later.` `No artifacts named "${artifactName}" were found for this workflow run. Ensure artifacts are uploaded with actions/artifact@v4 or later.`
) )
} else if (artifactCount > 1) { } else if (artifactCount > 1) {
throw new Error( throw new Error(
`Multiple artifact unexpectedly found for workflow run ${runId}. Artifact count is ${artifactCount}.` `Multiple artifacts named "${artifactName}" were unexpectedly found for this workflow run. Artifact count is ${artifactCount}.`
) )
} }
const artifact = response.data.artifacts[0] const artifact = filteredArtifacts[0]
core.debug(`Artifact: ${JSON.stringify(artifact)}`) core.debug(`Artifact: ${JSON.stringify(artifact)}`)
const artifactSize = artifact.size_in_bytes if (!artifact.size) {
if (!artifactSize) {
core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.') core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.')
} }
return { return artifact
id: artifact.id,
size: artifactSize
}
} catch (error) { } catch (error) {
core.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.', '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 error
) )
throw error throw error

View File

@@ -63,11 +63,7 @@ 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 getArtifactMetadata({ const artifactData = await getArtifactMetadata({ artifactName: this.artifactName })
githubToken: this.githubToken,
runId: this.workflowRun,
artifactName: this.artifactName
})
if (artifactData?.size > ONE_GIGABYTE) { if (artifactData?.size > ONE_GIGABYTE) {
core.warning( core.warning(
@@ -102,14 +98,14 @@ class Deployment {
// build customized error message based on server response // build customized error message based on server response
if (error.response) { if (error.response) {
let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}. ` let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}.`
if (error.status === 400) { if (error.status === 400) {
errorMessage += `Responded with: ${error.message}` errorMessage += ` Responded with: ${error.message}`
} else if (error.status === 403) { } else if (error.status === 403) {
errorMessage += 'Ensure GITHUB_TOKEN has permission "pages: write".' errorMessage += ' Ensure GITHUB_TOKEN has permission "pages: write".'
} else if (error.status === 404) { } else if (error.status === 404) {
const pagesSettingsUrl = `${this.githubServerUrl}/${this.repositoryNwo}/settings/pages` const pagesSettingsUrl = `${this.githubServerUrl}/${this.repositoryNwo}/settings/pages`
errorMessage += `Ensure GitHub Pages has been enabled: ${pagesSettingsUrl}` errorMessage += ` Ensure GitHub Pages has been enabled: ${pagesSettingsUrl}`
// If using GHES, add a special note about compatibility // If using GHES, add a special note about compatibility
if (new URL(this.githubServerUrl).hostname.toLowerCase() !== 'github.com') { if (new URL(this.githubServerUrl).hostname.toLowerCase() !== 'github.com') {
errorMessage += errorMessage +=
@@ -117,7 +113,7 @@ class Deployment {
} }
} else if (error.status >= 500) { } else if (error.status >= 500) {
errorMessage += errorMessage +=
'Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.' ' Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.'
} }
throw new Error(errorMessage) throw new Error(errorMessage)
} else { } else {