Skip to content

Portable Release

Portable Release #71

name: Portable Release
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches:
- main
workflow_dispatch:
inputs:
ref:
description: Branch, tag, or commit SHA to package after CI is green
required: true
default: main
release_tag:
description: Optional v* tag to create or update with portable assets
required: false
default: ""
permissions:
contents: write
actions: read
concurrency:
group: portable-release-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false
jobs:
resolve-release:
name: Resolve portable release version
runs-on: ubuntu-latest
timeout-minutes: 5
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
)
outputs:
checkout-ref: ${{ steps.version.outputs.checkout-ref }}
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || inputs.ref }}
fetch-depth: 0
- name: Resolve version
id: version
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
INPUT_REF: ${{ inputs.ref }}
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
git fetch --tags --force
if [[ "$EVENT_NAME" == "workflow_run" ]]; then
checkout_ref="$WORKFLOW_HEAD_SHA"
else
checkout_ref="$INPUT_REF"
fi
tag=""
if [[ -n "$INPUT_RELEASE_TAG" ]]; then
tag_commit="$(git rev-list -n 1 "$INPUT_RELEASE_TAG")"
head_commit="$(git rev-parse HEAD)"
if [[ "$tag_commit" != "$head_commit" ]]; then
echo "release_tag $INPUT_RELEASE_TAG does not point at the checked out commit." >&2
exit 1
fi
tag="$INPUT_RELEASE_TAG"
else
tag="$(git tag --points-at HEAD --list "v*" | head -n 1 || true)"
fi
if [[ -n "$tag" ]]; then
version="${tag#v}"
else
short="$(git rev-parse --short HEAD)"
version="dev-${short}"
fi
{
echo "checkout-ref=$checkout_ref"
echo "tag=$tag"
echo "version=$version"
} >> "$GITHUB_OUTPUT"
windows-portable:
name: Windows portable package
runs-on: windows-latest
timeout-minutes: 40
needs: resolve-release
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-release.outputs.checkout-ref }}
fetch-depth: 0
- name: Build Windows portable package
shell: pwsh
run: ./packaging/windows/build-portable.ps1 -OutputDir dist -Version "${{ needs.resolve-release.outputs.version }}"
- name: Upload Windows portable artifact
uses: actions/upload-artifact@v6
with:
name: ilab-gpt-conjure-windows-portable-x64
path: |
dist/*.zip
dist/*.sha256.txt
dist/portable-build.json
if-no-files-found: error
windows-app-x64:
name: Windows standard app ZIP
runs-on: windows-latest
timeout-minutes: 40
needs: resolve-release
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-release.outputs.checkout-ref }}
fetch-depth: 0
- name: Build Windows standard app package
shell: pwsh
run: ./packaging/windows/build-app.ps1 -OutputDir dist -Version "${{ needs.resolve-release.outputs.version }}"
- name: Upload Windows standard app artifact
uses: actions/upload-artifact@v6
with:
name: ilab-gpt-conjure-windows-app-x64
path: |
dist/*.zip
dist/*.sha256.txt
dist/windows-app-build.json
if-no-files-found: error
macos-portable:
name: macOS ${{ matrix.arch }} portable package
runs-on: ${{ matrix.runner }}
timeout-minutes: 40
needs: resolve-release
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15
artifact: ilab-gpt-conjure-macos-portable-arm64
- arch: x64
runner: macos-15-intel
artifact: ilab-gpt-conjure-macos-portable-x64
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-release.outputs.checkout-ref }}
fetch-depth: 0
- name: Build macOS portable package
shell: bash
run: ./packaging/macos/build-portable.sh --output-dir "dist/${{ matrix.arch }}" --version "${{ needs.resolve-release.outputs.version }}"
- name: Verify macOS package architecture
shell: bash
run: |
set -euo pipefail
test -f "dist/${{ matrix.arch }}/macos-portable-build.json"
grep -q '"architecture": "${{ matrix.arch }}"' "dist/${{ matrix.arch }}/macos-portable-build.json"
ls "dist/${{ matrix.arch }}"/ilab-gpt-conjure_macos_portable_${{ matrix.arch }}_*.zip
- name: Upload macOS portable artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact }}
path: |
dist/${{ matrix.arch }}/*.zip
dist/${{ matrix.arch }}/*.sha256.txt
dist/${{ matrix.arch }}/macos-portable-build.json
if-no-files-found: error
macos-dmg:
name: macOS ${{ matrix.arch }} standard DMG
runs-on: ${{ matrix.runner }}
timeout-minutes: 40
needs: resolve-release
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15
artifact: ilab-gpt-conjure-macos-dmg-arm64
- arch: x64
runner: macos-15-intel
artifact: ilab-gpt-conjure-macos-dmg-x64
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-release.outputs.checkout-ref }}
fetch-depth: 0
- name: Build macOS standard app DMG
shell: bash
run: ./packaging/macos/build-app.sh --output-dir "dist/${{ matrix.arch }}" --version "${{ needs.resolve-release.outputs.version }}"
- name: Verify macOS DMG architecture
shell: bash
run: |
set -euo pipefail
test -f "dist/${{ matrix.arch }}/macos-app-build.json"
grep -q '"architecture": "${{ matrix.arch }}"' "dist/${{ matrix.arch }}/macos-app-build.json"
ls "dist/${{ matrix.arch }}"/iLab-GPT-CONJURE-macos-${{ matrix.arch }}-*.dmg
- name: Upload macOS DMG artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact }}
path: |
dist/${{ matrix.arch }}/*.dmg
dist/${{ matrix.arch }}/*.sha256.txt
dist/${{ matrix.arch }}/macos-app-build.json
if-no-files-found: error
github-release-assets:
name: Upload GitHub release assets
runs-on: ubuntu-latest
timeout-minutes: 10
needs:
- resolve-release
- windows-portable
- windows-app-x64
- macos-portable
- macos-dmg
if: ${{ needs.resolve-release.outputs.tag != '' }}
steps:
- name: Check out release notes
uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-release.outputs.checkout-ref }}
fetch-depth: 1
- name: Download package artifacts
uses: actions/download-artifact@v8
with:
path: dist-assets
- name: Upload release assets
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }}
ILAB_CONJURE_UPDATE_SIGNING_PRIVATE_KEY_B64: ${{ secrets.ILAB_CONJURE_UPDATE_SIGNING_PRIVATE_KEY_B64 }}
run: |
set -euo pipefail
release_version="${RELEASE_TAG#v}"
release_summary="$(awk '
/^当前版本:/ || /^本版重点:/ {
print
print ""
next
}
/^本版详情:/ {
in_details = 1
print
next
}
in_details && /^## / {
in_details = 0
}
in_details {
print
}
' RELEASES.md)"
if [[ -z "${release_summary//[[:space:]]/}" ]]; then
release_summary="此版本包含 iLab CONJURE 对应 tag 的源码和免安装一键包。"
fi
cat > release-notes.md <<EOF
## 发布说明
${release_summary}
完整版本说明见 [RELEASES.md](https://github.com/${GITHUB_REPOSITORY}/blob/${RELEASE_TAG}/RELEASES.md)。
## 推荐下载
- macOS Apple Silicon:iLab-GPT-CONJURE-macos-arm64-${release_version}.dmg
- macOS Intel:iLab-GPT-CONJURE-macos-x64-${release_version}.dmg
- Windows x64:iLab-GPT-CONJURE-windows-x64_${release_version}.zip
新用户建议优先下载标准包。v0.5.4 及更早 portable 用户首次升级到 0.5.5 时,建议手动下载完整标准包或完整 portable 包;旧更新器不保证能安装新的小兔子启动器、标准 .app / .exe 入口和迁移助手。
## 免安装一键包
- Windows x64:ilab-gpt-conjure_windows_portable_x64_${release_version}.zip
- macOS Apple Silicon:ilab-gpt-conjure_macos_portable_arm64_${release_version}.zip
- macOS Intel:ilab-gpt-conjure_macos_portable_x64_${release_version}.zip
包含标准更新助手的 macOS App 可在用户确认后校验 signed latest.json 与 DMG SHA256,自动覆盖并重新启动;旧版 macOS App 需要先手动安装首个支持版本,Windows 标准 ZIP 仍手动覆盖。portable 包继续使用原有自动替换更新器。macOS 包暂未签名、未 notarize。如果 macOS 拦截启动,可以右键或 Control-click App,选择 Open;portable zip 仍可对解压目录执行 xattr -dr com.apple.quarantine。
EOF
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --notes-file release-notes.md
else
gh release edit "$RELEASE_TAG" --title "$RELEASE_TAG" --notes-file release-notes.md
fi
python scripts/build-update-manifest.py \
--assets-dir dist-assets \
--version "$release_version" \
--tag "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--notes-file release-notes.md \
--signing-private-key-b64 "$ILAB_CONJURE_UPDATE_SIGNING_PRIVATE_KEY_B64" \
--require-signature \
--output dist-assets/latest.json
mapfile -d '' assets < <(find dist-assets -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.sha256.txt" -o -name "latest.json" \) -print0)
if [[ "${#assets[@]}" -eq 0 ]]; then
echo "Portable assets were not created." >&2
exit 1
fi
gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber