From c712fd666fb0586e0290bd8674284076bd74e1d6 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 10 Aug 2026 13:56:05 +0200 Subject: [PATCH 1/7] Add manual workflow to build a version-code-consistent APK for ad-hoc distribution Adds prepare-publication.yml (workflow_dispatch) that reuses the release signing key and computes version_code as the combined run count of itself and build-release.yml, so the two never collide. A shared concurrency group on both workflows prevents a race on that computation. --- .github/workflows/build-release.yml | 4 ++ .github/workflows/prepare-publication.yml | 69 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/prepare-publication.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 26ea5f9a..2a25410b 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -7,6 +7,10 @@ on: permissions: contents: write +concurrency: + group: android-version-code-lock + cancel-in-progress: false + jobs: build-release: runs-on: ubuntu-latest diff --git a/.github/workflows/prepare-publication.yml b/.github/workflows/prepare-publication.yml new file mode 100644 index 00000000..0bb26be0 --- /dev/null +++ b/.github/workflows/prepare-publication.yml @@ -0,0 +1,69 @@ +name: prepare publication + +on: + workflow_dispatch: + +permissions: + contents: read + actions: read + +concurrency: + group: android-version-code-lock + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + environment: android-release + env: + NETBIRD_UPLOAD_KEY_ALIAS: ${{ secrets.NETBIRD_UPLOAD_KEY_ALIAS }} + NETBIRD_UPLOAD_KEY_PASSWORD: ${{ secrets.NETBIRD_UPLOAD_KEY_PASSWORD }} + NETBIRD_UPLOAD_STORE_PASSWORD: ${{ secrets.NETBIRD_UPLOAD_STORE_PASSWORD }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Get version name + id: version + run: | + SHORT_GIT_SHA=$(git rev-parse --short HEAD) + echo "version_name=ci-${SHORT_GIT_SHA}" >> "$GITHUB_OUTPUT" + + - name: Compute version code + id: version_code + env: + GH_TOKEN: ${{ github.token }} + run: | + release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + publication_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/prepare-publication.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + version_code=$((release_runs + publication_runs + 40)) + echo "Release runs: $release_runs, publication runs: $publication_runs -> version_code=$version_code" + echo "version_code=$version_code" >> "$GITHUB_OUTPUT" + + - name: Write google-services.json + run: | + echo "${{ secrets.GOOGLE_JSON }}" | base64 -d > app/google-services.json + + - name: Write keystore file + run: | + echo "${{ secrets.GPLAY_KEYSTORE }}" | base64 -d > gplay.keystore + echo "NETBIRD_UPLOAD_STORE_FILE=$GITHUB_WORKSPACE/gplay.keystore" >> "$GITHUB_ENV" + + - name: Build Android + id: build + uses: ./.github/actions/build-android + with: + version_name: ${{ steps.version.outputs.version_name }} + version_code: ${{ steps.version_code.outputs.version_code }} + build_type: release + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: prepare-publication-${{ steps.version.outputs.version_name }} + path: | + ${{ steps.build.outputs.apk_path }} + ${{ steps.build.outputs.bundle_path }} + retention-days: 14 From ea0145d0c64067b4ef0bb48e632ee62cd45bd017 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 21:29:21 +0200 Subject: [PATCH 2/7] Draw the release version code from the shared run counter build-release.yml derived its version code from github.run_number, which counts only its own runs and is blind to prepare-publication.yml. With 25 release runs so far, the first ad-hoc build would take 25+1+40=66 and the next release would take 26+40=66 as well, then fall behind: 67 after 68 was already published. The concurrency group cannot fix this, because run_number is assigned when the run is queued, not when the step executes. Both workflows now sum the same two counters, so every run of either one advances the code by exactly one. Reading those counts needs actions: read, which an explicit permissions block otherwise withholds. --- .github/workflows/build-release.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2a25410b..423869b9 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -6,6 +6,8 @@ on: permissions: contents: write + # Needed to read the run counts the version code is derived from. + actions: read concurrency: group: android-version-code-lock @@ -43,18 +45,28 @@ jobs: echo "${{ secrets.GPLAY_KEYSTORE }}" | base64 -d > gplay.keystore echo "NETBIRD_UPLOAD_STORE_FILE=$GITHUB_WORKSPACE/gplay.keystore" >> $GITHUB_ENV + # Must stay identical to the same step in prepare-publication.yml: both + # workflows produce APKs for the same Play Store listing, so they have to + # draw from one shared counter. github.run_number counts only this + # workflow's own runs, which would hand out a code an ad-hoc build already + # used. The concurrency group above keeps the two from computing at once. - name: Compute version code id: version_code + env: + GH_TOKEN: ${{ github.token }} run: | - adjusted=$(( ${{ github.run_number }} + 40 )) - echo "adjusted=$adjusted" >> $GITHUB_OUTPUT + release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + publication_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/prepare-publication.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + version_code=$((release_runs + publication_runs + 40)) + echo "Release runs: $release_runs, publication runs: $publication_runs -> version_code=$version_code" + echo "version_code=$version_code" >> "$GITHUB_OUTPUT" - name: Build Android id: build uses: ./.github/actions/build-android with: version_name: ${{ steps.version.outputs.version_name }} - version_code: ${{ steps.version_code.outputs.adjusted }} + version_code: ${{ steps.version_code.outputs.version_code }} build_type: release - name: Upload files to existing release From d3f4c1784d36b7c8186602009cd884d309af145b Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 21:38:46 +0200 Subject: [PATCH 3/7] Name the manual workflow build-snapshot prepare-publication named a step in a process, while the workflows beside it name what they produce: build-debug, build-release. What this one produces is a release-signed build from an arbitrary commit with no tag behind it, which is what snapshot means. Not build-rc: release candidates already exist here as published pre-release tags (v0.6.0-rc.1, v0.3.3-rc.2) and are built by build-release.yml, so the name would claim a meaning the repository has already given away. The version code counter is keyed by workflow file name, so the rename is free only while the workflow has no runs yet. --- .github/workflows/build-release.yml | 18 ++++++++++-------- ...are-publication.yml => build-snapshot.yml} | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 14 deletions(-) rename .github/workflows/{prepare-publication.yml => build-snapshot.yml} (67%) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 423869b9..27a7816d 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -45,20 +45,22 @@ jobs: echo "${{ secrets.GPLAY_KEYSTORE }}" | base64 -d > gplay.keystore echo "NETBIRD_UPLOAD_STORE_FILE=$GITHUB_WORKSPACE/gplay.keystore" >> $GITHUB_ENV - # Must stay identical to the same step in prepare-publication.yml: both - # workflows produce APKs for the same Play Store listing, so they have to - # draw from one shared counter. github.run_number counts only this - # workflow's own runs, which would hand out a code an ad-hoc build already - # used. The concurrency group above keeps the two from computing at once. + # Must stay identical to the Compute version code step in the sibling + # workflow. build-release.yml and build-snapshot.yml feed the same Play + # Store listing, so they have to draw from one shared counter; a + # github.run_number sees only its own workflow's runs and would hand out a + # code the other already used. The concurrency group above keeps the two + # from computing at once. Renaming either workflow file resets the count + # GitHub keeps for it, which would send version codes backwards. - name: Compute version code id: version_code env: GH_TOKEN: ${{ github.token }} run: | release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - publication_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/prepare-publication.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - version_code=$((release_runs + publication_runs + 40)) - echo "Release runs: $release_runs, publication runs: $publication_runs -> version_code=$version_code" + snapshot_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-snapshot.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + version_code=$((release_runs + snapshot_runs + 40)) + echo "Release runs: $release_runs, snapshot runs: $snapshot_runs -> version_code=$version_code" echo "version_code=$version_code" >> "$GITHUB_OUTPUT" - name: Build Android diff --git a/.github/workflows/prepare-publication.yml b/.github/workflows/build-snapshot.yml similarity index 67% rename from .github/workflows/prepare-publication.yml rename to .github/workflows/build-snapshot.yml index 0bb26be0..85e54619 100644 --- a/.github/workflows/prepare-publication.yml +++ b/.github/workflows/build-snapshot.yml @@ -1,4 +1,4 @@ -name: prepare publication +name: build snapshot on: workflow_dispatch: @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: false jobs: - build: + build-snapshot: runs-on: ubuntu-latest environment: android-release env: @@ -31,15 +31,22 @@ jobs: SHORT_GIT_SHA=$(git rev-parse --short HEAD) echo "version_name=ci-${SHORT_GIT_SHA}" >> "$GITHUB_OUTPUT" + # Must stay identical to the Compute version code step in the sibling + # workflow. build-release.yml and build-snapshot.yml feed the same Play + # Store listing, so they have to draw from one shared counter; a + # github.run_number sees only its own workflow's runs and would hand out a + # code the other already used. The concurrency group above keeps the two + # from computing at once. Renaming either workflow file resets the count + # GitHub keeps for it, which would send version codes backwards. - name: Compute version code id: version_code env: GH_TOKEN: ${{ github.token }} run: | release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - publication_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/prepare-publication.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - version_code=$((release_runs + publication_runs + 40)) - echo "Release runs: $release_runs, publication runs: $publication_runs -> version_code=$version_code" + snapshot_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-snapshot.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + version_code=$((release_runs + snapshot_runs + 40)) + echo "Release runs: $release_runs, snapshot runs: $snapshot_runs -> version_code=$version_code" echo "version_code=$version_code" >> "$GITHUB_OUTPUT" - name: Write google-services.json @@ -62,7 +69,7 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: prepare-publication-${{ steps.version.outputs.version_name }} + name: snapshot-artifacts-${{ steps.version.outputs.version_name }} path: | ${{ steps.build.outputs.apk_path }} ${{ steps.build.outputs.bundle_path }} From 1ac14ae6ced358ad85b322549891638e414dc1b7 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 21:43:43 +0200 Subject: [PATCH 4/7] Label snapshot builds snapshot- rather than ci- The version name travels to the management server as the peer's ui_version and is what the about screen shows, so it is the only thing telling support which build a peer is running. build-debug.yml already emits ci- from the same expression, which left an unsigned PR build and a release-signed hand-out looking identical in the peer list. The artifact keeps just the version name; prefixing it again read as snapshot-artifacts-snapshot-. --- .github/workflows/build-snapshot.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-snapshot.yml b/.github/workflows/build-snapshot.yml index 85e54619..910a0f0e 100644 --- a/.github/workflows/build-snapshot.yml +++ b/.github/workflows/build-snapshot.yml @@ -25,11 +25,16 @@ jobs: with: submodules: recursive + # Reaches the management server as the peer's ui_version and is shown on + # the app's about screen, so it has to say at a glance that this build was + # signed and handed out rather than released. build-debug.yml already + # claims the ci- prefix, and sharing it would make an unsigned PR build + # and a signed hand-out indistinguishable in the peer list. - name: Get version name id: version run: | SHORT_GIT_SHA=$(git rev-parse --short HEAD) - echo "version_name=ci-${SHORT_GIT_SHA}" >> "$GITHUB_OUTPUT" + echo "version_name=snapshot-${SHORT_GIT_SHA}" >> "$GITHUB_OUTPUT" # Must stay identical to the Compute version code step in the sibling # workflow. build-release.yml and build-snapshot.yml feed the same Play @@ -69,7 +74,7 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: snapshot-artifacts-${{ steps.version.outputs.version_name }} + name: ${{ steps.version.outputs.version_name }} path: | ${{ steps.build.outputs.apk_path }} ${{ steps.build.outputs.bundle_path }} From 20eeb8935e41b5743a99d984a4a866aaa393a763 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 22:24:27 +0200 Subject: [PATCH 5/7] Resolve the Go version from the submodule's release tags in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI builds used the version only when the submodule sat exactly on a tag and fell back to ci- otherwise, which the management server rejects in NBVersionCheck posture checks: it treats ci- as a development build everywhere except there. Since the submodule is bumped more often than it is tagged, release builds effectively always shipped as ci-. CI now resolves the version by walking the pinned commit's ancestry back to the last stable release tag and appending the commit as SemVer build metadata, e.g. 0.77.0+f06b8c762. The server strips build metadata before every comparison, so this passes the same gates as a plain 0.77.0 while still naming the exact commit in the dashboard. Pre-release tags are skipped as a base: a suffix like -rc.2 lands in SemVer pre-release position, which the server compares differently from a release. Local builds now always produce dev-, which skips every server-side version gate; a developer who needs a real version passes it as the argument. The ancestry walk needs full history, but actions/checkout clones submodules shallow — the tags arrive without the commits between HEAD and the tag, and the walk would silently come up empty. The composite action therefore unshallows the submodule before building, guarded because --unshallow on a complete repository is a hard error. --- .github/actions/build-android/action.yml | 13 +++++- build-android-lib.sh | 59 ++++++++++++++++-------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index f9579e6a..3a4e7504 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -32,11 +32,20 @@ runs: echo "Version Code: ${{ inputs.version_code }}" echo "Build Type: ${{ inputs.build_type }}" - - name: Fetch tags for submodule + - name: Fetch tags and history for submodule shell: bash run: | cd netbird - git fetch --tags + # build-android-lib.sh resolves the version by walking HEAD's ancestry + # back to the last release tag. actions/checkout clones submodules + # shallow, which fetches the tags but not the commits between HEAD and + # the tag, so without full history the walk comes up empty and the + # build silently falls back to a ci- version. + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + git fetch --unshallow --tags + else + git fetch --tags + fi - name: Setup Java uses: actions/setup-java@v4 diff --git a/build-android-lib.sh b/build-android-lib.sh index ea10750d..39f244a6 100755 --- a/build-android-lib.sh +++ b/build-android-lib.sh @@ -1,16 +1,28 @@ #!/bin/bash # Script to build NetBird mobile bindings using gomobile # Usage: ./script.sh [version] -# - If a version is provided, it will be used (with leading 'v' stripped if present). -# - If no version is provided: -# * Uses the latest Git tag if available (with leading 'v' stripped if present). -# * Otherwise, defaults to "dev-". -# - When running in GitHub Actions, uses "ci-" instead of "dev-". +# +# Version resolution (first match wins): +# 1. explicit argument -> the argument ('v' prefix stripped) +# 2. local build (any HEAD) -> dev- +# 3. CI, HEAD on a release tag -> that tag, e.g. 0.77.0 +# 4. CI, commits on top of a tag -> 0.77.0+ +# 5. CI, no reachable tag -> ci- +# +# The base tag is the last stable release tag (vX.Y.Z, no pre-release) found +# walking back HEAD's ancestry in the netbird submodule — the last tag on this +# branch, not the newest tag in the repository. is the submodule commit. set -euo pipefail app_path=$(pwd) +# Stable release tags only ("v" + digits, no pre-release suffix): a pre-release +# base such as "0.75.0-rc.2" would land in SemVer pre-release position, which +# the management server compares differently from a plain release. +readonly RELEASE_TAG_MATCH='v[0-9]*' +readonly RELEASE_TAG_EXCLUDE='*-*' + # Normalize semantic versions to drop a leading 'v' (e.g., v1.2.3 -> 1.2.3). # Only strips if the string starts with 'v' followed by a digit, so it won't affect # dev/ci strings or other non-semver values. @@ -22,33 +34,44 @@ normalize_version() { echo "$ver" } +describe_release_tag() { + git describe --tags "$@" --match "$RELEASE_TAG_MATCH" --exclude "$RELEASE_TAG_EXCLUDE" 2>/dev/null || true +} + get_version() { if [ -n "${1:-}" ]; then normalize_version "$1" return fi - # Try to get an exact tag - local tag - tag=$(git describe --tags --exact-match 2>/dev/null || true) + local short_hash + short_hash=$(git rev-parse --short HEAD) + + if [ "${GITHUB_ACTIONS:-}" != "true" ]; then + echo "dev-$short_hash" + return + fi + local tag + tag=$(describe_release_tag --exact-match) if [ -n "$tag" ]; then normalize_version "$tag" return fi - # Fallback to "-" - local short_hash - short_hash=$(git rev-parse --short HEAD) - - local new_version - if [ "${GITHUB_ACTIONS:-}" = "true" ]; then - new_version="ci-$short_hash" - else - new_version="dev-$short_hash" + # Walks HEAD's ancestry, so this is the last release tag on this branch, + # not the most recently created tag in the repository. + tag=$(describe_release_tag --abbrev=0) + if [ -n "$tag" ]; then + echo "$(normalize_version "$tag")+$short_hash" + return fi - echo "$new_version" + echo "WARNING: no release tag reachable from HEAD; using ci-$short_hash" >&2 + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + echo "WARNING: the submodule is a shallow clone; the tag lookup needs full history" >&2 + fi + echo "ci-$short_hash" } cd netbird From f89fd0c6aae438b62cb5c06e2f0d7adf2f6355ba Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 22:34:36 +0200 Subject: [PATCH 6/7] Document the three build workflows and their differences --- docs/versioning.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/versioning.md diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..d92b450a --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,52 @@ +# Android client build and versioning + +## The three workflows + +### `build-debug.yml` — the CI gate + +Runs automatically on every pull request and on every push to `main`. It has +three jobs: `build-debug` produces the AAR and a debug APK/AAB, then +`unit-tests` and `instrumented-tests` download that AAR and run against it, the +latter on an emulator. + +Its product is a pass/fail signal, plus the `netbird-aar` artifact that the two +test jobs consume. The debug APK it uploads is a convenience for humans; nothing +in CI reads it. + +### `build-release.yml` — the published release + +Triggered by `release: published`, which fires for pre-releases too. It builds +the signed APK and AAB and attaches them to the GitHub release. This is the +source of anything that goes to the Play Store. + +Release candidates run through this workflow: they are ordinary GitHub +pre-releases tagged `vX.Y.Z-rc.N` (for example `v0.6.0-rc.1`, `v0.3.3-rc.2`). + +### `build-snapshot.yml` — the hand-distributed build + +Manually dispatched. Produces a release-signed build from an arbitrary commit +with no tag behind it, uploaded as a 14-day artifact rather than published. + +Use it when someone needs a real, installable build of work in progress and you +do not want a public pre-release for it. Every run consumes a version code from +the shared counter, so it is not something to run per pull request. + +--- + +## Differences + +| | `build-debug` | `build-release` | `build-snapshot` | +|---|---|---|---| +| Trigger | `pull_request`, `push` → `main` | `release: published` | `workflow_dispatch` | +| Build type | debug | release | release | +| **Version name** | `ci-` | the release tag verbatim (`v0.5.0`, `v0.6.0-rc.1`) | `snapshot-` | +| **Version code** | `9999` (fallback from `version.properties`) | `release_runs + snapshot_runs + 40` | `release_runs + snapshot_runs + 40` | +| **Signing key** | default Android debug keystore | Play upload keystore (`gplay.keystore`) | Play upload keystore (`gplay.keystore`) | +| **Firebase Crashlytics + Analytics** | **absent** | present | present | +| Runs tests | yes (unit + instrumented) | no | no | +| Artifact | `debug-artifacts-`, 3 days; `netbird-aar`, 1 day | attached to the GitHub release | `snapshot-`, 14 days | +| Permissions | `contents: read` | `contents: write`, `actions: read` | `contents: read`, `actions: read` | +| Concurrency group | none | `android-version-code-lock` | `android-version-code-lock` | + +`` is the short commit hash of the `android-client` repository, not of the +submodule. From 30061ecaa5310416cfaaa2ae170e57c59a6c19c1 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sat, 15 Aug 2026 22:39:48 +0200 Subject: [PATCH 7/7] Fail the build when a run count cannot be fetched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The zero fallback existed for one legitimate case: the runs endpoint returns 404 until a workflow has run or reached the default branch, and treating that as zero is what lets build-release compute a code before build-snapshot's first run. But it also swallowed every other failure — a network error or a revoked token minted a version code far below the published ones, silently for hand-distributed snapshots. Keep the 404-means-zero case and abort on everything else, including a non-numeric response, which bash arithmetic would otherwise fold to zero. --- .github/workflows/build-release.yml | 23 +++++++++++++++++++++-- .github/workflows/build-snapshot.yml | 23 +++++++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 27a7816d..ec7d146a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -52,13 +52,32 @@ jobs: # code the other already used. The concurrency group above keeps the two # from computing at once. Renaming either workflow file resets the count # GitHub keeps for it, which would send version codes backwards. + # + # A 404 means the workflow has never run and is not on the default branch + # yet, which is a true zero. Any other failure aborts the build: falling + # back to zero would mint a version code below already-published ones. - name: Compute version code id: version_code env: GH_TOKEN: ${{ github.token }} run: | - release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - snapshot_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-snapshot.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + count_runs() { + local runs + if runs=$(gh api "repos/${{ github.repository }}/actions/workflows/$1/runs?per_page=1" --jq '.total_count' 2>gh_err.txt); then + if ! [[ "$runs" =~ ^[0-9]+$ ]]; then + echo "::error::unexpected run count for $1: '$runs'" >&2 + return 1 + fi + echo "$runs" + elif grep -q 'HTTP 404' gh_err.txt; then + echo 0 + else + echo "::error::failed to fetch run count for $1: $(cat gh_err.txt)" >&2 + return 1 + fi + } + release_runs=$(count_runs build-release.yml) + snapshot_runs=$(count_runs build-snapshot.yml) version_code=$((release_runs + snapshot_runs + 40)) echo "Release runs: $release_runs, snapshot runs: $snapshot_runs -> version_code=$version_code" echo "version_code=$version_code" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-snapshot.yml b/.github/workflows/build-snapshot.yml index 910a0f0e..1df097f3 100644 --- a/.github/workflows/build-snapshot.yml +++ b/.github/workflows/build-snapshot.yml @@ -43,13 +43,32 @@ jobs: # code the other already used. The concurrency group above keeps the two # from computing at once. Renaming either workflow file resets the count # GitHub keeps for it, which would send version codes backwards. + # + # A 404 means the workflow has never run and is not on the default branch + # yet, which is a true zero. Any other failure aborts the build: falling + # back to zero would mint a version code below already-published ones. - name: Compute version code id: version_code env: GH_TOKEN: ${{ github.token }} run: | - release_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-release.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) - snapshot_runs=$(gh api "repos/${{ github.repository }}/actions/workflows/build-snapshot.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null || echo 0) + count_runs() { + local runs + if runs=$(gh api "repos/${{ github.repository }}/actions/workflows/$1/runs?per_page=1" --jq '.total_count' 2>gh_err.txt); then + if ! [[ "$runs" =~ ^[0-9]+$ ]]; then + echo "::error::unexpected run count for $1: '$runs'" >&2 + return 1 + fi + echo "$runs" + elif grep -q 'HTTP 404' gh_err.txt; then + echo 0 + else + echo "::error::failed to fetch run count for $1: $(cat gh_err.txt)" >&2 + return 1 + fi + } + release_runs=$(count_runs build-release.yml) + snapshot_runs=$(count_runs build-snapshot.yml) version_code=$((release_runs + snapshot_runs + 40)) echo "Release runs: $release_runs, snapshot runs: $snapshot_runs -> version_code=$version_code" echo "version_code=$version_code" >> "$GITHUB_OUTPUT"