diff --git a/.github/workflows/desktop_publish.yaml b/.github/workflows/desktop_publish.yaml index dd558dbabe..1c3d3e9e79 100644 --- a/.github/workflows/desktop_publish.yaml +++ b/.github/workflows/desktop_publish.yaml @@ -63,6 +63,10 @@ jobs: echo "::error::Dry-run workflow ID must be a positive integer" exit 1 fi + if [[ "$INPUT_INCLUDE_WINDOWS" != "true" ]]; then + echo "::error::Stable releases must include Windows for automatic Microsoft Store submission" + exit 1 + fi VERSION="$INPUT_VERSION" CANDIDATE_SHA="${INPUT_CANDIDATE_SHA,,}" @@ -560,6 +564,38 @@ jobs: echo "- Silent install argument: \`/S\`" } >> "$GITHUB_STEP_SUMMARY" + store-publish: + needs: [parse, gh-release] + if: ${{ !cancelled() && needs.parse.result == 'success' && needs.gh-release.result == 'success' }} + permissions: + contents: read + uses: ./.github/workflows/desktop_store_publish.yaml + with: + version: ${{ needs.parse.outputs.version }} + candidate_sha: ${{ needs.parse.outputs.candidate_sha }} + include_macos: true + include_windows: true + submit_to_stores: true + secrets: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} + APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} + APPSTORE_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }} + AZURE_AD_APPLICATION_SECRET: ${{ secrets.AZURE_AD_APPLICATION_SECRET }} + CN_API_KEY: ${{ secrets.CN_API_KEY }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + MAC_APP_STORE_APPLICATION_CERTIFICATE: ${{ secrets.MAC_APP_STORE_APPLICATION_CERTIFICATE }} + MAC_APP_STORE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.MAC_APP_STORE_APPLICATION_CERTIFICATE_PASSWORD }} + MAC_APP_STORE_INSTALLER_CERTIFICATE: ${{ secrets.MAC_APP_STORE_INSTALLER_CERTIFICATE }} + MAC_APP_STORE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MAC_APP_STORE_INSTALLER_CERTIFICATE_PASSWORD }} + MAC_APP_STORE_PROVISIONING_PROFILE: ${{ secrets.MAC_APP_STORE_PROVISIONING_PROFILE }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + SELLER_ID: ${{ secrets.SELLER_ID }} + SENTRY_DSN_HYPRNOTE_2: ${{ secrets.SENTRY_DSN_HYPRNOTE_2 }} + VITE_PRO_PRODUCT_ID: ${{ secrets.VITE_PRO_PRODUCT_ID }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} + VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} + linux-package-bump: needs: [parse, gh-release] if: ${{ !cancelled() && needs.parse.outputs.include_linux == 'true' && needs.gh-release.result == 'success' }} diff --git a/.github/workflows/desktop_store_publish.yaml b/.github/workflows/desktop_store_publish.yaml index 14e110febd..a665af08cf 100644 --- a/.github/workflows/desktop_store_publish.yaml +++ b/.github/workflows/desktop_store_publish.yaml @@ -1,6 +1,65 @@ name: Desktop Store Publish on: + workflow_call: + inputs: + version: + description: "Stable desktop version to release (e.g., 1.4.12)" + required: true + type: string + candidate_sha: + description: "Exact 40-character commit tagged for the stable release" + required: true + type: string + include_macos: + description: "Build the signed Mac App Store package" + type: boolean + default: true + include_windows: + description: "Prepare the Microsoft Store package update" + type: boolean + default: true + submit_to_stores: + description: "Upload to App Store Connect and submit the Microsoft Store update" + type: boolean + default: false + secrets: + APPLE_TEAM_ID: + required: false + APPSTORE_API_KEY_ID: + required: false + APPSTORE_API_PRIVATE_KEY: + required: false + APPSTORE_ISSUER_ID: + required: false + AZURE_AD_APPLICATION_SECRET: + required: false + CN_API_KEY: + required: false + KEYCHAIN_PASSWORD: + required: false + MAC_APP_STORE_APPLICATION_CERTIFICATE: + required: false + MAC_APP_STORE_APPLICATION_CERTIFICATE_PASSWORD: + required: false + MAC_APP_STORE_INSTALLER_CERTIFICATE: + required: false + MAC_APP_STORE_INSTALLER_CERTIFICATE_PASSWORD: + required: false + MAC_APP_STORE_PROVISIONING_PROFILE: + required: false + POSTHOG_API_KEY: + required: false + SELLER_ID: + required: false + SENTRY_DSN_HYPRNOTE_2: + required: false + VITE_PRO_PRODUCT_ID: + required: false + VITE_SUPABASE_ANON_KEY: + required: false + VITE_SUPABASE_URL: + required: false workflow_dispatch: inputs: version: diff --git a/scripts/desktop-release-provenance.test.mjs b/scripts/desktop-release-provenance.test.mjs index 8867e69d00..2037447e83 100644 --- a/scripts/desktop-release-provenance.test.mjs +++ b/scripts/desktop-release-provenance.test.mjs @@ -268,6 +268,75 @@ test("repository release workflows match the authored release plan", async () => verifyWorkflowPlatformCoverage({ publishWorkflow, cdWorkflow }); }); +test("stable desktop releases submit both store packages", async () => { + const [publishWorkflow, storeWorkflow] = await Promise.all([ + readFile(".github/workflows/desktop_publish.yaml", "utf8"), + readFile(".github/workflows/desktop_store_publish.yaml", "utf8"), + ]); + + assert.match(storeWorkflow, /\n workflow_call:\n/); + assert.match( + publishWorkflow, + /Stable releases must include Windows for automatic Microsoft Store submission/, + ); + + const jobStart = publishWorkflow.indexOf("\n store-publish:\n"); + assert.notEqual(jobStart, -1, "missing store-publish job"); + const remainingWorkflow = publishWorkflow.slice(jobStart + 1); + const nextJob = remainingWorkflow.slice(1).search(/\n [a-z][a-z0-9-]*:\n/); + const storePublishJob = + nextJob === -1 + ? remainingWorkflow + : remainingWorkflow.slice(0, nextJob + 1); + + assert.match(storePublishJob, /needs: \[parse, gh-release\]/); + assert.match( + storePublishJob, + /uses: \.\/\.github\/workflows\/desktop_store_publish\.yaml/, + ); + assert.match(storePublishJob, /include_macos: true/); + assert.match(storePublishJob, /include_windows: true/); + assert.match(storePublishJob, /submit_to_stores: true/); + assert.doesNotMatch(storePublishJob, /secrets: inherit/); + + const expectedSecrets = [ + "APPLE_TEAM_ID", + "APPSTORE_API_KEY_ID", + "APPSTORE_API_PRIVATE_KEY", + "APPSTORE_ISSUER_ID", + "AZURE_AD_APPLICATION_SECRET", + "CN_API_KEY", + "KEYCHAIN_PASSWORD", + "MAC_APP_STORE_APPLICATION_CERTIFICATE", + "MAC_APP_STORE_APPLICATION_CERTIFICATE_PASSWORD", + "MAC_APP_STORE_INSTALLER_CERTIFICATE", + "MAC_APP_STORE_INSTALLER_CERTIFICATE_PASSWORD", + "MAC_APP_STORE_PROVISIONING_PROFILE", + "POSTHOG_API_KEY", + "SELLER_ID", + "SENTRY_DSN_HYPRNOTE_2", + "VITE_PRO_PRODUCT_ID", + "VITE_SUPABASE_ANON_KEY", + "VITE_SUPABASE_URL", + ]; + const declaredSecrets = [ + ...storeWorkflow.matchAll( + /^ ([A-Z0-9_]+):\n required: false$/gm, + ), + ].map((match) => match[1]); + const forwardedSecrets = [ + ...storePublishJob.matchAll( + /^ ([A-Z0-9_]+): \$\{\{ secrets\.([A-Z0-9_]+) \}\}$/gm, + ), + ].map((match) => { + assert.equal(match[1], match[2]); + return match[1]; + }); + + assert.deepEqual(declaredSecrets, expectedSecrets); + assert.deepEqual(forwardedSecrets, expectedSecrets); +}); + test("binds every release asset to a candidate run and detects replacement", async () => { const directory = await mkdtemp( path.join(os.tmpdir(), "anarlog-release-provenance-"),