chore(release): support manual draft builds
This commit is contained in:
parent
4a8cec9dc2
commit
7742c528ad
6 changed files with 195 additions and 161 deletions
185
.github/workflows/release.yml
vendored
185
.github/workflows/release.yml
vendored
|
|
@ -5,14 +5,29 @@ on:
|
|||
tags:
|
||||
- 'v*'
|
||||
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:
|
||||
contents: write
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -36,14 +51,26 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set version from tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Validate release metadata
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
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"
|
||||
|
||||
- name: Verify Sentry release env
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -54,14 +81,14 @@ jobs:
|
|||
- name: Build app
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
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') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: pnpm build
|
||||
|
||||
- name: Verify Sentry source map upload
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -70,16 +97,28 @@ jobs:
|
|||
run: node ./scripts/ci/verify-sentry-release.cjs postbuild
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$TAG" \
|
||||
--generate-notes \
|
||||
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"
|
||||
--draft=true
|
||||
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
|
|
@ -98,30 +137,55 @@ jobs:
|
|||
- name: Checkout
|
||||
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
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$TAG" \
|
||||
--generate-notes \
|
||||
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"
|
||||
--draft=true
|
||||
|
||||
- name: Skip runtime asset preparation for manual builds
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: echo "Runtime asset preparation is only needed for tagged releases."
|
||||
- name: Skip runtime asset preparation for non-release builds
|
||||
if: ${{ env.IS_RELEASE_BUILD != 'true' }}
|
||||
run: echo "Runtime asset preparation is only needed for release builds."
|
||||
|
||||
- name: Check runtime assets
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
id: runtime-assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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)"
|
||||
missing=0
|
||||
while IFS= read -r asset; do
|
||||
|
|
@ -144,7 +208,7 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_TAG="${GITHUB_REF#refs/tags/}"
|
||||
TARGET_TAG="${RELEASE_TAG}"
|
||||
SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)"
|
||||
SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)"
|
||||
RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)"
|
||||
|
|
@ -215,6 +279,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TAG="${RELEASE_TAG:-$TAG}"
|
||||
for attempt in $(seq 1 12); do
|
||||
existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
|
||||
all_found=1
|
||||
|
|
@ -281,10 +346,10 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set version from tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Set version from release tag
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
pnpm pkg set version="$VERSION"
|
||||
|
||||
- name: Stage bundled runtime (macOS ${{ matrix.arch }})
|
||||
|
|
@ -293,15 +358,15 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
|
||||
TAG="${RELEASE_TAG}"
|
||||
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" --release-tag "$TAG"
|
||||
else
|
||||
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}"
|
||||
fi
|
||||
|
||||
- name: Verify Sentry release env (macOS ${{ matrix.arch }})
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -312,14 +377,14 @@ jobs:
|
|||
- name: Build app (macOS ${{ matrix.arch }})
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
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') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: pnpm build
|
||||
|
||||
- name: Verify Sentry source map upload (macOS ${{ matrix.arch }})
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
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
|
||||
|
||||
- name: Upload assets to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
for f in release/*.dmg release/*.zip release/*.blockmap; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading: $f"
|
||||
|
|
@ -397,11 +462,11 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set version from tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Set version from release tag
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
pnpm pkg set version="$VERSION"
|
||||
|
||||
- name: Stage bundled runtime (Windows)
|
||||
|
|
@ -410,15 +475,15 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:GITHUB_REF -like 'refs/tags/v*') {
|
||||
$tag = $env:GITHUB_REF.Replace('refs/tags/', '')
|
||||
if ($env:IS_RELEASE_BUILD -eq 'true') {
|
||||
$tag = $env:RELEASE_TAG
|
||||
node ./scripts/stage-runtime.mjs --platform win32-x64 --release-tag $tag
|
||||
} else {
|
||||
node ./scripts/stage-runtime.mjs --platform win32-x64
|
||||
}
|
||||
|
||||
- name: Verify Sentry release env (Windows)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -429,14 +494,14 @@ jobs:
|
|||
- name: Build app (Windows)
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
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') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: pnpm build
|
||||
|
||||
- name: Verify Sentry source map upload (Windows)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -467,12 +532,12 @@ jobs:
|
|||
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/win-unpacked" win32
|
||||
|
||||
- name: Upload assets to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
for f in release/*.exe release/*.blockmap; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "Uploading: $f"
|
||||
|
|
@ -518,10 +583,10 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set version from tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Set version from release tag
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
pnpm pkg set version="$VERSION"
|
||||
|
||||
- name: Stage bundled runtime (Linux)
|
||||
|
|
@ -530,15 +595,15 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
|
||||
TAG="${RELEASE_TAG}"
|
||||
node ./scripts/stage-runtime.mjs --platform linux-x64 --release-tag "$TAG"
|
||||
else
|
||||
node ./scripts/stage-runtime.mjs --platform linux-x64
|
||||
fi
|
||||
|
||||
- name: Verify Sentry release env (Linux)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
|
@ -549,14 +614,14 @@ jobs:
|
|||
- name: Build app (Linux)
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=8192'
|
||||
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
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') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: pnpm build
|
||||
|
||||
- name: Verify Sentry source map upload (Linux)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
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
|
||||
|
||||
- name: Upload assets to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -x
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
ls -la release/ || true
|
||||
for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do
|
||||
[ -f "$f" ] || continue
|
||||
|
|
@ -602,7 +667,7 @@ jobs:
|
|||
needs: [release-mac, release-win, release-linux]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: Upload stable-named assets for /latest/download links
|
||||
|
|
@ -610,7 +675,8 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
|
@ -629,13 +695,13 @@ jobs:
|
|||
for STABLE_NAME in "${!FILES[@]}"; do
|
||||
VERSIONED_NAME="${FILES[$STABLE_NAME]}"
|
||||
echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}"
|
||||
gh release download "v${VERSION}" \
|
||||
gh release download "${TAG}" \
|
||||
--repo "$REPO" \
|
||||
--pattern "${VERSIONED_NAME}" \
|
||||
--dir "$TMP_DIR" \
|
||||
--clobber
|
||||
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
|
||||
|
||||
- name: Publish canonical updater metadata
|
||||
|
|
@ -643,8 +709,8 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
TAG="v${VERSION}"
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
|
|
@ -724,9 +790,14 @@ jobs:
|
|||
gh release upload "${TAG}" latest.yml latest-linux.yml latest-mac.yml --repo "${REPO}" --clobber
|
||||
|
||||
- name: Publish release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_release }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TAG="${RELEASE_TAG}"
|
||||
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."
|
||||
|
|
|
|||
|
|
@ -1397,7 +1397,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
});
|
||||
const [creatingTask, setCreatingTask] = useState(false);
|
||||
const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false);
|
||||
const [runtimeTelemetryPreviewVisible, setRuntimeTelemetryPreviewVisible] = useState(false);
|
||||
const [addingMemberLoading, setAddingMemberLoading] = useState(false);
|
||||
const [removeMemberConfirm, setRemoveMemberConfirm] = useState<string | null>(null);
|
||||
const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false);
|
||||
|
|
@ -2872,68 +2871,63 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<CollapsibleTeamSection
|
||||
sectionId="team"
|
||||
title="Team"
|
||||
icon={<Users size={14} />}
|
||||
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
|
||||
defaultOpen
|
||||
afterBadge={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAddMemberDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<UserPlus size={12} />
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
action={
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 pr-3 text-[11px] font-medium leading-none text-[var(--color-text-muted)] transition-opacity duration-150',
|
||||
runtimeTelemetryPreviewVisible ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<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)]" />
|
||||
Memory
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" />
|
||||
CPU
|
||||
</span>
|
||||
<div className="runtime-telemetry-hover-scope">
|
||||
<CollapsibleTeamSection
|
||||
sectionId="team"
|
||||
title="Team"
|
||||
icon={<Users size={14} />}
|
||||
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
|
||||
defaultOpen
|
||||
afterBadge={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAddMemberDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<UserPlus size={12} />
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
action={
|
||||
<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">
|
||||
<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)]" />
|
||||
Memory
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" />
|
||||
CPU
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
|
||||
>
|
||||
<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>
|
||||
}
|
||||
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<CollapsibleTeamSection
|
||||
sectionId="sessions"
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ interface MemberCardProps {
|
|||
spawnLaunchState?: MemberLaunchState;
|
||||
spawnRuntimeAlive?: boolean;
|
||||
isLaunchSettling?: boolean;
|
||||
runtimeTelemetryVisible?: boolean;
|
||||
runtimeTelemetryScale?: RuntimeTelemetryScale;
|
||||
onOpenTask?: () => void;
|
||||
onOpenReviewTask?: () => void;
|
||||
|
|
@ -502,11 +501,9 @@ function buildRuntimeTelemetryPaths(
|
|||
|
||||
const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
|
||||
runtimeEntry,
|
||||
visible,
|
||||
scale,
|
||||
}: {
|
||||
runtimeEntry?: TeamAgentRuntimeEntry;
|
||||
visible: boolean;
|
||||
scale?: RuntimeTelemetryScale;
|
||||
}): React.JSX.Element | null {
|
||||
const paths = useMemo(
|
||||
|
|
@ -521,10 +518,7 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
|
|||
<div
|
||||
aria-hidden="true"
|
||||
data-testid="member-runtime-telemetry-strip"
|
||||
className={cn(
|
||||
'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'
|
||||
)}
|
||||
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"
|
||||
style={{
|
||||
WebkitMaskImage:
|
||||
'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,
|
||||
spawnRuntimeAlive,
|
||||
isLaunchSettling,
|
||||
runtimeTelemetryVisible = false,
|
||||
runtimeTelemetryScale,
|
||||
onOpenTask,
|
||||
onOpenReviewTask,
|
||||
|
|
@ -696,7 +689,7 @@ export const MemberCard = memo(function MemberCard({
|
|||
? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}`
|
||||
: undefined;
|
||||
const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry);
|
||||
const showRuntimeTelemetryTooltip = runtimeTelemetryVisible && Boolean(runtimeTelemetryTitle);
|
||||
const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle);
|
||||
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
|
||||
const runtimeTelemetryTooltipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [runtimeTelemetryTooltipOpen, setRuntimeTelemetryTooltipOpen] = useState(false);
|
||||
|
|
@ -920,11 +913,7 @@ export const MemberCard = memo(function MemberCard({
|
|||
}}
|
||||
>
|
||||
{!isRemoved ? (
|
||||
<MemberRuntimeTelemetryStrip
|
||||
runtimeEntry={runtimeEntry}
|
||||
visible={runtimeTelemetryVisible}
|
||||
scale={runtimeTelemetryScale}
|
||||
/>
|
||||
<MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} />
|
||||
) : null}
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ interface MemberListProps {
|
|||
isTeamProvisioning?: boolean;
|
||||
leadActivity?: LeadActivityState;
|
||||
launchParams?: TeamLaunchParams;
|
||||
runtimeTelemetryVisible?: boolean;
|
||||
onRuntimeTelemetryHoverChange?: (visible: boolean) => void;
|
||||
onMemberClick?: (member: ResolvedTeamMember) => void;
|
||||
onSendMessage?: (member: ResolvedTeamMember) => void;
|
||||
onAssignTask?: (member: ResolvedTeamMember) => void;
|
||||
|
|
@ -464,8 +462,6 @@ function areMemberListPropsEqual(
|
|||
prev.isTeamAlive === next.isTeamAlive &&
|
||||
prev.isTeamProvisioning === next.isTeamProvisioning &&
|
||||
prev.leadActivity === next.leadActivity &&
|
||||
prev.runtimeTelemetryVisible === next.runtimeTelemetryVisible &&
|
||||
prev.onRuntimeTelemetryHoverChange === next.onRuntimeTelemetryHoverChange &&
|
||||
prev.onRestartMember === next.onRestartMember &&
|
||||
prev.onSkipMemberForLaunch === next.onSkipMemberForLaunch &&
|
||||
areLaunchParamsEquivalent(prev.launchParams, next.launchParams)
|
||||
|
|
@ -502,7 +498,6 @@ interface MemberCardRowProps {
|
|||
isTeamProvisioning?: boolean;
|
||||
leadActivity?: LeadActivityState;
|
||||
isLaunchSettling?: boolean;
|
||||
runtimeTelemetryVisible?: boolean;
|
||||
runtimeTelemetryScale?: RuntimeTelemetryScale;
|
||||
onOpenTask?: (taskId: string) => void;
|
||||
onMemberClick?: (member: ResolvedTeamMember) => void;
|
||||
|
|
@ -538,7 +533,6 @@ const MemberCardRow = memo(function MemberCardRow({
|
|||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
isLaunchSettling,
|
||||
runtimeTelemetryVisible,
|
||||
runtimeTelemetryScale,
|
||||
onOpenTask,
|
||||
onMemberClick,
|
||||
|
|
@ -589,7 +583,6 @@ const MemberCardRow = memo(function MemberCardRow({
|
|||
spawnLaunchState={spawnLaunchState}
|
||||
spawnRuntimeAlive={spawnRuntimeAlive}
|
||||
isLaunchSettling={isLaunchSettling}
|
||||
runtimeTelemetryVisible={runtimeTelemetryVisible}
|
||||
runtimeTelemetryScale={runtimeTelemetryScale}
|
||||
onOpenTask={currentTask ? handleOpenTask : undefined}
|
||||
onOpenReviewTask={reviewTask ? handleOpenReviewTask : undefined}
|
||||
|
|
@ -714,8 +707,6 @@ export const MemberList = memo(function MemberList({
|
|||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
launchParams,
|
||||
runtimeTelemetryVisible = false,
|
||||
onRuntimeTelemetryHoverChange,
|
||||
onMemberClick,
|
||||
onSendMessage,
|
||||
onAssignTask,
|
||||
|
|
@ -742,14 +733,6 @@ export const MemberList = memo(function MemberList({
|
|||
return () => observer.disconnect();
|
||||
}, [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 activeMembers = useMemo(
|
||||
() =>
|
||||
|
|
@ -941,12 +924,7 @@ export const MemberList = memo(function MemberList({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-col gap-1"
|
||||
onMouseEnter={handleRuntimeTelemetryMouseEnter}
|
||||
onMouseLeave={handleRuntimeTelemetryMouseLeave}
|
||||
>
|
||||
<div ref={containerRef} className="runtime-telemetry-list flex flex-col gap-1">
|
||||
<div className={gridClass}>
|
||||
{activeMembers.map((member) => {
|
||||
const spawnEntry = memberSpawnStatuses?.get(member.name);
|
||||
|
|
@ -1017,7 +995,6 @@ export const MemberList = memo(function MemberList({
|
|||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={leadActivity}
|
||||
isLaunchSettling={isLaunchSettling}
|
||||
runtimeTelemetryVisible={runtimeTelemetryVisible}
|
||||
runtimeTelemetryScale={runtimeTelemetryScale}
|
||||
onOpenTask={onOpenTask}
|
||||
onMemberClick={onMemberClick}
|
||||
|
|
@ -1063,7 +1040,6 @@ export const MemberList = memo(function MemberList({
|
|||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={leadActivity}
|
||||
isLaunchSettling={false}
|
||||
runtimeTelemetryVisible={runtimeTelemetryVisible}
|
||||
runtimeTelemetryScale={runtimeTelemetryScale}
|
||||
onOpenTask={onOpenTask}
|
||||
onMemberClick={onMemberClick}
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,13 @@ a[href],
|
|||
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) {
|
||||
.kanban-comment-badge-pulse,
|
||||
.message-composer-orbit-path,
|
||||
|
|
|
|||
|
|
@ -800,7 +800,6 @@ describe('MemberCard starting-state visuals', () => {
|
|||
],
|
||||
updatedAt: '2026-04-24T12:00:05.000Z',
|
||||
},
|
||||
runtimeTelemetryVisible: true,
|
||||
runtimeTelemetryScale: {
|
||||
cpuCapPercent: 100,
|
||||
memoryCapBytes: 512 * 1024 * 1024,
|
||||
|
|
@ -861,7 +860,6 @@ describe('MemberCard starting-state visuals', () => {
|
|||
resourceHistory: 'not-an-array',
|
||||
updatedAt: '2026-04-24T12:00:05.000Z',
|
||||
} as any,
|
||||
runtimeTelemetryVisible: true,
|
||||
isTeamAlive: true,
|
||||
isTeamProvisioning: false,
|
||||
})
|
||||
|
|
@ -911,7 +909,6 @@ describe('MemberCard starting-state visuals', () => {
|
|||
],
|
||||
updatedAt: '2026-04-24T12:00:05.000Z',
|
||||
} as any,
|
||||
runtimeTelemetryVisible: true,
|
||||
isTeamAlive: true,
|
||||
isTeamProvisioning: false,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue