ci(release): add overwrite release rebuild mode

This commit is contained in:
Aethersailor
2026-05-27 21:24:10 +08:00
parent 8cbc63cf2d
commit 6554bf6288
2 changed files with 166 additions and 37 deletions

View File

@@ -16,7 +16,17 @@ on:
branches: branches:
- master - master
- dev - dev
workflow_dispatch: {} workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to rebuild, e.g. v1.2.3"
required: false
type: string
overwrite_existing_release:
description: "Overwrite assets/body of an existing GitHub Release"
required: false
default: false
type: boolean
schedule: schedule:
- cron: '0 19 * * 0' - cron: '0 19 * * 0'
@@ -53,12 +63,44 @@ jobs:
- name: Determine version - name: Determine version
id: version id: version
env:
DISPATCH_RELEASE_TAG: ${{ inputs.release_tag }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: | run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT echo "is_release=true" >> $GITHUB_OUTPUT
echo "Detected version from tag: $VERSION" echo "Detected version from tag: $VERSION"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "$OVERWRITE_EXISTING_RELEASE" == "true" ]]; then
git fetch --tags --force origin
VERSION="${DISPATCH_RELEASE_TAG:-}"
VERSION="${VERSION#"${VERSION%%[![:space:]]*}"}"
VERSION="${VERSION%"${VERSION##*[![:space:]]}"}"
if [[ "$VERSION" != v* ]]; then
VERSION="v$VERSION"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid release tag '$VERSION'. Use vX.Y.Z, for example v1.2.3."
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "::error::Tag '$VERSION' does not exist on origin."
exit 1
fi
TAG_COMMIT="$(git rev-list -n 1 "$VERSION")"
MASTER_COMMIT="$(git rev-parse HEAD)"
echo "Overwrite release tag: $VERSION"
echo "Tag commit: $TAG_COMMIT"
echo "Master commit used for rebuilt assets: $MASTER_COMMIT"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/heads/master ]] && ([[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "${{ github.event_name }}" == "schedule" ]]); then elif [[ "${{ github.ref }}" == refs/heads/master ]] && ([[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "${{ github.event_name }}" == "schedule" ]]); then
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then if [ -n "$LATEST_TAG" ]; then
@@ -505,7 +547,7 @@ jobs:
name: "🚀 Release Artifacts" name: "🚀 Release Artifacts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare, build-linux, build-windows-amd64, merge-manifest] needs: [prepare, build-linux, build-windows-amd64, merge-manifest]
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.overwrite_existing_release == true)
permissions: permissions:
contents: write contents: write
steps: steps:
@@ -515,32 +557,42 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Prepare Release Notes Context - name: Prepare Release Notes Context
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: | run: |
set -euo pipefail set -euo pipefail
git fetch --tags --force origin git fetch --tags --force origin
CURRENT_TAG="${GITHUB_REF_NAME}" CURRENT_TAG="$RELEASE_VERSION"
CURRENT_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")" if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ] && [ "${OVERWRITE_EXISTING_RELEASE}" = "true" ]; then
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${CURRENT_COMMIT}^" 2>/dev/null || true)" CURRENT_COMMIT="$(git rev-parse HEAD)"
TAG_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${TAG_COMMIT}^" 2>/dev/null || true)"
else
CURRENT_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${CURRENT_COMMIT}^" 2>/dev/null || true)"
fi
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904" EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
if [ -n "$PREVIOUS_TAG" ]; then if [ -n "$PREVIOUS_TAG" ]; then
RANGE_LABEL="$PREVIOUS_TAG..$CURRENT_TAG" RANGE_LABEL="$PREVIOUS_TAG..$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$PREVIOUS_TAG..$CURRENT_TAG" > commits.md git log --pretty=format:'- %h %s' "$PREVIOUS_TAG..$CURRENT_COMMIT" > commits.md
git diff --name-only "$PREVIOUS_TAG" "$CURRENT_TAG" > changed-files.md git diff --name-only "$PREVIOUS_TAG" "$CURRENT_COMMIT" > changed-files.md
git diff --stat "$PREVIOUS_TAG" "$CURRENT_TAG" > diffstat.md git diff --stat "$PREVIOUS_TAG" "$CURRENT_COMMIT" > diffstat.md
else else
RANGE_LABEL="$CURRENT_TAG" RANGE_LABEL="$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$CURRENT_TAG" > commits.md git log --pretty=format:'- %h %s' "$CURRENT_COMMIT" > commits.md
git diff --name-only "$EMPTY_TREE" "$CURRENT_TAG^{tree}" > changed-files.md git diff --name-only "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > changed-files.md
git diff --stat "$EMPTY_TREE" "$CURRENT_TAG^{tree}" > diffstat.md git diff --stat "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > diffstat.md
fi fi
{ {
echo "# Release Context" echo "# Release Context"
echo echo
echo "Current tag: $CURRENT_TAG" echo "Current tag: $CURRENT_TAG"
echo "Current commit: $CURRENT_COMMIT"
echo "Previous tag: ${PREVIOUS_TAG:-none}" echo "Previous tag: ${PREVIOUS_TAG:-none}"
echo "Range: $RANGE_LABEL" echo "Range: $RANGE_LABEL"
echo echo
@@ -687,6 +739,14 @@ jobs:
- `SHA256SUMS` - `SHA256SUMS`
EOF EOF
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.overwrite_existing_release }}" = "true" ]; then
{
echo
echo "<!-- rebuilt-from: $(git rev-parse HEAD) -->"
echo "<!-- rebuilt-at: $(date -u +%Y-%m-%dT%H:%M:%SZ) -->"
} >> release-notes.md
fi
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
with: with:
@@ -707,9 +767,11 @@ jobs:
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with: with:
tag_name: ${{ needs.prepare.outputs.version }}
files: | files: |
artifacts/linux-*/*.tar.gz artifacts/linux-*/*.tar.gz
artifacts/openwrt-*/*.apk artifacts/openwrt-*/*.apk
artifacts/windows-*/*.zip artifacts/windows-*/*.zip
SHA256SUMS SHA256SUMS
body_path: release-notes.md body_path: release-notes.md
overwrite_files: true

View File

@@ -3,15 +3,23 @@ name: Sync Dev to Master
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release_mode:
description: "Release mode: new creates a tag, overwrite rebuilds an existing release, sync_only only syncs branches."
required: true
default: "new"
type: choice
options:
- new
- overwrite
- sync_only
version: version:
description: "Release tag, e.g. v1.2.3. Leave empty to auto bump patch. Used only when create_release_tag is true." description: "Release tag, e.g. v1.2.3. Empty auto bumps for new or uses latest existing tag for overwrite."
required: false required: false
type: string type: string
create_release_tag: confirm_overwrite:
description: "Create and push a new release tag after syncing." description: "Type OVERWRITE to confirm rebuilding an existing release."
required: false required: false
default: true type: string
type: boolean
concurrency: concurrency:
group: sync-dev-to-master group: sync-dev-to-master
@@ -84,10 +92,13 @@ jobs:
git push origin master git push origin master
fi fi
- name: Create and Push Release Tag - name: Resolve Release Tag
if: ${{ inputs.create_release_tag != false }} id: release_tag
if: ${{ inputs.release_mode != 'sync_only' }}
env: env:
REQUESTED_VERSION: ${{ github.event.inputs.version }} RELEASE_MODE: ${{ inputs.release_mode }}
REQUESTED_VERSION: ${{ inputs.version }}
CONFIRM_OVERWRITE: ${{ inputs.confirm_overwrite }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -97,6 +108,11 @@ jobs:
REQUESTED_VERSION="${REQUESTED_VERSION#"${REQUESTED_VERSION%%[![:space:]]*}"}" REQUESTED_VERSION="${REQUESTED_VERSION#"${REQUESTED_VERSION%%[![:space:]]*}"}"
REQUESTED_VERSION="${REQUESTED_VERSION%"${REQUESTED_VERSION##*[![:space:]]}"}" REQUESTED_VERSION="${REQUESTED_VERSION%"${REQUESTED_VERSION##*[![:space:]]}"}"
if [ "$RELEASE_MODE" = "overwrite" ] && [ "${CONFIRM_OVERWRITE:-}" != "OVERWRITE" ]; then
echo "::error::confirm_overwrite must be exactly 'OVERWRITE' when release_mode=overwrite."
exit 1
fi
if [ -n "$REQUESTED_VERSION" ]; then if [ -n "$REQUESTED_VERSION" ]; then
TAG_NAME="$REQUESTED_VERSION" TAG_NAME="$REQUESTED_VERSION"
if [[ "$TAG_NAME" != v* ]]; then if [[ "$TAG_NAME" != v* ]]; then
@@ -107,13 +123,23 @@ jobs:
LATEST_TAG="$(git tag --list 'v*.*.*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" LATEST_TAG="$(git tag --list 'v*.*.*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)"
if [ -z "$LATEST_TAG" ]; then if [ -z "$LATEST_TAG" ]; then
TAG_NAME="v0.1.0" if [ "$RELEASE_MODE" = "new" ]; then
echo "No existing vX.Y.Z tag found. Starting at $TAG_NAME" TAG_NAME="v0.1.0"
echo "No existing vX.Y.Z tag found. Starting at $TAG_NAME"
else
echo "::error::No existing vX.Y.Z tag found to overwrite."
exit 1
fi
else else
VERSION="${LATEST_TAG#v}" if [ "$RELEASE_MODE" = "new" ]; then
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" VERSION="${LATEST_TAG#v}"
TAG_NAME="v${MAJOR}.${MINOR}.$((PATCH + 1))" IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "Auto bumped release tag from $LATEST_TAG to $TAG_NAME" TAG_NAME="v${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "Auto bumped release tag from $LATEST_TAG to $TAG_NAME"
else
TAG_NAME="$LATEST_TAG"
echo "No release tag requested. Using latest existing release tag: $TAG_NAME"
fi
fi fi
fi fi
@@ -122,21 +148,62 @@ jobs:
exit 1 exit 1
fi fi
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then if [ "$RELEASE_MODE" = "new" ]; then
echo "::error::Tag '$TAG_NAME' already exists locally." if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
exit 1 echo "::error::Tag '$TAG_NAME' already exists locally."
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
echo "::error::Tag '$TAG_NAME' already exists on origin."
exit 1
fi
else
if ! git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
echo "::error::Tag '$TAG_NAME' does not exist locally."
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
echo "::error::Tag '$TAG_NAME' does not exist on origin."
exit 1
fi
TAG_COMMIT="$(git rev-list -n 1 "$TAG_NAME")"
MASTER_COMMIT="$(git rev-parse HEAD)"
echo "Overwrite release tag: $TAG_NAME"
echo "Tag commit: $TAG_COMMIT"
echo "Master commit used for rebuilt assets: $MASTER_COMMIT"
fi fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "::error::Tag '$TAG_NAME' already exists on origin."
exit 1 - name: Create and Push Release Tag
fi if: ${{ inputs.release_mode == 'new' }}
env:
TAG_NAME: ${{ steps.release_tag.outputs.tag_name }}
run: |
set -euo pipefail
git tag -a "$TAG_NAME" -m "Release $TAG_NAME" git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
git push origin "$TAG_NAME" git push origin "$TAG_NAME"
- name: Skip Release Tag - name: Dispatch Overwrite Release Build
if: ${{ inputs.create_release_tag == false }} if: ${{ inputs.release_mode == 'overwrite' }}
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
TAG_NAME: ${{ steps.release_tag.outputs.tag_name }}
run: | run: |
echo "create_release_tag=false; synced dev to master without creating a release tag." set -euo pipefail
gh workflow run build-dockerhub.yml \
--ref master \
-f release_tag="$TAG_NAME" \
-f overwrite_existing_release=true
echo "Dispatched release rebuild for $TAG_NAME from master."
- name: Skip Release
if: ${{ inputs.release_mode == 'sync_only' }}
run: |
echo "release_mode=sync_only; synced dev to master without creating or rebuilding a release."