diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml index a09468e168..777f28c7e4 100644 --- a/.github/workflows/create-or-promote-release.yml +++ b/.github/workflows/create-or-promote-release.yml @@ -28,9 +28,16 @@ permissions: id-token: write attestations: write +# Never allow two release pipelines to run at once — they race on tags, +# release objects, and Play track state. Later dispatches queue. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: determine-tags: runs-on: ubuntu-24.04-arm + timeout-minutes: 10 outputs: tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }} release_name: ${{ steps.calculate_tags.outputs.release_name }} @@ -158,6 +165,7 @@ jobs: needs: [determine-tags, call-release-workflow] if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }} runs-on: ubuntu-24.04-arm + timeout-minutes: 10 steps: - name: Checkout code uses: actions/checkout@v7.0.1 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index d0c9807177..1fc764cdc6 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -33,6 +33,7 @@ jobs: build: if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-24.04 + timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@v7.0.1 @@ -82,6 +83,7 @@ jobs: if: github.repository == 'meshtastic/Meshtastic-Android' needs: build runs-on: ubuntu-24.04-arm + timeout-minutes: 15 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index a596e8132e..bcc05dce10 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -19,6 +19,7 @@ jobs: build: if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ubuntu-24.04 + timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@v7.0.1 @@ -101,6 +102,7 @@ jobs: if: github.repository == 'meshtastic/Meshtastic-Android' needs: build runs-on: ubuntu-24.04-arm + timeout-minutes: 15 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/main-check.yml b/.github/workflows/main-check.yml index b77a06604b..956c2ef0de 100644 --- a/.github/workflows/main-check.yml +++ b/.github/workflows/main-check.yml @@ -50,6 +50,7 @@ jobs: # APK build itself failed, the artifact download below fails and this job goes red. if: github.repository == 'meshtastic/Meshtastic-Android' && !cancelled() runs-on: ubuntu-24.04-arm + timeout-minutes: 10 permissions: contents: write env: diff --git a/.github/workflows/msstore-publish.yml b/.github/workflows/msstore-publish.yml index 7ba5a650f9..c16ddd872d 100644 --- a/.github/workflows/msstore-publish.yml +++ b/.github/workflows/msstore-publish.yml @@ -26,12 +26,19 @@ on: permissions: contents: read +# Partner Center submissions must never race (a release event overlapping a +# manual retry would collide on the same in-progress submission). Serialize. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: msstore: # Belt and braces for release events — `released` should already exclude # these. workflow_dispatch has no release payload and passes through. if: ${{ !github.event.release.prerelease && !github.event.release.draft }} runs-on: ubuntu-latest + timeout-minutes: 60 env: # Secrets aren't readable in step `if:` expressions; skip cleanly until # Partner Center is configured. Presence is gated on PRODUCT_ID alone so diff --git a/.github/workflows/post-release-cleanup.yml b/.github/workflows/post-release-cleanup.yml index 3804306de7..64798f6306 100644 --- a/.github/workflows/post-release-cleanup.yml +++ b/.github/workflows/post-release-cleanup.yml @@ -16,9 +16,16 @@ on: permissions: contents: write +# Destructive (deletes releases + tags): serialize dispatches so two cleanups +# can never interleave. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: cleanup_prereleases: runs-on: ubuntu-24.04-arm + timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v7.0.1 diff --git a/.github/workflows/pr-closed-cleanup.yml b/.github/workflows/pr-closed-cleanup.yml new file mode 100644 index 0000000000..738951adf8 --- /dev/null +++ b/.github/workflows/pr-closed-cleanup.yml @@ -0,0 +1,42 @@ +name: PR Closed Cleanup + +# When a PR is closed (merged or abandoned) its in-flight CI runs keep burning +# runner slots to completion: per-PR concurrency groups only cancel on a NEW +# push, and nothing pushes to a closed PR. Reap queued/in-progress +# pull_request-event runs for the closed PR's head SHA (pull-request.yml, +# verify-flatpak.yml, ...). Runs for older SHAs were already cancelled by the +# per-PR concurrency group when that SHA was superseded. +# +# pull_request_target is required: the plain pull_request event gets a +# read-only GITHUB_TOKEN for fork PRs, which cannot cancel runs. Per the +# pull_request_target warnings, this workflow must never check out or execute +# PR code — it only calls the Actions API. +on: + pull_request_target: + types: [closed] + +permissions: + actions: write + +jobs: + cancel-pr-runs: + if: github.repository == 'meshtastic/Meshtastic-Android' + runs-on: ubuntu-24.04-arm + timeout-minutes: 5 + steps: + - name: Cancel in-flight CI runs for the closed PR + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + ids_file="$(mktemp)" + trap 'rm -f "$ids_file"' EXIT + for status in queued in_progress; do + gh api --paginate "repos/${{ github.repository }}/actions/runs?event=pull_request&status=${status}&head_sha=${HEAD_SHA}&per_page=100" \ + --jq '.workflow_runs[].id' + done >"$ids_file" + sort -u "$ids_file" | while read -r run_id; do + echo "Cancelling run $run_id" + gh run cancel "$run_id" --repo "${{ github.repository }}" || true + done diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index c7111fb196..ddd2614ebc 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -69,6 +69,7 @@ permissions: jobs: prepare-build-info: runs-on: ubuntu-24.04-arm + timeout-minutes: 10 outputs: APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} @@ -106,6 +107,7 @@ jobs: promote-release: runs-on: ubuntu-24.04-arm + timeout-minutes: 30 needs: [ prepare-build-info ] env: FROM_TRACK: ${{ inputs.from_channel == 'closed' && 'NewAlpha' || (inputs.from_channel == 'open' && 'beta' || 'internal') }} @@ -144,6 +146,7 @@ jobs: update-github-release: runs-on: ubuntu-24.04-arm + timeout-minutes: 10 needs: [ prepare-build-info, promote-release ] # actions: write is scoped here — only this job's publish-workflow # dispatch needs it, and the other jobs must not get it. Job-level @@ -345,6 +348,7 @@ jobs: update-homebrew-cask: if: ${{ inputs.channel == 'production' }} runs-on: ubuntu-24.04-arm + timeout-minutes: 15 needs: [ update-github-release ] steps: - name: Checkout code diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index 080027eb39..4af8c8ed6b 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -15,6 +15,7 @@ jobs: contents: read pull-requests: write runs-on: ubuntu-24.04-arm + timeout-minutes: 5 steps: - name: Auto-label PR uses: actions/github-script@v9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 163c29003f..26165c5aa8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,7 @@ permissions: jobs: prepare-build-info: runs-on: ubuntu-24.04-arm + timeout-minutes: 10 outputs: APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} @@ -130,6 +131,7 @@ jobs: release-google: runs-on: ubuntu-24.04 + timeout-minutes: 90 needs: [prepare-build-info] env: GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} @@ -214,6 +216,7 @@ jobs: release-fdroid: runs-on: ubuntu-24.04 + timeout-minutes: 90 needs: [prepare-build-info] env: GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} @@ -274,6 +277,7 @@ jobs: release-desktop: if: ${{ inputs.build_desktop }} runs-on: ${{ matrix.os }} + timeout-minutes: 90 needs: [prepare-build-info] strategy: fail-fast: false @@ -431,6 +435,7 @@ jobs: create-flatpak-src: if: ${{ inputs.build_flatpak_src }} runs-on: ${{ matrix.os }} + timeout-minutes: 60 needs: [prepare-build-info] strategy: fail-fast: false @@ -487,6 +492,7 @@ jobs: release-flatpak-src: if: ${{ inputs.build_flatpak_src }} runs-on: ubuntu-24.04 + timeout-minutes: 30 needs: [create-flatpak-src] steps: - name: Download Flatpak source artifacts @@ -524,6 +530,7 @@ jobs: github-release: if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-24.04-arm + timeout-minutes: 15 needs: - prepare-build-info - release-google diff --git a/.github/workflows/scheduled-baseline.yml b/.github/workflows/scheduled-baseline.yml index 5a639e48b0..c10521124c 100644 --- a/.github/workflows/scheduled-baseline.yml +++ b/.github/workflows/scheduled-baseline.yml @@ -8,9 +8,16 @@ on: - cron: '0 0 * * *' workflow_dispatch: # Allow manual triggering +# Emulator runs take up to ~1 h; a manual dispatch overlapping the daily cron +# would race on the scheduled-baseline branch. Later runs queue, never stack. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: regenerate: runs-on: ubuntu-24.04 + timeout-minutes: 90 if: github.repository == 'meshtastic/Meshtastic-Android' permissions: contents: write # To commit files and push branches diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml index 61842b7294..ea344fc9fb 100644 --- a/.github/workflows/scheduled-updates.yml +++ b/.github/workflows/scheduled-updates.yml @@ -7,9 +7,16 @@ on: - cron: '0 * * * *' workflow_dispatch: # Allow manual triggering +# Hourly cron + manual dispatch must never stack: overlapping runs race on the +# scheduled-updates branch force-push. Later runs queue (at most one pending). +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: update_assets: runs-on: ubuntu-24.04 + timeout-minutes: 30 if: github.repository == 'meshtastic/Meshtastic-Android' permissions: contents: write # To commit files and push branches diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f1c88066f0..166a3e6cc6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,10 +8,15 @@ permissions: issues: write pull-requests: write +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: stale_issues: name: Close Stale Issues runs-on: ubuntu-24.04-arm + timeout-minutes: 15 if: github.repository == 'meshtastic/Meshtastic-Android' steps: diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 4f5762b208..3b14db79fa 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -19,6 +19,7 @@ concurrency: jobs: update-changelog: runs-on: ubuntu-24.04-arm + timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v7.0.1 diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml index c5b31d99d9..bc85eab598 100644 --- a/.github/workflows/winget-publish.yml +++ b/.github/workflows/winget-publish.yml @@ -25,12 +25,19 @@ on: # nothing in this repo is written. permissions: {} +# Serialize submissions: a release event overlapping a manual retry would open +# duplicate winget-pkgs PRs for the same version. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: winget: # Belt and braces for release events — `released` should already exclude # these. workflow_dispatch has no release payload and passes through. if: ${{ !github.event.release.prerelease && !github.event.release.draft }} runs-on: ubuntu-latest + timeout-minutes: 30 env: # Secrets aren't readable in step `if:` expressions; skip cleanly until # the token is configured.