auto-deploy-sh
Version:
Automated Docker deployment tool
233 lines (231 loc) • 9.14 kB
JavaScript
import { log, ora, run, fs, exit, chalk } from "./utils/index.js";
import path from 'path';
import archiver from 'archiver';
export class Deploy {
config;
context;
uploadFileName;
remotePath;
REMOTEAPPPATH = '/tmp/www/app';
constructor(config, context) {
this.config = config;
this.context = context;
this.uploadFileName = `${this.config.containerName}.tar.gz`;
this.remotePath = `/tmp/${this.uploadFileName}`;
}
async runCommon(_options, std) {
ora.start(std.startMsg);
const { command, options, args } = _options;
try {
const { stdout, stderr, failed, signal, exitCode } = await run(command, this.context, options, args);
const isFailure = failed || signal || exitCode !== 0;
if (isFailure) {
throw new Error(`${stderr}\nexecution ${command} failed`);
}
ora.stop('succeed', std.succMsg);
if (stdout) {
log.info(stdout);
}
if (stderr) {
log.warn(stderr);
}
}
catch (err) {
ora.stop('fail', std.errMsg);
log.error(err.message ?? '');
throw err;
}
}
async preBeforeRelease() {
if (this.config.beforLaunch) {
if (Array.isArray(this.config.beforLaunch)) {
for (let i = 0; i < this.config.beforLaunch.length; ++i) {
const command = this.config.beforLaunch[i];
command &&
(await this.runCommon({ command, options: { reject: false } }, {
startMsg: `start execution ${command}....`,
succMsg: `success execution ${command}`,
errMsg: `error execution ${command}`,
}));
}
}
else {
log.error(`beforLaunch should be an array, but the type you set is ${typeof this.config.beforLaunch}`);
throw new Error();
}
}
}
async compressFiles() {
const files = [];
if (this.config.dockerBuildFiles && this.config.dockerBuildFiles.length > 0) {
let dockerfileFlag = false, dockerignoreFlag = false;
for (let i = 0; i < this.config.dockerBuildFiles.length; ++i) {
const file = this.config.dockerBuildFiles[i];
if (/\bDockerfile\b/.test(file)) {
dockerfileFlag = true;
}
if (/\bDockerfile\b/.test(file)) {
dockerignoreFlag = true;
}
if (dockerfileFlag && dockerignoreFlag)
break;
}
if (!dockerfileFlag) {
this.config.dockerBuildFiles.push('Dockerfile');
}
if (!dockerignoreFlag && fs._.existsSync(path.resolve(this.context, '.dockerignore'))) {
this.config.dockerBuildFiles.push('.dockerignore');
}
this.config.dockerBuildFiles.forEach(async (file) => {
const fullPath = path.resolve(this.context, file);
if (fs._.existsSync(fullPath)) {
files.push(fullPath);
}
else {
log.error(`The file name is ${file}, and the file does not exist`);
throw new Error();
}
});
}
else {
log.error(`The file to build the image cannot be empty. Please check whether dockerBuildFiles is set correctly`);
throw new Error();
}
const output = fs.createWriteStream(path.resolve(this.context, this.uploadFileName));
const archive = archiver('tar', { gzip: true, gzipOptions: { level: 9 } });
archive.pipe(output);
const waitForArchiveEnd = new Promise((resolve, rejects) => {
archive.on('error', function (err) {
log.error(`Error in compressed file`);
rejects(new Error(err.message));
});
archive.on('end', () => {
log.done(`✔ Success in compressed file`);
resolve('');
});
});
const fileNames = [];
for (const file of files) {
const fileName = path.basename(file);
fileNames.push(fileName);
if (await fs.isDirectory(file)) {
archive.directory(file, fileName);
}
else {
archive.file(file, { name: fileName });
}
}
await archive.finalize();
await waitForArchiveEnd;
}
async uploadSSH(ssh) {
await this.compressFiles();
const uploadPath = path.resolve(this.context, this.uploadFileName);
if (await fs.exist(uploadPath)) {
const state = await fs.getFileStat(uploadPath);
const localSize = state.size;
await ora.start('📤 开始上传...', async () => {
await ssh.putFile(uploadPath, this.remotePath);
}, status => {
if (status)
return `文件上传完成`;
else {
exit(1);
return chalk.error(`exit code: 1`);
}
});
const exists = await ssh.execCommand(`test -f "${this.remotePath}" && echo "yes" || echo "no"`);
if (exists.stdout.trim() !== 'yes') {
log.error('文件上传检测失败');
throw new Error();
}
const sizeResult = await ssh.execCommand(`stat -c %s "${this.remotePath}"`);
const remoteSize = parseInt(sizeResult.stdout.trim(), 10);
if (isNaN(remoteSize)) {
log.error('无法获取远程文件大小');
throw new Error();
}
if (remoteSize !== localSize && remoteSize < localSize - 10) {
log.error(`文件大小不匹配: 本地=${localSize}, 远程=${remoteSize}`);
throw new Error();
}
}
else {
log.error(`The file to be uploaded is not found.`);
throw new Error();
}
}
async execCommand(ssh, std, command) {
if (!command || !std.startMsg || !std.succMsg || !ssh)
throw new Error();
ora.start(std.startMsg);
const { stderr, code } = await ssh.execCommand(command);
if (code !== 0 || (stderr && code !== 0)) {
ora.stop('fail', stderr);
throw new Error();
}
ora.stop('succeed', std.succMsg);
stderr && log.info(stderr);
}
async dockcerImageBuild(ssh) {
await this.uploadSSH(ssh);
try {
const REMOTEAPPPATH = this.REMOTEAPPPATH;
const CONTAINER_NAME = this.config.containerName;
const REMOTE_PATH = this.remotePath;
const IMAGE_TAG = this.config.imageTag;
const TARGET_DIR = `./${CONTAINER_NAME}`;
await this.execCommand(ssh, {
startMsg: 'Start decompress project....',
succMsg: 'Decompress project completed.',
}, `
if ! cd "${REMOTEAPPPATH}" &>/dev/null; then
mkdir -p "${REMOTEAPPPATH}";
cd "${REMOTEAPPPATH}";
fi
mkdir -p "${TARGET_DIR}"
if [ ! -f "${REMOTE_PATH}" ]; then
echo "❌ 错误:压缩包不存在: ${REMOTE_PATH}";
exit 1
fi
tar -xzf "${REMOTE_PATH}" -C "${TARGET_DIR}";
`);
await this.execCommand(ssh, {
startMsg: `🐳 构建镜像: ${IMAGE_TAG}...`,
succMsg: `🐳 构建镜像: ${IMAGE_TAG}成功.`,
}, `
cd "${REMOTEAPPPATH}"
docker build -t ${IMAGE_TAG} ${TARGET_DIR}
`);
await this.execCommand(ssh, {
startMsg: `🔄 停止并清理旧容器: ${CONTAINER_NAME}...`,
succMsg: `旧容器已经清理成功.`,
}, `
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
docker stop "${CONTAINER_NAME}";
docker rm "${CONTAINER_NAME}";
else
echo "ℹ️ 容器 ${CONTAINER_NAME} 不存在,跳过清理";
fi
`);
await this.execCommand(ssh, {
startMsg: `🐳 启动容器: ${CONTAINER_NAME}...`,
succMsg: `🐳 启动容器成功.`,
}, `
docker run -d --name ${CONTAINER_NAME} -p ${this.config.BindPorts} ${IMAGE_TAG}
`);
log.done('✔ 部署完成');
}
catch {
log.error('✖ 部署失败');
}
finally {
await this.clear(ssh);
}
}
async clear(ssh) {
await ssh.execCommand(`rm -f ${this.remotePath}`);
await ssh.execCommand(`rm -rf ${this.REMOTEAPPPATH}/${this.config.containerName}`);
await ssh.execCommand(`docker image prune -f`);
}
}