Skip to content

Release v1.6.0

Release v1.6.0 #130

Workflow file for this run

name: Release
run-name: >
${{
(github.event_name == 'workflow_dispatch' && inputs.version && format('chore(release): v{0}', inputs.version)) ||
(github.event_name == 'workflow_dispatch' && !inputs.version && 'chore(release): auto-increment') ||
(startsWith(github.ref, 'refs/tags/') && format('Release {0}', github.ref_name)) ||
format('Release #{0}', github.run_number)
}}
# 🚀 Release Workflow Guide
# -------------------------
# This workflow supports two modes of operation:
#
# 1. Manual Release (Only for an Already-Prepared Release)
# - How: Go to "Actions" -> "Release" -> "Run workflow".
# - Input: Optional "Version" (e.g., 1.2.0). If left empty, it auto-increments the patch version.
# - Requirement: The target version's 10-locale in-product changelog must already be committed.
# This workflow can bump and tag, but it does not author changelog content.
# - What it does:
# 1. Calculates the next version.
# 2. Bumps the web manifests and Xcode app/extension versions together.
# 3. Commits "chore(release): vX.Y.Z".
# 4. Creates git tag "vX.Y.Z".
# 5. Pushes commit & tag to main.
# 6. Builds artifacts and creates a GitHub Release.
#
# 2. Tag-based Release (Manual Tagging)
# - How: run `git commit -m "..." && git tag v1.2.0 && git push origin v1.2.0` locally.
# - What it does:
# 1. Detects the pushed tag (v1.2.0).
# 2. SKIPS version calculation and bumping (assumes you already did it).
# 3. Builds artifacts from that tag.
# 4. Creates a GitHub Release for that tag.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 1.0.8) - leave empty to auto-increment patch version'
required: false
type: string
notes:
description: 'Release notes (optional)'
required: false
type: string
publish_only:
description: 'Only re-publish an existing release to the Chrome Web Store (no bump/build). Requires version.'
required: false
type: boolean
default: false
publish_edge_only:
description: 'Only publish an existing release package to Microsoft Edge Add-ons (no bump/build). Requires version.'
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
group: release
cancel-in-progress: false
env:
BUN_VERSION: '1.3.12'
jobs:
# Job 1: Calculate & Bump Version (Only runs on manual trigger)
bump-version:
if: >-
github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main' &&
!inputs.publish_only &&
!inputs.publish_edge_only
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
new_version: ${{ steps.next_version.outputs.version }}
tag_name: ${{ steps.tag.outputs.name }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Calculate next version
id: next_version
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
if [ -n "$REQUESTED_VERSION" ]; then
NEXT="$REQUESTED_VERSION"
echo "Using manual version: $NEXT"
else
CURRENT=$(node -e "console.log(require('./package.json').version)")
echo "Current version: ${CURRENT}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
PATCH=$((PATCH + 1))
# Simple carry logic
if [ $PATCH -ge 10 ]; then
MINOR=$((MINOR + 1))
PATCH=0
fi
if [ $MINOR -ge 10 ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
fi
NEXT="${MAJOR}.${MINOR}.${PATCH}"
echo "Auto-calculated next version: ${NEXT}"
fi
if [[ ! "$NEXT" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Invalid release version: $NEXT" >&2
exit 64
fi
printf 'version=%s\n' "$NEXT" >> "$GITHUB_OUTPUT"
- name: Compute tag name
id: tag
env:
VERSION: ${{ steps.next_version.outputs.version }}
run: printf 'name=v%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
- name: Update files, commit and push
env:
VERSION: ${{ steps.next_version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Bumping to version ${VERSION}"
node scripts/bump-version.js "${VERSION}" --no-format
if [ ! -f "src/pages/content/changelog/notes/${VERSION}.md" ]; then
echo "Missing 10-locale changelog: src/pages/content/changelog/notes/${VERSION}.md" >&2
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json manifest.json manifest.dev.json "Voyager/Voyager.xcodeproj/project.pbxproj"
if git diff --cached --quiet; then
echo "Version files already match v${VERSION}"
else
git commit -m "chore(release): v${VERSION}"
fi
TAG="v${VERSION}"
if git show-ref --verify --quiet "refs/tags/${TAG}"; then
if [ "$(git rev-parse "${TAG}^{commit}")" != "$(git rev-parse HEAD)" ]; then
echo "Existing ${TAG} does not point at HEAD" >&2
exit 1
fi
else
git tag "$TAG"
fi
# Push commit and tag
git push origin HEAD:main
git push origin "$TAG"
# Job 2: Build, sign and notarize the Safari direct-distribution app.
build-safari-release:
needs: bump-version
if: >-
always() &&
(github.event_name == 'push' || needs.bump-version.result == 'success') &&
!(github.event_name == 'workflow_dispatch' && (inputs.publish_only || inputs.publish_edge_only))
runs-on: macos-15
environment: release
permissions:
contents: read
steps:
- name: Set Release Tag Variable
id: vars
env:
EVENT_NAME: ${{ github.event_name }}
MANUAL_TAG: ${{ needs.bump-version.outputs.tag_name }}
PUSH_TAG: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
TAG="$MANUAL_TAG"
else
TAG="$PUSH_TAG"
fi
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Invalid release tag: $TAG" >&2
exit 64
fi
printf 'tag_name=%s\n' "$TAG" >> "$GITHUB_OUTPUT"
- name: Checkout trusted release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.vars.outputs.tag_name }}
fetch-depth: 0
persist-credentials: false
- name: Verify release commit is on main
run: |
git fetch origin main --no-tags
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "Release tag must point to a commit already on origin/main" >&2
exit 1
fi
- name: Check required Safari release secrets
env:
APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_APP_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_APP_PROVISIONING_PROFILE_BASE64 }}
APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64 }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
missing=()
for name in \
APPLE_CERTIFICATE_P12_BASE64 \
APPLE_CERTIFICATE_PASSWORD \
APPLE_ID \
APPLE_APP_SPECIFIC_PASSWORD \
APPLE_APP_PROVISIONING_PROFILE_BASE64 \
APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64 \
SPARKLE_PRIVATE_KEY
do
if [ -z "${!name:-}" ]; then
missing+=("$name")
fi
done
if [ ${#missing[@]} -ne 0 ]; then
printf 'Missing Safari release secrets: %s\n' "${missing[*]}" >&2
exit 1
fi
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build Safari web extension
run: bun run build:safari
- name: Import Developer ID certificate
uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Install Developer ID provisioning profiles
env:
APPLE_APP_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_APP_PROVISIONING_PROFILE_BASE64 }}
APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64 }}
CLOUDKIT_PRODUCTION_SCHEMA_SHA256: ${{ vars.CLOUDKIT_PRODUCTION_SCHEMA_SHA256 }}
run: |
profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$profile_dir"
install_profile() {
local encoded=$1
local label=$2
local expected_bundle_id=$3
local profile="$RUNNER_TEMP/$label.provisionprofile"
local plist="$RUNNER_TEMP/$label.plist"
printf '%s' "$encoded" | base64 --decode > "$profile"
security cms -D -i "$profile" > "$plist"
local uuid
local name
uuid=$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$plist")
name=$(/usr/libexec/PlistBuddy -c 'Print :Name' "$plist")
python3 - "$plist" "$expected_bundle_id" "$label" <<'PY'
import datetime
import plistlib
import sys
path, expected_bundle_id, label = sys.argv[1:]
team_id = "PJM828YBFJ"
with open(path, "rb") as handle:
profile = plistlib.load(handle)
expires = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if isinstance(expires, datetime.datetime) and expires.tzinfo is None:
expires = expires.replace(tzinfo=datetime.timezone.utc)
if not isinstance(expires, datetime.datetime) or expires <= now:
raise SystemExit(f"{label} provisioning profile is expired or has no expiration date")
entitlements = profile.get("Entitlements", {})
application_id = entitlements.get("application-identifier")
if application_id != f"{team_id}.{expected_bundle_id}":
raise SystemExit(f"{label} provisioning profile has the wrong application identifier")
profile_team = entitlements.get("com.apple.developer.team-identifier")
if profile_team not in (None, team_id):
raise SystemExit(f"{label} provisioning profile has the wrong Team ID")
if label == "extension":
services = entitlements.get("com.apple.developer.icloud-services", [])
containers = entitlements.get("com.apple.developer.icloud-container-identifiers", [])
environment = entitlements.get("com.apple.developer.icloud-container-environment")
if "CloudKit" not in services and "*" not in services:
raise SystemExit("extension provisioning profile does not permit CloudKit")
if "iCloud.com.yourCompany.Gemini-Voyager" not in containers:
raise SystemExit("extension provisioning profile has the wrong iCloud container")
if environment != "Production":
raise SystemExit("extension provisioning profile is not for Production CloudKit")
PY
if [[ "$label" == "extension" ]]; then
expected_schema_hash=$(shasum -a 256 Voyager/CloudKit/VoyagerSchema.ckdb | awk '{print $1}')
if [ "${CLOUDKIT_PRODUCTION_SCHEMA_SHA256:-}" != "$expected_schema_hash" ]; then
echo "CloudKit Production schema is not acknowledged for this release." >&2
echo "After deploying Voyager/CloudKit/VoyagerSchema.ckdb to Production, set" >&2
echo "the release environment variable CLOUDKIT_PRODUCTION_SCHEMA_SHA256=$expected_schema_hash" >&2
exit 1
fi
fi
cp "$profile" "$profile_dir/$uuid.provisionprofile"
printf '%s|%s\n' "$uuid" "$name"
}
app_profile=$(install_profile \
"$APPLE_APP_PROVISIONING_PROFILE_BASE64" \
app \
com.yourCompany.Gemini-Voyager)
extension_profile=$(install_profile \
"$APPLE_EXTENSION_PROVISIONING_PROFILE_BASE64" \
extension \
com.yourCompany.Gemini-Voyager.Extension)
app_profile_uuid=${app_profile%%|*}
app_profile_name=${app_profile#*|}
extension_profile_uuid=${extension_profile%%|*}
extension_profile_name=${extension_profile#*|}
echo "VOYAGER_APP_PROFILE_NAME=$app_profile_name" >> "$GITHUB_ENV"
echo "VOYAGER_EXTENSION_PROFILE_NAME=$extension_profile_name" >> "$GITHUB_ENV"
echo "VOYAGER_APP_PROFILE_UUID=$app_profile_uuid" >> "$GITHUB_ENV"
echo "VOYAGER_EXTENSION_PROFILE_UUID=$extension_profile_uuid" >> "$GITHUB_ENV"
- name: Build, sign, notarize and prepare Sparkle update
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: PJM828YBFJ
VOYAGER_APP_PROFILE_NAME: ${{ env.VOYAGER_APP_PROFILE_NAME }}
VOYAGER_EXTENSION_PROFILE_NAME: ${{ env.VOYAGER_EXTENSION_PROFILE_NAME }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
RELEASE_TAG: ${{ steps.vars.outputs.tag_name }}
run: |
chmod +x scripts/build-safari-release.sh scripts/generate-sparkle-appcast.sh
scripts/build-safari-release.sh \
"$RELEASE_TAG" \
"$RUNNER_TEMP/safari-release"
- name: Upload Safari release artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: safari-release
path: |
${{ runner.temp }}/safari-release/voyager-*.dmg
${{ runner.temp }}/safari-release/appcast.xml
if-no-files-found: error
retention-days: 7
- name: Remove provisioning profiles
if: always()
run: |
profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
rm -f \
"$profile_dir/${VOYAGER_APP_PROFILE_UUID:-missing}.provisionprofile" \
"$profile_dir/${VOYAGER_EXTENSION_PROFILE_UUID:-missing}.provisionprofile" \
"$RUNNER_TEMP/app.provisionprofile" \
"$RUNNER_TEMP/app.plist" \
"$RUNNER_TEMP/extension.provisionprofile" \
"$RUNNER_TEMP/extension.plist"
# Job 3: Build & Release (Runs on both manual and tag push)
build-and-release:
needs: [bump-version, build-safari-release]
if: >-
always() &&
needs.build-safari-release.result == 'success' &&
(needs.bump-version.result == 'success' || github.event_name == 'push')
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
steps:
- name: Set Release Tag Variable
id: vars
env:
EVENT_NAME: ${{ github.event_name }}
MANUAL_TAG: ${{ needs.bump-version.outputs.tag_name }}
PUSH_TAG: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
TAG="$MANUAL_TAG"
else
TAG="$PUSH_TAG"
fi
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Invalid release tag: $TAG" >&2
exit 64
fi
printf 'tag_name=%s\n' "$TAG" >> "$GITHUB_OUTPUT"
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.vars.outputs.tag_name }}
fetch-depth: 0
persist-credentials: false
- name: Verify release commit is on main
run: |
git fetch origin main --no-tags
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "Release tag must point to a commit already on origin/main" >&2
exit 1
fi
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install deps
run: bun install --frozen-lockfile
- name: Build All
run: bun run build:all
- name: Download Safari release artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: safari-release
path: .
- name: Check release artifacts for private data
env:
RELEASE_TAG: ${{ steps.vars.outputs.tag_name }}
run: >-
node scripts/verify-release-privacy.mjs dist_chrome dist_firefox dist_safari
voyager-${RELEASE_TAG}.dmg
appcast.xml
- name: Archive artifacts
env:
TAG: ${{ steps.vars.outputs.tag_name }}
run: |
cd dist_chrome && zip -r ../voyager-chrome-${TAG}.zip . && cd ..
- name: Validate Chrome artifact
env:
TAG: ${{ steps.vars.outputs.tag_name }}
run: |
ZIP="voyager-chrome-${TAG}.zip"
unzip -Z1 "$ZIP" > chrome-zip-files.txt
grep -qx 'manifest.json' chrome-zip-files.txt
grep -qx '_locales/en/messages.json' chrome-zip-files.txt
- name: Sign Firefox Extension and Submit to AMO
env:
AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }}
AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }}
TAG: ${{ steps.vars.outputs.tag_name }}
run: |
node_modules/.bin/web-ext sign \
--source-dir=dist_firefox \
--api-key="$AMO_JWT_ISSUER" \
--api-secret="$AMO_JWT_SECRET" \
--channel=listed
# Move signed XPI to root with proper naming
mv web-ext-artifacts/*.xpi voyager-firefox-${TAG}.xpi
- name: Prepare Release Notes
id: release_body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.vars.outputs.tag_name }}
NOTES: ${{ inputs.notes }}
run: |
if [ -n "$NOTES" ]; then
delimiter="BODY_$(openssl rand -hex 16)"
{
echo "body<<$delimiter"
printf '%s\n' "$NOTES"
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
else
# Get the previous tag for auto-generated notes
PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
# Generate release notes via GitHub API
AUTO_NOTES=""
if [ -n "$PREV_TAG" ]; then
AUTO_NOTES=$(gh api "repos/${GH_REPOSITORY}/releases/generate-notes" \
-f tag_name="${TAG}" \
-f target_commitish="main" \
-f previous_tag_name="${PREV_TAG}" \
--jq '.body' 2>/dev/null || echo "")
fi
cat > release_body.md << 'INSTALL_EOF'
## 📥 Installation
<div align="center">
<a href="https://chromewebstore.google.com/detail/iifacdnjakkhjjiengaffnegbndgingi?utm_source=github&utm_medium=readme&utm_campaign=organic_growth&utm_content=en" target="_blank">
<img src="https://img.shields.io/badge/Chrome%20Web%20Store-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Chrome Web Store" height="36">
</a>
&nbsp;&nbsp;
<a href="https://microsoftedge.microsoft.com/addons/detail/voyager/gibmkggjijalcjinbdhcpklodjkhhlne" target="_blank">
<img src="https://img.shields.io/badge/Edge%20Add--ons-0078D7?style=for-the-badge&logo=microsoftedge&logoColor=white" alt="Edge Add-ons" height="36">
</a>
&nbsp;&nbsp;
<a href="https://addons.mozilla.org/firefox/addon/gemini-voyager/" target="_blank">
<img src="https://img.shields.io/badge/Firefox%20Add--ons-FF7139?style=for-the-badge&logo=firefox&logoColor=white" alt="Firefox Add-ons" height="36">
</a>
</div>
<p align="center">
<sub><b>Edge users:</b> Voyager is still maintained on Edge Add-ons for users who need Edge on mobile or tablet. If review is delayed, Chrome Web Store and GitHub manual packages remain available.</sub>
</p>
INSTALL_EOF
# Append version-specific file names
cat >> release_body.md << EOF
- **Chrome / Edge / Chromium browsers**: \`voyager-chrome-${TAG}.zip\`
- **Firefox**: \`voyager-firefox-${TAG}.xpi\`
- **Safari**: \`voyager-${TAG}.dmg\`
> Do not use GitHub's auto-generated **Source code (zip/tar.gz)** archives for manual extension installs. Those files are repository source snapshots, not browser-ready extension packages.
### 🍎 Safari 安装与限制 (Safari Installation & Limitations)
- **安装 (Installation)**: 下载并打开 \`voyager-${TAG}.dmg\`,按提示安装应用。
Download and open \`voyager-${TAG}.dmg\`, then follow the prompts to install the app.
- **Safari 差异 (Safari differences)**: Image Refinement 水印去除、Google Drive 与 iCloud 云同步均受支持。聊天导出中的图片提取仍受 Safari 限制,建议使用 PDF 导出。
Image Refinement watermark removal and Google Drive/iCloud sync are supported. Image extraction in chat exports remains limited by Safari; use PDF export when images matter.
EOF
# Combine: auto-generated notes first, then installation
delimiter="BODY_$(openssl rand -hex 16)"
{
echo "body<<$delimiter"
if [ -n "$AUTO_NOTES" ]; then
printf '%s\n\n' "$AUTO_NOTES"
fi
cat release_body.md
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
name: Voyager ${{ steps.vars.outputs.tag_name }}
tag_name: ${{ steps.vars.outputs.tag_name }}
files: |
voyager-chrome-*.zip
voyager-firefox-*.xpi
voyager-*.dmg
appcast.xml
body: ${{ steps.release_body.outputs.body }}
# Store publishing happens after the GitHub Release and Firefox/AMO
# submission, so a store-side failure does not prevent release assets.
- name: Publish to Chrome Web Store
env:
TAG: ${{ steps.vars.outputs.tag_name }}
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
run: |
# Chrome Web Store rejects a manifest that still carries the local dev `key`
# field (PKG_MANIFEST_KEY_NOT_MATCH). Strip it into a store-only copy; the
# GitHub Release zip keeps the key for manual unpacked installs.
rm -rf cws_pkg "voyager-chrome-cws-${TAG}.zip"
mkdir cws_pkg
unzip -q "voyager-chrome-${TAG}.zip" -d cws_pkg
node -e "const fs=require('fs');const m=JSON.parse(fs.readFileSync('cws_pkg/manifest.json','utf8'));delete m.key;fs.writeFileSync('cws_pkg/manifest.json',JSON.stringify(m,null,2)+'\n');"
(cd cws_pkg && zip -qr "../voyager-chrome-cws-${TAG}.zip" .)
node_modules/.bin/chrome-webstore-upload \
--source "voyager-chrome-cws-${TAG}.zip" \
--extension-id "$EXTENSION_ID"
- name: Build and publish to Edge Add-ons
env:
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }}
TAG: ${{ steps.vars.outputs.tag_name }}
run: |
bun run build:edge
node scripts/verify-release-privacy.mjs dist_edge
node scripts/publish-edge.js "voyager-edge-${TAG}.zip" \
--notes "Voyager ${TAG} automated release submission."
# Job 4: Re-publish Chrome only (manual) — recover a failed Chrome Web Store
# upload without re-cutting the release. Downloads the existing release's Chrome
# zip, strips the dev `key`, and publishes. No version bump, no rebuild.
republish-chrome:
if: github.event_name == 'workflow_dispatch' && inputs.publish_only && inputs.version != ''
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
steps:
- name: Checkout release tooling
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate release version
id: vars
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Invalid release version: $VERSION" >&2
exit 64
fi
printf 'tag=v%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
- name: Download Chrome zip from the existing release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.vars.outputs.tag }}
run: |
gh release download "$TAG" --repo "$GH_REPOSITORY" --pattern "voyager-chrome-${TAG}.zip"
ls -la "voyager-chrome-${TAG}.zip"
- name: Strip dev key and publish to Chrome Web Store
env:
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
TAG: ${{ steps.vars.outputs.tag }}
run: |
ZIP="voyager-chrome-${TAG}.zip"
rm -rf cws_pkg "voyager-chrome-cws-${TAG}.zip"
mkdir cws_pkg
unzip -q "$ZIP" -d cws_pkg
node -e "const fs=require('fs');const m=JSON.parse(fs.readFileSync('cws_pkg/manifest.json','utf8'));delete m.key;fs.writeFileSync('cws_pkg/manifest.json',JSON.stringify(m,null,2)+'\n');"
(cd cws_pkg && zip -qr "../voyager-chrome-cws-${TAG}.zip" .)
node_modules/.bin/chrome-webstore-upload \
--source "voyager-chrome-cws-${TAG}.zip" \
--extension-id "$EXTENSION_ID"
# Job 5: Publish Edge only (manual) — recover or retry Microsoft Edge Add-ons
# submission without re-cutting the release. Downloads the existing release's
# Chrome zip, strips the dev `key`, and submits the package to Edge review.
republish-edge:
if: github.event_name == 'workflow_dispatch' && inputs.publish_edge_only && inputs.version != ''
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
steps:
- name: Checkout scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate release version
id: vars
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Invalid release version: $VERSION" >&2
exit 64
fi
printf 'tag=v%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
- name: Download Chrome zip from the existing release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.vars.outputs.tag }}
run: |
gh release download "$TAG" --repo "$GH_REPOSITORY" --pattern "voyager-chrome-${TAG}.zip"
ls -la "voyager-chrome-${TAG}.zip"
- name: Prepare Edge package and publish to Edge Add-ons
env:
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }}
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }}
EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }}
TAG: ${{ steps.vars.outputs.tag }}
run: |
ZIP="voyager-chrome-${TAG}.zip"
rm -rf edge_pkg "voyager-edge-${TAG}.zip"
mkdir edge_pkg
unzip -q "$ZIP" -d edge_pkg
node -e "const fs=require('fs');const m=JSON.parse(fs.readFileSync('edge_pkg/manifest.json','utf8'));delete m.key;fs.writeFileSync('edge_pkg/manifest.json',JSON.stringify(m,null,2)+'\n');"
(cd edge_pkg && zip -qr "../voyager-edge-${TAG}.zip" .)
node scripts/publish-edge.js "voyager-edge-${TAG}.zip" \
--notes "Voyager ${TAG} automated Edge Add-ons submission."