diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index dce6e1c83..7597a5b55 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -11,15 +11,18 @@ on: - master workflow_dispatch: -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - jobs: build: name: Build on ${{ matrix.os }} ${{ matrix.arch }} runs-on: ${{ matrix.runner }} timeout-minutes: 120 + # Concurrency lives on the jobs, not the workflow: cancelling a whole + # run could interrupt action-gh-release mid-update and leave the + # rolling draft with missing assets. Build slots cancel superseded PR + # work; the release job below only serializes and is never cancelled. + concurrency: + group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.linux_profile }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} strategy: matrix: include: @@ -669,6 +672,9 @@ jobs: needs: build if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-release-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false permissions: contents: write @@ -848,12 +854,33 @@ jobs: echo "name=${NAME}" echo "tag=${TAG}" echo "commitish=${HEAD_SHA}" - echo "body<> "${GITHUB_OUTPUT}" + if [ -n "${BODY}" ]; then + { + echo "body<> "${GITHUB_OUTPUT}" + else + echo "body=" >> "${GITHUB_OUTPUT}" + fi + + # A PR can be closed while this workflow is still running; the + # cleanup workflow deletes the PR draft on close. Re-check the live + # PR state right before touching the draft so a late-finishing run + # cannot recreate a draft for a closed PR. + - name: Check PR is still open + if: github.event_name == 'pull_request' + id: pr-state + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + echo "state=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}" --jq '.state')" >> "${GITHUB_OUTPUT}" + - name: Create Draft Release + if: github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open' uses: softprops/action-gh-release@v2 with: draft: true diff --git a/.github/workflows/cleanup-pr-draft.yml b/.github/workflows/cleanup-pr-draft.yml index 661892ec3..aca2db6f6 100644 --- a/.github/workflows/cleanup-pr-draft.yml +++ b/.github/workflows/cleanup-pr-draft.yml @@ -12,6 +12,39 @@ jobs: name: Delete PR draft release runs-on: ubuntu-latest steps: + # A build for this PR may still be running and would recreate the + # rolling draft after we delete it. Cancel those runs (dead work + # for a closed PR anyway) and wait for them to wind down. The + # release job additionally re-checks the live PR state, so this + # wait is defense in depth, not the only guard. + - name: Cancel in-progress builds for the closed PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + + list_active_runs() { + gh run list --repo "${GITHUB_REPOSITORY}" \ + --workflow 'Build and Make Electron App' \ + --branch "${HEAD_BRANCH}" \ + --json databaseId,status \ + --jq '.[] | select(.status == "queued" or .status == "in_progress" or .status == "waiting" or .status == "requested" or .status == "pending") | .databaseId' + } + + for run_id in $(list_active_runs); do + echo "Cancelling run ${run_id}" + gh run cancel "${run_id}" --repo "${GITHUB_REPOSITORY}" || true + done + + # Cancellation is asynchronous; poll until the runs settle. + for _ in $(seq 1 18); do + if [ -z "$(list_active_runs)" ]; then + break + fi + sleep 10 + done + # Draft releases have no real git tag, so a lookup via # releases/tags/ returns 404. List releases and match the # draft by its stored tag_name (test-pr-) instead.