Release v1.6.0 #130
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| | |
| <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> | |
| | |
| <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." |