update deps, linting, test cases, etc.
This commit is contained in:
+82
-51
@@ -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
@@ -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
@@ -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
@@ -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('/', '-')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user