check github_ref tag and sha are checked out on parse

This commit is contained in:
Conor Sloan
2024-04-15 13:45:54 +01:00
parent 507635d01b
commit 17c0582657
10 changed files with 5290 additions and 9 deletions
+47
View File
@@ -212,3 +212,50 @@ describe('readFileContents', () => {
expect(fsHelper.readFileContents(tempFile).toString()).toEqual(fileContent)
})
})
describe('ensureCorrectShaCheckedOut', () => {
let dir: string
let commit1: string
let commit2: string
const tag1 = 'tag1'
const tag2 = 'tag2'
beforeEach(() => {
dir = fsHelper.createTempDir(tmpFileDir, 'subdir')
// Set up a git repository with two commits
execSync('git init', { cwd: dir })
execSync('git commit --allow-empty -m "test"', { cwd: dir })
execSync('git commit --allow-empty -m "test"', { cwd: dir })
// Grab the two commits
commit1 = execSync('git rev-parse HEAD~1', { cwd: dir }).toString().trim()
commit2 = execSync('git rev-parse HEAD', { cwd: dir }).toString().trim()
// Create a tag for each commit
execSync(`git tag ${tag1} ${commit1}`, { cwd: dir })
execSync(`git tag ${tag2} ${commit2}`, { cwd: dir })
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('does not throw an error if the correct SHA is checked out', async () => {
await expect(
fsHelper.ensureCorrectShaCheckedOut(`refs/tags/${tag2}`, commit2, dir)
).resolves.toBeUndefined()
})
it('throws an error if the correct SHA is not checked out', async () => {
await expect(
fsHelper.ensureCorrectShaCheckedOut(`refs/tags/${tag1}`, commit1, dir)
).rejects.toThrow()
})
it('throws an error if the sha of the tag does not match expected sha', async () => {
await expect(async () =>
fsHelper.ensureCorrectShaCheckedOut(`refs/tags/${tag1}`, commit2, dir)
).rejects.toThrow()
})
})
+35
View File
@@ -23,6 +23,7 @@ let setOutputMock: jest.SpyInstance
let createTempDirMock: jest.SpyInstance
let createArchivesMock: jest.SpyInstance
let stageActionFilesMock: jest.SpyInstance
let ensureCorrectShaCheckedOutMock: jest.SpyInstance
let publishOCIArtifactMock: jest.SpyInstance
// Mock the config resolution
@@ -49,6 +50,9 @@ describe('run', () => {
stageActionFilesMock = jest
.spyOn(fsHelper, 'stageActionFiles')
.mockImplementation()
ensureCorrectShaCheckedOutMock = jest
.spyOn(fsHelper, 'ensureCorrectShaCheckedOut')
.mockImplementation()
// GHCR Client mocks
publishOCIArtifactMock = jest
@@ -93,9 +97,24 @@ describe('run', () => {
}
})
it('fails if ensuring the correct SHA is checked out errors', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating staging temp directory fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
@@ -110,6 +129,8 @@ describe('run', () => {
it('fails if staging files fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'tmpDir/staging'
})
@@ -128,6 +149,8 @@ describe('run', () => {
it('fails if creating archives temp directory fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation((_, path: string) => {
if (path === 'staging') {
return 'staging'
@@ -147,6 +170,8 @@ describe('run', () => {
it('fails if creating archives fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
@@ -167,6 +192,8 @@ describe('run', () => {
it('fails if publishing OCI artifact fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
@@ -202,6 +229,8 @@ describe('run', () => {
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
@@ -246,6 +275,8 @@ describe('run', () => {
options.isEnterprise = true
resolvePublishActionOptionsMock.mockReturnValue(options)
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
@@ -302,6 +333,8 @@ describe('run', () => {
it('uploads the artifact, returns package metadata from GHCR, and creates an attestation in non-enterprise for public repo', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
@@ -383,6 +416,8 @@ describe('run', () => {
resolvePublishActionOptionsMock.mockReturnValue(opts)
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
+1 -1
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: 96.63%"><title>Coverage: 96.63%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.63%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.63%</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: 96.78%"><title>Coverage: 96.78%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">96.78%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">96.78%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Generated Vendored
+5087 -3
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+52
View File
@@ -82,6 +82,55 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
@kwsites/file-exists
MIT
The MIT License (MIT)
Copyright (c) 2015 Steve King
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@kwsites/promise-deferred
MIT
MIT License
Copyright (c) 2018 kwsites
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@npmcli/agent
ISC
@@ -3213,6 +3262,9 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
simple-git
MIT
smart-buffer
MIT
The MIT License (MIT)
+28
View File
@@ -16,6 +16,7 @@
"@types/fs-extra": "^11.0.4",
"archiver": "^6.0.1",
"fs-extra": "^11.2.0",
"simple-git": "^3.22.0",
"tar": "^6.2.0"
},
"devDependencies": {
@@ -1408,6 +1409,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kwsites/file-exists": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
"dependencies": {
"debug": "^4.1.1"
}
},
"node_modules/@kwsites/promise-deferred": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7496,6 +7510,20 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/simple-git": {
"version": "3.24.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.24.0.tgz",
"integrity": "sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==",
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
"debug": "^4.3.4"
},
"funding": {
"type": "github",
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+2 -1
View File
@@ -74,7 +74,8 @@
"@types/fs-extra": "^11.0.4",
"archiver": "^6.0.1",
"fs-extra": "^11.2.0",
"tar": "^6.2.0"
"tar": "^6.2.0",
"simple-git": "^3.22.0"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
+2 -2
View File
@@ -8,8 +8,6 @@ export interface PublishActionOptions {
nameWithOwner: string
// The GitHub token to use for API requests
token: string
// The commit SHA to reset back to after the action completes
sha: string
// The base URL for the GitHub API
apiBaseUrl: string
// The base URL for the GitHub Container Registry
@@ -30,6 +28,8 @@ export interface PublishActionOptions {
event: string
// The ref that triggered the action, associated with the event
ref: string
// The commit SHA associated with the ref that triggered the action
sha: string
}
export async function resolvePublishActionOptions(): Promise<PublishActionOptions> {
+26
View File
@@ -4,6 +4,7 @@ import * as path from 'path'
import * as tar from 'tar'
import * as archiver from 'archiver'
import * as crypto from 'crypto'
import * as simpleGit from 'simple-git'
export interface FileMetadata {
path: string
@@ -114,6 +115,31 @@ export function stageActionFiles(actionDir: string, targetDir: string): void {
}
}
// Ensure the correct SHA is checked out for the tag by inspecting the git metadata in the workspace
// and comparing it to the information actions provided us.
// Provided ref should be in format refs/tags/<tagname>.
export async function ensureCorrectShaCheckedOut(
tagRef: string,
expectedSha: string,
gitDir: string
): Promise<void> {
const git: simpleGit.SimpleGit = simpleGit.simpleGit(gitDir)
const tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef])
if (tagCommitSha.trim() !== expectedSha) {
throw new Error(
`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`
)
}
const currentlyCheckedOutSha = await git.revparse(['HEAD'])
if (currentlyCheckedOutSha.trim() !== expectedSha) {
throw new Error(
`The expected commit associated with the tag ${tagRef} is not checked out.`
)
}
}
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
async function fileMetadata(filePath: string): Promise<FileMetadata> {
const stats = fs.statSync(filePath)
+10 -2
View File
@@ -18,7 +18,7 @@ export async function run(): Promise<void> {
core.info(`Publishing action package version with options:`)
core.info(cfg.serializeOptions(options))
const semverTag: semver.SemVer = parseSemverTagFromRef(options.ref)
const semverTag: semver.SemVer = await parseSemverTagFromRef(options)
const stagedActionFilesDir = fsHelper.createTempDir(
options.runnerTempDir,
@@ -77,7 +77,11 @@ export async function run(): Promise<void> {
// This action can be triggered by any workflow that specifies a tag as its GITHUB_REF.
// This includes releases, creating or pushing tags, or workflow_dispatch.
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#about-events-that-trigger-workflows.
function parseSemverTagFromRef(ref: string): semver.SemVer {
async function parseSemverTagFromRef(
opts: cfg.PublishActionOptions
): Promise<semver.SemVer> {
const ref = opts.ref
if (!ref.startsWith('refs/tags/')) {
throw new Error(`The ref ${ref} is not a valid tag reference.`)
}
@@ -89,6 +93,10 @@ function parseSemverTagFromRef(ref: string): semver.SemVer {
`${rawTag} is not a valid semantic version tag, and so cannot be uploaded to the action package.`
)
}
// Ensure the correct SHA is checked out for the tag we're parsing, otherwise the bundled content will be incorrect.
await fsHelper.ensureCorrectShaCheckedOut(ref, opts.sha, opts.workspaceDir)
return semverTag
}