diff --git a/extra/env2arg.js b/extra/env2arg.js deleted file mode 100644 index f89a91e4..00000000 --- a/extra/env2arg.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -const childProcess = require("child_process"); -let env = process.env; - -let cmd = process.argv[2]; -let args = process.argv.slice(3); -let replacedArgs = []; - -for (let arg of args) { - for (let key in env) { - arg = arg.replaceAll(`$${key}`, env[key]); - } - replacedArgs.push(arg); -} - -let child = childProcess.spawn(cmd, replacedArgs); -child.stdout.pipe(process.stdout); -child.stderr.pipe(process.stderr); diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js index ada2aca8..d2cb8cb4 100644 --- a/extra/mark-as-nightly.js +++ b/extra/mark-as-nightly.js @@ -15,7 +15,6 @@ if (newVersion) { // Process package.json pkg.version = newVersion; pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); - pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); // Process README.md diff --git a/extra/press-any-key.js b/extra/press-any-key.js deleted file mode 100644 index 42fc363c..00000000 --- a/extra/press-any-key.js +++ /dev/null @@ -1,6 +0,0 @@ -console.log("Git Push and Publish the release note on github, then press any key to continue"); - -process.stdin.setRawMode(true); -process.stdin.resume(); -process.stdin.on("data", process.exit.bind(process, 0)); - diff --git a/extra/release/beta.mjs b/extra/release/beta.mjs new file mode 100644 index 00000000..a2c9809f --- /dev/null +++ b/extra/release/beta.mjs @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { + ver, + buildDist, + buildImage, + checkDocker, + checkTagExists, + checkVersionFormat, + dryRun, + getRepoName, + pressAnyKey, + execSync, uploadArtifacts, +} from "./lib.mjs"; +import semver from "semver"; + +const repoName = getRepoName(); +const version = process.env.RELEASE_BETA_VERSION; +const githubToken = process.env.RELEASE_GITHUB_TOKEN; + +console.log("RELEASE_BETA_VERSION:", version); + +if (!githubToken) { + console.error("GITHUB_TOKEN is required"); + process.exit(1); +} + +// Check if the version is a valid semver +checkVersionFormat(version); + +// Check if the semver identifier is "beta" +const semverIdentifier = semver.prerelease(version); +console.log("Semver identifier:", semverIdentifier); +if (semverIdentifier[0] !== "beta") { + console.error("VERSION should have a semver identifier of 'beta'"); + process.exit(1); +} + +// Check if docker is running +checkDocker(); + +// Check if the tag exists +await checkTagExists(repoName, version); + +// node extra/beta/update-version.js +execSync("node ./extra/beta/update-version.js"); + +// Build frontend dist +buildDist(); + +// Build slim image (rootless) +buildImage(repoName, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image (rootless) +buildImage(repoName, [ "beta-rootless", ver(version, "rootless") ], "rootless"); + +// Build slim image +buildImage(repoName, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image +buildImage(repoName, [ "beta", version ], "release"); + +await pressAnyKey(); + +// npm run upload-artifacts +uploadArtifacts(); diff --git a/extra/release/final.mjs b/extra/release/final.mjs new file mode 100644 index 00000000..d190ac0b --- /dev/null +++ b/extra/release/final.mjs @@ -0,0 +1,57 @@ +import "dotenv/config"; +import { + ver, + buildDist, + buildImage, + checkDocker, + checkTagExists, + checkVersionFormat, + getRepoName, + pressAnyKey, execSync, uploadArtifacts +} from "./lib.mjs"; + +const repoName = getRepoName(); +const version = process.env.RELEASE_VERSION; +const githubToken = process.env.RELEASE_GITHUB_TOKEN; + +console.log("RELEASE_VERSION:", version); + +if (!githubToken) { + console.error("GITHUB_TOKEN is required"); + process.exit(1); +} + +// Check if the version is a valid semver +checkVersionFormat(version); + +// Check if docker is running +checkDocker(); + +// Check if the tag exists +await checkTagExists(repoName, version); + +// node extra/beta/update-version.js +execSync("node extra/update-version.js"); + +// Build frontend dist +buildDist(); + +// Build slim image (rootless) +buildImage(repoName, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image (rootless) +buildImage(repoName, [ "2-rootless", ver(version, "rootless") ], "rootless"); + +// Build slim image +buildImage(repoName, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image +buildImage(repoName, [ "next", "2", version ], "release"); + +await pressAnyKey(); + +// npm run upload-artifacts +uploadArtifacts(); + +// node extra/update-wiki-version.js +execSync("node extra/update-wiki-version.js"); diff --git a/extra/release/lib.mjs b/extra/release/lib.mjs new file mode 100644 index 00000000..d6c294c1 --- /dev/null +++ b/extra/release/lib.mjs @@ -0,0 +1,191 @@ +import "dotenv/config"; +import * as childProcess from "child_process"; +import semver from "semver"; + +export const dryRun = process.env.RELEASE_DRY_RUN === "1"; + +if (dryRun) { + console.info("Dry run enabled."); +} + +/** + * Check if docker is running + * @returns {void} + */ +export function checkDocker() { + try { + childProcess.execSync("docker ps"); + } catch (error) { + console.error("Docker is not running. Please start docker and try again."); + process.exit(1); + } +} + +/** + * Get Docker Hub repository name + */ +export function getRepoName() { + return process.env.RELEASE_REPO_NAME || "louislam/uptime-kuma"; +} + +/** + * Build frontend dist + * @returns {void} + */ +export function buildDist() { + if (!dryRun) { + childProcess.execSync("npm run build", { stdio: "inherit" }); + } else { + console.info("[DRY RUN] npm run build"); + } +} + +/** + * Build docker image and push to Docker Hub + * @param {string} repoName Docker Hub repository name + * @param {string[]} tags Docker image tags + * @param {string} target Dockerfile's target name + * @param {string} buildArgs Docker build args + * @param {string} dockerfile Path to Dockerfile + * @param {string} platform Build platform + * @returns {void} + */ +export function buildImage(repoName, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") { + let args = [ + "buildx", + "build", + "-f", + dockerfile, + "--platform", + platform, + ]; + + // Add tags + for (let tag of tags) { + args.push("-t", `${repoName}:${tag}`); + } + + args = [ + ...args, + "--target", + target, + ]; + + // Add build args + if (buildArgs) { + args.push("--build-arg", buildArgs); + } + + args = [ + ...args, + ".", + "--push", + ]; + + if (!dryRun) { + childProcess.spawnSync("docker", args, { stdio: "inherit" }); + } else { + console.log(`[DRY RUN] docker ${args.join(" ")}`); + } +} + +/** + * Check if the version already exists on Docker Hub + * TODO: use semver to compare versions if it is greater than the previous? + * @param {string} repoName Docker Hub repository name + * @param {string} version Version to check + * @returns {void} + */ +export async function checkTagExists(repoName, version) { + console.log(`Checking if version ${version} exists on Docker Hub`); + + // Get a list of tags from the Docker Hub repository + let tags = []; + + // It is mainly to check my careless mistake that I forgot to update the release version in .env, so `page_size` is set to 100 is enough, I think. + const response = await fetch(`https://hub.docker.com/v2/repositories/${repoName}/tags/?page_size=100`); + if (response.ok) { + const data = await response.json(); + tags = data.results.map((tag) => tag.name); + } else { + console.error("Failed to get tags from Docker Hub"); + process.exit(1); + } + + // Check if the version already exists + if (tags.includes(version)) { + console.error(`Version ${version} already exists`); + process.exit(1); + } +} + +/** + * Check the version format + * @param {string} version Version to check + * @returns {void} + */ +export function checkVersionFormat(version) { + if (!version) { + console.error("VERSION is required"); + process.exit(1); + } + + // Check the version format, it should be a semver and must be like this: "2.0.0-beta.0" + if (!semver.valid(version)) { + console.error("VERSION is not a valid semver version"); + process.exit(1); + } +} + +/** + * Press any key to continue + * @returns {Promise} + */ +export function pressAnyKey() { + console.log("Git Push and Publish the release note on github, then press any key to continue"); + process.stdin.setRawMode(true); + process.stdin.resume(); + return new Promise(resolve => process.stdin.once("data", data => { + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(); + })); +} + +/** + * Append version identifier + * @param {string} version Version + * @param {string} identifier Identifier + * @returns {string} Version with identifier + */ +export function ver(version, identifier) { + const obj = semver.parse(version); + + if (obj.prerelease.length === 0) { + obj.prerelease = [ identifier ]; + } else { + obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-"); + } + return obj.format(); +} + +/** + * Upload artifacts to GitHub + * @returns {void} + */ +export function uploadArtifacts() { + execSync("npm run upload-artifacts"); +} + +/** + * Execute a command + * @param {string} cmd Command to execute + * @returns {void} + */ +export function execSync(cmd) { + if (!dryRun) { + childProcess.execSync(cmd, { stdio: "inherit" }); + } else { + console.info(`[DRY RUN] ${cmd}`); + } +} diff --git a/extra/release/nightly.mjs b/extra/release/nightly.mjs new file mode 100644 index 00000000..c6641bad --- /dev/null +++ b/extra/release/nightly.mjs @@ -0,0 +1,16 @@ +import { buildDist, buildImage, checkDocker, getRepoName } from "./lib.mjs"; + +// Docker Hub repository name +const repoName = getRepoName(); + +// Check if docker is running +checkDocker(); + +// Build frontend dist (it will build on the host machine, TODO: build on a container?) +buildDist(); + +// Build full image (rootless) +buildImage(repoName, [ "nightly2-rootless" ], "nightly-rootless"); + +// Build full image +buildImage(repoName, [ "nightly2" ], "nightly"); diff --git a/extra/test-docker.js b/extra/test-docker.js deleted file mode 100644 index d7926fcc..00000000 --- a/extra/test-docker.js +++ /dev/null @@ -1,9 +0,0 @@ -// Check if docker is running -const { exec } = require("child_process"); - -exec("docker ps", (err, stdout, stderr) => { - if (err) { - console.error("Docker is not running. Please start docker and try again."); - process.exit(1); - } -}); diff --git a/package.json b/package.json index ad9aac91..e4a94b04 100644 --- a/package.json +++ b/package.json @@ -34,20 +34,13 @@ "playwright-show-report": "playwright show-report ./private/playwright-report", "tsc": "tsc", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js", - "build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim", "build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push", "build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push", "build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push", - "build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:next-slim -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push", - "build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:next -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push", - "build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push", - "build-docker-slim-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim-rootless -t louislam/uptime-kuma:$VERSION-slim-rootless --target rootless --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push", - "build-docker-full-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-rootless -t louislam/uptime-kuma:$VERSION-rootless --target rootless . --push", - "build-docker-nightly-rootless": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2-rootless --target nightly-rootless . --push", "build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", - "setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist", + "setup": "git checkout 1.23.15 && npm ci --production && npm run download-dist", "download-dist": "node extra/download-dist.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", @@ -58,8 +51,9 @@ "simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres", "simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb", "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix", - "release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", - "release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", + "release-final": "node ./extra/release/final.mjs", + "release-beta": "node ./extra/release/beta.mjs", + "release-nightly": "node ./extra/release/nightly.mjs", "git-remove-tag": "git tag -d", "build-dist-and-restart": "npm run build && npm run start-server-dev", "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",