update deps, linting, test cases, etc.

This commit is contained in:
Conor Sloan
2024-02-02 12:55:29 -05:00
committed by Edwin Sirko
parent 651b1739d1
commit 4ac7dfc3cb
19 changed files with 4111 additions and 1995 deletions
+82 -51
View File
@@ -1,14 +1,12 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import fsExtra from 'fs-extra'
import * as path from 'path'
import * as tar from 'tar'
import * as archiver from 'archiver'
import * as crypto from 'crypto'
import * as os from 'os'
import * as zlib from 'zlib'
export function createTempDir() {
export function createTempDir(): string {
const randomDirName = crypto.randomBytes(4).toString('hex')
const tempDir = path.join(os.tmpdir(), randomDirName)
@@ -19,8 +17,10 @@ export function createTempDir() {
return tempDir
}
export function removeDir(dir: string) {
fs.rmSync(dir, { recursive: true })
export function removeDir(dir: string): void {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true })
}
}
export interface FileMetadata {
@@ -38,59 +38,90 @@ export async function createArchives(
const zipPath = path.join(archiveTargetPath, `archive.zip`)
const tarPath = path.join(archiveTargetPath, `archive.tar.gz`)
return Promise.all([
new Promise<FileMetadata>((resolve, reject) => {
const output = fs.createWriteStream(zipPath)
const archive = archiver.create('zip')
const createZipPromise = new Promise<FileMetadata>((resolve, reject) => {
const output = fs.createWriteStream(zipPath)
const archive = archiver.create('zip')
output.on('error', (err: Error) => {
reject(err)
})
archive.on('error', (err: Error) => {
reject(err)
})
output.on('close', () => {
resolve(fileMetadata(zipPath))
})
archive.pipe(output)
archive.directory(distPath, false)
archive.finalize()
}),
new Promise<FileMetadata>((resolve, reject) => {
const tarStream = tar
.c(
{
file: tarPath,
C: distPath, // Change to the source directory for relative paths (TODO)
gzip: true
},
['.']
)
.then(() => {
resolve(fileMetadata(tarPath))
})
.catch((err: Error) => reject(err))
output.on('error', (err: Error) => {
reject(err)
})
]).then(([zipFile, tarFile]) => ({ zipFile, tarFile }))
archive.on('error', (err: Error) => {
reject(err)
})
output.on('close', () => {
resolve(fileMetadata(zipPath))
})
archive.pipe(output)
archive.directory(distPath, false)
archive.finalize()
})
const createTarPromise = new Promise<FileMetadata>((resolve, reject) => {
tar
.c(
{
file: tarPath,
C: distPath, // Change to the source directory for relative paths (TODO)
gzip: true
},
['.']
)
// eslint-disable-next-line github/no-then
.catch(err => {
reject(err)
})
// eslint-disable-next-line github/no-then
.then(() => {
resolve(fileMetadata(tarPath))
})
})
const [zipFile, tarFile] = await Promise.all([
createZipPromise,
createTarPromise
])
return { zipFile, tarFile }
}
export function isDirectory(path: string): boolean {
return fs.existsSync(path) && fs.lstatSync(path).isDirectory()
export function isDirectory(dirPath: string): boolean {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
}
export function readFileContents(path: string): Buffer {
return fs.readFileSync(path)
export function readFileContents(filePath: string): Buffer {
return fs.readFileSync(filePath)
}
export function bundleFilesintoDirectory(
files: string[],
targetDir: string = createTempDir()
): string {
for (const file of files) {
if (!fs.existsSync(file)) {
throw new Error(`File ${file} does not exist`)
}
if (isDirectory(file)) {
const targetFolder = path.join(targetDir, path.basename(file))
fsExtra.copySync(file, targetFolder)
} else {
const targetFile = path.join(targetDir, path.basename(file))
fs.copyFileSync(file, targetFile)
}
}
return targetDir
}
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
async function fileMetadata(path: string): Promise<FileMetadata> {
const stats = fs.statSync(path)
async function fileMetadata(filePath: string): Promise<FileMetadata> {
const stats = fs.statSync(filePath)
const size = stats.size
const hash = crypto.createHash('sha256')
const fileStream = fs.createReadStream(path)
const fileStream = fs.createReadStream(filePath)
return new Promise((resolve, reject) => {
fileStream.on('data', data => {
hash.update(data)
@@ -98,9 +129,9 @@ async function fileMetadata(path: string): Promise<FileMetadata> {
fileStream.on('end', () => {
const sha256 = hash.digest('hex')
resolve({
path: path,
size: size,
sha256: 'sha256:' + sha256
path: filePath,
size,
sha256: `sha256:${sha256}`
})
})
fileStream.on('error', err => {
+16 -19
View File
@@ -2,9 +2,6 @@ import * as core from '@actions/core'
import { FileMetadata } from './fs-helper'
import * as ociContainer from './oci-container'
import axios from 'axios'
import { fieldEnds } from 'tar'
import * as fs from 'fs'
import { promiseHooks } from 'v8'
import * as fsHelper from './fs-helper'
import axiosDebugLog from 'axios-debug-log'
@@ -18,7 +15,7 @@ export async function publishOCIArtifact(
zipFile: FileMetadata,
tarFile: FileMetadata,
manifest: ociContainer.Manifest,
debugRequests: boolean = false
debugRequests = false
): Promise<URL> {
if (debugRequests) {
configureRequestDebugLogging()
@@ -43,7 +40,7 @@ export async function publishOCIArtifact(
`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`
)
let layerUploads: Promise<void>[] = manifest.layers.map(layer => {
const layerUploads: Promise<void>[] = manifest.layers.map(async layer => {
switch (layer.mediaType) {
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
return uploadLayer(
@@ -98,7 +95,7 @@ async function uploadLayer(
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: function (status: number) {
validateStatus: () => {
return true // Allow non 2xx responses
}
}
@@ -124,12 +121,12 @@ async function uploadLayer(
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: function (status: number) {
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (initiateUploadResponse.status != 202) {
if (initiateUploadResponse.status !== 202) {
core.error(
`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`
)
@@ -139,17 +136,17 @@ async function uploadLayer(
}
const locationResponseHeader = initiateUploadResponse.headers['location']
if (locationResponseHeader == undefined) {
if (locationResponseHeader === undefined) {
throw new Error(
`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`
)
}
let pathname = (locationResponseHeader as string) + '?digest=' + layer.digest
const pathname = `${locationResponseHeader}?digest=${layer.digest}`
const uploadBlobUrl = new URL(pathname, registryURL).toString()
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
var data: Buffer
let data: Buffer
if (file.size === 0) {
data = Buffer.alloc(0)
} else {
@@ -163,12 +160,12 @@ async function uploadLayer(
'Accept-Encoding': 'gzip', // TODO: What about for the config layer?
'Content-Length': layer.size.toString()
},
validateStatus: function (status: number) {
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (putResponse.status != 201) {
if (putResponse.status !== 201) {
throw new Error(
`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`
)
@@ -187,27 +184,27 @@ async function uploadManifest(
Authorization: `Bearer ${b64Token}`,
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
},
validateStatus: function (status: number) {
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (putResponse.status != 201) {
if (putResponse.status !== 201) {
throw new Error(
`Unexpected response from PUT manifest ${putResponse.status}`
)
}
}
function configureRequestDebugLogging() {
function configureRequestDebugLogging(): void {
axiosDebugLog({
request: function (debug, config) {
request: (debug, config) => {
core.debug(`Request with ${config}`)
},
response: function (debug, response) {
response: (debug, response) => {
core.debug(`Response with ${response}`)
},
error: function (debug, error) {
error: (debug, error) => {
core.debug(`Error with ${error}`)
}
})
+24 -15
View File
@@ -4,14 +4,13 @@ import * as fsHelper from './fs-helper'
import * as ociContainer from './oci-container'
import * as ghcr from './ghcr-client'
import semver from 'semver'
import { url } from 'inspector'
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
let tmpDir: string = ''
const tmpDirs: string[] = []
try {
// Parse and validate Actions execution context, including the repository name, release name and event type
@@ -29,8 +28,9 @@ export async function run(): Promise<void> {
// Strip any leading 'v' from the tag in case the release format is e.g. 'v1.0.0' as recommended by GitHub docs
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
let targetVersion = semver.parse(releaseTag.replace(/^v/, ''))
const targetVersion = semver.parse(releaseTag.replace(/^v/, ''))
if (!targetVersion) {
// TODO: We may want to limit semvers to only x.x.x, without the pre-release tags, but for now we'll allow them.
core.setFailed(
`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
)
@@ -39,20 +39,27 @@ export async function run(): Promise<void> {
// Gather & validate user inputs
const token: string = core.getInput('token')
const path: string = core.getInput('path')
const registryURL: URL = new URL(core.getInput('registry')) // TODO: Should this be dynamic? Maybe an API endpoint to grab the registry for GHES/proxima purposes.
if (!fsHelper.isDirectory(path)) {
core.setFailed(
`The path ${path} is not a directory. Please provide a path to a valid directory.`
)
return
// Paths to be included in the OCI image
const paths: string[] = core.getInput('path').split(' ')
let path = ''
if (paths.length === 1 && fsHelper.isDirectory(paths[0])) {
// If the path is a single directory, we can skip the bundling step
path = paths[0]
} else {
// Otherwise, we need to bundle the files & folders into a temporary directory
const bundleDir = fsHelper.createTempDir()
tmpDirs.push(bundleDir)
path = fsHelper.bundleFilesintoDirectory(paths, bundleDir)
}
// Create a temporary directory to store the archives
tmpDir = fsHelper.createTempDir()
const archiveDir = fsHelper.createTempDir()
tmpDirs.push(archiveDir)
const archives = await fsHelper.createArchives(path)
const archives = await fsHelper.createArchives(path, archiveDir)
const manifest = ociContainer.createActionPackageManifest(
archives.tarFile,
@@ -62,7 +69,7 @@ export async function run(): Promise<void> {
new Date()
)
let packageURL = await ghcr.publishOCIArtifact(
const packageURL = await ghcr.publishOCIArtifact(
token,
registryURL,
repository,
@@ -84,9 +91,11 @@ export async function run(): Promise<void> {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
} finally {
// Clean up the temporary directory if it exists
if (tmpDir !== '') {
fsHelper.removeDir(tmpDir)
// Clean up any temporary directories that exist
for (const tmpDir of tmpDirs) {
if (tmpDir !== '') {
fsHelper.removeDir(tmpDir)
}
}
}
}
+13 -7
View File
@@ -1,4 +1,3 @@
import { Tracing } from 'trace_events'
import { FileMetadata } from './fs-helper'
export interface Manifest {
@@ -7,14 +6,14 @@ export interface Manifest {
artifactType: string
config: Layer
layers: Layer[]
annotations: {}
annotations: { [key: string]: string }
}
export interface Layer {
mediaType: string
size: number
digest: string
annotations: {}
annotations: { [key: string]: string }
}
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
@@ -26,8 +25,9 @@ export function createActionPackageManifest(
created: Date
): Manifest {
const configLayer = createConfigLayer()
const tarLayer = createTarLayer(tarFile, repository, version)
const zipLayer = createZipLayer(zipFile, repository, version)
const sanitizedRepo = sanitizeRepository(repository)
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version)
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version)
const manifest: Manifest = {
schemaVersion: 2,
@@ -71,7 +71,7 @@ function createZipLayer(
size: zipFile.size,
digest: zipFile.sha256,
annotations: {
'org.opencontainers.image.title': `${repository}-${version}.zip`
'org.opencontainers.image.title': `${repository}_${version}.zip`
}
}
@@ -88,9 +88,15 @@ function createTarLayer(
size: tarFile.size,
digest: tarFile.sha256,
annotations: {
'org.opencontainers.image.title': `${repository}-${version}.tar.gz`
'org.opencontainers.image.title': `${repository}_${version}.tar.gz`
}
}
return tarLayer
}
// Remove slashes so we can use the repository in a filename
// repository usually includes the namespace too, e.g. my-org/my-repo
function sanitizeRepository(repository: string): string {
return repository.replace('/', '-')
}