Tying up loose ends (#54)

* various qol updates to publish action

* review comments and run bundle
This commit is contained in:
Conor Sloan
2024-02-02 13:02:14 -05:00
committed by Edwin Sirko
parent 3c4259bfdd
commit 1f47b19ed3
23 changed files with 811 additions and 514 deletions
Generated Vendored
+115 -80
View File
@@ -74659,6 +74659,43 @@ ZipStream.prototype.finalize = function() {
*/
/***/ }),
/***/ 95707:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getContainerRegistryURL = exports.getRepositoryMetadata = void 0;
async function getRepositoryMetadata(repository, token) {
const response = await fetch(`${process.env.GITHUB_API_URL}/repos/${repository}`);
if (!response.ok) {
throw new Error(`Failed to fetch repository metadata due to bad status code: ${response.status}`);
}
const data = await response.json();
// Check that the response contains the expected data
if (!data.id || !data.owner.id) {
throw new Error(`Failed to fetch repository metadata: unexpected response format`);
}
return { repoId: data.id, ownerId: data.owner.id };
}
exports.getRepositoryMetadata = getRepositoryMetadata;
async function getContainerRegistryURL() {
const response = await fetch(`${process.env.GITHUB_API_URL}/packages/container-registry-url`);
if (!response.ok) {
throw new Error(`Failed to fetch container registry url due to bad status code: ${response.status}`);
}
const data = await response.json();
if (!data.url) {
throw new Error(`Failed to fetch repository metadata: unexpected response format`);
}
const registryURL = new URL(data.url);
return registryURL;
}
exports.getContainerRegistryURL = getContainerRegistryURL;
/***/ }),
/***/ 76642:
@@ -74693,7 +74730,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.readFileContents = exports.isActionRepo = exports.isDirectory = exports.createArchives = exports.getConsolidatedDirectory = exports.removeDir = exports.createTempDir = void 0;
exports.stageActionFiles = exports.readFileContents = exports.isDirectory = exports.createArchives = exports.removeDir = exports.createTempDir = void 0;
const fs = __importStar(__nccwpck_require__(57147));
const fs_extra_1 = __importDefault(__nccwpck_require__(5630));
const path = __importStar(__nccwpck_require__(71017));
@@ -74716,24 +74753,6 @@ function removeDir(dir) {
}
}
exports.removeDir = removeDir;
// TODO: rename this function, it is not state-preserving, so it shouldn't just be called "get'"
function getConsolidatedDirectory(filePathSpec) {
const paths = filePathSpec.split(' '); // TODO: handle files with spaces
// TODO: do check on paths to make sure they're valid and not reaching outside the space
let consolidatedPath = '';
let needToCleanUpDir = false;
if (paths.length === 1 && isDirectory(paths[0])) {
// If the path is a single directory, we can skip the bundling step
consolidatedPath = paths[0];
}
else {
// Otherwise, we need to bundle the files & folders into a temporary directory
consolidatedPath = bundleFilesintoDirectory(paths);
needToCleanUpDir = true;
}
return { consolidatedPath, needToCleanUpDir };
}
exports.getConsolidatedDirectory = getConsolidatedDirectory;
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
// as well as the size/sha256 hash of each file.
async function createArchives(distPath, archiveTargetPath = createTempDir()) {
@@ -74782,32 +74801,29 @@ function isDirectory(dirPath) {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
}
exports.isDirectory = isDirectory;
function isActionRepo(stagingDir) {
return (fs.existsSync(path.join(stagingDir, 'action.yml')) ||
fs.existsSync(path.join(stagingDir, 'action.yaml')));
}
exports.isActionRepo = isActionRepo;
function readFileContents(filePath) {
return fs.readFileSync(filePath);
}
exports.readFileContents = readFileContents;
function bundleFilesintoDirectory(filePaths) {
const targetDir = createTempDir();
for (const filePath of filePaths) {
if (!fs.existsSync(filePath)) {
throw new Error(`filePath ${filePath} does not exist`);
}
if (isDirectory(filePath)) {
const targetFolder = path.join(targetDir, path.basename(filePath)); // TODO: basename is probably not what we actually want here. Or is it? Maybe conflicts between dir1/dir2 and dir1/dir3/dir2 are just user error or ??
fs_extra_1.default.copySync(filePath, targetFolder); // TODO: ignore files preceded by .
}
else {
const targetFile = path.join(targetDir, path.basename(filePath));
fs.copyFileSync(filePath, targetFile);
// Copy actions files from sourceDir to targetDir, excluding files and folders not relevant to the action
// Errors if the repo appears to not contain any action files, such as an action.yml file
function stageActionFiles(actionDir, targetDir) {
var actionYmlFound = false;
fs_extra_1.default.copySync(actionDir, targetDir, {
filter: (src, dest) => {
const basename = path.basename(src);
if (basename === 'action.yml' || basename === 'action.yaml') {
actionYmlFound = true;
}
// Filter out hidden folers like .git and .github
return !basename.startsWith('.');
}
});
if (!actionYmlFound) {
throw new Error(`No action.yml or action.yaml file found in source repository`);
}
return targetDir;
}
exports.stageActionFiles = stageActionFiles;
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
async function fileMetadata(filePath) {
const stats = fs.statSync(filePath);
@@ -74873,7 +74889,7 @@ const axios_1 = __importDefault(__nccwpck_require__(88757));
const fsHelper = __importStar(__nccwpck_require__(76642));
const axios_debug_log_1 = __importDefault(__nccwpck_require__(79301));
// Publish the OCI artifact and return the URL where it can be downloaded
async function publishOCIArtifact(token, registry, repository, releaseId, semver, zipFile, tarFile, manifest, debugRequests = false) {
async function publishOCIArtifact(token, registry, repository, semver, zipFile, tarFile, manifest, debugRequests = false) {
if (debugRequests) {
configureRequestDebugLogging();
}
@@ -74895,8 +74911,11 @@ async function publishOCIArtifact(token, registry, repository, releaseId, semver
}
});
await Promise.all(layerUploads);
await uploadManifest(JSON.stringify(manifest), manifestEndpoint, b64Token);
return new URL(`${repository}:${semver}`, registry);
const digest = await uploadManifest(JSON.stringify(manifest), manifestEndpoint, b64Token);
return {
packageURL: new URL(`${repository}:${semver}`, registry),
manifestDigest: digest
};
}
exports.publishOCIArtifact = publishOCIArtifact;
async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBlobEndpoint, b64Token) {
@@ -74958,6 +74977,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
throw new Error(`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`);
}
}
// Uploads the manifest and returns the digest returned by GHCR
async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
core.info(`Uploading manifest to ${manifestEndpoint}.`);
const putResponse = await axios_1.default.put(manifestEndpoint, manifestJSON, {
@@ -74972,6 +74992,11 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
if (putResponse.status !== 201) {
throw new Error(`Unexpected response from PUT manifest ${putResponse.status}`);
}
const digestResponseHeader = putResponse.headers['Docker-Content-Digest'];
if (digestResponseHeader === undefined) {
throw new Error(`No digest header in response from PUT manifest ${manifestEndpoint}`);
}
return digestResponseHeader;
}
function configureRequestDebugLogging() {
(0, axios_debug_log_1.default)({
@@ -75049,8 +75074,8 @@ const github = __importStar(__nccwpck_require__(95438));
const fsHelper = __importStar(__nccwpck_require__(76642));
const ociContainer = __importStar(__nccwpck_require__(33207));
const ghcr = __importStar(__nccwpck_require__(62894));
const api = __importStar(__nccwpck_require__(95707));
const semver_1 = __importDefault(__nccwpck_require__(11383));
const crypto_1 = __importDefault(__nccwpck_require__(6113));
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
@@ -75058,55 +75083,38 @@ const crypto_1 = __importDefault(__nccwpck_require__(6113));
async function run(pathInput) {
const tmpDirs = [];
try {
// Parse and validate Actions execution context, including the repository name, release name and event type
const repository = process.env.GITHUB_REPOSITORY || '';
if (repository === '') {
core.setFailed(`Could not find Repository.`);
return;
}
if (github.context.eventName !== 'release') {
core.setFailed('Please ensure you have the workflow trigger as release.');
const token = process.env.TOKEN || '';
const sourceCommit = process.env.GITHUB_SHA || '';
if (token === '') {
core.setFailed(`Could not find GITHUB_TOKEN.`);
return;
}
const releaseId = github.context.payload.release.id;
const releaseTag = github.context.payload.release.tag_name;
// 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
const targetVersion = semver_1.default.parse(releaseTag.replace(/^v/, ''));
if (!targetVersion) {
core.setFailed(`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
return;
}
const token = process.env.TOKEN;
const { consolidatedPath, needToCleanUpDir } = fsHelper.getConsolidatedDirectory(pathInput);
if (needToCleanUpDir) {
tmpDirs.push(consolidatedPath);
}
if (!fsHelper.isActionRepo(consolidatedPath)) {
core.setFailed('action.y(a)ml not found. Action packages can be created only for action repositories.');
if (sourceCommit === '') {
core.setFailed(`Could not find source commit.`);
return;
}
const semanticVersion = parseSourceSemanticVersion();
// Create a temporary directory to stage files for packaging in archives
const stagedActionFilesDir = fsHelper.createTempDir();
tmpDirs.push(stagedActionFilesDir);
fsHelper.stageActionFiles('.', stagedActionFilesDir);
// Create a temporary directory to store the archives
const archiveDir = fsHelper.createTempDir();
tmpDirs.push(archiveDir);
const archives = await fsHelper.createArchives(consolidatedPath, archiveDir);
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, targetVersion.raw, new Date());
// Generate SHA-256 hash of the manifest
const manifestSHA = crypto_1.default.createHash('sha256');
const manifestHash = manifestSHA
.update(JSON.stringify(manifest))
.digest('hex');
const response = await fetch(`${process.env.GITHUB_API_URL}/packages/container-registry-url`);
if (!response.ok) {
throw new Error(`Failed to fetch status page: ${response.statusText}`);
}
const data = await response.json();
const registryURL = new URL(data.url);
console.log(`Container registry URL: ${registryURL}`);
const packageURL = await ghcr.publishOCIArtifact(token, registryURL, repository, releaseId.toString(), targetVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
const archives = await fsHelper.createArchives(stagedActionFilesDir, archiveDir);
const { repoId, ownerId } = await api.getRepositoryMetadata(repository, token);
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, repoId, ownerId, sourceCommit, semanticVersion.raw, new Date());
const containerRegistryURL = await api.getContainerRegistryURL();
console.log(`Container registry URL: ${containerRegistryURL}`);
const { packageURL, manifestDigest } = await ghcr.publishOCIArtifact(token, containerRegistryURL, repository, semanticVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
core.setOutput('package-url', packageURL.toString());
core.setOutput('package-manifest', JSON.stringify(manifest));
core.setOutput('package-manifest-sha', `sha256:${manifestHash}`);
core.setOutput('package-manifest-sha', `sha256:${manifestDigest}`);
}
catch (error) {
// Fail the workflow run if an error occurs
@@ -75123,6 +75131,29 @@ async function run(pathInput) {
}
}
exports.run = run;
// This action can be triggered by release events or tag push events.
// In each case, the source event should produce a Semantic Version compliant tag representing the code to be packaged.
function parseSourceSemanticVersion() {
const event = github.context.eventName;
var semverTag = '';
// Grab the raw tag
if (event === 'release')
semverTag = github.context.payload.release.tag_name;
else if (event === 'push' && github.context.ref.startsWith('refs/tags/')) {
semverTag = github.context.ref.replace(/^refs\/tags\//, '');
}
else {
throw new Error(`This action can only be triggered by release events or tag push events.`);
}
if (semverTag === '') {
throw new Error(`Could not find a Semantic Version tag in the event payload.`);
}
const semanticVersion = semver_1.default.parse(semverTag.replace(/^v/, ''));
if (!semanticVersion) {
throw new Error(`${semverTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
}
return semanticVersion;
}
/***/ }),
@@ -75135,7 +75166,7 @@ exports.run = run;
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createActionPackageManifest = void 0;
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
function createActionPackageManifest(tarFile, zipFile, repository, version, created) {
function createActionPackageManifest(tarFile, zipFile, repository, repoId, ownerId, sourceCommit, version, created) {
const configLayer = createConfigLayer();
const sanitizedRepo = sanitizeRepository(repository);
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version);
@@ -75143,14 +75174,18 @@ function createActionPackageManifest(tarFile, zipFile, repository, version, crea
const manifest = {
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: configLayer,
layers: [configLayer, tarLayer, zipLayer],
annotations: {
'org.opencontainers.image.created': created.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': version,
'com.github.source.repo.id': repoId,
'com.github.source.repo.owner.id': ownerId,
'com.github.source.commit': sourceCommit
}
};
return manifest;