mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
* feat: add nightly prerelease release flow and updater support * feat(ci): auto-generate nightly release notes from latest stable tag * fix(ci): correct nightly release notes heredoc YAML indentation * fix(ci): align nightly notes heredoc terminator * fix(ci): remove heredoc body indentation in nightly notes script * fix: align nightly release metadata and prerelease rules * fix: harden nightly release flow and updater release resolution * fix: improve nightly branch resolution and updater logging * fix: simplify updater target resolution and nightly release assets * fix: avoid inputs lookup on non-dispatch release events * fix: split nightly release fetch and simplify updater flow * refactor: simplify updater target resolvers and nightly error checks * fix: type release fetch errors and streamline updater resolution * refactor: simplify updater target branching and release artifacts * refactor: simplify release fetching and harden nightly git diagnostics * fix: validate release payload shape before parsing * refactor: harden prerelease handling and nightly constants * refactor: derive archive urls and enrich fetch errors * refactor: simplify update target resolution flow * refactor: linearize update target resolution * refactor: validate update target inputs and sync nightly tag source * refactor: simplify updater mode resolution and prerelease tests * refactor: simplify update target resolution flow * fix: avoid package import when resolving nightly tag * refactor: simplify updater resolution and centralize release constants * fix: harden nightly release notes generation in workflow * refactor: streamline update target resolution and errors * refactor: simplify updater target resolution and nightly handling * refactor: simplify updater errors and package release scripts * refactor: centralize release api constants and loader * fix(ci): resolve dispatch fallback tag from stable releases
419 lines
15 KiB
YAML
419 lines
15 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
schedule:
|
|
# Daily at 00:00 UTC
|
|
- cron: "0 0 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: "Git ref to build (branch/tag/SHA)"
|
|
required: false
|
|
default: "master"
|
|
tag:
|
|
description: "Release tag to publish assets to (for example: v4.14.6)"
|
|
required: false
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
resolve-release-context:
|
|
name: Resolve Release Context
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
checkout_ref: ${{ steps.checkout-ref.outputs.ref }}
|
|
tag: ${{ steps.tag.outputs.tag }}
|
|
nightly_tag: ${{ steps.nightly-tag.outputs.nightly_tag }}
|
|
repo_slug: ${{ steps.repo-slug.outputs.repo_slug }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve nightly tag
|
|
id: nightly-tag
|
|
shell: bash
|
|
run: |
|
|
nightly_tag="$(python3 -m scripts.release.print_release_constant NIGHTLY_TAG)"
|
|
if [ -z "$nightly_tag" ]; then
|
|
echo "Failed to resolve NIGHTLY_TAG from astrbot/core/release_constants.py." >&2
|
|
exit 1
|
|
fi
|
|
echo "nightly_tag=$nightly_tag" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve repository slug
|
|
id: repo-slug
|
|
shell: bash
|
|
run: |
|
|
repo_slug="$(python3 -m scripts.release.print_release_constant GITHUB_REPO_SLUG)"
|
|
if [ -z "$repo_slug" ]; then
|
|
echo "Failed to resolve GITHUB_REPO_SLUG from astrbot/core/release_constants.py." >&2
|
|
exit 1
|
|
fi
|
|
echo "repo_slug=$repo_slug" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve checkout ref
|
|
id: checkout-ref
|
|
shell: bash
|
|
run: |
|
|
event_name="${{ github.event_name }}"
|
|
nightly_tag="${{ steps.nightly-tag.outputs.nightly_tag }}"
|
|
input_tag=""
|
|
input_ref=""
|
|
if [ "$event_name" = "workflow_dispatch" ]; then
|
|
input_tag="$(jq -r '.inputs.tag // ""' "$GITHUB_EVENT_PATH")"
|
|
input_ref="$(jq -r '.inputs.ref // ""' "$GITHUB_EVENT_PATH")"
|
|
fi
|
|
default_branch="${{ github.event.repository.default_branch }}"
|
|
if [ -z "$default_branch" ]; then
|
|
default_branch="master"
|
|
fi
|
|
|
|
if [ "$event_name" = "schedule" ]; then
|
|
ref="refs/heads/${default_branch}"
|
|
elif [ "$event_name" = "workflow_dispatch" ] && [ -n "$input_ref" ]; then
|
|
ref="$input_ref"
|
|
elif [ "$event_name" = "workflow_dispatch" ] && [ "$input_tag" = "$nightly_tag" ]; then
|
|
ref="refs/heads/${default_branch}"
|
|
elif [ -n "$input_ref" ]; then
|
|
ref="$input_ref"
|
|
else
|
|
ref="${{ github.ref }}"
|
|
fi
|
|
|
|
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Switch repository to release ref
|
|
shell: bash
|
|
run: |
|
|
target_ref="${{ steps.checkout-ref.outputs.ref }}"
|
|
if [ -z "$target_ref" ]; then
|
|
echo "Resolved checkout ref is empty." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! git checkout --force "$target_ref" >/tmp/git_checkout_ref.log 2>&1; then
|
|
if ! git fetch --force origin "$target_ref" >/tmp/git_fetch_ref.log 2>&1; then
|
|
echo "Failed to fetch checkout ref: $target_ref" >&2
|
|
cat /tmp/git_fetch_ref.log >&2 || true
|
|
exit 1
|
|
fi
|
|
git checkout --force FETCH_HEAD
|
|
fi
|
|
|
|
- name: Resolve tag
|
|
id: tag
|
|
shell: bash
|
|
run: |
|
|
event_name="${{ github.event_name }}"
|
|
nightly_tag="${{ steps.nightly-tag.outputs.nightly_tag }}"
|
|
dispatch_tag=""
|
|
if [ "$event_name" = "workflow_dispatch" ]; then
|
|
dispatch_tag="$(jq -r '.inputs.tag // ""' "$GITHUB_EVENT_PATH")"
|
|
fi
|
|
|
|
if [ "$event_name" = "schedule" ]; then
|
|
tag="$nightly_tag"
|
|
elif [ "$event_name" = "push" ]; then
|
|
tag="${GITHUB_REF_NAME}"
|
|
elif [ -n "$dispatch_tag" ]; then
|
|
tag="$dispatch_tag"
|
|
else
|
|
# workflow_dispatch without explicit tag should default to latest stable v-tag.
|
|
tag="$(git tag --list 'v*' --sort=-version:refname | grep -E '^v[0-9]+(\.[0-9]+)*$' | head -n1)"
|
|
if [ -z "$tag" ]; then
|
|
echo "Failed to resolve latest stable v-tag." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
if [ -z "$tag" ]; then
|
|
echo "Failed to resolve tag." >&2
|
|
exit 1
|
|
fi
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
|
|
verify-core:
|
|
name: Verify Core Quality Gate
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- resolve-release-context
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ needs.resolve-release-context.outputs.checkout_ref }}
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install uv
|
|
shell: bash
|
|
run: python -m pip install uv
|
|
|
|
- name: Run local PR gate checks
|
|
shell: bash
|
|
run: make pr-test-neo
|
|
|
|
build-dashboard:
|
|
name: Build Dashboard
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- resolve-release-context
|
|
env:
|
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ needs.resolve-release-context.outputs.checkout_ref }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 10.28.2
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '24.13.0'
|
|
cache: "pnpm"
|
|
cache-dependency-path: dashboard/pnpm-lock.yaml
|
|
|
|
- name: Build dashboard dist
|
|
shell: bash
|
|
run: |
|
|
pnpm --dir dashboard install --frozen-lockfile
|
|
pnpm --dir dashboard run build
|
|
echo "${{ needs.resolve-release-context.outputs.tag }}" > dashboard/dist/assets/version
|
|
cd dashboard
|
|
zip -r "AstrBot-${{ needs.resolve-release-context.outputs.tag }}-dashboard.zip" dist
|
|
|
|
- name: Upload dashboard artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: Dashboard-${{ needs.resolve-release-context.outputs.tag }}
|
|
if-no-files-found: error
|
|
path: dashboard/AstrBot-${{ needs.resolve-release-context.outputs.tag }}-dashboard.zip
|
|
|
|
- name: Upload dashboard package to Cloudflare R2
|
|
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' && needs.resolve-release-context.outputs.tag != needs.resolve-release-context.outputs.nightly_tag }}
|
|
env:
|
|
R2_BUCKET_NAME: "astrbot"
|
|
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
|
VERSION_TAG: ${{ needs.resolve-release-context.outputs.tag }}
|
|
shell: bash
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y rclone
|
|
|
|
mkdir -p ~/.config/rclone
|
|
cat <<EOF > ~/.config/rclone/rclone.conf
|
|
[r2]
|
|
type = s3
|
|
provider = Cloudflare
|
|
access_key_id = $R2_ACCESS_KEY_ID
|
|
secret_access_key = $R2_SECRET_ACCESS_KEY
|
|
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
|
EOF
|
|
|
|
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
|
|
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
|
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
|
|
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
|
|
|
|
publish-release:
|
|
name: Publish GitHub Release
|
|
runs-on: ubuntu-24.04
|
|
concurrency:
|
|
group: ${{ needs.resolve-release-context.outputs.tag == needs.resolve-release-context.outputs.nightly_tag && 'nightly-release' || format('release-{0}', needs.resolve-release-context.outputs.tag) }}
|
|
cancel-in-progress: ${{ needs.resolve-release-context.outputs.tag == needs.resolve-release-context.outputs.nightly_tag }}
|
|
needs:
|
|
- resolve-release-context
|
|
- verify-core
|
|
- build-dashboard
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ needs.resolve-release-context.outputs.checkout_ref }}
|
|
|
|
- name: Resolve release title
|
|
id: release-meta
|
|
shell: bash
|
|
run: |
|
|
tag="${{ needs.resolve-release-context.outputs.tag }}"
|
|
nightly_tag="${{ needs.resolve-release-context.outputs.nightly_tag }}"
|
|
if [ "$tag" = "$nightly_tag" ]; then
|
|
if ! short_sha="$(git rev-parse --short=8 HEAD 2>/tmp/git_rev_parse_error.log)"; then
|
|
echo "Failed to resolve HEAD short SHA for nightly title." >&2
|
|
cat /tmp/git_rev_parse_error.log >&2 || true
|
|
exit 1
|
|
fi
|
|
base_version="$(git tag --list 'v*' --sort=-version:refname | grep -E '^v[0-9]+(\.[0-9]+)*$' | head -n1)"
|
|
if [ -z "$base_version" ]; then
|
|
base_version="v0.0.0"
|
|
fi
|
|
title="${base_version}-${nightly_tag}-${short_sha}"
|
|
else
|
|
base_version="$tag"
|
|
title="$tag"
|
|
fi
|
|
echo "title=$title" >> "$GITHUB_OUTPUT"
|
|
echo "base_version=$base_version" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Force-update nightly tag
|
|
if: ${{ needs.resolve-release-context.outputs.tag == needs.resolve-release-context.outputs.nightly_tag }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
nightly_tag="${{ needs.resolve-release-context.outputs.nightly_tag }}"
|
|
if ! current_sha="$(git rev-parse HEAD 2>/tmp/git_rev_parse_error.log)"; then
|
|
echo "Failed to resolve HEAD SHA before updating nightly tag." >&2
|
|
cat /tmp/git_rev_parse_error.log >&2 || true
|
|
exit 1
|
|
fi
|
|
git tag -f "$nightly_tag" "${current_sha}"
|
|
git push --force origin "refs/tags/${nightly_tag}"
|
|
|
|
- name: Download dashboard artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: Dashboard-${{ needs.resolve-release-context.outputs.tag }}
|
|
path: release-assets
|
|
|
|
|
|
- name: Resolve release notes
|
|
id: notes
|
|
shell: bash
|
|
run: |
|
|
tag="${{ needs.resolve-release-context.outputs.tag }}"
|
|
nightly_tag="${{ needs.resolve-release-context.outputs.nightly_tag }}"
|
|
if [ "$tag" = "$nightly_tag" ]; then
|
|
note_file="$(mktemp)"
|
|
if [ -f "scripts/release/generate_nightly_release_notes.py" ]; then
|
|
python3 -m scripts.release.generate_nightly_release_notes \
|
|
--base-tag "${{ steps.release-meta.outputs.base_version }}" \
|
|
--repo "${{ needs.resolve-release-context.outputs.repo_slug }}" \
|
|
--output "$note_file"
|
|
else
|
|
short_sha="$(git rev-parse --short=8 HEAD)"
|
|
echo "Nightly notes script is missing at this ref; using fallback notes."
|
|
{
|
|
echo "## What's Changed"
|
|
echo ""
|
|
echo "- Baseline tag: \`${{ steps.release-meta.outputs.base_version }}\`"
|
|
echo "- Nightly commit: \`$short_sha\`"
|
|
echo "- No changes summary generator available in this ref."
|
|
} > "$note_file"
|
|
fi
|
|
else
|
|
note_file="changelogs/${tag}.md"
|
|
if [ ! -f "$note_file" ]; then
|
|
note_file="$(mktemp)"
|
|
echo "Release ${tag}" > "$note_file"
|
|
fi
|
|
fi
|
|
echo "file=$note_file" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Ensure release exists
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
tag="${{ needs.resolve-release-context.outputs.tag }}"
|
|
title="${{ steps.release-meta.outputs.title }}"
|
|
nightly_tag="${{ needs.resolve-release-context.outputs.nightly_tag }}"
|
|
if [ "$tag" = "$nightly_tag" ]; then
|
|
pre_flag="--prerelease"
|
|
else
|
|
pre_flag=""
|
|
fi
|
|
if ! gh release view "$tag" >/dev/null 2>&1; then
|
|
gh release create "$tag" --title "$title" --notes-file "${{ steps.notes.outputs.file }}" $pre_flag
|
|
elif [ "$tag" = "$nightly_tag" ]; then
|
|
gh release edit "$tag" --title "$title" --notes-file "${{ steps.notes.outputs.file }}" --prerelease
|
|
fi
|
|
|
|
- name: Remove stale assets from release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
tag="${{ needs.resolve-release-context.outputs.tag }}"
|
|
while IFS= read -r asset; do
|
|
case "$asset" in
|
|
AstrBot-*-dashboard.zip)
|
|
gh release delete-asset "$tag" "$asset" -y || true
|
|
;;
|
|
esac
|
|
done < <(gh release view "$tag" --json assets --jq '.assets[].name')
|
|
|
|
- name: Upload assets to release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
tag="${{ needs.resolve-release-context.outputs.tag }}"
|
|
gh release upload "$tag" "release-assets/AstrBot-${tag}-dashboard.zip" --clobber
|
|
|
|
publish-pypi:
|
|
name: Publish PyPI
|
|
if: ${{ needs.resolve-release-context.outputs.tag != needs.resolve-release-context.outputs.nightly_tag }}
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- resolve-release-context
|
|
- publish-release
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ needs.resolve-release-context.outputs.checkout_ref }}
|
|
|
|
- name: Download dashboard artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: Dashboard-${{ needs.resolve-release-context.outputs.tag }}
|
|
path: dashboard-artifact
|
|
|
|
- name: Unpack dashboard dist into package tree
|
|
shell: bash
|
|
run: |
|
|
mkdir -p astrbot/dashboard/dist
|
|
unzip -q "dashboard-artifact/AstrBot-${{ needs.resolve-release-context.outputs.tag }}-dashboard.zip" -d dashboard-artifact/unpacked
|
|
cp -r dashboard-artifact/unpacked/dist/. astrbot/dashboard/dist/
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install uv
|
|
shell: bash
|
|
run: python -m pip install uv
|
|
|
|
- name: Build package
|
|
shell: bash
|
|
# Dashboard assets are already in astrbot/dashboard/dist/;
|
|
# ASTRBOT_BUILD_DASHBOARD is intentionally unset so the hatch hook skips npm.
|
|
run: uv build
|
|
|
|
- name: Publish to PyPI
|
|
env:
|
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
shell: bash
|
|
run: uv publish
|