Skip to content

ci: resolve build number from TestFlight API instead of run number#103

Closed
evgeniyChepelev wants to merge 12 commits into
mainfrom
ci/unified-build-number-from-testflight
Closed

ci: resolve build number from TestFlight API instead of run number#103
evgeniyChepelev wants to merge 12 commits into
mainfrom
ci/unified-build-number-from-testflight

Conversation

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator

@evgeniyChepelev evgeniyChepelev commented Apr 22, 2026

Description

/testflight

Summary by CodeRabbit

  • Build & Release
    • Automated build-number lookup from App Store Connect/TestFlight and automatic incrementing based on existing builds.
    • Optional manual workflow dispatch inputs for version and build-number override (forces upload=false when used).
    • Deterministic build-number resolution precedence: override → computed from store → run number, with explicit final value exported.
    • Downstream release workflow now receives the resolved build-number.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 011dca0b-b393-4429-b05e-b0d8bf2db14b

📥 Commits

Reviewing files that changed from the base of the PR and between fe2109b and b9a57d1.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • .github/workflows/testflight.yml

📝 Walkthrough

Walkthrough

release.yml now computes and exports a TestFlight/App Store Connect build-number; testflight.yml adds dispatch inputs and overrides, queries ASC for latest pre-release build when needed, and resolves the gate job output build-number by precedence (override → ASC lookup → github.run_number).

Changes

Cohort / File(s) Summary
Release workflow
/.github/workflows/release.yml
prepare job derives version from pushed v* tag, generates ES256 JWT, queries ASC /builds for that pre-release version, computes next numeric build (max + 1), and exposes jobs.prepare.outputs.build-number. Also passes build-number into the reusable build-upload.yml invocation.
TestFlight workflow
/.github/workflows/testflight.yml
Adds workflow_dispatch inputs (version, build-number), stores CLI/dispatch overrides as version-override/build-number-override, adds tfbuild step to query ASC for latest build when no override, and finalizes jobs.gate.outputs.build-number by precedence: override → tfbuildgithub.run_number.

Sequence Diagram(s)

sequenceDiagram
    participant GHA as GitHub Actions
    participant JWT as JWT Generator
    participant ASC as App Store Connect API
    participant Workflow as CI Workflow

    GHA->>JWT: generate ES256 JWT (App Store Connect key)
    JWT-->>GHA: return signed JWT
    GHA->>ASC: GET /builds?filter[app]=<id>&filter[preReleaseVersion]=<version> (Auth: JWT)
    ASC-->>GHA: 200 OK + builds list
    GHA->>Workflow: compute max build.attributes.version
    Workflow-->>Workflow: increment build-number
    Workflow-->>GHA: export resolved build-number (override → tfbuild → run_number)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • mlsmaycon

Poem

🐰 I signed a tiny JWT by starlit code,

I tallied builds upon the road,
I nudged the number just one more,
I handed it to CI's door,
Hopping off — the pipeline glowed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely empty, containing only a single command reference '/testflight' with no substantive explanation of the changes, objectives, or implementation details. Provide a comprehensive description explaining the changes made, why they were necessary, and how they affect the build process. Include sections such as: What changed, Why these changes, How to test, and any breaking changes or side effects.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: resolving build numbers from the TestFlight API instead of using GitHub run numbers, which aligns with the significant modifications to both release.yml and testflight.yml workflows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/unified-build-number-from-testflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator Author

/testflight

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 45-52: The curl call that sets RESPONSE currently masks failures
and allows shell globbing; update the call that produces RESPONSE (the block
using curl and assigning RESPONSE) to add --globoff to disable URL globbing,
remove the "|| echo '{}'" fallback so failures propagate, then parse LATEST from
RESPONSE as before but validate that LATEST is a numeric value (the variable
LATEST extracted from jq) and if not numeric or empty, print a clear error and
exit non-zero instead of defaulting to 0; finally compute NEXT and write
build-number only after validation succeeds (symbols to edit: RESPONSE
assignment, LATEST, NEXT).
- Around line 37-42: The JWT signature generation uses openssl dgst -sign which
outputs a DER-encoded ECDSA signature but App Store Connect ES256 requires raw
IEEE-P1363 r||s; update the SIGNATURE generation to produce a raw r||s 64-byte
signature (concatenate r and s, each fixed-width 32 bytes) before
base64url-encoding. Replace the current SIGNATURE creation that references
HEADER, PAYLOAD, and KEY_FILE with a step that either: (a) calls a small
Node/crypto script to sign the string "%s.%s" (HEADER and PAYLOAD) with the
private key using ECDSA and obtains the raw r||s signature, or (b)
post-processes the DER signature from openssl to extract r and s and left-pad
each to 32 bytes and concat; then base64url-encode that raw signature and set
JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}" so the final TOKEN uses the correct
ES256 format.

In @.github/workflows/testflight.yml:
- Around line 176-183: The curl invocation that sets RESPONSE should be changed
to include --globoff and to stop masking failures by removing the "|| echo '{}'"
fallback so API/network/auth errors cause the job to fail; update the command
that builds RESPONSE (the curl line referenced by RESPONSE) to add --globoff and
drop the fallback, then ensure the subsequent logic that parses LATEST and
computes NEXT (variables LATEST and NEXT) relies on a valid RESPONSE (or add an
explicit check that RESPONSE is non-empty or exit with an error) so you don't
silently default the build number to 1.
- Around line 168-173: The SIGNATURE generation currently uses openssl dgst
-sign which yields a DER-encoded ECDSA signature; replace that step so the
signature is produced in raw IEEE P1363 format (concatenated r||s, 64 bytes for
P-256) and then base64url-encode it before assembling JWT (HEADER, PAYLOAD,
SIGNATURE, JWT). Implement this by replacing the openssl dgst invocation that
sets SIGNATURE with a step that produces the raw r||s signature (for example,
use a Node.js one-liner calling crypto.sign(..., { dsaEncoding: 'ieee-p1363' })
and base64url-encode the result) and apply the same change in both
testflight.yml and release.yml.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c82c78f-a30e-476f-a007-bba93a62c193

📥 Commits

Reviewing files that changed from the base of the PR and between f82e687 and f0e5661.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • .github/workflows/testflight.yml

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment on lines +45 to +52
RESPONSE=$(curl -sf \
-H "Authorization: Bearer $JWT" \
"https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
|| echo '{}')

LATEST=$(echo "$RESPONSE" | jq -r '.data[0].attributes.version // "0"')
NEXT=$((LATEST + 1))
echo "build-number=$NEXT" >> "$GITHUB_OUTPUT"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set +e

echo "Without --globoff:"
curl -sS -o /dev/null 'https://example.invalid/v1/builds?filter[app]=123' 2>&1
echo "exit=$?"

echo
echo "With --globoff:"
curl --globoff -sS -o /dev/null --connect-timeout 1 'https://example.invalid/v1/builds?filter[app]=123' 2>&1
echo "exit=$?"

Repository: netbirdio/ios-client

Length of output: 296


🏁 Script executed:

cat -n .github/workflows/release.yml | sed -n '40,60p'

Repository: netbirdio/ios-client

Length of output: 1264


Do not mask TestFlight lookup failures as build number 1.

The URL contains filter[app]-style brackets, which curl treats as URL globbing unless --globoff is used. Failures are currently swallowed with || echo '{}', silently converting auth/query/network failures into LATEST=0 and emitting build-number=1. Add --globoff to disable globbing, remove the failure-masking pattern, and validate the response is numeric:

🛠️ Proposed fix to fail closed and disable curl globbing
-          RESPONSE=$(curl -sf \
+          RESPONSE=$(curl --globoff -fsS \
             -H "Authorization: Bearer $JWT" \
             "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
-            || echo '{}')
+          )
 
-          LATEST=$(echo "$RESPONSE" | jq -r '.data[0].attributes.version // "0"')
+          LATEST=$(jq -r '.data[0].attributes.version // "0"' <<< "$RESPONSE")
+          if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then
+            echo "::error::Latest TestFlight build number is not numeric: '$LATEST'"
+            exit 1
+          fi
           NEXT=$((LATEST + 1))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 45 - 52, The curl call that sets
RESPONSE currently masks failures and allows shell globbing; update the call
that produces RESPONSE (the block using curl and assigning RESPONSE) to add
--globoff to disable URL globbing, remove the "|| echo '{}'" fallback so
failures propagate, then parse LATEST from RESPONSE as before but validate that
LATEST is a numeric value (the variable LATEST extracted from jq) and if not
numeric or empty, print a clear error and exit non-zero instead of defaulting to
0; finally compute NEXT and write build-number only after validation succeeds
(symbols to edit: RESPONSE assignment, LATEST, NEXT).

Comment thread .github/workflows/testflight.yml Outdated
Comment thread .github/workflows/testflight.yml Outdated
Comment on lines +176 to +183
RESPONSE=$(curl -sf \
-H "Authorization: Bearer $JWT" \
"https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
|| echo '{}')

LATEST=$(echo "$RESPONSE" | jq -r '.data[0].attributes.version // "0"')
NEXT=$((LATEST + 1))
echo "build-number=$NEXT" >> "$GITHUB_OUTPUT"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set +e

echo "Without --globoff:"
curl -sS -o /dev/null 'https://example.invalid/v1/builds?filter[app]=123' 2>&1
echo "exit=$?"

echo
echo "With --globoff:"
curl --globoff -sS -o /dev/null --connect-timeout 1 'https://example.invalid/v1/builds?filter[app]=123' 2>&1
echo "exit=$?"

Repository: netbirdio/ios-client

Length of output: 296


Add --globoff to the curl command and fail if the API request fails.

The URL contains square brackets (filter[app], filter[preReleaseVersion.version], fields[builds]) which curl interprets as glob patterns by default, causing it to exit with code 3 before reaching the API. The || echo '{}' fallback then masks this (and any legitimate auth/network/API failures) by silently returning an empty object, which causes the build number to default to 1. This risks overwriting existing TestFlight builds.

Proposed fix
-          RESPONSE=$(curl -sf \
+          RESPONSE=$(curl --globoff -fsS \
             -H "Authorization: Bearer $JWT" \
             "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
-            || echo '{}')
+          )
 
-          LATEST=$(echo "$RESPONSE" | jq -r '.data[0].attributes.version // "0"')
+          LATEST=$(jq -r '.data[0].attributes.version // "0"' <<< "$RESPONSE")
+          if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then
+            echo "::error::Latest TestFlight build number is not numeric: '$LATEST'"
+            exit 1
+          fi
           NEXT=$((LATEST + 1))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RESPONSE=$(curl -sf \
-H "Authorization: Bearer $JWT" \
"https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
|| echo '{}')
LATEST=$(echo "$RESPONSE" | jq -r '.data[0].attributes.version // "0"')
NEXT=$((LATEST + 1))
echo "build-number=$NEXT" >> "$GITHUB_OUTPUT"
RESPONSE=$(curl --globoff -fsS \
-H "Authorization: Bearer $JWT" \
"https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=1&fields[builds]=version" \
)
LATEST=$(jq -r '.data[0].attributes.version // "0"' <<< "$RESPONSE")
if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then
echo "::error::Latest TestFlight build number is not numeric: '$LATEST'"
exit 1
fi
NEXT=$((LATEST + 1))
echo "build-number=$NEXT" >> "$GITHUB_OUTPUT"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 176 - 183, The curl invocation
that sets RESPONSE should be changed to include --globoff and to stop masking
failures by removing the "|| echo '{}'" fallback so API/network/auth errors
cause the job to fail; update the command that builds RESPONSE (the curl line
referenced by RESPONSE) to add --globoff and drop the fallback, then ensure the
subsequent logic that parses LATEST and computes NEXT (variables LATEST and
NEXT) relies on a valid RESPONSE (or add an explicit check that RESPONSE is
non-empty or exit with an error) so you don't silently default the build number
to 1.

@github-actions
Copy link
Copy Markdown

TestFlight builds uploaded 0.1.6 (157) for f0e5661 — iOS + tvOS

View workflow run

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator Author

/testflight

1 similar comment
@evgeniyChepelev
Copy link
Copy Markdown
Collaborator Author

/testflight

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator Author

/testflight

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator Author

/testflight

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
.github/workflows/testflight.yml (1)

213-215: ⚠️ Potential issue | 🔴 Critical

Add --globoff here too — same URL-globbing issue as release.yml.

This curl uses the same bracket-laden URL (filter[app], filter[preReleaseVersion.version], fields[builds]) and will be rejected by curl's URL globbing. Same fix as the release.yml counterpart.

🛠️ Proposed fix
-          HTTP_STATUS=$(curl -s -o /tmp/tf_response.json -w "%{http_code}" \
+          HTTP_STATUS=$(curl --globoff -sS -o /tmp/tf_response.json -w "%{http_code}" \
             -H "Authorization: Bearer $JWT" \
             "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=25&fields[builds]=version")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 213 - 215, The curl invocation
that sets HTTP_STATUS uses a bracketed App Store Connect URL (the filter[app],
filter[preReleaseVersion.version], fields[builds] query params) which will be
rejected by curl's URL globbing; update the curl command that assigns
HTTP_STATUS to include the --globoff option so curl treats the URL literally
(i.e., add --globoff to the existing curl invocation that writes to
/tmp/tf_response.json and sets HTTP_STATUS).
.github/workflows/release.yml (1)

49-51: ⚠️ Potential issue | 🔴 Critical

Add --globoff to the curl call — URL brackets still break curl's URL globbing.

The URL contains filter[app], filter[preReleaseVersion.version], and fields[builds], all of which curl interprets as glob ranges by default and will reject with curl: (3) bad range specification before any request is made. With set -euo pipefail, HTTP_STATUS=$(curl …) then propagates the failure, but the error is opaque ("bad range" rather than a TestFlight problem). This was flagged previously on the DER-masking pair and only the || echo '{}' half was fixed.

🛠️ Proposed fix
-          HTTP_STATUS=$(curl -s -o /tmp/tf_response.json -w "%{http_code}" \
+          HTTP_STATUS=$(curl --globoff -sS -o /tmp/tf_response.json -w "%{http_code}" \
             -H "Authorization: Bearer $JWT" \
             "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=25&fields[builds]=version")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 49 - 51, The curl invocation that
sets HTTP_STATUS is failing due to URL globbing on bracketed query parameters;
update the curl command used in the HTTP_STATUS assignment to include the
--globoff flag so curl treats the URL literally (i.e., modify the curl call that
writes to /tmp/tf_response.json and sets HTTP_STATUS to add --globoff while
preserving the existing -s -o -w, Authorization header and URL).
🧹 Nitpick comments (3)
.github/workflows/release.yml (2)

35-47: Minor: use trap to guarantee the private-key tempfile is removed.

If any step between mktemp and rm -f "$KEY_FILE" fails (e.g., the openssl | python3 pipeline under set -euo pipefail), the decoded .p8 key stays on disk for the rest of the job. Runner is ephemeral, but subsequent steps (uploading logs, artifacts) could inadvertently capture it. A trap on EXIT makes cleanup unconditional.

🛠️ Proposed fix
           KEY_FILE=$(mktemp)
+          trap 'rm -f "$KEY_FILE"' EXIT
           echo "$API_KEY_BASE64" | base64 --decode > "$KEY_FILE"
@@
           JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
-          rm -f "$KEY_FILE"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 35 - 47, Add an unconditional
cleanup trap immediately after creating the temporary key file (after
KEY_FILE=$(mktemp)) so the tempfile is removed on EXIT even if a later command
fails; reference the KEY_FILE variable and ensure the trap runs rm -f
"$KEY_FILE" (or equivalent) on EXIT and remains in effect for the entire block
that uses HEADER, PAYLOAD, SIGNATURE and JWT so the file is always removed
before the job ends.

25-62: Consider extracting the JWT + TestFlight lookup into a composite action.

The entire ES256 JWT mint + Builds API query + jq max logic is duplicated verbatim in .github/workflows/testflight.yml (lines 170–226). Any future fix (e.g., the --globoff issue, rotating to a different App Store Connect endpoint, or replacing the inline Python DER parser) has to be applied twice and kept in sync. Moving it to .github/actions/resolve-testflight-build-number/action.yml (composite action) taking app-id/version as inputs and emitting build-number would eliminate the drift risk.

Not a blocker for this PR — fine to defer if you'd rather land the feature first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 25 - 62, Extract the ES256 JWT
minting + TestFlight builds lookup (the "Resolve build number from TestFlight"
step that generates JWT, SIGNATURE, calls the App Store Connect builds API,
computes LATEST via jq and sets "build-number") into a reusable composite action
(e.g., resolve-testflight-build-number) that accepts inputs app-id and version
and exposes output build-number; move all JWT/key handling and curl/jq logic
into the composite action, accept/send required secrets as action inputs, and
replace the duplicated inline blocks in both workflows with a single step that
calls the new composite action so future fixes (DER parser, --globoff,
endpoints) are changed once.
.github/workflows/testflight.yml (1)

238-243: Redundant but harmless fallback chain.

tfbuild already short-circuits when BUILD_OVERRIDE is set (lines 182–186), so steps.tfbuild.outputs.build-number will already equal the override when one is supplied — the first term of this OR chain never wins over the second in practice. And if tfbuild runs and fails to produce an output, the step itself exits non-zero, so the github.run_number tier only fires when should-build == 'false' (build job is skipped anyway).

Not a bug; just flagging so the precedence isn't mistaken for actual defense-in-depth. If you want a genuine safety net for "tfbuild silently produced nothing", make it explicit with a warning log.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 238 - 243, The fallback chain
for buildNumber is redundant because steps.tfbuild already respects
BUILD_OVERRIDE; update the logic around the buildNumber assignment (the
buildNumber variable and the core.setOutput/core.info usage) to either remove
the unnecessary first term ('${{ steps.pre.outputs.build-number-override }}')
or, if you want a real safety net, explicitly read
steps.tfbuild.outputs.build-number and if it is empty log a warning via
core.warning before falling back to '${{ github.run_number }}' and then call
core.setOutput('build-number', buildNumber) and core.info; reference the
buildNumber variable, steps.pre.outputs.build-number-override,
steps.tfbuild.outputs.build-number, github.run_number, core.setOutput, and
core.info when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/release.yml:
- Around line 49-51: The curl invocation that sets HTTP_STATUS is failing due to
URL globbing on bracketed query parameters; update the curl command used in the
HTTP_STATUS assignment to include the --globoff flag so curl treats the URL
literally (i.e., modify the curl call that writes to /tmp/tf_response.json and
sets HTTP_STATUS to add --globoff while preserving the existing -s -o -w,
Authorization header and URL).

In @.github/workflows/testflight.yml:
- Around line 213-215: The curl invocation that sets HTTP_STATUS uses a
bracketed App Store Connect URL (the filter[app],
filter[preReleaseVersion.version], fields[builds] query params) which will be
rejected by curl's URL globbing; update the curl command that assigns
HTTP_STATUS to include the --globoff option so curl treats the URL literally
(i.e., add --globoff to the existing curl invocation that writes to
/tmp/tf_response.json and sets HTTP_STATUS).

---

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 35-47: Add an unconditional cleanup trap immediately after
creating the temporary key file (after KEY_FILE=$(mktemp)) so the tempfile is
removed on EXIT even if a later command fails; reference the KEY_FILE variable
and ensure the trap runs rm -f "$KEY_FILE" (or equivalent) on EXIT and remains
in effect for the entire block that uses HEADER, PAYLOAD, SIGNATURE and JWT so
the file is always removed before the job ends.
- Around line 25-62: Extract the ES256 JWT minting + TestFlight builds lookup
(the "Resolve build number from TestFlight" step that generates JWT, SIGNATURE,
calls the App Store Connect builds API, computes LATEST via jq and sets
"build-number") into a reusable composite action (e.g.,
resolve-testflight-build-number) that accepts inputs app-id and version and
exposes output build-number; move all JWT/key handling and curl/jq logic into
the composite action, accept/send required secrets as action inputs, and replace
the duplicated inline blocks in both workflows with a single step that calls the
new composite action so future fixes (DER parser, --globoff, endpoints) are
changed once.

In @.github/workflows/testflight.yml:
- Around line 238-243: The fallback chain for buildNumber is redundant because
steps.tfbuild already respects BUILD_OVERRIDE; update the logic around the
buildNumber assignment (the buildNumber variable and the
core.setOutput/core.info usage) to either remove the unnecessary first term
('${{ steps.pre.outputs.build-number-override }}') or, if you want a real safety
net, explicitly read steps.tfbuild.outputs.build-number and if it is empty log a
warning via core.warning before falling back to '${{ github.run_number }}' and
then call core.setOutput('build-number', buildNumber) and core.info; reference
the buildNumber variable, steps.pre.outputs.build-number-override,
steps.tfbuild.outputs.build-number, github.run_number, core.setOutput, and
core.info when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6dd6a281-5461-42ee-81d1-cd5d9f9fd0ef

📥 Commits

Reviewing files that changed from the base of the PR and between f0e5661 and 316b15b.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • .github/workflows/testflight.yml

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
.github/workflows/testflight.yml (1)

214-222: ⚠️ Potential issue | 🔴 Critical

Add --globoff and treat curl transport failures as fatal.

The URL contains filter[app], filter[preReleaseVersion.version], fields[builds] — curl interprets [...] as URL-globbing and exits with code 3 before sending the request. The current invocation has no --globoff and captures only %{http_code} (which will be empty on a transport failure); with set -e, a failed command substitution assignment does not abort the script, so HTTP_STATUS becomes empty and your != "200" branch does fire — but the error message will be misleading ("HTTP " with no status) and any other transport/TLS/DNS error is similarly swallowed into the same generic branch. Add --globoff and --fail-with-body (or check the curl exit code explicitly) so real failures surface clearly.

🔒️ Proposed fix
-          HTTP_STATUS=$(curl -s -o /tmp/tf_response.json -w "%{http_code}" \
+          HTTP_STATUS=$(curl --globoff -sS -o /tmp/tf_response.json -w "%{http_code}" \
             -H "Authorization: Bearer $JWT" \
-            "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=25&fields[builds]=version")
+            "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${APP_ID}&filter[preReleaseVersion.version]=${VERSION}&sort=-version&limit=25&fields[builds]=version") \
+            || { echo "::error::curl failed talking to App Store Connect"; exit 1; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 214 - 222, The curl invocation
that sets HTTP_STATUS/RESPONSE should be made robust by disabling URL globbing
and surfacing transport failures: add --globoff and --fail-with-body to the curl
options (or explicitly check curl's exit code after the command) when calling
curl that assigns HTTP_STATUS and writes /tmp/tf_response.json; then if curl
exits non‑zero log a clear error (including curl's exit code and any response
body) and exit 1 instead of relying only on HTTP_STATUS != "200". Ensure you
update the curl command that produces HTTP_STATUS and the subsequent branch that
checks HTTP_STATUS/RESPONSE to handle and report transport failures distinctly.
🧹 Nitpick comments (2)
.github/workflows/testflight.yml (2)

224-225: tonumber + bash arithmetic assumes strictly integer build numbers.

If any returned attributes.version is non-integer (e.g. "5.1", accidentally pushed via Xcode), jq | tonumber yields a float, max returns a float, and NEXT=$((LATEST + 1)) then fails with "invalid arithmetic operator". iOS TestFlight typically uses monotonic integers so this is unlikely in practice, but a defensive guard (or flooring in jq) would make the failure mode explicit:

-          LATEST=$(echo "$RESPONSE" | jq -r '[.data[].attributes.version | tonumber] | max // 0')
+          LATEST=$(jq -r '[.data[].attributes.version | tonumber | floor] | max // 0' <<<"$RESPONSE")
+          if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then
+            echo "::error::Unexpected non-integer TestFlight build number: '$LATEST'"
+            exit 1
+          fi
           NEXT=$((LATEST + 1))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 224 - 225, The current
pipeline computes LATEST via jq using "tonumber" which can produce floats and
break NEXT arithmetic; update the jq expression that sets LATEST so it
coerces/guards to integers (e.g., use tonumber? and floor or fallback to 0)
before taking max, so LATEST is always an integer and NEXT=$((LATEST + 1)) will
not fail; modify the command that assigns LATEST (the expression producing
LATEST from RESPONSE) to apply floor and safe tonumber handling.

200-212: Clean up the decoded key file on any failure path.

Between writing KEY_FILE (line 201) and rm -f (line 212), several commands can fail (openssl, python3, base64 pipes). With set -e, a mid-pipeline failure leaves the decoded private key on the runner's disk. Use a trap to guarantee cleanup.

🛡️ Proposed hardening
           KEY_FILE=$(mktemp)
+          trap 'rm -f "$KEY_FILE"' EXIT
           echo "$API_KEY_BASE64" | base64 --decode > "$KEY_FILE"
@@
           JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
-          rm -f "$KEY_FILE"

Also optional: the inline Python DER→IEEE-P1363 converter is correct for P-256 (single-byte DER length always < 128) but quite dense. If you expect to touch this again, consider extracting it into a small scripts/der_to_p1363.py with a brief comment on the length-byte assumption — it'll make future review of the JWT pipeline a lot easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml around lines 200 - 212, Add a guaranteed
cleanup trap so the temporary decoded key (KEY_FILE) is removed on any failure
or exit: after creating KEY_FILE with mktemp, install a trap on EXIT/ERR that rm
-f "$KEY_FILE" (or unset the trap at the end) so failures in the openssl /
python3 pipeline that generate SIGNATURE (and before the final rm -f) cannot
leave the private key on disk; ensure the trap is set before running the
HEADER/PAYLOAD/SIGNATURE pipeline and optionally clear the trap only after JWT
is built and the explicit rm -f "$KEY_FILE" has run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/testflight.yml:
- Line 5: Remove the feature branch "ci/unified-build-number-from-testflight"
from the push trigger in the TestFlight workflow so it doesn't run on pushes to
the PR source after merge; edit the branches array in
.github/workflows/testflight.yml (the line containing branches: [main,
ci/unified-build-number-from-testflight]) and delete the feature branch entry so
only the intended branch(es) (e.g., main) remain.

---

Duplicate comments:
In @.github/workflows/testflight.yml:
- Around line 214-222: The curl invocation that sets HTTP_STATUS/RESPONSE should
be made robust by disabling URL globbing and surfacing transport failures: add
--globoff and --fail-with-body to the curl options (or explicitly check curl's
exit code after the command) when calling curl that assigns HTTP_STATUS and
writes /tmp/tf_response.json; then if curl exits non‑zero log a clear error
(including curl's exit code and any response body) and exit 1 instead of relying
only on HTTP_STATUS != "200". Ensure you update the curl command that produces
HTTP_STATUS and the subsequent branch that checks HTTP_STATUS/RESPONSE to handle
and report transport failures distinctly.

---

Nitpick comments:
In @.github/workflows/testflight.yml:
- Around line 224-225: The current pipeline computes LATEST via jq using
"tonumber" which can produce floats and break NEXT arithmetic; update the jq
expression that sets LATEST so it coerces/guards to integers (e.g., use
tonumber? and floor or fallback to 0) before taking max, so LATEST is always an
integer and NEXT=$((LATEST + 1)) will not fail; modify the command that assigns
LATEST (the expression producing LATEST from RESPONSE) to apply floor and safe
tonumber handling.
- Around line 200-212: Add a guaranteed cleanup trap so the temporary decoded
key (KEY_FILE) is removed on any failure or exit: after creating KEY_FILE with
mktemp, install a trap on EXIT/ERR that rm -f "$KEY_FILE" (or unset the trap at
the end) so failures in the openssl / python3 pipeline that generate SIGNATURE
(and before the final rm -f) cannot leave the private key on disk; ensure the
trap is set before running the HEADER/PAYLOAD/SIGNATURE pipeline and optionally
clear the trap only after JWT is built and the explicit rm -f "$KEY_FILE" has
run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5695f66f-0db4-4bce-863e-cfd3e5653a90

📥 Commits

Reviewing files that changed from the base of the PR and between 316b15b and fe2109b.

📒 Files selected for processing (1)
  • .github/workflows/testflight.yml

on:
push:
branches: [main]
branches: [main, ci/unified-build-number-from-testflight]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the feature branch from the push trigger before merging.

ci/unified-build-number-from-testflight is the PR's source branch and shouldn't run TestFlight on every push to it once merged (it also bypasses the PR gate). Drop it before merge.

♻️ Proposed change
-    branches: [main, ci/unified-build-number-from-testflight]
+    branches: [main]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
branches: [main, ci/unified-build-number-from-testflight]
branches: [main]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testflight.yml at line 5, Remove the feature branch
"ci/unified-build-number-from-testflight" from the push trigger in the
TestFlight workflow so it doesn't run on pushes to the PR source after merge;
edit the branches array in .github/workflows/testflight.yml (the line containing
branches: [main, ci/unified-build-number-from-testflight]) and delete the
feature branch entry so only the intended branch(es) (e.g., main) remain.

@evgeniyChepelev evgeniyChepelev deleted the ci/unified-build-number-from-testflight branch April 23, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant