initial mvp version

This commit is contained in:
Conor Sloan
2023-11-17 20:04:42 +00:00
committed by Edwin Sirko
parent 5d945681fa
commit d057826061
36 changed files with 90248 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
import * as fsHelper from '../src/fs-helper'
import * as fs from 'fs'
import * as os from 'os'
import { execSync } from 'child_process'
const fileContent = 'This is the content of the file'
describe('createArchives', () => {
let tmpDir: string
let distDir: string
beforeAll(() => {
distDir = fsHelper.createTempDir()
fs.writeFileSync(`${distDir}/hello.txt`, fileContent)
fs.writeFileSync(`${distDir}/world.txt`, fileContent)
})
beforeEach(() => {
tmpDir = fsHelper.createTempDir()
})
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true })
})
afterAll(() => {
fs.rmSync(distDir, { recursive: true })
})
it('creates archives', async () => {
const { zipFile, tarFile } = await fsHelper.createArchives(distDir, tmpDir)
expect(zipFile.path).toEqual(`${tmpDir}/archive.zip`)
expect(fs.existsSync(zipFile.path)).toEqual(true)
expect(fs.statSync(zipFile.path).size).toBeGreaterThan(0)
expect(zipFile.sha256.startsWith('sha256:')).toEqual(true)
expect(tarFile.path).toEqual(`${tmpDir}/archive.tar.gz`)
expect(fs.existsSync(tarFile.path)).toEqual(true)
expect(fs.statSync(tarFile.path).size).toBeGreaterThan(0)
expect(tarFile.sha256.startsWith('sha256:')).toEqual(true)
// Validate the hashes by comparing to the output of the system's hashing utility
let zipSHA = zipFile.sha256.substring(7) // remove "sha256:" prefix
let tarSHA = tarFile.sha256.substring(7) // remove "sha256:" prefix
// sha256 hash is 64 characters long
expect(zipSHA).toHaveLength(64)
expect(tarSHA).toHaveLength(64)
let systemZipHash: string
let systemTarHash: string
if (os.platform() === 'win32') {
// Windows
systemZipHash = execSync(`CertUtil -hashfile ${zipFile.path} SHA256`)
.toString()
.split(' ')[1]
.trim()
systemTarHash = execSync(`CertUtil -hashfile ${tarFile.path} SHA256`)
.toString()
.split(' ')[1]
.trim()
} else {
// Unix-based systems
systemZipHash = execSync(`shasum -a 256 ${zipFile.path}`)
.toString()
.split(' ')[0]
systemTarHash = execSync(`shasum -a 256 ${tarFile.path}`)
.toString()
.split(' ')[0]
}
expect(zipSHA).toEqual(systemZipHash)
expect(tarSHA).toEqual(systemTarHash)
})
// TODO: Test the failure cases
})
describe('createTempDir', () => {
let dirs: string[] = []
beforeEach(() => {
dirs = []
})
afterEach(() => {
dirs.forEach(dir => {
fs.rmSync(dir, { recursive: true })
})
})
it('creates a temporary directory in the OS temporary dir', () => {
let tmpDir = fsHelper.createTempDir()
dirs.push(tmpDir)
expect(fs.existsSync(tmpDir)).toEqual(true)
expect(fs.statSync(tmpDir).isDirectory()).toEqual(true)
expect(tmpDir.startsWith(os.tmpdir())).toEqual(true)
})
it('creates a unique temporary directory', () => {
let dir1 = fsHelper.createTempDir()
dirs.push(dir1)
let dir2 = fsHelper.createTempDir()
dirs.push(dir2)
expect(dir1).not.toEqual(dir2)
})
})
describe('isDirectory', () => {
let dir: string
beforeEach(() => {
dir = fsHelper.createTempDir()
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('returns true if the path is a directory', () => {
expect(fsHelper.isDirectory(dir)).toEqual(true)
})
it('returns false if the path is not a directory', () => {
const tempFile = `${dir}/file.txt`
fs.writeFileSync(tempFile, fileContent)
expect(fsHelper.isDirectory(tempFile)).toEqual(false)
})
})
describe('readFileContents', () => {
let dir: string
beforeEach(() => {
dir = fsHelper.createTempDir()
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('reads the contents of a file', () => {
const tempFile = `${dir}/file.txt`
fs.writeFileSync(tempFile, fileContent)
expect(fsHelper.readFileContents(tempFile).toString()).toEqual(fileContent)
})
})
describe('removeDir', () => {
let dir: string
beforeEach(() => {
dir = fsHelper.createTempDir()
})
afterEach(() => {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true })
}
})
it('removes a directory', () => {
fsHelper.removeDir(dir)
expect(fs.existsSync(dir)).toEqual(false)
})
})
+480
View File
@@ -0,0 +1,480 @@
import { publishOCIArtifact } from '../src/ghcr-client'
import axios, { AxiosRequestConfig } from 'axios'
import * as fs from 'fs'
import * as fsHelper from '../src/fs-helper'
import * as ociContainer from '../src/oci-container'
// Mocks
let fsReadFileSyncMock: jest.SpyInstance
let axiosPostMock: jest.SpyInstance
let axiosPutMock: jest.SpyInstance
let axiosHeadMock: jest.SpyInstance
const token = '1234567890'
const registry = new URL('https://ghcr.io')
const repository = 'test/test'
const releaseId = '1234567890'
const semver = '1.0.0'
const zipFile: fsHelper.FileMetadata = {
path: 'test-repo-1.0.0.zip',
size: 100,
sha256: '1234567890'
}
const tarFile: fsHelper.FileMetadata = {
path: 'test-repo-1.0.0.tar.gz',
size: 100,
sha256: '1234567890'
}
const testManifest: ociContainer.Manifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
artifactType: 'application/vnd.oci.image.manifest.v1+json',
config: {
mediaType: 'application/vnd.github.actions.package.config.v1+json',
size: 0,
digest:
'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
annotations: {
'org.opencontainers.image.title': 'config.json'
}
},
layers: [
{
mediaType: 'application/vnd.github.actions.package.config.v1+json',
size: 0,
digest:
'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
annotations: {
'org.opencontainers.image.title': 'config.json'
}
},
{
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
size: 100,
digest: 'sha256:1234567890',
annotations: {
'org.opencontainers.image.title': 'test-repo-1.0.0.tar.gz'
}
},
{
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
size: 100,
digest: 'sha256:1234567890',
annotations: {
'org.opencontainers.image.title': 'test-repo-1.0.0.zip'
}
}
],
annotations: {
'org.opencontainers.image.created': '2021-01-01T00:00:00.000Z',
'action.tar.gz.digest': '1234567890',
'action.zip.digest': '1234567890',
'com.github.package.type': 'actions_oci_pkg'
}
}
describe('publishOCIArtifact', () => {
beforeEach(() => {
jest.clearAllMocks()
fsReadFileSyncMock = jest
.spyOn(fsHelper, 'readFileContents')
.mockImplementation()
axiosPostMock = jest.spyOn(axios, 'post').mockImplementation()
axiosPutMock = jest.spyOn(axios, 'put').mockImplementation()
axiosHeadMock = jest.spyOn(axios, 'head').mockImplementation()
})
it('publishes layer blobs & then a manifest to the provided registry', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(
async (url: string, config: AxiosRequestConfig) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
}
)
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(async path => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(3)
expect(axiosPutMock).toHaveBeenCalledTimes(4)
// TODO: Check that the base64 encoded token is sent in the Authorization header
})
it('skips uploading layer blobs that already exist', async () => {
// Simulate all blobs already existing
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(200, url, config)
return {
status: 200
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(async path => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
// We should only head all the blobs and then upload the manifest
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(0)
expect(axiosPutMock).toHaveBeenCalledTimes(1)
})
it('throws an error if checking for existing blobs fails', async () => {
// Simulate failed response code
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from blob check for layer/)
})
it('throws an error if initiating layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate failed initiation of uploads
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow('Unexpected response from POST upload 503')
})
it('throws an error if the upload endpoint does not return a location', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful response code but no location header
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {}
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^No location header in response from upload post/)
})
it('throws an error if a layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(async path => {
return Buffer.from('test')
})
// Simulate fails upload of all blobs & manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(500, url, config)
return {
status: 500
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from PUT upload 500/)
})
it('throws an error if a manifest upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(async path => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
if (url.includes('manifest')) {
validateRequestConfig(500, url, config)
return {
status: 500
}
}
validateRequestConfig(201, url, config)
return {
status: 201
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from PUT manifest 500/)
})
it('throws an error if reading one of the files fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(path => {
throw new Error('failed to read a file: test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
return {
status: 201
}
})
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow('failed to read a file: test')
})
it('throws an error if one of the layers has the wrong media type', async () => {
let modifiedTestManifest = testManifest
modifiedTestManifest.layers[0].mediaType = 'application/json'
expect(
publishOCIArtifact(
token,
registry,
repository,
releaseId,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow('Unknown media type application/json')
})
})
// We expect all axios calls to have auth headers set and to not intercept any status codes so we can handle them.
// This function verifies that given an axios request config.
function validateRequestConfig(
status: number,
url: string,
config: AxiosRequestConfig
) {
// Basic URL checks
expect(url).toBeDefined()
if (!url.startsWith(registry.toString())) {
console.log(url)
}
expect(url.startsWith(registry.toString())).toBe(true)
// Config checks
expect(config).toBeDefined()
expect(config.validateStatus).toBeDefined()
if (config.validateStatus) {
// Check axios will not intercept this status
expect(config.validateStatus(status)).toBe(true)
}
expect(config.headers).toBeDefined()
if (config.headers) {
// Check the auth header is set
expect(config.headers.Authorization).toBeDefined()
// Check the auth header is the base 64 encoded token
expect(config.headers.Authorization).toBe(
`Bearer ${Buffer.from(token).toString('base64')}`
)
}
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Unit tests for the action's entrypoint, src/index.ts
*/
import * as main from '../src/main'
// Mock the action's entrypoint
const runMock = jest.spyOn(main, 'run').mockImplementation()
describe('index', () => {
it('calls run when imported', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../src/index')
expect(runMock).toHaveBeenCalled()
})
})
+250
View File
@@ -0,0 +1,250 @@
/**
* Unit tests for the action's main functionality, src/main.ts
*
* These should be run as if the action was called from a workflow.
* Specifically, the inputs listed in `action.yml` should be set as environment
* variables following the pattern `INPUT_<INPUT_NAME>`.
*/
import * as core from '@actions/core'
import * as main from '../src/main'
import * as github from '@actions/github'
import * as fsHelper from '../src/fs-helper'
import * as ociContainer from '../src/oci-container'
import * as ghcr from '../src/ghcr-client'
// Mock the action's main function
const runMock = jest.spyOn(main, 'run')
// Mock the GitHub Actions core library
let debugMock: jest.SpyInstance
let errorMock: jest.SpyInstance
let getInputMock: jest.SpyInstance
let setFailedMock: jest.SpyInstance
let setOutputMock: jest.SpyInstance
// Mock the filesystem helper
let createTempDirMock: jest.SpyInstance
let isDirectoryMock: jest.SpyInstance
let createArchivesMock: jest.SpyInstance
let removeDirMock: jest.SpyInstance
// Mock the GHCR Client
let publishOCIArtifactMock: jest.SpyInstance
describe('action', () => {
beforeEach(() => {
jest.clearAllMocks()
// Core mocks
debugMock = jest.spyOn(core, 'debug').mockImplementation()
errorMock = jest.spyOn(core, 'error').mockImplementation()
getInputMock = jest.spyOn(core, 'getInput').mockImplementation()
setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation()
setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation()
// FS mocks
createTempDirMock = jest
.spyOn(fsHelper, 'createTempDir')
.mockImplementation()
isDirectoryMock = jest.spyOn(fsHelper, 'isDirectory').mockImplementation()
createArchivesMock = jest
.spyOn(fsHelper, 'createArchives')
.mockImplementation()
removeDirMock = jest.spyOn(fsHelper, 'removeDir').mockImplementation()
// GHCR Client mocks
publishOCIArtifactMock = jest
.spyOn(ghcr, 'publishOCIArtifact')
.mockImplementation()
})
it('fails if no repository found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = ''
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
})
it('fails if event is not a release', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test/test'
github.context.eventName = 'push'
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith(
'Please ensure you have the workflow trigger as release.'
)
})
it('fails if release tag is not a valid semantic version', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test/test'
github.context.eventName = 'release'
github.context.payload = {
release: {
id: '123',
tag_name: 'invalid-tag'
}
}
// Run the action
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.'
)
})
it('fails if path is not a directory', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test/test'
github.context.eventName = 'release'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.0.0'
}
}
getInputMock.mockImplementation((name: string) => {
if (name === 'path') {
return 'not-a-directory'
} else if (name === 'registry') {
return 'https://ghcr.io'
}
return ''
})
isDirectoryMock.mockImplementation(() => false)
// Run the action
await main.run()
// Check the results
expect(isDirectoryMock).toHaveBeenCalledWith('not-a-directory')
expect(setFailedMock).toHaveBeenCalledWith(
'The path not-a-directory is not a directory. Please provide a path to a valid directory.'
)
})
it('fails if an error is thrown from dependent code', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test/test'
github.context.eventName = 'release'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.0.0'
}
}
getInputMock.mockImplementation((name: string) => {
if (name === 'path') {
return 'directory'
} else if (name === 'registry') {
return 'https://ghcr.io'
}
return ''
})
isDirectoryMock.mockImplementation(() => true)
createTempDirMock.mockImplementation(() => '/tmp/test')
createArchivesMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(isDirectoryMock).toHaveBeenCalledWith('directory')
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
// Expect the files to be cleaned up
expect(removeDirMock).toHaveBeenCalledWith('/tmp/test')
})
it('uploads and returns the manifest & package URL if all succeeds', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test/test'
github.context.eventName = 'release'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.0.0'
}
}
getInputMock.mockImplementation((name: string) => {
if (name === 'path') {
return 'test'
} else if (name === 'registry') {
return 'https://ghcr.io'
}
return ''
})
isDirectoryMock.mockImplementation(() => true)
createTempDirMock.mockImplementation(() => '/tmp/test')
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
publishOCIArtifactMock.mockImplementation(() => {
return new URL('https://ghcr.io/v2/test/test:1.0.0')
})
// Run the action
await main.run()
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
// Check manifest is in output
expect(setOutputMock).toHaveBeenCalledWith(
'package-url',
'https://ghcr.io/v2/test/test:1.0.0'
)
expect(setOutputMock).toHaveBeenCalledWith(
'package-manifest',
expect.any(String)
)
// 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'
)
// Expect the files to be cleaned up
expect(removeDirMock).toHaveBeenCalledWith('/tmp/test')
})
})
+85
View File
@@ -0,0 +1,85 @@
import { createActionPackageManifest } from '../src/oci-container'
import { FileMetadata } from '../src/fs-helper'
describe('createActionPackageManigest', () => {
it('creates a manifest containing the provided information', () => {
let date = new Date()
let repo = 'test-repo'
let version = '1.0.0'
let tarFile: FileMetadata = {
path: '/test/test/test',
sha256: '1234567890',
size: 100
}
let zipFile: FileMetadata = {
path: '/test/test/test',
sha256: '1234567890',
size: 100
}
let expectedJSON: String = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.github.actions.package.config.v1+json",
"size": 0,
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"annotations": {
"org.opencontainers.image.title":"config.json"
}
},
"layers":[
{
"mediaType":"application/vnd.github.actions.package.config.v1+json",
"size":0,
"digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"annotations":{
"org.opencontainers.image.title":"config.json"
}
},
{
"mediaType":"application/vnd.github.actions.package.layer.v1.tar+gzip",
"size":${tarFile.size},
"digest":"${tarFile.sha256}",
"annotations":{
"org.opencontainers.image.title":"${repo}-${version}.tar.gz"
}
},
{
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
"size":${tarFile.size},
"digest":"${tarFile.sha256}",
"annotations":{
"org.opencontainers.image.title":"${repo}-${version}.zip"
}
}
],
"annotations":{
"org.opencontainers.image.created":"${date.toISOString()}",
"action.tar.gz.digest":"${tarFile.sha256}",
"action.zip.digest":"${zipFile.sha256}",
"com.github.package.type":"actions_oci_pkg"
}
}`
let manifest = createActionPackageManifest(
{
path: 'test.tar.gz',
size: 100,
sha256: '1234567890'
},
{
path: 'test.zip',
size: 100,
sha256: '1234567890'
},
'test-repo',
'1.0.0',
date
)
let manifestJSON = JSON.stringify(manifest)
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
})
})