-
Notifications
You must be signed in to change notification settings - Fork 4
Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| name: Build and Publish Beta | ||
|
|
||
| # Builds PUMP-1 firmware from the beta branch and publishes it as assets on a | ||
| # rolling "beta-fw" pre-release. The on-device "Firmware Channel" select points | ||
| # OTA updates at these assets. Stable firmware is built/published separately | ||
| # by build.yml (push to main -> GitHub Pages). | ||
|
|
||
| on: | ||
| push: | ||
| branches: [beta] | ||
| paths: | ||
| - 'Integrations/ESPHome/**' | ||
| workflow_dispatch: | ||
|
|
||
| # Least privilege: read-only by default; only publish-beta is elevated to write. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| version: | ||
| name: Read version | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| v: ${{ steps.read.outputs.v }} | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| persist-credentials: false | ||
| - id: read | ||
| run: | | ||
| v=$(awk '/substitutions:/ {f=1} f && /version:/ {print $2; exit}' \ | ||
| Integrations/ESPHome/Core.yaml | tr -d '"') | ||
| echo "v=$v" >> "$GITHUB_OUTPUT" | ||
| echo "Beta version: $v" | ||
|
|
||
| build: | ||
| name: Build firmware | ||
| needs: version | ||
| # Beta serves OTA updates only, so it builds the end-user image | ||
| # (PUMP-1_Minimal.yaml), not the first-flash improv image. | ||
| uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 | ||
| with: | ||
| files: Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml | ||
| esphome-version: stable | ||
| combined-name: firmware-beta | ||
| release-version: ${{ needs.version.outputs.v }} | ||
|
|
||
| publish-beta: | ||
| name: Publish beta release assets | ||
| needs: [version, build] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - name: Download firmware artifacts | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| path: fw | ||
| pattern: firmware* | ||
|
|
||
| - name: Ensure rolling 'beta-fw' pre-release exists | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh release view beta-fw -R "${{ github.repository }}" >/dev/null 2>&1 \ | ||
| || gh release create beta-fw -R "${{ github.repository }}" \ | ||
| --prerelease --title "Beta (rolling)" \ | ||
| --notes "Latest PUMP-1 beta firmware. Auto-updated on every push to the beta branch." | ||
|
|
||
| - name: Rewrite manifest to absolute URLs and upload assets | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" | ||
| man=$(find fw/firmware-beta -name manifest.json | head -1) | ||
| if [ -z "$man" ]; then | ||
| echo "::error::manifest.json not found" | ||
| exit 1 | ||
| fi | ||
| echo "Rewriting $man" | ||
| # Make ota.path and parts[].path absolute release-asset URLs so the | ||
| # device never has to resolve a relative path against a redirect. | ||
| jq --arg base "$BASE" ' | ||
| .builds[0].ota.path = ($base + "/" + (.builds[0].ota.path | sub(".*/"; ""))) | ||
| | .builds[0].parts |= map(.path = ($base + "/" + (.path | sub(".*/"; "")))) | ||
| ' "$man" > manifest.json | ||
| cat manifest.json | ||
| gh release upload beta-fw manifest.json -R "${{ github.repository }}" --clobber | ||
| find fw/firmware-beta -name '*.bin' -print -exec \ | ||
| gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; | ||
| echo "Beta assets published." | ||
|
|
||
| - name: Point beta-fw tag at the built commit | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # gh release create tags default-branch HEAD, and uploads never move | ||
| # the tag, so without this the release's source commit drifts away | ||
| # from the assets actually published. | ||
| gh api -X PATCH "repos/${{ github.repository }}/git/refs/tags/beta-fw" \ | ||
| -f sha="${{ github.sha }}" -F force=true | ||
| echo "beta-fw -> ${{ github.sha }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,32 @@ | ||
| substitutions: | ||
| name: apollo-pump-1 | ||
| version: "26.3.2.1" | ||
| version: "26.7.14.1" | ||
| device_description: ${name} made by Apollo Automation - version ${version}. | ||
| # Default update channel on first boot (no stored user choice yet, i.e. a | ||
| # fresh flash). The beta-channel builds override this to "Beta" (see | ||
| # Integrations/ESPHome/beta-channel/) so firmware obtained from the beta | ||
| # channel keeps tracking it instead of offering a stable "downgrade". | ||
| firmware_channel_default: "Stable" | ||
| # Manifest URL bases. Stable = GitHub Pages (main branch builds). | ||
| # Beta = rolling "beta-fw" pre-release assets (beta branch builds). | ||
| stable_manifest_base: "https://apolloautomation.github.io/PUMP-1" | ||
| beta_manifest_base: "https://github.com/ApolloAutomation/PUMP-1/releases/download/beta-fw" | ||
| # OTA manifest URLs picked by apply_ota_source. | ||
| ota_stable_manifest: "${stable_manifest_base}/firmware/manifest.json" | ||
| ota_beta_manifest: "${beta_manifest_base}/manifest.json" | ||
|
|
||
| esp32: | ||
| variant: esp32c6 | ||
| flash_size: 8MB | ||
| framework: | ||
| type: esp-idf | ||
|
|
||
| esp32_ble_tracker: | ||
| id: ble_tracker | ||
| scan_parameters: | ||
| continuous: true | ||
|
Comment on lines
+24
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | grep -F 'Integrations/ESPHome/Core.yaml' || true
echo "== file snippet =="
if [ -f Integrations/ESPHome/Core.yaml ]; then
nl -ba Integrations/ESPHome/Core.yaml | sed -n '1,120p'
fi
echo "== esp32_ble_tracker references =="
rg -n "esp32_ble_tracker|continuous|start_scan|stop_scan|on_turn_on|on_boot|on_state" Integrations/ESPHome/Core.yaml .github 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1 Length of output: 269 🌐 Web query:
💡 Result: In ESPHome, the Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file section =="
awk '{ printf "%6d ", NR } { print }' Integrations/ESPHome/Core.yaml | sed -n '1,120p'
echo "== esp32_ble_tracker references in Core =="
grep -nE "esp32_ble_tracker|continuous:|start_scan|stop_scan|on_boot|on_turn_on|on_state" Integrations/ESPHome/Core.yaml || true
echo "== deterministic YAML control-flow model =="
python3 - <<'PY'
from pathlib import Path
text = Path("Integrations/ESPHome/Core.yaml").read_text()
ops = [
("component setup esp32_ble_tracker", "esp32_ble_tracker:"),
("continuous true flag", "continuous: true"),
("boot action start_scan", "on_boot:"),
("boot action stop_scan", "stop_scan"),
("switch turn_on action start_scan", "on_turn_on:"),
("switch turn_off action stop_scan", "on_turn_off:"),
]
for name, needle in ops:
idx = (text + "\n").index(needle + "\n", 0) if needle in text else -1
line = text.count("\n", 0, idx) + 1 if idx != -1 else None
print(f"{name}: line={line} found={idx != -1}")
print("order:", [(l, name) for name, line in ops if (l := text.count("\n", 0, text.index(needle))) != -1])
PYRepository: ApolloAutomation/PUMP-1 Length of output: 5310 🌐 Web query:
💡 Result: In ESPHome, the esp32_ble_tracker component provides two primary actions for manual scan control: esp32_ble_tracker.start_scan and esp32_ble_tracker.stop_scan [1][2]. These actions are typically used when the component is configured with continuous: false, allowing you to trigger scanning on demand through automations [1]. esp32_ble_tracker.start_scan Action: - Function: Initiates a Bluetooth scan [1]. - Behavior: If a scan is already in progress, the action is ignored [1]. - Configuration: You can optionally specify the continuous parameter within the action, which determines if the scan should run in continuous mode (defaults to false) [1]. Example: - esp32_ble_tracker.start_scan: continuous: true esp32_ble_tracker.stop_scan Action: - Function: Terminates the current Bluetooth scan [1]. - Behavior: Once stopped, scanning can be restarted using the start_scan action [1]. Usage Context: To use these actions, your esp32_ble_tracker configuration must generally have continuous set to false if you intend to control scanning manually rather than running it continuously [1]. This is useful for battery-saving scenarios or specific event-driven logic [1]. For instance, you might start a scan when a client connects to the API and stop it when it disconnects [1]. Citations: 🌐 Web query:
💡 Result: In ESPHome, the Citations:
🌐 Web query:
💡 Result: In ESPHome versions released after the change implemented in September 2025, the bluetooth_proxy component's active parameter defaults to true [1][2]. Prior to this update, the default behavior was false (passive mode only) [3][4]. If you wish to restore the previous behavior—where active GATT connections are disabled—you must explicitly set the parameter to false in your configuration [1][2]: bluetooth_proxy: active: false It is important to distinguish this setting from the esp32_ble_tracker scan_parameters: active setting [5]. The bluetooth_proxy: active parameter only enables the capability for the proxy to support active GATT connections when requested by Home Assistant; it does not autonomously connect to devices or impact the battery life of BLE sensors [5]. Conversely, setting esp32_ble_tracker: active: true enables active scanning, which can significantly affect battery-powered devices [5]. Citations:
🌐 Web query:
💡 Result: In ESPHome, the esp32_ble_tracker component manages Bluetooth Low Energy scanning through its configuration parameters and available actions [1][2]. Configuration: The Citations:
Do not enable BLE scanning while the proxy switch is off.
🤖 Prompt for AI Agents |
||
|
|
||
| bluetooth_proxy: | ||
|
|
||
| esphome: | ||
| on_boot: | ||
|
|
@@ -30,6 +49,22 @@ esphome: | |
| - switch.turn_off: stop_pump_when_full | ||
| - switch.turn_on: stop_pump_when_dry | ||
| - switch.turn_off: auto_refill | ||
| # Point the update entity at the selected channel's manifest. | ||
| - priority: -100 | ||
| then: | ||
| - script.execute: apply_ota_source | ||
| # Re-apply the Bluetooth Proxy switch after all components set up, so BLE | ||
| # scanning matches the persisted switch (proxy stays off by default). | ||
| - priority: -300 | ||
| then: | ||
| - if: | ||
| condition: | ||
| switch.is_on: bluetooth_proxy_switch | ||
| then: | ||
| - esp32_ble_tracker.start_scan: | ||
| continuous: true | ||
| else: | ||
| - esp32_ble_tracker.stop_scan: | ||
| - priority: -100 | ||
| then: | ||
| - delay: 1000ms | ||
|
|
@@ -43,6 +78,7 @@ esphome: | |
| - lambda: "id(testScript).execute();" | ||
|
|
||
| api: | ||
| encryption: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate relevant file =="
git ls-files | grep -F 'Integrations/ESPHome/Core.yaml' || true
echo
echo "== Core.yaml lines around encryption =="
if [ -f 'Integrations/ESPHome/Core.yaml' ]; then
nl -ba 'Integrations/ESPHome/Core.yaml' | sed -n '65,90p'
fi
echo
echo "== Inspect API component occurrences/usages in ESPHome configs =="
rg -n "api:|encryption:|key:" Integrations/ESPHome || true
echo
echo "== Search for PR title/objective references ="
git diff --stat || trueRepository: ApolloAutomation/PUMP-1 Length of output: 301 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Read Core.yaml lines around encryption =="
awk 'NR>=65 && NR<=90 { printf "%6d\t%s\n", NR, $0 }' Integrations/ESPHome/Core.yaml
echo
echo "== ESPHome API/encryption occurrences =="
grep -RInE 'api:|encryption:|key:' Integrations/ESPHome || true
echo
echo "== Read ESPHome API config context =="
grep -RInE -C 8 '^api:| encryption:|api:' Integrations/ESPHome || trueRepository: ApolloAutomation/PUMP-1 Length of output: 2176 🌐 Web query:
💡 Result: In ESPHome, the Citations:
Configure a valid API encryption key. The empty 🤖 Prompt for AI Agents |
||
| actions: | ||
| - action: play_buzzer | ||
| variables: | ||
|
|
@@ -83,6 +119,17 @@ web_server: | |
| port: 80 | ||
| version: 3 | ||
|
|
||
| http_request: | ||
| verify_ssl: true | ||
| # GitHub release-asset downloads answer with a redirect carrying a | ||
| # ~3.6 KB Content-Security-Policy header; each header line must fit | ||
| # this buffer or the request fails with "HTTP_CLIENT: Out of buffer". | ||
| buffer_size_rx: 5120 | ||
| # The redirect target is a signed URL with a ~850-char query string; the | ||
| # follow-up request line must fit the TX buffer or esp_http_client_open | ||
| # fails with "Out of buffer" before sending anything. | ||
| buffer_size_tx: 2048 | ||
|
|
||
| # Buzzer | ||
| output: | ||
| - platform: ledc | ||
|
|
@@ -96,6 +143,21 @@ button: | |
| icon: mdi:power-cycle | ||
| name: "ESP Reboot" | ||
|
|
||
| - platform: template | ||
| name: "Firmware Update" | ||
| id: update_firmware | ||
| icon: mdi:cloud-download | ||
| entity_category: "config" | ||
| on_press: | ||
| - logger.log: "Applying firmware update for the selected channel" | ||
| - delay: 3s | ||
| - script.execute: apply_ota_source | ||
| - script.wait: apply_ota_source | ||
| # The manifest fetch runs in its own task; give it a fixed window to land | ||
| # (update.is_available stays false for same-version switches). | ||
| - delay: 5s | ||
| - lambda: id(update_http_request).perform(true); | ||
|
|
||
|
Comment on lines
+146
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Repository files (selected):"
git ls-files | rg '(^|/)Core\.ya?ml$|(^|/)ESPHome/|integration' || true
echo
echo "File outline/size:"
wc -l Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Relevant sections:"
sed -n '130,175p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
sed -n '455,500p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Search for update/select/ota/manifest symbols:"
rg -n "update_http_request|apply_ota_source|update_firmware|ota|select|channel|available|force|version" Integrations/ESPHome/Core.yaml 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1 Length of output: 4720 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Repository files (selected):"
git ls-files | rg '(^|/)Core\.ya?ml$|(^|/)ESPHome/|integration' || true
echo
echo "File outline/size:"
wc -l Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Relevant sections:"
sed -n '130,175p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
sed -n '455,500p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Search for update/select/ota/manifest symbols:"
rg -n "update_http_request|apply_ota_source|update_firmware|ota|select|channel|available|force|version" Integrations/ESPHome/Core.yaml 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1 Length of output: 4720 🌐 Web query:
💡 Result: In ESPHome, the http_request update component manages firmware updates by fetching a JSON manifest file from a specified URL [1][2]. On ESP32 devices, the manifest fetching process is designed to run in a separate FreeRTOS task to prevent blocking the main loop while performing network operations [3][4]. Regarding your specific technical queries: Separate ESP32 Task: The http_request update component utilizes a dedicated task (often referred to as update_task) on ESP32-based devices to perform the manifest fetch and associated HTTP operations [3][4]. This architecture allows the device to remain responsive while checking for updates [4]. Cached/MD5: The update component uses the manifest to check for new firmware versions based on metadata [1][2]. It requires the manifest to provide an MD5 hash for the firmware binary [1][5]. This MD5 hash is used during the subsequent flashing process (initiated by the user via the update.perform action) to verify the integrity of the downloaded firmware binary [4][6]. If the MD5 hash of the downloaded file does not match the value provided in the manifest, the update is rejected, and the device continues to run the existing firmware [6]. The update component itself does not inherently implement generic HTTP-level caching for the manifest file, though standard network stack behavior may apply. Note that caching mechanisms (like ETag or Last-Modified) are more explicitly documented for other components, such as online_image [7][8]. If you are encountering issues with this process, such as failed fetches at startup, it is often because the component attempts to reach the network before the connection is fully established [9]. This has been addressed in recent updates to ensure checks are skipped if the network is not yet connected [9]. Citations:
Synchronize manifest refresh before forcing installation.
🤖 Prompt for AI Agents |
||
| - platform: factory_reset | ||
| disabled_by_default: True | ||
| name: "Factory Reset ESP" | ||
|
|
@@ -220,6 +282,19 @@ switch: | |
| id(pump_start_time) = 0; | ||
| id(safety_alert_active) = false; | ||
|
|
||
| - platform: template | ||
| name: "Bluetooth Proxy" | ||
| id: bluetooth_proxy_switch | ||
| icon: mdi:bluetooth | ||
| entity_category: "config" | ||
| restore_mode: RESTORE_DEFAULT_OFF | ||
| optimistic: true | ||
| on_turn_on: | ||
| - esp32_ble_tracker.start_scan: | ||
| continuous: true | ||
| on_turn_off: | ||
| - esp32_ble_tracker.stop_scan: | ||
|
|
||
| binary_sensor: | ||
| - platform: status | ||
| name: Online | ||
|
|
@@ -384,7 +459,33 @@ text_sensor: | |
| update_interval: never | ||
| entity_category: "diagnostic" | ||
|
|
||
| select: | ||
| - platform: template | ||
| name: "Firmware Channel" | ||
| id: firmware_channel | ||
| icon: mdi:source-branch | ||
| entity_category: "config" | ||
| optimistic: true | ||
| restore_value: true | ||
| options: | ||
| - "Stable" | ||
| - "Beta" | ||
| initial_option: "${firmware_channel_default}" | ||
| on_value: | ||
| then: | ||
| - script.execute: apply_ota_source | ||
|
|
||
| script: | ||
| - id: apply_ota_source | ||
| # Sets the OTA manifest URL from the Firmware Channel select (Stable/Beta). | ||
| then: | ||
| - lambda: |- | ||
| const bool beta = id(firmware_channel).current_option() == "Beta"; | ||
| std::string url = beta ? "${ota_beta_manifest}" : "${ota_stable_manifest}"; | ||
| ESP_LOGI("firmware", "OTA manifest set to: %s", url.c_str()); | ||
| id(update_http_request).set_source_url(url); | ||
| - component.update: update_http_request | ||
|
|
||
| - id: pumpUntilFull | ||
| then: | ||
| - switch.turn_on: stop_pump_when_full | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Beta-channel build of PUMP-1_Minimal.yaml: the identical image except the Firmware | ||
| # Channel select defaults to "Beta" on first boot, so firmware obtained from | ||
| # the beta channel keeps tracking it. Built by build-beta.yml only; the | ||
| # stable (GitHub Pages) builds use PUMP-1_Minimal.yaml directly. | ||
| substitutions: | ||
| firmware_channel_default: "Beta" | ||
|
|
||
| packages: | ||
| base: !include ../PUMP-1_Minimal.yaml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ApolloAutomation/PUMP-1
Length of output: 455
🏁 Script executed:
Repository: ApolloAutomation/PUMP-1
Length of output: 6921
🌐 Web query:
GitHub Actions workflow concurrency workflow concurrency group cancel-in-progress same workflow branch multiple runs💡 Result:
To manage concurrency in GitHub Actions workflows effectively, you can use the concurrency key to group runs and control whether in-progress runs are cancelled when a new run is triggered [1][2][3]. Key Concepts: Group: This defines a unique label for a concurrency group. All workflow runs that share the same group name are subjected to the same concurrency limits [1][3]. To restrict cancellation to the same workflow and the same branch (or pull request), a common and recommended practice is to use an expression incorporating both the workflow name and the ref [4][5]: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: This boolean (or expression that evaluates to one) determines the behavior when a new run enters a group that already has an active run [1][6]. - When set to false (default), new runs are queued in a pending state until the in-progress run finishes [7][5][3]. - When set to true, any currently running or pending runs within that same group are cancelled immediately, allowing the new run to proceed [1][4][3]. Conditional Cancellation: You can use expressions to dynamically set cancel-in-progress [1][8]. This is highly useful for preventing unwanted cancellations on critical branches (like main or release branches) while still automatically cancelling outdated runs on feature branches or pull requests [7][4][8]: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref!= 'refs/heads/main' }} Important Considerations: - Avoid Unique Keys: Never include values that are unique to every run, such as github.sha or github.run_id, in your group key. Doing so will create a unique group for every run, rendering the concurrency and cancellation settings ineffective [4]. - Default Behavior: Even with cancel-in-progress set to false, GitHub Actions ensures that at most one run is active and one run is pending per group; additional pending runs will cancel previous pending runs [9][6]. - Scope: If multiple workflows in your repository use the same group name, they will all share the same concurrency limit and cancellation behavior. Including ${{ github.workflow }} in your group key ensures that your concurrency settings apply only to that specific workflow [1][5][8].
Citations:
🌐 Web query:
GitHub Actions workflow concurrency group same group workflow branch runs cancelled queued cancelled older newer behavior💡 Result:
In GitHub Actions, the concurrency group behavior depends on your configuration of the
concurrencykey, which ensures that only one workflow or job with a specific group name runs at any given time [1][2]. How runs are handled: 1. In-Progress Runs: If a new run starts and a previous run is already active in the same concurrency group, the behavior depends on thecancel-in-progresssetting [1][3]. - Whencancel-in-progress: trueis set, the currently running workflow or job in that group is canceled immediately, allowing the new run to proceed [1][3]. - Whencancel-in-progress: false(the default) is set, the active run is allowed to finish [3]. 2. Queued/Pending Runs: - Traditionally, GitHub Actions allowed at most one pending run in a concurrency group; if a new run was triggered while another was already waiting, the older pending run was automatically canceled and replaced by the new one [1][2][4]. - As of May 2026, you can now enable larger queues by addingqueue: maxto the concurrency block [4]. This allows multiple runs to wait in line and execute sequentially, rather than canceling pending runs [5][4]. Key Considerations: - Group Scope: Concurrency group names are evaluated at the repository level [1][2]. If multiple different workflows use the same group name (e.g., a shared string likemy-group), they will interfere with each other and cancel/queue one another [1][6]. To scope behavior to a specific workflow or branch, it is best practice to include variables in the group name, such as${{ github.workflow }}-${{ github.ref }}[1][6][3]. - Branch Behavior: If you includegithub.refin your group name, runs on different branches will belong to different concurrency groups and will not affect each other [3]. If you use a static group name without reference to the branch, runs from all branches will share the same group and trigger cancellation or queuing across the entire repository [1][2]. Summary ofcancel-in-progressvsqueue: -cancel-in-progress: true: Prioritizes the newest run by killing older in-progress runs [3]. -cancel-in-progress: false(Default) +queue: max: Ensures all runs complete in sequence, preventing loss of work [4].Citations:
Serialize beta release publication.
build-beta.ymlhas no concurrency guard, so multiple beta runs can upload tobeta-fwand update the same Git tag in parallel. An older run finishing after a newer one can overwrite the newer manifest/binaries and repoint the tag to an older commit. Add workflow-level concurrency with a stable beta publication group name, e.g.group: ${{ github.workflow }}-${{ github.ref }}, and eithercancel-in-progress: truefor the latest commit orqueue: maxfor strict serialization.Proposed guard
🤖 Prompt for AI Agents