Files
AstrBot/.github/workflows/release.yml
エイカク 3d1c3946f6 feat(ci): add nightly prerelease release flow and updater support (#5744)
* 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
2026-03-05 01:23:49 +09:00

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