From 258cdec0264d304a1174deb725ac8f15d3df6c92 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 7 Feb 2026 09:45:35 +0800 Subject: [PATCH] chore: Update final release workflow (#6875) --- .../{beta-release.yml => release-beta.yml} | 0 .github/workflows/release-final.yml | 93 +++++++++++++++++++ ...ightly-release.yml => release-nightly.yml} | 0 extra/release/beta.mjs | 1 - extra/release/final.mjs | 76 ++++++++++----- extra/sort-contributors.js | 22 ----- .../{update-version.js => update-version.mjs} | 30 ++++-- extra/update-wiki-version.js | 58 ------------ extra/upload-github-release-asset.sh | 64 ------------- package.json | 1 - 10 files changed, 166 insertions(+), 179 deletions(-) rename .github/workflows/{beta-release.yml => release-beta.yml} (100%) create mode 100644 .github/workflows/release-final.yml rename .github/workflows/{nightly-release.yml => release-nightly.yml} (100%) delete mode 100644 extra/sort-contributors.js rename extra/{update-version.js => update-version.mjs} (70%) delete mode 100644 extra/update-wiki-version.js delete mode 100644 extra/upload-github-release-asset.sh diff --git a/.github/workflows/beta-release.yml b/.github/workflows/release-beta.yml similarity index 100% rename from .github/workflows/beta-release.yml rename to .github/workflows/release-beta.yml diff --git a/.github/workflows/release-final.yml b/.github/workflows/release-final.yml new file mode 100644 index 000000000..3c20b1ecf --- /dev/null +++ b/.github/workflows/release-final.yml @@ -0,0 +1,93 @@ +name: Final Release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version number (e.g., 2.1.0)" + required: true + type: string + previous_version: + description: "Previous version tag for changelog (e.g., 2.1.0-beta.3)" + required: true + type: string + dry_run: + description: "Dry Run (The docker image will not be pushed to registries. PR will still be created.)" + required: false + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 120 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: master + persist-credentials: true + fetch-depth: 0 # Fetch all history for changelog generation + + - name: Set up Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: 24 + + - name: Create release branch + env: + VERSION: ${{ inputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" + # Delete remote branch if it exists + git push origin --delete "release-${VERSION}" || true + # Delete local branch if it exists + git branch -D "release-${VERSION}" || true + # For testing purpose + # git checkout beta-workflow + git checkout -b "release-${VERSION}" + + - name: Install dependencies + run: npm clean-install --no-fund + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_USERNAME }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Run release-final + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }} + DRY_RUN: ${{ inputs.dry_run }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: npm run release-final + + - name: Upload dist.tar.gz as artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-${{ inputs.version }} + path: ./tmp/dist.tar.gz + retention-days: 90 diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/release-nightly.yml similarity index 100% rename from .github/workflows/nightly-release.yml rename to .github/workflows/release-nightly.yml diff --git a/extra/release/beta.mjs b/extra/release/beta.mjs index 9e3d86848..2a28b8ec2 100644 --- a/extra/release/beta.mjs +++ b/extra/release/beta.mjs @@ -7,7 +7,6 @@ import { checkTagExists, checkVersionFormat, getRepoNames, - execSync, checkReleaseBranch, createDistTarGz, createReleasePR, diff --git a/extra/release/final.mjs b/extra/release/final.mjs index 73c5a4cab..5b374df31 100644 --- a/extra/release/final.mjs +++ b/extra/release/final.mjs @@ -7,26 +7,39 @@ import { checkTagExists, checkVersionFormat, getRepoNames, - pressAnyKey, execSync, uploadArtifacts, checkReleaseBranch + checkReleaseBranch, + createDistTarGz, + createReleasePR, } from "./lib.mjs"; +import semver from "semver"; const repoNames = getRepoNames(); const version = process.env.RELEASE_VERSION; -const githubToken = process.env.RELEASE_GITHUB_TOKEN; +const dryRun = process.env.DRY_RUN === "true"; +const previousVersion = process.env.RELEASE_PREVIOUS_VERSION; +const branchName = `release-${version}`; +const githubRunId = process.env.GITHUB_RUN_ID; + +if (dryRun) { + console.log("Dry run mode enabled. No images will be pushed."); +} console.log("RELEASE_VERSION:", version); -if (!githubToken) { - console.error("GITHUB_TOKEN is required"); - process.exit(1); -} - -// Check if the current branch is "release" -checkReleaseBranch(); +// Check if the current branch is "release-{version}" +checkReleaseBranch(branchName); // Check if the version is a valid semver checkVersionFormat(version); +// Check if the semver identifier is empty +const semverIdentifier = semver.prerelease(version); +console.log("Semver identifier:", semverIdentifier); +if (semverIdentifier) { + console.error("VERSION should not have a semver identifier for final release"); + process.exit(1); +} + // Check if docker is running checkDocker(); @@ -34,27 +47,42 @@ checkDocker(); await checkTagExists(repoNames, version); // node extra/beta/update-version.js -execSync("node extra/update-version.js"); +await import("../update-version.mjs"); + +// Create Pull Request (gh pr create will handle pushing the branch) +await createReleasePR(version, previousVersion, dryRun, branchName, githubRunId); // Build frontend dist buildDist(); -// Build slim image (rootless) -buildImage(repoNames, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); +if (!dryRun) { + // Build slim image (rootless) + buildImage( + repoNames, + ["2-slim-rootless", ver(version, "slim-rootless")], + "rootless", + "BASE_IMAGE=louislam/uptime-kuma:base2-slim" + ); -// Build full image (rootless) -buildImage(repoNames, [ "2-rootless", ver(version, "rootless") ], "rootless"); + // Build full image (rootless) + buildImage(repoNames, ["2-rootless", ver(version, "rootless")], "rootless"); -// Build slim image -buildImage(repoNames, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + // Build slim image + buildImage( + repoNames, + ["next-slim", "2-slim", ver(version, "slim")], + "release", + "BASE_IMAGE=louislam/uptime-kuma:base2-slim" + ); -// Build full image -buildImage(repoNames, [ "next", "2", version ], "release"); + // Build full image + buildImage(repoNames, ["next", "2", version], "release"); +} else { + console.log("Dry run mode - skipping image build and push."); +} -await pressAnyKey(); +// Create dist.tar.gz +await createDistTarGz(); -// npm run upload-artifacts -uploadArtifacts(version, githubToken); - -// node extra/update-wiki-version.js -execSync("node extra/update-wiki-version.js"); +// Removed update wiki to keep it simple +// Do this in the wiki repo instead diff --git a/extra/sort-contributors.js b/extra/sort-contributors.js deleted file mode 100644 index 7228e0e20..000000000 --- a/extra/sort-contributors.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require("fs"); - -// Read the file from private/sort-contributors.txt -const file = fs.readFileSync("private/sort-contributors.txt", "utf8"); - -// Convert to an array of lines -let lines = file.split("\n"); - -// Remove empty lines -lines = lines.filter((line) => line !== ""); - -// Remove duplicates -lines = [...new Set(lines)]; - -// Remove @weblate and @UptimeKumaBot -lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam"); - -// Sort the lines -lines = lines.sort(); - -// Output the lines, concat with " " -console.log(lines.join(" ")); diff --git a/extra/update-version.js b/extra/update-version.mjs similarity index 70% rename from extra/update-version.js rename to extra/update-version.mjs index b88cce5ef..b79813929 100644 --- a/extra/update-version.js +++ b/extra/update-version.mjs @@ -1,3 +1,6 @@ +import { createRequire } from "module"; +const require = createRequire(import.meta.url); + const pkg = require("../package.json"); const fs = require("fs"); const childProcess = require("child_process"); @@ -5,28 +8,28 @@ const util = require("../src/util"); util.polyfill(); -const newVersion = process.env.RELEASE_VERSION; +const version = process.env.RELEASE_VERSION; -console.log("New Version: " + newVersion); +console.log("New Version: " + version); -if (!newVersion) { +if (!version) { console.error("invalid version"); process.exit(1); } -const exists = tagExists(newVersion); +const exists = tagExists(version); if (!exists) { // Process package.json - pkg.version = newVersion; + pkg.version = version; // Replace the version: https://regex101.com/r/hmj2Bc/1 - pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); + pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${version}`); fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); // Also update package-lock.json const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; - const resultVersion = childProcess.spawnSync(npm, ["--no-git-tag-version", "version", newVersion], { shell: true }); + const resultVersion = childProcess.spawnSync(npm, ["--no-git-tag-version", "version", version], { shell: true }); if (resultVersion.error) { console.error(resultVersion.error); console.error("error npm version!"); @@ -38,9 +41,10 @@ if (!exists) { console.error("error update package-lock!"); process.exit(1); } - commit(newVersion); + commit(version); } else { - console.log("version exists"); + console.log("version tag exists, please delete the tag or use another tag"); + process.exit(1); } /** @@ -59,6 +63,14 @@ function commit(version) { if (stdout.includes("no changes added to commit")) { throw new Error("commit error"); } + + // Get the current branch name + res = childProcess.spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]); + let branchName = res.stdout.toString().trim(); + console.log("Current branch:", branchName); + + // Git push the branch + childProcess.spawnSync("git", ["push", "origin", branchName, "--force"], { stdio: "inherit" }); } /** diff --git a/extra/update-wiki-version.js b/extra/update-wiki-version.js deleted file mode 100644 index 1b667b058..000000000 --- a/extra/update-wiki-version.js +++ /dev/null @@ -1,58 +0,0 @@ -const childProcess = require("child_process"); -const fs = require("fs"); - -const newVersion = process.env.RELEASE_VERSION; - -if (!newVersion) { - console.log("Missing version"); - process.exit(1); -} - -updateWiki(newVersion); - -/** - * Update the wiki with new version number - * @param {string} newVersion Version to update to - * @returns {void} - */ -function updateWiki(newVersion) { - const wikiDir = "./tmp/wiki"; - const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md"; - - safeDelete(wikiDir); - - childProcess.spawnSync("git", ["clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir]); - let content = fs.readFileSync(howToUpdateFilename).toString(); - - // Replace the version: https://regex101.com/r/hmj2Bc/1 - content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); - fs.writeFileSync(howToUpdateFilename, content); - - childProcess.spawnSync("git", ["add", "-A"], { - cwd: wikiDir, - }); - - childProcess.spawnSync("git", ["commit", "-m", `Update to ${newVersion}`], { - cwd: wikiDir, - }); - - console.log("Pushing to Github"); - childProcess.spawnSync("git", ["push"], { - cwd: wikiDir, - }); - - safeDelete(wikiDir); -} - -/** - * Check if a directory exists and then delete it - * @param {string} dir Directory to delete - * @returns {void} - */ -function safeDelete(dir) { - if (fs.existsSync(dir)) { - fs.rm(dir, { - recursive: true, - }); - } -} diff --git a/extra/upload-github-release-asset.sh b/extra/upload-github-release-asset.sh deleted file mode 100644 index 206e3cd6f..000000000 --- a/extra/upload-github-release-asset.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# -# Author: Stefan Buck -# License: MIT -# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447 -# -# -# This script accepts the following parameters: -# -# * owner -# * repo -# * tag -# * filename -# * github_api_token -# -# Script to upload a release asset using the GitHub API v3. -# -# Example: -# -# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip -# - -# Check dependencies. -set -e -xargs=$(which gxargs || which xargs) - -# Validate settings. -[ "$TRACE" ] && set -x - -CONFIG=$@ - -for line in $CONFIG; do - eval "$line" -done - -# Define variables. -GH_API="https://api.github.com" -GH_REPO="$GH_API/repos/$owner/$repo" -GH_TAGS="$GH_REPO/releases/tags/$tag" -AUTH="Authorization: token $github_api_token" -WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie" -CURL_ARGS="-LJO#" - -if [[ "$tag" == 'LATEST' ]]; then - GH_TAGS="$GH_REPO/releases/latest" -fi - -# Validate token. -curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; } - -# Read asset tags. -response=$(curl -sH "$AUTH" $GH_TAGS) - -# Get ID of the asset based on given filename. -eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=') -[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; } - -# Upload asset -echo "Uploading asset... " - -# Construct url -GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)" - -curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET diff --git a/package.json b/package.json index ea402f7f4..ff6e86622 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "start-pr-test": "node extra/checkout-pr.mjs && npm install && npm run dev", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go", "deploy-demo-server": "node extra/deploy-demo-server.js", - "sort-contributors": "node extra/sort-contributors.js", "quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2", "start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate", "rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",