/**
* @fileoverview Container operations for GitHub
* @license Apache-2.0
* @version 3.0.0
* @author Michael Hay <michael.hay@mediumroast.io>
* @copyright 2025 Mediumroast, Inc. All rights reserved.
*/
import ResponseFactory from './response.js';
import { encodeContent } from './utils.js';
/**
* Manages container operations (locking, object manipulation)
*/
class ContainerOperations {
/**
* @constructor
* @param {Object} octokit - Octokit instance
* @param {String} orgName - GitHub organization name
* @param {String} repoName - GitHub repository name
* @param {String} mainBranchName - Main branch name
* @param {String} lockFileName - Lock file name
*/
constructor(octokit, orgName, repoName, mainBranchName, lockFileName) {
this.octokit = octokit;
this.orgName = orgName;
this.repoName = repoName;
this.mainBranchName = mainBranchName;
this.lockFileName = lockFileName;
}
/**
* Checks if a container is locked
* @param {String} containerName - Container name
* @returns {Promise<Array>} ResponseFactory result
*/
async checkForLock(containerName) {
try {
// Get the latest commit
const latestCommit = await this.octokit.rest.repos.getCommit({
owner: this.orgName,
repo: this.repoName,
ref: this.mainBranchName,
});
// Check if the lock file exists
const mainContents = await this.octokit.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
ref: latestCommit.data.sha,
path: containerName
});
const lockExists = mainContents.data.some(
item => item.path === `${containerName}/${this.lockFileName}`
);
if (lockExists) {
return ResponseFactory.success(
`Container ${containerName} is locked with lock file ${this.lockFileName}`,
lockExists,
200
);
} else {
return ResponseFactory.success(
`Container ${containerName} is not locked with lock file ${this.lockFileName}`,
lockExists,
404
);
}
} catch (err) {
return ResponseFactory.error(
`Failed to check if container ${containerName} is locked: ${err.message}`,
err
);
}
}
/**
* Locks a container
* @param {String} containerName - Container name
* @returns {Promise<Array>} ResponseFactory result
*/
async lockContainer(containerName) {
// Define the full path to the lockfile
const lockFile = `${containerName}/${this.lockFileName}`;
try {
// Get the latest commit
const { data: latestCommit } = await this.octokit.rest.repos.getCommit({
owner: this.orgName,
repo: this.repoName,
ref: this.mainBranchName,
});
const lockResponse = await this.octokit.rest.repos.createOrUpdateFileContents({
owner: this.orgName,
repo: this.repoName,
path: lockFile,
content: encodeContent(''),
branch: this.mainBranchName,
message: `Locking container [${containerName}]`,
sha: latestCommit.sha
});
return ResponseFactory.success(
`Locked the container ${containerName}`,
lockResponse.data
);
} catch (err) {
return ResponseFactory.error(
`Unable to lock the container ${containerName}: ${err.message}`,
err
);
}
}
/**
* Unlocks a container
* @param {String} containerName - Container name
* @param {String} commitSha - SHA of the lock file
* @param {String} branchName - Branch name
* @returns {Promise<Array>} ResponseFactory result
*/
async unlockContainer(containerName, commitSha, branchName) {
// Define the full path to the lockfile
const lockFile = `${containerName}/${this.lockFileName}`;
try {
const lockExists = await this.checkForLock(containerName);
if (lockExists[0] && lockExists[2]) {
const unlockResponse = await this.octokit.rest.repos.deleteFile({
owner: this.orgName,
repo: this.repoName,
path: lockFile,
branch: branchName,
message: `Unlocking container [${containerName}]`,
sha: commitSha
});
return ResponseFactory.success(
`Unlocked the container ${containerName}`,
unlockResponse.data
);
} else {
return ResponseFactory.error(
`Unable to unlock the container ${containerName}: Lock file not found`,
null
);
}
} catch (err) {
return ResponseFactory.error(
`Error unlocking container ${containerName}: ${err.message}`,
err
);
}
}
/**
* Catches multiple containers (locks them and prepares for operations)
* @param {Object} repoMetadata - Container metadata object
* @param {Object} objectFiles - Mapping of container names to their object files
* @param {Function} createBranchFn - Function to create a branch
* @param {Function} readObjectsFn - Function to read container objects
* @returns {Promise<Array>} ResponseFactory result
*/
async catchContainers(repoMetadata, objectFiles, createBranchFn, readObjectsFn) {
// Check locks
for (const container in repoMetadata.containers) {
const lockExists = await this.checkForLock(container);
if (lockExists[0] && lockExists[2]) {
return ResponseFactory.error(
`The container [${container}] is locked unable and cannot perform creates, updates or deletes on objects.`,
lockExists,
503
);
}
}
// Lock containers
for (const container in repoMetadata.containers) {
const locked = await this.lockContainer(container);
if (!locked[0]) {
return ResponseFactory.error(
`Unable to lock [${container}] and cannot perform creates, updates or deletes on objects.`,
locked,
503
);
}
repoMetadata.containers[container].lockSha = locked[2].content.sha;
}
// Create branch
const branchCreated = await createBranchFn();
if (!branchCreated[0]) {
return ResponseFactory.error(
'Unable to create new branch',
branchCreated,
503
);
}
// Extract branch ref (remove 'refs/heads/' prefix)
const branchRef = branchCreated[2].ref.replace('refs/heads/', '');
repoMetadata.branch = {
name: branchRef,
sha: branchCreated[2].object.sha
};
// Read objects
for (const container in repoMetadata.containers) {
const readResponse = await readObjectsFn(container);
if (!readResponse[0]) {
return ResponseFactory.error(
`Unable to read the source objects [${container}/${objectFiles[container]}].`,
readResponse,
503
);
}
repoMetadata.containers[container].objectSha = readResponse[2].sha;
repoMetadata.containers[container].objects = readResponse[2].mrJson;
}
return ResponseFactory.success(
`${Object.keys(repoMetadata.containers).length} containers are ready for use.`,
repoMetadata,
200
);
}
/**
* Releases containers (unlocks them and merges changes)
* @param {Object} repoMetadata - Container metadata object
* @param {Function} mergeBranchFn - Function to merge branch to main
* @returns {Promise<Array>} ResponseFactory result
*/
async releaseContainers(repoMetadata, mergeBranchFn) {
// Merge branch to main
const mergeResponse = await mergeBranchFn(
repoMetadata.branch.name,
repoMetadata.branch.sha
);
if (!mergeResponse[0]) {
return ResponseFactory.error(
'Unable to merge the branch to main.',
mergeResponse,
503
);
}
// Unlock containers
for (const container in repoMetadata.containers) {
// Unlock branch
const branchUnlocked = await this.unlockContainer(
container,
repoMetadata.containers[container].lockSha,
repoMetadata.branch.name
);
if (!branchUnlocked[0]) {
return ResponseFactory.error(
`Unable to unlock the container, objects may have been written please check [${container}] for objects and the lock file.`,
branchUnlocked,
503
);
}
// Unlock main
const mainUnlocked = await this.unlockContainer(
container,
repoMetadata.containers[container].lockSha,
this.mainBranchName
);
if (!mainUnlocked[0]) {
return ResponseFactory.error(
`Unable to unlock the container, objects may have been written please check [${container}] for objects and the lock file.`,
mainUnlocked,
503
);
}
}
// Return success
return ResponseFactory.success(
`Released [${Object.keys(repoMetadata.containers).length}] containers.`,
null,
200
);
}
}
export default ContainerOperations;