Skip to content

fix: prevent worker from stopping when another session is still active #449

fix: prevent worker from stopping when another session is still active

fix: prevent worker from stopping when another session is still active #449

Workflow file for this run

---
name: Release
"on":
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: "Version to build binaries for (e.g., 3.3.0)"
required: true
type: string
changelog:
description: "Custom changelog content (optional - overrides generated changelog)"
required: false
type: string
jobs:
# Check if this commit should trigger a release
check-trigger:
name: Check Release Trigger
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 2
- name: Check commit message
id: check
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Manual trigger - proceeding"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
COMMIT_MSG=$(git log -1 --pretty=%B)
echo "Commit message: $COMMIT_MSG"
# Check for fix: or feat: prefix (first line or squash merge body)
if echo "$COMMIT_MSG" | grep -qE "(^|\* )(fix|feat):"; then
echo "Release commit detected (fix: or feat:)"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check for merge from dev branch
if echo "$COMMIT_MSG" | grep -qE "^Merge (branch 'dev'|pull request .* from .*/dev)"; then
echo "Merge from dev detected"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Not a release trigger commit - skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"
security-scan:
name: Security Scan (Trivy)
permissions:
contents: read
runs-on: ubuntu-latest
needs: check-trigger
if: needs.check-trigger.outputs.should_run == 'true'
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Run Trivy vulnerability and secret scanner
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
skip-dirs: '.venv,node_modules,console/node_modules,launcher,docs/site/api'
trivyignores: '.trivyignore'
format: 'table'
output: trivy-results.txt
- name: Publish Trivy results to step summary
if: always()
run: |
if [[ -s trivy-results.txt ]]; then
{
echo "### Security Scan Results"
echo "<details><summary>Click to expand Trivy output</summary>"
echo ""
echo '```'
cat trivy-results.txt
echo '```'
echo "</details>"
} >> $GITHUB_STEP_SUMMARY
else
echo "### Security Scan: No issues found" >> $GITHUB_STEP_SUMMARY
fi
python-tests:
name: Python Unit Tests
permissions:
contents: read
runs-on: ubuntu-latest
needs: check-trigger
if: needs.check-trigger.outputs.should_run == 'true'
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Set up Python 3.12
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Install Python dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install . pytest pytest-cov pytest-asyncio
- name: Run unit tests with coverage
run: |
python3 -m pytest installer/tests/unit/ launcher/tests/unit/ pilot/hooks/tests/ -v \
--cov=installer --cov=launcher --cov=pilot.hooks \
--cov-report=term --cov-report=xml
console-tests:
name: Console Unit Tests
permissions:
contents: read
runs-on: ubuntu-latest
needs: check-trigger
if: needs.check-trigger.outputs.should_run == 'true'
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
with:
bun-version: latest
- name: Install dependencies
working-directory: console
run: bun install
- name: Run console tests
working-directory: console
run: bun test
console-build:
name: Console Build & Typecheck
permissions:
contents: read
runs-on: ubuntu-latest
needs: check-trigger
if: needs.check-trigger.outputs.should_run == 'true'
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: "22"
- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
with:
bun-version: latest
- name: Install dependencies
working-directory: console
run: bun install
- name: Typecheck
working-directory: console
run: bun run typecheck
- name: Build hooks
working-directory: console
run: bun run build
- name: Build viewer
working-directory: console
run: bun run build:viewer
# Determine the version to release (semantic-release dry-run or manual input)
prepare-release:
name: Prepare Release
permissions:
contents: write
runs-on: ubuntu-latest
needs: check-trigger
if: needs.check-trigger.outputs.should_run == 'true'
outputs:
should_release: ${{ steps.check.outputs.should_release }}
version: ${{ steps.check.outputs.version }}
current_version: ${{ steps.check.outputs.current_version }}
changelog: ${{ steps.git-cliff.outputs.content }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: "22"
- name: Get current version
id: current
run: |
CURRENT=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
echo "Current version: $CURRENT"
echo "version=$CURRENT" >> "$GITHUB_OUTPUT"
- name: Check for release
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "current_version=${{ steps.current.outputs.version }}" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Manual trigger with version ${{ inputs.version }}"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
# Install semantic-release and plugins
npm install -g semantic-release \
@semantic-release/git \
@semantic-release/exec
# Run dry-run to check if release is needed
SR_OUT=/tmp/sr-output.txt
if npx semantic-release --dry-run 2>&1 | tee $SR_OUT \
| grep -q "Published release"; then
VERSION=$(grep "next release version" $SR_OUT \
| grep -oP '\d+\.\d+\.\d+' | head -1)
if [ -z "$VERSION" ]; then
VERSION=$(grep -oP 'Published release \K\d+\.\d+\.\d+' \
$SR_OUT | head -1)
fi
echo "Will release version: $VERSION"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
else
echo "No release needed"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "version=" >> "$GITHUB_OUTPUT"
fi
fi
- name: Generate changelog with git-cliff
if: steps.check.outputs.should_release == 'true'
id: git-cliff
uses: orhun/git-cliff-action@e16f179f0be49ecdfe63753837f20b9531642772 # v4
with:
config: cliff.toml
args: v6.0.0.. --unreleased --tag v${{ steps.check.outputs.version }}
env:
OUTPUT: CHANGES.md
GITHUB_REPO: ${{ github.repository }}
# Build jobs run in parallel after prepare-release
build-pilot-x86:
name: Build Pilot Linux x86_64
permissions:
contents: read
runs-on: ubuntu-latest
needs: prepare-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Verify version
run: |
echo "Building version: ${{ needs.prepare-release.outputs.version }}"
cat launcher/__init__.py | grep __version__
- name: Build binary
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e BUILD_VERSION=${{ needs.prepare-release.outputs.version }} \
python:3.12-slim-bullseye \
bash -c "
apt-get update && apt-get install -y binutils build-essential && \
pip install . && \
python -m launcher.build --release --version \$BUILD_VERSION && \
ls -la launcher/dist/
"
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pilot-linux-x86_64
path: |
launcher/dist/pilot-linux-x86_64.so
launcher/dist/pilot
retention-days: 1
build-pilot-arm64:
name: Build Pilot Linux arm64
permissions:
contents: read
runs-on: ubuntu-24.04-arm
needs: prepare-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Verify version
run: |
echo "Building version: ${{ needs.prepare-release.outputs.version }}"
cat launcher/__init__.py | grep __version__
- name: Build binary
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e BUILD_VERSION=${{ needs.prepare-release.outputs.version }} \
python:3.12-slim-bullseye \
bash -c "
apt-get update && apt-get install -y binutils build-essential && \
pip install . && \
python -m launcher.build --release --version \$BUILD_VERSION && \
ls -la launcher/dist/
"
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pilot-linux-arm64
path: launcher/dist/pilot-linux-arm64.so
retention-days: 1
build-pilot-darwin-x86:
name: Build Pilot Darwin x86_64
permissions:
contents: read
runs-on: macos-15-intel
needs: prepare-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: brew install git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Set up Python 3.12
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Cache pip dependencies
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-pyinstaller
restore-keys: ${{ runner.os }}-pip-
- name: Verify version
run: |
echo "Building version: ${{ needs.prepare-release.outputs.version }}"
cat launcher/__init__.py | grep __version__
- name: Build binary
run: |
pip install .
python -m launcher.build --release --version ${{ needs.prepare-release.outputs.version }}
ls -la launcher/dist/
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pilot-darwin-x86_64
path: launcher/dist/pilot-darwin-x86_64.so
retention-days: 1
build-pilot-darwin-arm64:
name: Build Pilot Darwin arm64
permissions:
contents: read
runs-on: macos-14
needs: prepare-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: brew install git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Set up Python 3.12
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Cache pip dependencies
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-pyinstaller
restore-keys: ${{ runner.os }}-pip-
- name: Verify version
run: |
echo "Building version: ${{ needs.prepare-release.outputs.version }}"
cat launcher/__init__.py | grep __version__
- name: Build binary
run: |
pip install .
python -m launcher.build --release --version ${{ needs.prepare-release.outputs.version }}
ls -la launcher/dist/
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pilot-darwin-arm64
path: launcher/dist/pilot-darwin-arm64.so
retention-days: 1
# Manual approval gate - displays version and changelog, waits for approval
approve-release:
name: Approve Release
permissions:
contents: read
runs-on: ubuntu-latest
needs:
- prepare-release
- python-tests
- console-tests
- console-build
- build-pilot-x86
- build-pilot-arm64
- build-pilot-darwin-x86
- build-pilot-darwin-arm64
- security-scan
if: needs.prepare-release.outputs.should_release == 'true'
environment: production
steps:
- name: Display release details for approval
env:
CHANGELOG_CONTENT: ${{ needs.prepare-release.outputs.changelog }}
run: |
cat >> $GITHUB_STEP_SUMMARY << SUMMARY
## 🚀 Release Approval Required
### Version Bump
| Current | → | New |
|---------|---|-----|
| v${{ needs.prepare-release.outputs.current_version }} | → | **v${{ needs.prepare-release.outputs.version }}** |
### Build Status
- ✅ Security scan (Trivy) passed
- ✅ Python unit tests passed
- ✅ Console unit tests passed
- ✅ Console build & typecheck passed
- ✅ Linux x86_64 build ready
- ✅ Linux arm64 build ready
- ✅ Darwin x86_64 build ready
- ✅ Darwin arm64 build ready
### Changelog for v${{ needs.prepare-release.outputs.version }}
SUMMARY
# Write changelog safely via environment variable
printf '%s\n' "$CHANGELOG_CONTENT" >> $GITHUB_STEP_SUMMARY
cat >> $GITHUB_STEP_SUMMARY << 'SUMMARY'
---
**Approve this deployment to publish the release.**
SUMMARY
- name: Release approved
run: |
echo "✅ Release v${{ needs.prepare-release.outputs.version }} approved!"
echo "Proceeding to publish..."
# Create release and upload all artifacts only after approval
publish-release:
name: Publish Release
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
attestations: write
runs-on: ubuntu-latest
needs:
- prepare-release
- approve-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Sync with remote before release
if: github.event_name != 'workflow_dispatch'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git reset --hard origin/main
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
- name: List artifacts
run: |
echo "Downloaded artifacts:"
find artifacts -type f -ls
- name: Attest build provenance
uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
with:
subject-path: |
artifacts/pilot-linux-x86_64/pilot-linux-x86_64.so
artifacts/pilot-linux-arm64/pilot-linux-arm64.so
artifacts/pilot-darwin-x86_64/pilot-darwin-x86_64.so
artifacts/pilot-darwin-arm64/pilot-darwin-arm64.so
artifacts/pilot-linux-x86_64/pilot
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: "22"
- name: Prepend new release to CHANGELOG.md with git-cliff
if: ${{ github.event.inputs.changelog == '' }}
uses: orhun/git-cliff-action@e16f179f0be49ecdfe63753837f20b9531642772 # v4
with:
config: cliff.toml
args: --unreleased --tag v${{ needs.prepare-release.outputs.version }} --prepend CHANGELOG.md
env:
GITHUB_REPO: ${{ github.repository }}
- name: Use custom changelog if provided
if: ${{ github.event.inputs.changelog != '' }}
env:
CHANGELOG: ${{ github.event.inputs.changelog }}
run: |
echo "$CHANGELOG" > CHANGELOG.md
echo "Using custom changelog content"
- name: Create release with semantic-release
if: github.event_name != 'workflow_dispatch'
id: semantic
uses: cycjimmy/semantic-release-action@b12c8f6015dc215fe37bc154d4ad456dd3833c90 # v6.0.0
with:
extra_plugins: |
@semantic-release/git
@semantic-release/exec
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update version files for manual trigger
if: github.event_name == 'workflow_dispatch'
run: |
VERSION="${{ needs.prepare-release.outputs.version }}"
echo "Updating version files to ${VERSION}..."
# Update version in Python files (same as semantic-release prepareCmd)
sed -i 's/^__version__ = ".*"/__version__ = "'"${VERSION}"'"/' installer/__init__.py launcher/__init__.py
# Update version in README.md
sed -i "s/export VERSION=[0-9.]*/export VERSION=${VERSION}/g" README.md
echo "Version files updated:"
grep __version__ installer/__init__.py launcher/__init__.py
grep "VERSION=" README.md | head -1
- name: Commit and push version updates for manual trigger
if: github.event_name == 'workflow_dispatch'
run: |
VERSION="${{ needs.prepare-release.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Stage all updated files
git add installer/__init__.py launcher/__init__.py README.md CHANGELOG.md
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "chore(release): ${VERSION} [skip ci]"
git push origin main
echo "Version updates committed and pushed"
fi
- name: Create release for manual trigger
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.prepare-release.outputs.version }}"
if ! gh release view "v${VERSION}" >/dev/null 2>&1; then
echo "Creating release v${VERSION}..."
gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes
fi
- name: Generate tree.json manifest
run: |
echo "Generating tree.json from repository files..."
git ls-tree -r HEAD | python3 -c "
import sys, json
items = []
for line in sys.stdin:
parts = line.strip().split('\t', 1)
if len(parts) == 2:
meta, path = parts
fields = meta.split()
if len(fields) == 3:
items.append({'path': path, 'type': 'blob', 'sha': fields[2]})
json.dump({'tree': items}, sys.stdout, separators=(', ', ': '))
" > tree.json
echo "Validating tree.json..."
python3 -c "import json; data=json.load(open('tree.json')); assert 'tree' in data and len(data['tree']) > 0, 'Invalid tree.json'"
echo "tree.json generated successfully with $(python3 -c "import json; print(len(json.load(open('tree.json'))['tree']))") files"
- name: Upload artifacts to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.prepare-release.outputs.version }}"
echo "Uploading artifacts for v${VERSION}"
# Wait for release to exist with retry logic
MAX_RETRIES=12
RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do
if gh release view "v${VERSION}" >/dev/null 2>&1; then
echo "Release v${VERSION} found"
break
fi
if [ $i -eq $MAX_RETRIES ]; then
echo "Release v${VERSION} not found after ${MAX_RETRIES} retries"
echo "Creating release manually..."
gh release create "v${VERSION}" --title "v${VERSION}" --notes "Release v${VERSION}"
fi
echo "Waiting for release... (attempt $i/$MAX_RETRIES)"
sleep $RETRY_DELAY
done
# Upload all .so files and tree.json
gh release upload "v${VERSION}" \
artifacts/pilot-linux-x86_64/pilot-linux-x86_64.so \
artifacts/pilot-linux-arm64/pilot-linux-arm64.so \
artifacts/pilot-darwin-x86_64/pilot-darwin-x86_64.so \
artifacts/pilot-darwin-arm64/pilot-darwin-arm64.so \
artifacts/pilot-linux-x86_64/pilot \
tree.json \
--clobber
echo "All artifacts uploaded successfully"
- name: Output release info
run: |
echo "Released version: ${{ needs.prepare-release.outputs.version }}"
echo "All platform binaries are now available"
# Deploy website to production in parallel with publish-release (both after approval)
deploy-website:
name: Deploy Website (Production)
permissions:
contents: read
runs-on: ubuntu-latest
needs:
- prepare-release
- approve-release
if: needs.prepare-release.outputs.should_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt
- name: Unlock repository
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: bash .github/workflows/scripts/setup-git-crypt.sh
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: "22"
- name: Install Vercel CLI
run: npm install -g vercel
- name: Deploy to Vercel (Production)
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: team_jAsHrk71vRyWK6bCTYGJyp0q
VERCEL_PROJECT_ID: prj_TXccrJI83HyNvQUZxqStUFgus9NB
run: |
DEPLOY_URL=$(vercel deploy --token=$VERCEL_TOKEN --prod)
echo "Deployed to: $DEPLOY_URL"