chore(release): support manual draft builds

This commit is contained in:
777genius 2026-05-18 02:04:13 +03:00
parent 4a8cec9dc2
commit 7742c528ad
6 changed files with 195 additions and 161 deletions

View file

@ -5,14 +5,29 @@ on:
tags: tags:
- 'v*' - 'v*'
workflow_dispatch: workflow_dispatch:
inputs:
release_tag:
description: Release tag to build into a draft, for example v2.0.0
required: true
type: string
publish_release:
description: Publish the release after all assets are uploaded
required: true
default: false
type: boolean
permissions: permissions:
contents: write contents: write
concurrency: concurrency:
group: release-${{ github.ref }} group: release-${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || inputs.release_tag || github.ref }}
cancel-in-progress: false cancel-in-progress: false
env:
IS_RELEASE_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
RELEASE_TAG: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || inputs.release_tag }}
PUBLISH_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_release }}
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -36,14 +51,26 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Set version from tag - name: Validate release metadata
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: | run: |
VERSION="${GITHUB_REF#refs/tags/v}" set -euo pipefail
case "${RELEASE_TAG:-}" in
v[0-9]*) ;;
*)
echo "release_tag must start with v and include a numeric version, got '${RELEASE_TAG:-<empty>}'" >&2
exit 1
;;
esac
- name: Set version from release tag
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION" pnpm pkg set version="$VERSION"
- name: Verify Sentry release env - name: Verify Sentry release env
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -54,14 +81,14 @@ jobs:
- name: Build app - name: Build app
env: env:
NODE_OPTIONS: '--max-old-space-size=8192' NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }} SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }} SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build run: pnpm build
- name: Verify Sentry source map upload - name: Verify Sentry source map upload
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -70,16 +97,28 @@ jobs:
run: node ./scripts/ci/verify-sentry-release.cjs postbuild run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Create GitHub Release - name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}" TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing_is_draft="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
if [ -n "$existing_is_draft" ]; then
if [ "${PUBLISH_RELEASE}" != "true" ] && [ "$existing_is_draft" != "true" ]; then
echo "Release $TAG already exists and is not a draft; refusing to overwrite it from draft mode." >&2
exit 1
fi
echo "Release $TAG already exists, skipping creation"
exit 0
fi
gh release create "$TAG" \ gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \ --repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "$TAG" \ --title "$TAG" \
--generate-notes \ --generate-notes \
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation" --draft=true
- name: Upload dist artifact - name: Upload dist artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
@ -98,30 +137,55 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Validate release metadata
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
set -euo pipefail
case "${RELEASE_TAG:-}" in
v[0-9]*) ;;
*)
echo "release_tag must start with v and include a numeric version, got '${RELEASE_TAG:-<empty>}'" >&2
exit 1
;;
esac
- name: Create GitHub Release - name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}" TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing_is_draft="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
if [ -n "$existing_is_draft" ]; then
if [ "${PUBLISH_RELEASE}" != "true" ] && [ "$existing_is_draft" != "true" ]; then
echo "Release $TAG already exists and is not a draft; refusing to overwrite it from draft mode." >&2
exit 1
fi
echo "Release $TAG already exists, skipping creation"
exit 0
fi
gh release create "$TAG" \ gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \ --repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "$TAG" \ --title "$TAG" \
--generate-notes \ --generate-notes \
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation" --draft=true
- name: Skip runtime asset preparation for manual builds - name: Skip runtime asset preparation for non-release builds
if: ${{ !startsWith(github.ref, 'refs/tags/v') }} if: ${{ env.IS_RELEASE_BUILD != 'true' }}
run: echo "Runtime asset preparation is only needed for tagged releases." run: echo "Runtime asset preparation is only needed for release builds."
- name: Check runtime assets - name: Check runtime assets
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
id: runtime-assets id: runtime-assets
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}" TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
missing=0 missing=0
while IFS= read -r asset; do while IFS= read -r asset; do
@ -144,7 +208,7 @@ jobs:
exit 1 exit 1
fi fi
TARGET_TAG="${GITHUB_REF#refs/tags/}" TARGET_TAG="${RELEASE_TAG}"
SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)" SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)"
SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)" SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)"
RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)" RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)"
@ -215,6 +279,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}" TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
for attempt in $(seq 1 12); do for attempt in $(seq 1 12); do
existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
all_found=1 all_found=1
@ -281,10 +346,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Set version from tag - name: Set version from release tag
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: | run: |
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION" pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (macOS ${{ matrix.arch }}) - name: Stage bundled runtime (macOS ${{ matrix.arch }})
@ -293,15 +358,15 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" --release-tag "$TAG" node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" --release-tag "$TAG"
else else
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}"
fi fi
- name: Verify Sentry release env (macOS ${{ matrix.arch }}) - name: Verify Sentry release env (macOS ${{ matrix.arch }})
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -312,14 +377,14 @@ jobs:
- name: Build app (macOS ${{ matrix.arch }}) - name: Build app (macOS ${{ matrix.arch }})
env: env:
NODE_OPTIONS: '--max-old-space-size=8192' NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }} SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }} SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build run: pnpm build
- name: Verify Sentry source map upload (macOS ${{ matrix.arch }}) - name: Verify Sentry source map upload (macOS ${{ matrix.arch }})
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -352,11 +417,11 @@ jobs:
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin
- name: Upload assets to release - name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
for f in release/*.dmg release/*.zip release/*.blockmap; do for f in release/*.dmg release/*.zip release/*.blockmap; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
echo "Uploading: $f" echo "Uploading: $f"
@ -397,11 +462,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Set version from tag - name: Set version from release tag
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
shell: bash shell: bash
run: | run: |
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION" pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (Windows) - name: Stage bundled runtime (Windows)
@ -410,15 +475,15 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
if ($env:GITHUB_REF -like 'refs/tags/v*') { if ($env:IS_RELEASE_BUILD -eq 'true') {
$tag = $env:GITHUB_REF.Replace('refs/tags/', '') $tag = $env:RELEASE_TAG
node ./scripts/stage-runtime.mjs --platform win32-x64 --release-tag $tag node ./scripts/stage-runtime.mjs --platform win32-x64 --release-tag $tag
} else { } else {
node ./scripts/stage-runtime.mjs --platform win32-x64 node ./scripts/stage-runtime.mjs --platform win32-x64
} }
- name: Verify Sentry release env (Windows) - name: Verify Sentry release env (Windows)
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -429,14 +494,14 @@ jobs:
- name: Build app (Windows) - name: Build app (Windows)
env: env:
NODE_OPTIONS: '--max-old-space-size=8192' NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }} SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }} SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build run: pnpm build
- name: Verify Sentry source map upload (Windows) - name: Verify Sentry source map upload (Windows)
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -467,12 +532,12 @@ jobs:
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/win-unpacked" win32 run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/win-unpacked" win32
- name: Upload assets to release - name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
shell: bash shell: bash
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
for f in release/*.exe release/*.blockmap; do for f in release/*.exe release/*.blockmap; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
echo "Uploading: $f" echo "Uploading: $f"
@ -518,10 +583,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Set version from tag - name: Set version from release tag
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: | run: |
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION" pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (Linux) - name: Stage bundled runtime (Linux)
@ -530,15 +595,15 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
node ./scripts/stage-runtime.mjs --platform linux-x64 --release-tag "$TAG" node ./scripts/stage-runtime.mjs --platform linux-x64 --release-tag "$TAG"
else else
node ./scripts/stage-runtime.mjs --platform linux-x64 node ./scripts/stage-runtime.mjs --platform linux-x64
fi fi
- name: Verify Sentry release env (Linux) - name: Verify Sentry release env (Linux)
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -549,14 +614,14 @@ jobs:
- name: Build app (Linux) - name: Build app (Linux)
env: env:
NODE_OPTIONS: '--max-old-space-size=8192' NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }} SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }} SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build run: pnpm build
- name: Verify Sentry source map upload (Linux) - name: Verify Sentry source map upload (Linux)
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -584,12 +649,12 @@ jobs:
run: xvfb-run -a node ./scripts/electron-builder/smokePackagedApp.cjs "release/linux-unpacked" linux run: xvfb-run -a node ./scripts/electron-builder/smokePackagedApp.cjs "release/linux-unpacked" linux
- name: Upload assets to release - name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v') if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -x set -x
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
ls -la release/ || true ls -la release/ || true
for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
@ -602,7 +667,7 @@ jobs:
needs: [release-mac, release-win, release-linux] needs: [release-mac, release-win, release-linux]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
if: startsWith(github.ref, 'refs/tags/v') if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
steps: steps:
- name: Upload stable-named assets for /latest/download links - name: Upload stable-named assets for /latest/download links
@ -610,7 +675,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${RELEASE_TAG#v}"
TAG="${RELEASE_TAG}"
REPO="${GITHUB_REPOSITORY}" REPO="${GITHUB_REPOSITORY}"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT trap 'rm -rf "$TMP_DIR"' EXIT
@ -629,13 +695,13 @@ jobs:
for STABLE_NAME in "${!FILES[@]}"; do for STABLE_NAME in "${!FILES[@]}"; do
VERSIONED_NAME="${FILES[$STABLE_NAME]}" VERSIONED_NAME="${FILES[$STABLE_NAME]}"
echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}" echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}"
gh release download "v${VERSION}" \ gh release download "${TAG}" \
--repo "$REPO" \ --repo "$REPO" \
--pattern "${VERSIONED_NAME}" \ --pattern "${VERSIONED_NAME}" \
--dir "$TMP_DIR" \ --dir "$TMP_DIR" \
--clobber --clobber
cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}" cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}"
gh release upload "v${VERSION}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber gh release upload "${TAG}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber
done done
- name: Publish canonical updater metadata - name: Publish canonical updater metadata
@ -643,8 +709,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${RELEASE_TAG#v}"
TAG="v${VERSION}" TAG="${RELEASE_TAG}"
REPO="${GITHUB_REPOSITORY}" REPO="${GITHUB_REPOSITORY}"
RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
@ -724,9 +790,14 @@ jobs:
gh release upload "${TAG}" latest.yml latest-linux.yml latest-mac.yml --repo "${REPO}" --clobber gh release upload "${TAG}" latest.yml latest-linux.yml latest-mac.yml --repo "${REPO}" --clobber
- name: Publish release - name: Publish release
if: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_release }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}" TAG="${RELEASE_TAG}"
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false --latest gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false --latest
- name: Keep release as draft
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.publish_release }}
run: echo "Draft release ${RELEASE_TAG} is ready. It was not published because publish_release=false."

View file

@ -1397,7 +1397,6 @@ export const TeamDetailView = memo(function TeamDetailView({
}); });
const [creatingTask, setCreatingTask] = useState(false); const [creatingTask, setCreatingTask] = useState(false);
const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false); const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false);
const [runtimeTelemetryPreviewVisible, setRuntimeTelemetryPreviewVisible] = useState(false);
const [addingMemberLoading, setAddingMemberLoading] = useState(false); const [addingMemberLoading, setAddingMemberLoading] = useState(false);
const [removeMemberConfirm, setRemoveMemberConfirm] = useState<string | null>(null); const [removeMemberConfirm, setRemoveMemberConfirm] = useState<string | null>(null);
const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false); const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false);
@ -2872,68 +2871,63 @@ export const TeamDetailView = memo(function TeamDetailView({
</div> </div>
) : null} ) : null}
<CollapsibleTeamSection <div className="runtime-telemetry-hover-scope">
sectionId="team" <CollapsibleTeamSection
title="Team" sectionId="team"
icon={<Users size={14} />} title="Team"
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount} icon={<Users size={14} />}
defaultOpen badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
afterBadge={ defaultOpen
<Button afterBadge={
variant="ghost" <Button
size="sm" variant="ghost"
className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]" size="sm"
onClick={(e) => { className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
e.stopPropagation(); onClick={(e) => {
setAddMemberDialogOpen(true); e.stopPropagation();
}} setAddMemberDialogOpen(true);
> }}
<UserPlus size={12} /> >
Add <UserPlus size={12} />
</Button> Add
} </Button>
action={ }
<div action={
className={cn( <div className="runtime-telemetry-legend flex items-center gap-3 pr-3 text-[11px] font-medium leading-none text-[var(--color-text-muted)] opacity-0 transition-opacity duration-150">
'flex items-center gap-3 pr-3 text-[11px] font-medium leading-none text-[var(--color-text-muted)] transition-opacity duration-150', <span className="flex items-center gap-1.5">
runtimeTelemetryPreviewVisible ? 'opacity-100' : 'opacity-0' <span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(34,197,94,0.3)]" />
)} Memory
> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(34,197,94,0.3)]" /> <span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" />
Memory CPU
</span> </span>
<span className="flex items-center gap-1.5"> </div>
<span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" /> }
CPU contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
</span> >
<div className="px-[calc(1rem-5px)]">
<TeamMemberListBridge
teamName={teamName}
members={membersWithLiveBranches}
expectedTeammateCount={activeTeammateCount}
memberTaskCounts={memberTaskCounts}
taskMap={taskMap}
pendingRepliesByMember={pendingRepliesByMember}
isRosterLoading={loading}
isTeamAlive={data.isAlive}
isTeamProvisioning={isTeamProvisioning}
launchParams={launchParams}
onMemberClick={handleSelectMember}
onSendMessage={handleSendMessageToMember}
onAssignTask={handleAssignTaskToMember}
onOpenTask={handleOpenTaskById}
onRestartMember={handleRestartMember}
onSkipMemberForLaunch={handleSkipMemberForLaunch}
/>
</div> </div>
} </CollapsibleTeamSection>
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]" </div>
>
<div className="px-[calc(1rem-5px)]">
<TeamMemberListBridge
teamName={teamName}
members={membersWithLiveBranches}
expectedTeammateCount={activeTeammateCount}
memberTaskCounts={memberTaskCounts}
taskMap={taskMap}
pendingRepliesByMember={pendingRepliesByMember}
isRosterLoading={loading}
isTeamAlive={data.isAlive}
isTeamProvisioning={isTeamProvisioning}
launchParams={launchParams}
runtimeTelemetryVisible={runtimeTelemetryPreviewVisible}
onRuntimeTelemetryHoverChange={setRuntimeTelemetryPreviewVisible}
onMemberClick={handleSelectMember}
onSendMessage={handleSendMessageToMember}
onAssignTask={handleAssignTaskToMember}
onOpenTask={handleOpenTaskById}
onRestartMember={handleRestartMember}
onSkipMemberForLaunch={handleSkipMemberForLaunch}
/>
</div>
</CollapsibleTeamSection>
<CollapsibleTeamSection <CollapsibleTeamSection
sectionId="sessions" sectionId="sessions"

View file

@ -90,7 +90,6 @@ interface MemberCardProps {
spawnLaunchState?: MemberLaunchState; spawnLaunchState?: MemberLaunchState;
spawnRuntimeAlive?: boolean; spawnRuntimeAlive?: boolean;
isLaunchSettling?: boolean; isLaunchSettling?: boolean;
runtimeTelemetryVisible?: boolean;
runtimeTelemetryScale?: RuntimeTelemetryScale; runtimeTelemetryScale?: RuntimeTelemetryScale;
onOpenTask?: () => void; onOpenTask?: () => void;
onOpenReviewTask?: () => void; onOpenReviewTask?: () => void;
@ -502,11 +501,9 @@ function buildRuntimeTelemetryPaths(
const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
runtimeEntry, runtimeEntry,
visible,
scale, scale,
}: { }: {
runtimeEntry?: TeamAgentRuntimeEntry; runtimeEntry?: TeamAgentRuntimeEntry;
visible: boolean;
scale?: RuntimeTelemetryScale; scale?: RuntimeTelemetryScale;
}): React.JSX.Element | null { }): React.JSX.Element | null {
const paths = useMemo( const paths = useMemo(
@ -521,10 +518,7 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
<div <div
aria-hidden="true" aria-hidden="true"
data-testid="member-runtime-telemetry-strip" data-testid="member-runtime-telemetry-strip"
className={cn( className="runtime-telemetry-strip pointer-events-none absolute inset-x-0 bottom-0 z-0 h-5 overflow-hidden rounded-b opacity-0 transition-opacity duration-150"
'pointer-events-none absolute inset-x-0 bottom-0 z-0 h-5 overflow-hidden rounded-b transition-opacity duration-150',
visible ? 'opacity-100' : 'opacity-0'
)}
style={{ style={{
WebkitMaskImage: WebkitMaskImage:
'linear-gradient(to right, transparent 0, black 44px, black calc(100% - 44px), transparent 100%)', 'linear-gradient(to right, transparent 0, black 44px, black calc(100% - 44px), transparent 100%)',
@ -602,7 +596,6 @@ export const MemberCard = memo(function MemberCard({
spawnLaunchState, spawnLaunchState,
spawnRuntimeAlive, spawnRuntimeAlive,
isLaunchSettling, isLaunchSettling,
runtimeTelemetryVisible = false,
runtimeTelemetryScale, runtimeTelemetryScale,
onOpenTask, onOpenTask,
onOpenReviewTask, onOpenReviewTask,
@ -696,7 +689,7 @@ export const MemberCard = memo(function MemberCard({
? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}` ? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}`
: undefined; : undefined;
const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry); const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry);
const showRuntimeTelemetryTooltip = runtimeTelemetryVisible && Boolean(runtimeTelemetryTitle); const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle);
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle; const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
const runtimeTelemetryTooltipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const runtimeTelemetryTooltipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [runtimeTelemetryTooltipOpen, setRuntimeTelemetryTooltipOpen] = useState(false); const [runtimeTelemetryTooltipOpen, setRuntimeTelemetryTooltipOpen] = useState(false);
@ -920,11 +913,7 @@ export const MemberCard = memo(function MemberCard({
}} }}
> >
{!isRemoved ? ( {!isRemoved ? (
<MemberRuntimeTelemetryStrip <MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} />
runtimeEntry={runtimeEntry}
visible={runtimeTelemetryVisible}
scale={runtimeTelemetryScale}
/>
) : null} ) : null}
<div className="pointer-events-none absolute inset-0 z-10 rounded transition-colors group-hover:bg-white/5" /> <div className="pointer-events-none absolute inset-0 z-10 rounded transition-colors group-hover:bg-white/5" />
<div className="relative z-20 flex items-center gap-2.5"> <div className="relative z-20 flex items-center gap-2.5">

View file

@ -45,8 +45,6 @@ interface MemberListProps {
isTeamProvisioning?: boolean; isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState; leadActivity?: LeadActivityState;
launchParams?: TeamLaunchParams; launchParams?: TeamLaunchParams;
runtimeTelemetryVisible?: boolean;
onRuntimeTelemetryHoverChange?: (visible: boolean) => void;
onMemberClick?: (member: ResolvedTeamMember) => void; onMemberClick?: (member: ResolvedTeamMember) => void;
onSendMessage?: (member: ResolvedTeamMember) => void; onSendMessage?: (member: ResolvedTeamMember) => void;
onAssignTask?: (member: ResolvedTeamMember) => void; onAssignTask?: (member: ResolvedTeamMember) => void;
@ -464,8 +462,6 @@ function areMemberListPropsEqual(
prev.isTeamAlive === next.isTeamAlive && prev.isTeamAlive === next.isTeamAlive &&
prev.isTeamProvisioning === next.isTeamProvisioning && prev.isTeamProvisioning === next.isTeamProvisioning &&
prev.leadActivity === next.leadActivity && prev.leadActivity === next.leadActivity &&
prev.runtimeTelemetryVisible === next.runtimeTelemetryVisible &&
prev.onRuntimeTelemetryHoverChange === next.onRuntimeTelemetryHoverChange &&
prev.onRestartMember === next.onRestartMember && prev.onRestartMember === next.onRestartMember &&
prev.onSkipMemberForLaunch === next.onSkipMemberForLaunch && prev.onSkipMemberForLaunch === next.onSkipMemberForLaunch &&
areLaunchParamsEquivalent(prev.launchParams, next.launchParams) areLaunchParamsEquivalent(prev.launchParams, next.launchParams)
@ -502,7 +498,6 @@ interface MemberCardRowProps {
isTeamProvisioning?: boolean; isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState; leadActivity?: LeadActivityState;
isLaunchSettling?: boolean; isLaunchSettling?: boolean;
runtimeTelemetryVisible?: boolean;
runtimeTelemetryScale?: RuntimeTelemetryScale; runtimeTelemetryScale?: RuntimeTelemetryScale;
onOpenTask?: (taskId: string) => void; onOpenTask?: (taskId: string) => void;
onMemberClick?: (member: ResolvedTeamMember) => void; onMemberClick?: (member: ResolvedTeamMember) => void;
@ -538,7 +533,6 @@ const MemberCardRow = memo(function MemberCardRow({
isTeamProvisioning, isTeamProvisioning,
leadActivity, leadActivity,
isLaunchSettling, isLaunchSettling,
runtimeTelemetryVisible,
runtimeTelemetryScale, runtimeTelemetryScale,
onOpenTask, onOpenTask,
onMemberClick, onMemberClick,
@ -589,7 +583,6 @@ const MemberCardRow = memo(function MemberCardRow({
spawnLaunchState={spawnLaunchState} spawnLaunchState={spawnLaunchState}
spawnRuntimeAlive={spawnRuntimeAlive} spawnRuntimeAlive={spawnRuntimeAlive}
isLaunchSettling={isLaunchSettling} isLaunchSettling={isLaunchSettling}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale} runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={currentTask ? handleOpenTask : undefined} onOpenTask={currentTask ? handleOpenTask : undefined}
onOpenReviewTask={reviewTask ? handleOpenReviewTask : undefined} onOpenReviewTask={reviewTask ? handleOpenReviewTask : undefined}
@ -714,8 +707,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning, isTeamProvisioning,
leadActivity, leadActivity,
launchParams, launchParams,
runtimeTelemetryVisible = false,
onRuntimeTelemetryHoverChange,
onMemberClick, onMemberClick,
onSendMessage, onSendMessage,
onAssignTask, onAssignTask,
@ -742,14 +733,6 @@ export const MemberList = memo(function MemberList({
return () => observer.disconnect(); return () => observer.disconnect();
}, [handleResize]); }, [handleResize]);
const handleRuntimeTelemetryMouseEnter = useCallback(() => {
onRuntimeTelemetryHoverChange?.(true);
}, [onRuntimeTelemetryHoverChange]);
const handleRuntimeTelemetryMouseLeave = useCallback(() => {
onRuntimeTelemetryHoverChange?.(false);
}, [onRuntimeTelemetryHoverChange]);
const gridClass = isWide ? 'grid grid-cols-2 gap-1' : 'grid grid-cols-1 gap-1'; const gridClass = isWide ? 'grid grid-cols-2 gap-1' : 'grid grid-cols-1 gap-1';
const activeMembers = useMemo( const activeMembers = useMemo(
() => () =>
@ -941,12 +924,7 @@ export const MemberList = memo(function MemberList({
} }
return ( return (
<div <div ref={containerRef} className="runtime-telemetry-list flex flex-col gap-1">
ref={containerRef}
className="flex flex-col gap-1"
onMouseEnter={handleRuntimeTelemetryMouseEnter}
onMouseLeave={handleRuntimeTelemetryMouseLeave}
>
<div className={gridClass}> <div className={gridClass}>
{activeMembers.map((member) => { {activeMembers.map((member) => {
const spawnEntry = memberSpawnStatuses?.get(member.name); const spawnEntry = memberSpawnStatuses?.get(member.name);
@ -1017,7 +995,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning={isTeamProvisioning} isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity} leadActivity={leadActivity}
isLaunchSettling={isLaunchSettling} isLaunchSettling={isLaunchSettling}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale} runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={onOpenTask} onOpenTask={onOpenTask}
onMemberClick={onMemberClick} onMemberClick={onMemberClick}
@ -1063,7 +1040,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning={isTeamProvisioning} isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity} leadActivity={leadActivity}
isLaunchSettling={false} isLaunchSettling={false}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale} runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={onOpenTask} onOpenTask={onOpenTask}
onMemberClick={onMemberClick} onMemberClick={onMemberClick}

View file

@ -1644,6 +1644,13 @@ a[href],
opacity: 0.96; opacity: 0.96;
} }
.runtime-telemetry-list:hover .runtime-telemetry-strip,
.runtime-telemetry-list:focus-within .runtime-telemetry-strip,
.runtime-telemetry-hover-scope:has(.runtime-telemetry-list:hover) .runtime-telemetry-legend,
.runtime-telemetry-hover-scope:has(.runtime-telemetry-list:focus-within) .runtime-telemetry-legend {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.kanban-comment-badge-pulse, .kanban-comment-badge-pulse,
.message-composer-orbit-path, .message-composer-orbit-path,

View file

@ -800,7 +800,6 @@ describe('MemberCard starting-state visuals', () => {
], ],
updatedAt: '2026-04-24T12:00:05.000Z', updatedAt: '2026-04-24T12:00:05.000Z',
}, },
runtimeTelemetryVisible: true,
runtimeTelemetryScale: { runtimeTelemetryScale: {
cpuCapPercent: 100, cpuCapPercent: 100,
memoryCapBytes: 512 * 1024 * 1024, memoryCapBytes: 512 * 1024 * 1024,
@ -861,7 +860,6 @@ describe('MemberCard starting-state visuals', () => {
resourceHistory: 'not-an-array', resourceHistory: 'not-an-array',
updatedAt: '2026-04-24T12:00:05.000Z', updatedAt: '2026-04-24T12:00:05.000Z',
} as any, } as any,
runtimeTelemetryVisible: true,
isTeamAlive: true, isTeamAlive: true,
isTeamProvisioning: false, isTeamProvisioning: false,
}) })
@ -911,7 +909,6 @@ describe('MemberCard starting-state visuals', () => {
], ],
updatedAt: '2026-04-24T12:00:05.000Z', updatedAt: '2026-04-24T12:00:05.000Z',
} as any, } as any,
runtimeTelemetryVisible: true,
isTeamAlive: true, isTeamAlive: true,
isTeamProvisioning: false, isTeamProvisioning: false,
}) })