diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index af33abd..6f8ffd6 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -12,6 +12,14 @@ on: description: "Existing protected macos-vMAJOR.MINOR.PATCH tag on main" required: true type: string + mode: + description: "Build a release or resume publishing existing verified artifacts" + required: true + default: build + type: choice + options: + - build + - resume permissions: contents: read @@ -49,8 +57,10 @@ jobs: env: REQUESTED_TAG: ${{ inputs.tag }} MACOS_RUNNER: ${{ vars.MACOS_RUNNER }} + RELEASE_MODE: ${{ inputs.mode || 'build' }} run: | set -Eeuo pipefail + [[ "${RELEASE_MODE:-build}" == build || "${RELEASE_MODE:-build}" == resume ]] || { echo 'Invalid release mode.' >&2; exit 1; } if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then [[ "$GITHUB_REF" == refs/heads/main ]] || { echo 'Dispatch this workflow from main.' >&2; exit 1; } tag="$REQUESTED_TAG" @@ -80,6 +90,7 @@ jobs: release: name: Sign, notarize, verify and publish macOS needs: prepare + if: ${{ inputs.mode != 'resume' }} # Gitea resolves runner labels before outputs from prepare are available. runs-on: ${{ vars.MACOS_RUNNER }} timeout-minutes: 120 @@ -224,3 +235,42 @@ jobs: finally: shutil.rmtree(directory) PY + + resume: + name: Resume verified release publication + needs: prepare + if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'resume' }} + runs-on: ${{ vars.MACOS_RUNNER }} + timeout-minutes: 15 + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + RELEASE_METADATA_DIRECTORY: release-source/native + steps: + - name: Check out current publishing tools + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Check out validated release metadata + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ needs.prepare.outputs.commit }} + path: release-source + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - name: Verify existing artifacts and resume publication + env: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} + AWS_EC2_METADATA_DISABLED: "true" + AWS_REQUEST_CHECKSUM_CALCULATION: when_required + AWS_RESPONSE_CHECKSUM_VALIDATION: when_required + run: | + aws --version | grep '^aws-cli/2\.' || { echo 'Install AWS CLI v2 permanently on this runner.' >&2; exit 1; } + node native/publish-macos.mjs resume diff --git a/native/publish-macos.mjs b/native/publish-macos.mjs index 8fdfd7e..10617cf 100644 --- a/native/publish-macos.mjs +++ b/native/publish-macos.mjs @@ -435,7 +435,13 @@ if ( process.argv[2] === 'resume' ? resumeMacos : publishMacos; const url = await publish({ directory: process.env.RELEASE_DIRECTORY, - metadata: releaseMetadata(), + metadata: releaseMetadata( + process.env, + process.argv[2] === 'resume' && + process.env.RELEASE_METADATA_DIRECTORY + ? resolve(process.env.RELEASE_METADATA_DIRECTORY) + : undefined, + ), }); console.log(`Verified public release: ${url}`); if (process.env.GITHUB_STEP_SUMMARY) { diff --git a/native/tests/macos-workflow.test.js b/native/tests/macos-workflow.test.js index fc35f76..8248215 100644 --- a/native/tests/macos-workflow.test.js +++ b/native/tests/macos-workflow.test.js @@ -113,6 +113,38 @@ test('manual release on main accepts an existing tag without moving it', (t) => assert.equal(f.git(f.checkout, 'rev-parse', 'macos-v1.2.3'), f.commit); }); +test('resume uses current tools, validated tagged metadata and release credentials without rebuilding', (t) => { + const f = fixture(t); + const result = f.run({ + GITHUB_EVENT_NAME: 'workflow_dispatch', + GITHUB_REF: 'refs/heads/main', + REQUESTED_TAG: 'macos-v1.2.3', + RELEASE_MODE: 'resume', + }); + assert.equal(result.status, 0, result.stderr); + const resume = workflow.split('\n resume:\n')[1]; + assert.match(resume, /needs: prepare/); + assert.match( + resume, + /github.event_name == 'workflow_dispatch' && inputs.mode == 'resume'/, + ); + assert.match(resume, /ref: \$\{\{ github.sha \}\}/); + assert.match(resume, /ref: \$\{\{ needs.prepare.outputs.commit \}\}/); + assert.match(resume, /RELEASE_METADATA_DIRECTORY: release-source\/native/); + assert.match(resume, /path: release-source/); + assert.match(resume, /secrets.S3_ACCESS_KEY_ID/); + assert.match(resume, /secrets.S3_SECRET_ACCESS_KEY/); + assert.match(resume, /node native\/publish-macos.mjs resume/); + assert.doesNotMatch( + resume, + /tauri-action|APPLE_PASSWORD|APPLE_CERTIFICATE|TAURI_SIGNING_PRIVATE_KEY/, + ); + assert.match( + workflow.split('\n release:\n')[1], + /if: \$\{\{ inputs.mode != 'resume' \}\}/, + ); +}); + test('release DMG packaging disables Finder automation in the Tauri action environment', () => { const build = workflow .split(' - name: Build, sign and notarize with Tauri\n')[1] @@ -127,6 +159,7 @@ test('release DMG packaging disables Finder automation in the Tauri action envir }); for (const [name, overrides] of [ + ['invalid release mode', { RELEASE_MODE: 'overwrite' }], ['web release tag', { GITHUB_REF: 'refs/tags/v1.2.3' }], ['missing tag', { GITHUB_REF: 'refs/tags/macos-v9.9.9' }], [