diff --git a/.github/workflows/finalize-existing-release.yml b/.github/workflows/finalize-existing-release.yml deleted file mode 100644 index 8d20f575..00000000 --- a/.github/workflows/finalize-existing-release.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Finalize Existing Release - -on: - workflow_dispatch: - inputs: - version: - description: Existing release version without the v prefix - required: true - type: string - -permissions: - contents: write - -jobs: - finalize-release: - runs-on: ubuntu-latest - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }} - steps: - - name: Verify release inventory - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPOSITORY: ${{ github.repository }} - VERSION: ${{ inputs.version }} - run: | - node - <<'NODE' - (async () => { - const { GITHUB_TOKEN: token, REPOSITORY: repo, VERSION: version } = process.env; - const expected = [ - `OpenChamber-${version}-mac-arm64.dmg`, - `OpenChamber-${version}-mac-arm64.zip`, - `OpenChamber-${version}-mac-x64.dmg`, - `OpenChamber-${version}-mac-x64.zip`, - `OpenChamber-${version}-win-x64.exe`, - 'latest-mac.yml', - 'latest.yml', - ]; - const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/v${version}`, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, - }); - if (!response.ok) throw new Error(`Failed to inspect release: ${response.status} ${await response.text()}`); - const release = await response.json(); - for (const name of expected) { - const matches = release.assets.filter((asset) => asset.name === name); - if (matches.length !== 1) throw new Error(`Expected exactly one ${name}, found ${matches.length}`); - if (!Number.isSafeInteger(matches[0].size) || matches[0].size <= 0) { - throw new Error(`Release asset ${name} has invalid size ${matches[0].size}`); - } - } - console.log(`Verified release v${version} inventory before finalization.`); - })().catch((error) => { - console.error(error); - process.exit(1); - }); - NODE - - - name: Publish release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 - with: - tag_name: v${{ inputs.version }} - draft: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Send release to Discord - if: ${{ env.DISCORD_WEBHOOK_URL != '' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ inputs.version }} - REPOSITORY: ${{ github.repository }} - UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }} - run: | - node - <<'NODE' - (async () => { - const tag = `v${process.env.VERSION}`; - const repo = process.env.REPOSITORY; - const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim(); - const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : ''; - - const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, { - headers: { - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - Accept: 'application/vnd.github+json', - }, - }); - if (!releaseRes.ok) { - throw new Error(`Failed to fetch release ${tag}: ${releaseRes.status} ${await releaseRes.text()}`); - } - - const release = await releaseRes.json(); - const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096); - const mention = updateRoleId ? `<@&${updateRoleId}>` : ''; - const payload = { - username: 'OpenChamber Releases', - ...(mention ? { content: mention } : {}), - ...(updateRoleId ? { allowed_mentions: { roles: [updateRoleId] } } : {}), - embeds: [{ - title: release.name || `OpenChamber ${tag}`, - url: release.html_url, - description, - color: 2105893, - footer: { text: 'OpenChamber Changelog' }, - }], - }; - - const discordRes = await fetch(process.env.DISCORD_WEBHOOK_URL, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(payload), - }); - if (!discordRes.ok) { - throw new Error(`Failed to send Discord release: ${discordRes.status} ${await discordRes.text()}`); - } - })().catch((error) => { - console.error(error); - process.exit(1); - }); - NODE - - - name: Trigger openchamber-website site refresh - env: - WEBSITE_REPO: openchamber/openchamber-website - WEBSITE_TOKEN: ${{ secrets.OPENCHAMBER_WEBSITE_REPO_TOKEN }} - VERSION: ${{ inputs.version }} - run: | - if [ -z "$WEBSITE_TOKEN" ]; then - echo "OPENCHAMBER_WEBSITE_REPO_TOKEN not set; skip site refresh dispatch." - exit 0 - fi - - curl --fail-with-body -sS -X POST \ - -H "Authorization: Bearer $WEBSITE_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$WEBSITE_REPO/dispatches" \ - -d @- <> "$GITHUB_OUTPUT" - - - name: Cache bundled OpenCode CLI artifact - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 - with: - path: packages/electron/.cache/opencode-cli - key: opencode-cli-${{ runner.os }}-x64-${{ steps.opencode_cli_version.outputs.version }} - restore-keys: | - opencode-cli-${{ runner.os }}-x64- - - - name: Install Apple Certificate - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" - security import "$RUNNER_TEMP/certificate.p12" \ - -P "$APPLE_CERTIFICATE_PASSWORD" \ - -A -t cert -f pkcs12 \ - -k "$KEYCHAIN_PATH" - - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - - - name: Build Electron app - working-directory: packages/electron - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - ELECTRON_BUILDER_ARCH: x64 - run: | - bun run build:web-assets - bun run prepare:opencode-cli - bun run verify:opencode-cli - bun run bundle:main - bun run rebuild:native - bunx electron-builder --mac --x64 --publish=never - bun run verify:opencode-cli:packaged - - - name: Verify signature, entitlements, and notarization - run: | - set -euo pipefail - - APP_PATH=$(find packages/electron/dist/mac -maxdepth 2 -name "*.app" -print -quit) - if [ -z "$APP_PATH" ]; then - echo "Error: x64 .app not found" - exit 1 - fi - - codesign -vv --deep --strict "$APP_PATH" - CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) - echo "$CS_INFO" - if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then - echo "Error: hardened runtime flag missing" - exit 1 - fi - xcrun stapler validate "$APP_PATH" - - ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) - if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then - echo "Error: app sandbox entitlement is present" - exit 1 - fi - for key in \ - com.apple.security.cs.allow-jit \ - com.apple.security.cs.allow-unsigned-executable-memory \ - com.apple.security.cs.disable-library-validation - do - if ! echo "$ENTITLEMENTS" | grep -q "$key"; then - echo "Error: required entitlement missing: $key" - exit 1 - fi - done - - - name: Download arm64 update manifest - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: latest-yml-aarch64-apple-darwin - path: artifacts/latest-yml-aarch64-apple-darwin - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ inputs.manifest_run_id }} - - - name: Finalize combined macOS update manifest - env: - LATEST_YML_DIR: ${{ github.workspace }}/artifacts - GH_REPO: ${{ github.repository }} - OPENCHAMBER_VERSION: ${{ inputs.version }} - run: | - mkdir -p artifacts/latest-yml-x86_64-apple-darwin - cp packages/electron/dist/latest-mac.yml artifacts/latest-yml-x86_64-apple-darwin/latest-mac.yml - node packages/electron/scripts/finalize-latest-yml.mjs - test -s "$RUNNER_TEMP/latest-mac.yml" - - - name: Upload repaired macOS release files - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 - with: - tag_name: v${{ inputs.version }} - draft: false - files: | - packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.dmg - packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.zip - packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.dmg.blockmap - packages/electron/dist/OpenChamber-${{ inputs.version }}-mac-x64.zip.blockmap - ${{ runner.temp }}/latest-mac.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Verify repaired release inventory - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPOSITORY: ${{ github.repository }} - VERSION: ${{ inputs.version }} - run: | - node - <<'NODE' - (async () => { - const { GITHUB_TOKEN: token, REPOSITORY: repo, VERSION: version } = process.env; - const expected = [ - `OpenChamber-${version}-mac-arm64.dmg`, - `OpenChamber-${version}-mac-arm64.zip`, - `OpenChamber-${version}-mac-x64.dmg`, - `OpenChamber-${version}-mac-x64.zip`, - 'latest-mac.yml', - ]; - const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/v${version}`, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, - }); - if (!response.ok) throw new Error(`Failed to inspect release: ${response.status} ${await response.text()}`); - const release = await response.json(); - if (release.draft) throw new Error(`Release v${version} is still a draft`); - for (const name of expected) { - const matches = release.assets.filter((asset) => asset.name === name); - if (matches.length !== 1) throw new Error(`Expected exactly one ${name}, found ${matches.length}`); - if (!Number.isSafeInteger(matches[0].size) || matches[0].size <= 0) { - throw new Error(`Release asset ${name} has invalid size ${matches[0].size}`); - } - } - console.log(`Verified published v${version} with arm64, x64, and combined macOS update assets.`); - })().catch((error) => { - console.error(error); - process.exit(1); - }); - NODE