Tying up loose ends (#54)
* various qol updates to publish action * review comments and run bundle
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
getRepositoryMetadata,
|
||||
getContainerRegistryURL
|
||||
} from '../src/api-client'
|
||||
|
||||
let fetchMock: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = jest.spyOn(global, 'fetch')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.mockRestore()
|
||||
})
|
||||
|
||||
describe('getRepositoryMetadata', () => {
|
||||
it('returns repository metadata when the fetch response is ok', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ id: '123', owner: { id: '456' } }))
|
||||
)
|
||||
const result = await getRepositoryMetadata('repository', 'token')
|
||||
expect(result).toEqual({ repoId: '123', ownerId: '456' })
|
||||
})
|
||||
|
||||
it('throws an error when the fetch errors', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('API is down'))
|
||||
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
|
||||
'API is down'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error when the response status is not ok', async () => {
|
||||
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
||||
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
|
||||
'Failed to fetch repository metadata due to bad status code: 500'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error when the response data is in the wrong format', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ wrong: 'format' }))
|
||||
)
|
||||
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
|
||||
'Failed to fetch repository metadata: unexpected response format'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getContainerRegistryURL', () => {
|
||||
it('returns container registry URL when the fetch response is ok', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ url: 'https://registry.example.com' }))
|
||||
)
|
||||
const result = await getContainerRegistryURL()
|
||||
expect(result).toEqual(new URL('https://registry.example.com'))
|
||||
})
|
||||
|
||||
it('throws an error when the fetch errors', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('API is down'))
|
||||
await expect(getContainerRegistryURL()).rejects.toThrow('API is down')
|
||||
})
|
||||
|
||||
it('throws an error when the response status is not ok', async () => {
|
||||
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
||||
await expect(getContainerRegistryURL()).rejects.toThrow(
|
||||
'Failed to fetch container registry url due to bad status code: 500'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error when the response data is in the wrong format', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ wrong: 'format' }))
|
||||
)
|
||||
await expect(getContainerRegistryURL()).rejects.toThrow(
|
||||
'Failed to fetch repository metadata: unexpected response format'
|
||||
)
|
||||
})
|
||||
})
|
||||
+44
-121
@@ -6,115 +6,65 @@ import { execSync } from 'child_process'
|
||||
|
||||
const fileContent = 'This is the content of the file'
|
||||
|
||||
describe('getConsolidatedDirectory', () => {
|
||||
describe('stageActionFiles', () => {
|
||||
let sourceDir: string
|
||||
let stagingDir: string
|
||||
|
||||
beforeAll(() => {
|
||||
sourceDir = `.` // fsHelper.createTempDir()
|
||||
fs.mkdirSync(`${sourceDir}/folder1`)
|
||||
fs.mkdirSync(`${sourceDir}/folder2`)
|
||||
fs.mkdirSync(`${sourceDir}/folder2/folder3`)
|
||||
fs.writeFileSync(`${sourceDir}/file0.txt`, fileContent)
|
||||
fs.writeFileSync(`${sourceDir}/folder1/file1.txt`, fileContent)
|
||||
fs.writeFileSync(`${sourceDir}/folder2/file2.txt`, fileContent)
|
||||
fs.writeFileSync(`${sourceDir}/folder2/folder3/file3.txt`, fileContent)
|
||||
beforeEach(() => {
|
||||
sourceDir = fsHelper.createTempDir()
|
||||
fs.mkdirSync(`${sourceDir}/src`)
|
||||
fs.writeFileSync(`${sourceDir}/src/main.js`, fileContent)
|
||||
fs.writeFileSync(`${sourceDir}/src/other.js`, fileContent)
|
||||
|
||||
stagingDir = fsHelper.createTempDir()
|
||||
})
|
||||
|
||||
beforeEach(() => {})
|
||||
|
||||
afterEach(() => {})
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(`file0.txt`)
|
||||
fs.rmSync(`folder1`, { recursive: true })
|
||||
fs.rmSync(`folder2`, { recursive: true })
|
||||
afterEach(() => {
|
||||
fs.rmSync(sourceDir, { recursive: true })
|
||||
fs.rmSync(stagingDir, { recursive: true })
|
||||
})
|
||||
|
||||
it('returns the directory itself if it is a single directory, and instructed to not clean it up', () => {
|
||||
// TODO: In these tests, we're not really distinguishing between the `publish-action-package` directory and the consumer repo directory, i.e., they share the same space.
|
||||
// In real life, when the consumer workflow runs, its own javascript is in ., but
|
||||
// the publish-action-package's code is in ${{github.action_path}}.
|
||||
// So.... I guess to emulate this, we should create a temp directory (representing the consumer repo)
|
||||
// and cd there before the test starts?
|
||||
const { consolidatedPath, needToCleanUpDir } =
|
||||
fsHelper.getConsolidatedDirectory('.')
|
||||
|
||||
expect(needToCleanUpDir).toBe(false)
|
||||
expect(consolidatedPath).toBe('.')
|
||||
expect(fsHelper.readFileContents(`file0.txt`).toString()).toEqual(
|
||||
fileContent
|
||||
it('returns an error if no action.yml file is present', () => {
|
||||
expect(() => fsHelper.stageActionFiles(sourceDir, stagingDir)).toThrow(
|
||||
/^No action.yml or action.yaml file found in source repository/
|
||||
)
|
||||
expect(fsHelper.readFileContents(`folder1/file1.txt`).toString()).toEqual(
|
||||
fileContent
|
||||
)
|
||||
expect(fsHelper.readFileContents(`folder2/file2.txt`).toString()).toEqual(
|
||||
fileContent
|
||||
)
|
||||
expect(
|
||||
fsHelper.readFileContents(`folder2/folder3/file3.txt`).toString()
|
||||
).toEqual(fileContent)
|
||||
})
|
||||
it('returns a new directory containing copies of the multiple paths if they are legally specified, and instruct to clean it up', () => {
|
||||
const { consolidatedPath, needToCleanUpDir } =
|
||||
fsHelper.getConsolidatedDirectory('file0.txt folder1')
|
||||
|
||||
expect(needToCleanUpDir).toBe(true)
|
||||
expect(consolidatedPath).not.toBe('.')
|
||||
expect(
|
||||
fsHelper
|
||||
.readFileContents(path.join(consolidatedPath, `file0.txt`))
|
||||
.toString()
|
||||
).toEqual(fileContent)
|
||||
expect(
|
||||
fsHelper
|
||||
.readFileContents(path.join(consolidatedPath, `folder1/file1.txt`))
|
||||
.toString()
|
||||
).toEqual(fileContent)
|
||||
expect(
|
||||
fs.existsSync(path.join(consolidatedPath, `folder2/file2.txt`))
|
||||
).toEqual(false)
|
||||
expect(
|
||||
fs.existsSync(path.join(consolidatedPath, `folder2/folder3/file3.txt`))
|
||||
).toEqual(false)
|
||||
})
|
||||
|
||||
it('what happens here?', () => {
|
||||
const { consolidatedPath, needToCleanUpDir } =
|
||||
fsHelper.getConsolidatedDirectory('folder1 folder2/folder3')
|
||||
it('copies all non-hidden files to the staging directory', () => {
|
||||
fs.writeFileSync(`${sourceDir}/action.yml`, fileContent)
|
||||
|
||||
expect(needToCleanUpDir).toBe(true)
|
||||
expect(consolidatedPath).not.toBe('.')
|
||||
expect(fs.existsSync(path.join(consolidatedPath, `file0.txt`))).toEqual(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
fsHelper
|
||||
.readFileContents(path.join(consolidatedPath, `folder1/file1.txt`))
|
||||
.toString()
|
||||
).toEqual(fileContent)
|
||||
expect(
|
||||
fs.existsSync(path.join(consolidatedPath, `folder2/file2.txt`))
|
||||
).toEqual(false)
|
||||
expect(
|
||||
fsHelper
|
||||
.readFileContents(path.join(consolidatedPath, `folder3/file3.txt`))
|
||||
.toString()
|
||||
).toEqual(fileContent) // <--- TODO: This is what I'm unsure of
|
||||
fs.mkdirSync(`${sourceDir}/.git`)
|
||||
fs.writeFileSync(`${sourceDir}/.git/HEAD`, fileContent)
|
||||
|
||||
fs.mkdirSync(`${sourceDir}/.github/workflows`, { recursive: true })
|
||||
fs.writeFileSync(`${sourceDir}/.github/workflows/workflow.yml`, fileContent)
|
||||
|
||||
fsHelper.stageActionFiles(sourceDir, stagingDir)
|
||||
expect(fs.existsSync(`${stagingDir}/action.yml`)).toBe(true)
|
||||
expect(fs.existsSync(`${stagingDir}/src/main.js`)).toBe(true)
|
||||
expect(fs.existsSync(`${stagingDir}/src/other.js`)).toBe(true)
|
||||
|
||||
// Hidden files should not be copied
|
||||
expect(fs.existsSync(`${stagingDir}/.git`)).toBe(false)
|
||||
expect(fs.existsSync(`${stagingDir}/.github`)).toBe(false)
|
||||
})
|
||||
|
||||
it('throws an error for illegal path spec - single', () => {
|
||||
expect(() => {
|
||||
fsHelper.getConsolidatedDirectory('folder4')
|
||||
}).toThrow('filePath folder4 does not exist')
|
||||
it('copies all non-hidden files to the staging directory, even if action.yml is in a subdirectory', () => {
|
||||
fs.mkdirSync(`${sourceDir}/my-sub-action`, { recursive: true })
|
||||
fs.writeFileSync(`${sourceDir}/my-sub-action/action.yml`, fileContent)
|
||||
|
||||
fsHelper.stageActionFiles(sourceDir, stagingDir)
|
||||
expect(fs.existsSync(`${stagingDir}/src/main.js`)).toBe(true)
|
||||
expect(fs.existsSync(`${stagingDir}/src/other.js`)).toBe(true)
|
||||
expect(fs.existsSync(`${stagingDir}/my-sub-action/action.yml`)).toBe(true)
|
||||
})
|
||||
|
||||
it('throws an error for illegal path spec - multiple', () => {
|
||||
expect(() => {
|
||||
fsHelper.getConsolidatedDirectory('folder1 folder4')
|
||||
}).toThrow('filePath folder4 does not exist')
|
||||
})
|
||||
it('accepts action.yaml as a valid action file as well as action.yml', () => {
|
||||
fs.writeFileSync(`${sourceDir}/action.yaml`, fileContent)
|
||||
|
||||
// TODO: consider doing the thing Michael suggested where we exclude directories starting with .
|
||||
fsHelper.stageActionFiles(sourceDir, stagingDir)
|
||||
expect(fs.existsSync(`${stagingDir}/action.yaml`)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createArchives', () => {
|
||||
@@ -243,33 +193,6 @@ describe('isDirectory', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isActionRepo', () => {
|
||||
let stagingDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
stagingDir = fsHelper.createTempDir()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(stagingDir, { recursive: true })
|
||||
})
|
||||
|
||||
it('returns true if action.yml exists at the root', () => {
|
||||
fs.writeFileSync(path.join(stagingDir, `action.yml`), fileContent)
|
||||
expect(fsHelper.isActionRepo(stagingDir)).toEqual(true)
|
||||
})
|
||||
|
||||
it('returns true if action.yaml exists at the root', () => {
|
||||
fs.writeFileSync(path.join(stagingDir, `action.yaml`), fileContent)
|
||||
expect(fsHelper.isActionRepo(stagingDir)).toEqual(true)
|
||||
})
|
||||
|
||||
it("returns false if action.y(a)ml doesn't exist at the root", () => {
|
||||
fs.writeFileSync(path.join(stagingDir, `action.yaaml`), fileContent)
|
||||
expect(fsHelper.isActionRepo(stagingDir)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readFileContents', () => {
|
||||
let dir: string
|
||||
|
||||
|
||||
@@ -115,6 +115,14 @@ describe('publishOCIArtifact', () => {
|
||||
// Simulate successful upload of all blobs & then the manifest
|
||||
axiosPutMock.mockImplementation(async (url, data, config) => {
|
||||
validateRequestConfig(201, url, config)
|
||||
|
||||
if ((url as string).includes('manifest')) {
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Docker-Content-Digest': '1234567678' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 201
|
||||
}
|
||||
@@ -124,7 +132,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -164,6 +171,14 @@ describe('publishOCIArtifact', () => {
|
||||
// Simulate successful upload of all blobs & then the manifest
|
||||
axiosPutMock.mockImplementation(async (url, data, config) => {
|
||||
validateRequestConfig(201, url, config)
|
||||
|
||||
if ((url as string).includes('manifest')) {
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Docker-Content-Digest': '1234567678' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 201
|
||||
}
|
||||
@@ -173,7 +188,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -226,6 +240,14 @@ describe('publishOCIArtifact', () => {
|
||||
// Simulate successful upload of all blobs & then the manifest
|
||||
axiosPutMock.mockImplementation(async (url, data, config) => {
|
||||
validateRequestConfig(201, url, config)
|
||||
|
||||
if ((url as string).includes('manifest')) {
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Docker-Content-Digest': '1234567678' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 201
|
||||
}
|
||||
@@ -235,7 +257,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -262,7 +283,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -293,7 +313,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -325,7 +344,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -372,7 +390,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -426,7 +443,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -473,7 +489,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
@@ -497,7 +512,6 @@ describe('publishOCIArtifact', () => {
|
||||
token,
|
||||
registry,
|
||||
repository,
|
||||
releaseId,
|
||||
semver,
|
||||
zipFile,
|
||||
tarFile,
|
||||
|
||||
+263
-137
@@ -12,6 +12,7 @@ import * as github from '@actions/github'
|
||||
|
||||
import * as fsHelper from '../src/fs-helper'
|
||||
import * as ghcr from '../src/ghcr-client'
|
||||
import * as api from '../src/api-client'
|
||||
|
||||
// Mock the GitHub Actions core library
|
||||
let getInputMock: jest.SpyInstance
|
||||
@@ -22,13 +23,16 @@ let setOutputMock: jest.SpyInstance
|
||||
let createTempDirMock: jest.SpyInstance
|
||||
let createArchivesMock: jest.SpyInstance
|
||||
let removeDirMock: jest.SpyInstance
|
||||
let getConsolidatedDirectoryMock: jest.SpyInstance
|
||||
let isActionRepoMock: jest.SpyInstance
|
||||
let stageActionFilesMock: jest.SpyInstance
|
||||
|
||||
// Mock the GHCR Client
|
||||
let publishOCIArtifactMock: jest.SpyInstance
|
||||
|
||||
describe('action', () => {
|
||||
// Mock the API Client
|
||||
let getContainerRegistryURLMock: jest.SpyInstance
|
||||
let getRepositoryMetadataMock: jest.SpyInstance
|
||||
|
||||
describe('run', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
@@ -45,15 +49,23 @@ describe('action', () => {
|
||||
.spyOn(fsHelper, 'createArchives')
|
||||
.mockImplementation()
|
||||
removeDirMock = jest.spyOn(fsHelper, 'removeDir').mockImplementation()
|
||||
getConsolidatedDirectoryMock = jest
|
||||
.spyOn(fsHelper, 'getConsolidatedDirectory')
|
||||
stageActionFilesMock = jest
|
||||
.spyOn(fsHelper, 'stageActionFiles')
|
||||
.mockImplementation()
|
||||
isActionRepoMock = jest.spyOn(fsHelper, 'isActionRepo').mockImplementation()
|
||||
|
||||
// GHCR Client mocks
|
||||
publishOCIArtifactMock = jest
|
||||
.spyOn(ghcr, 'publishOCIArtifact')
|
||||
.mockImplementation()
|
||||
|
||||
// API Client mocks
|
||||
getContainerRegistryURLMock = jest
|
||||
.spyOn(api, 'getContainerRegistryURL')
|
||||
.mockImplementation()
|
||||
|
||||
getRepositoryMetadataMock = jest
|
||||
.spyOn(api, 'getRepositoryMetadata')
|
||||
.mockImplementation()
|
||||
})
|
||||
|
||||
it('fails if no repository found', async () => {
|
||||
@@ -67,202 +79,316 @@ describe('action', () => {
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
|
||||
})
|
||||
|
||||
it('fails if event is not a release', async () => {
|
||||
it('fails if no token found', async () => {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'push'
|
||||
process.env.TOKEN = ''
|
||||
|
||||
// Run the action
|
||||
await main.run('directory1 directory2')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith(
|
||||
'Please ensure you have the workflow trigger as release.'
|
||||
)
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Could not find GITHUB_TOKEN.')
|
||||
})
|
||||
|
||||
it('fails if release tag is not a valid semantic version', async () => {
|
||||
it('fails if no source commit found', async () => {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'invalid-tag'
|
||||
}
|
||||
process.env.TOKEN = 'test'
|
||||
process.env.GITHUB_SHA = ''
|
||||
|
||||
// Run the action
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Could not find source commit.')
|
||||
})
|
||||
|
||||
it('fails if trigger is not release or tag push', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
|
||||
// TODO: If we want we can add all of these: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
|
||||
const invalidEvents = ['workflow_dispatch, pull_request, schedule']
|
||||
for (const event of invalidEvents) {
|
||||
github.context.eventName = event
|
||||
await main.run('')
|
||||
expect(setFailedMock).toHaveBeenCalledWith(
|
||||
'This action can only be triggered by release events or tag push events.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('directory1 directory2')
|
||||
it('fails if the trigger is a push, but not a tag push', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.eventName = 'push'
|
||||
github.context.ref = 'refs/heads/main' // This is a branch, not a tag
|
||||
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith(
|
||||
'invalid-tag is not a valid semantic version, and so cannot be uploaded as an Immutable Action.'
|
||||
'This action can only be triggered by release events or tag push events.'
|
||||
)
|
||||
})
|
||||
|
||||
it('fails if multiple paths are provided and staging files fails', async () => {
|
||||
it('fails if the value of the tag input is not a valid semver', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.eventName = 'release'
|
||||
|
||||
const tags = ['test', 'v1.0', 'chicken', '111111']
|
||||
|
||||
for (const tag of tags) {
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: tag
|
||||
}
|
||||
}
|
||||
|
||||
await main.run('')
|
||||
expect(setFailedMock).toHaveBeenCalledWith(
|
||||
`${tag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('fails if staging files fails', async () => {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
}
|
||||
getInputMock.mockImplementation((name: string) => {
|
||||
if (name === 'path') {
|
||||
return 'directory1 directory2'
|
||||
} else if (name === 'registry') {
|
||||
return 'https://ghcr.io'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||
stageActionFilesMock.mockImplementation(() => {
|
||||
throw new Error('Something went wrong')
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('directory1 directory2')
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||
})
|
||||
|
||||
it('fails if an error is thrown from dependent code', async () => {
|
||||
it('fails if creating temp directory fails', async () => {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
}
|
||||
getInputMock.mockImplementation((name: string) => {
|
||||
if (name === 'path') {
|
||||
return 'directory'
|
||||
} else if (name === 'registry') {
|
||||
return 'https://ghcr.io'
|
||||
|
||||
createTempDirMock.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 archives fails', async () => {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||
return { consolidatedDirectory: '/tmp/test', needToCleanUpDir: false }
|
||||
})
|
||||
isActionRepoMock.mockImplementation(() => true)
|
||||
|
||||
createTempDirMock.mockImplementation(() => '/tmp/test')
|
||||
}
|
||||
|
||||
createArchivesMock.mockImplementation(() => {
|
||||
throw new Error('Something went wrong')
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('directory')
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(getConsolidatedDirectoryMock).toHaveBeenCalledTimes(1)
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||
|
||||
// Expect the files to be cleaned up
|
||||
expect(removeDirMock).toHaveBeenCalledWith('/tmp/test')
|
||||
})
|
||||
|
||||
it('successfully uploads if the release tag is a semver without v prefix', async () => {
|
||||
await testHappyPath('1.2.3', 'test')
|
||||
})
|
||||
|
||||
it('successfully uploads if the release tag is a semver with v prefix', async () => {
|
||||
await testHappyPath('v1.2.3', 'test')
|
||||
})
|
||||
|
||||
it('successfully uploads if multiple paths are provided', async () => {
|
||||
await testHappyPath('v1.2.3', 'test test2')
|
||||
})
|
||||
})
|
||||
|
||||
// Test that main successfully uploads and returns the manifest & package URL
|
||||
async function testHappyPath(version: string, path: string): Promise<void> {
|
||||
// Mock the environment
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: version
|
||||
}
|
||||
}
|
||||
getInputMock.mockImplementation((name: string) => {
|
||||
if (name === 'path') {
|
||||
return path
|
||||
} else if (name === 'registry') {
|
||||
return 'https://ghcr.io'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
isActionRepoMock.mockImplementation(() => true)
|
||||
|
||||
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||
return { consolidatedDirectory: '/tmp/test', needToCleanUpDir: false } // TODO: I don't understand why I have to name the variables here but not in the implementation code
|
||||
})
|
||||
|
||||
createTempDirMock.mockImplementation(() => '/tmp/test')
|
||||
|
||||
createArchivesMock.mockImplementation(() => {
|
||||
return {
|
||||
zipFile: {
|
||||
path: 'test',
|
||||
size: 5,
|
||||
sha256: '123'
|
||||
},
|
||||
tarFile: {
|
||||
path: 'test2',
|
||||
size: 52,
|
||||
sha256: '1234'
|
||||
it('fails if getting container registry URL fails', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
}
|
||||
|
||||
createArchivesMock.mockImplementation(() => {
|
||||
return {
|
||||
zipFile: {
|
||||
path: 'test',
|
||||
size: 5,
|
||||
sha256: '123'
|
||||
},
|
||||
tarFile: {
|
||||
path: 'test2',
|
||||
size: 52,
|
||||
sha256: '1234'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
getRepositoryMetadataMock.mockImplementation(() => {
|
||||
return { repoId: 'test', ownerId: 'test' }
|
||||
})
|
||||
|
||||
getContainerRegistryURLMock.mockImplementation(() => {
|
||||
throw new Error('Something went wrong')
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||
})
|
||||
|
||||
publishOCIArtifactMock.mockImplementation(() => {
|
||||
return new URL('https://ghcr.io/v2/test-org/test-repo:1.2.3')
|
||||
it('fails if publishing OCI artifact fails', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
}
|
||||
|
||||
createArchivesMock.mockImplementation(() => {
|
||||
return {
|
||||
zipFile: {
|
||||
path: 'test',
|
||||
size: 5,
|
||||
sha256: '123'
|
||||
},
|
||||
tarFile: {
|
||||
path: 'test2',
|
||||
size: 52,
|
||||
sha256: '1234'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
getRepositoryMetadataMock.mockImplementation(() => {
|
||||
return { repoId: 'test', ownerId: 'test' }
|
||||
})
|
||||
|
||||
getContainerRegistryURLMock.mockImplementation(() => {
|
||||
return new URL('https://ghcr.io')
|
||||
})
|
||||
|
||||
publishOCIArtifactMock.mockImplementation(() => {
|
||||
throw new Error('Something went wrong')
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run(path)
|
||||
it('uploads the artifact, returns package metadata from GHCR, and cleans up tmp dirs', async () => {
|
||||
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||
github.context.eventName = 'release'
|
||||
process.env.GITHUB_SHA = 'test-sha'
|
||||
process.env.TOKEN = 'token'
|
||||
github.context.payload = {
|
||||
release: {
|
||||
id: '123',
|
||||
tag_name: 'v1.2.3'
|
||||
}
|
||||
}
|
||||
|
||||
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
|
||||
createTempDirMock.mockImplementation(() => '/tmp/test')
|
||||
|
||||
// Check manifest is in output
|
||||
expect(setOutputMock).toHaveBeenCalledWith(
|
||||
'package-url',
|
||||
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
|
||||
)
|
||||
expect(setOutputMock).toHaveBeenCalledWith(
|
||||
'package-manifest',
|
||||
expect.any(String)
|
||||
)
|
||||
createArchivesMock.mockImplementation(() => {
|
||||
return {
|
||||
zipFile: {
|
||||
path: 'test',
|
||||
size: 5,
|
||||
sha256: '123'
|
||||
},
|
||||
tarFile: {
|
||||
path: 'test2',
|
||||
size: 52,
|
||||
sha256: '1234'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Validate the manifest
|
||||
const manifest = JSON.parse(setOutputMock.mock.calls[1][1])
|
||||
expect(manifest.mediaType).toEqual(
|
||||
'application/vnd.oci.image.manifest.v1+json'
|
||||
)
|
||||
expect(manifest.config.mediaType).toEqual(
|
||||
'application/vnd.github.actions.package.config.v1+json'
|
||||
)
|
||||
expect(manifest.layers.length).toEqual(3)
|
||||
expect(manifest.annotations['com.github.package.type']).toEqual(
|
||||
'actions_oci_pkg'
|
||||
)
|
||||
getRepositoryMetadataMock.mockImplementation(() => {
|
||||
return { repoId: 'test', ownerId: 'test' }
|
||||
})
|
||||
|
||||
// Expect all the temp files to be cleaned up
|
||||
expect(removeDirMock).toHaveBeenCalledWith('/tmp/test')
|
||||
expect(removeDirMock).toHaveBeenCalledTimes(
|
||||
createTempDirMock.mock.calls.length
|
||||
)
|
||||
}
|
||||
getContainerRegistryURLMock.mockImplementation(() => {
|
||||
return new URL('https://ghcr.io')
|
||||
})
|
||||
|
||||
publishOCIArtifactMock.mockImplementation(() => {
|
||||
return {
|
||||
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
|
||||
manifestDigest: 'my-test-digest'
|
||||
}
|
||||
})
|
||||
|
||||
// Run the action
|
||||
await main.run('')
|
||||
|
||||
// Check the results
|
||||
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Check outputs
|
||||
expect(setOutputMock).toHaveBeenCalledTimes(3)
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(
|
||||
'package-url',
|
||||
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
|
||||
)
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(
|
||||
'package-manifest',
|
||||
expect.any(String)
|
||||
)
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(
|
||||
'package-manifest-sha',
|
||||
'sha256:my-test-digest'
|
||||
)
|
||||
|
||||
// Expect all the temp files to be cleaned up
|
||||
expect(removeDirMock).toHaveBeenCalledWith('/tmp/test')
|
||||
expect(removeDirMock).toHaveBeenCalledTimes(
|
||||
createTempDirMock.mock.calls.length
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,9 @@ describe('createActionPackageManifest', () => {
|
||||
const repo = 'test-org/test-repo'
|
||||
const sanitizedRepo = 'test-org-test-repo'
|
||||
const version = '1.2.3'
|
||||
const repoId = '123'
|
||||
const ownerId = '456'
|
||||
const sourceCommit = 'abc'
|
||||
const tarFile: FileMetadata = {
|
||||
path: '/test/test/test.tar.gz',
|
||||
sha256: 'tarSha',
|
||||
@@ -21,7 +24,7 @@ describe('createActionPackageManifest', () => {
|
||||
const expectedJSON = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.github.actions.package.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.github.actions.package.config.v1+json",
|
||||
"size": 0,
|
||||
@@ -60,7 +63,11 @@ describe('createActionPackageManifest', () => {
|
||||
"org.opencontainers.image.created":"${date.toISOString()}",
|
||||
"action.tar.gz.digest":"${tarFile.sha256}",
|
||||
"action.zip.digest":"${zipFile.sha256}",
|
||||
"com.github.package.type":"actions_oci_pkg"
|
||||
"com.github.package.type":"actions_oci_pkg",
|
||||
"com.github.package.version":"1.2.3",
|
||||
"com.github.source.repo.id":"123",
|
||||
"com.github.source.repo.owner.id":"456",
|
||||
"com.github.source.commit":"abc"
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -76,6 +83,9 @@ describe('createActionPackageManifest', () => {
|
||||
sha256: zipFile.sha256
|
||||
},
|
||||
repo,
|
||||
repoId,
|
||||
ownerId,
|
||||
sourceCommit,
|
||||
version,
|
||||
date
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user