From 6d71dbd20c2bd1f7077e384e03987f69076f80d1 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:59:07 +0200 Subject: [PATCH 01/13] Update testflight.yml --- .github/workflows/testflight.yml | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 932b69f..f03309e 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -1,6 +1,7 @@ name: TestFlight on: + workflow_dispatch: push: branches: [main] pull_request: @@ -149,6 +150,39 @@ jobs: echo "version=$NEXT" >> "$GITHUB_OUTPUT" echo "Tag: $LATEST_TAG → next: $NEXT" + - name: Fetch latest build number from App Store Connect + if: steps.pre.outputs.should-build == 'true' + env: + ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }} + APP_ID: ${{ secrets.APP_STORE_APP_ID_IOS }} + run: | + VERSION="${{ steps.pre.outputs.version-override || steps.derive.outputs.version }}" + + echo "$PRIVATE_KEY_BASE64" | base64 --decode > /tmp/AuthKey.p8 + + NOW=$(date +%s) + EXP=$(($NOW + 1200)) + HEADER=$(echo -n '{"alg":"ES256","kid":"'"$KEY_ID"'","typ":"JWT"}' | base64 -w0 | tr -d '=' | tr '/+' '_-') + PAYLOAD=$(echo -n '{"iss":"'"$ISSUER_ID"'","exp":'"$EXP"',"aud":"appstoreconnect-v1"}' | base64 -w0 | tr -d '=' | tr '/+' '_-') + SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign /tmp/AuthKey.p8 | base64 -w0 | tr -d '=' | tr '/+' '_-') + JWT="$HEADER.$PAYLOAD.$SIGNATURE" + + RESPONSE=$(curl -sf \ + "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=$APP_ID&filter[preReleaseVersion.version]=$VERSION&sort=-uploadedDate&limit=1" \ + -H "Authorization: Bearer $JWT") + + LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.buildNumber else "none" end') + + echo "=========================================" + echo " App Store Connect — latest build info" + echo " Version: $VERSION" + echo " Latest uploaded build: $LATEST_BUILD" + echo "=========================================" + + rm -f /tmp/AuthKey.p8 + - name: Finalize outputs id: finalize uses: actions/github-script@v7 From b8e21853933b44dd2426dab150f0badaa03a4532 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:04:27 +0200 Subject: [PATCH 02/13] Update testflight.yml --- .github/workflows/testflight.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index f03309e..f172fe6 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -3,7 +3,6 @@ name: TestFlight on: workflow_dispatch: push: - branches: [main] pull_request: branches: [main] types: [opened, edited] From 7382dec31500c347de13d02fb83f7d573a3540b7 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:05:23 +0200 Subject: [PATCH 03/13] Update testflight.yml --- .github/workflows/testflight.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index f172fe6..784b6c7 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -168,7 +168,7 @@ jobs: SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign /tmp/AuthKey.p8 | base64 -w0 | tr -d '=' | tr '/+' '_-') JWT="$HEADER.$PAYLOAD.$SIGNATURE" - RESPONSE=$(curl -sf \ + RESPONSE=$(curl -sfg \ "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=$APP_ID&filter[preReleaseVersion.version]=$VERSION&sort=-uploadedDate&limit=1" \ -H "Authorization: Bearer $JWT") From 30e5b0f48ccb6e6e2a5a7c02e9baaf8b71adfd61 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:06:38 +0200 Subject: [PATCH 04/13] Update testflight.yml --- .github/workflows/testflight.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 784b6c7..ccd563f 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -168,10 +168,21 @@ jobs: SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign /tmp/AuthKey.p8 | base64 -w0 | tr -d '=' | tr '/+' '_-') JWT="$HEADER.$PAYLOAD.$SIGNATURE" - RESPONSE=$(curl -sfg \ + HTTP_STATUS=$(curl -sg \ + -o /tmp/asc_response.json \ + -w "%{http_code}" \ "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=$APP_ID&filter[preReleaseVersion.version]=$VERSION&sort=-uploadedDate&limit=1" \ -H "Authorization: Bearer $JWT") + RESPONSE=$(cat /tmp/asc_response.json) + echo "HTTP status: $HTTP_STATUS" + + if [ "$HTTP_STATUS" != "200" ]; then + echo "API error response:" + echo "$RESPONSE" | jq . || echo "$RESPONSE" + exit 1 + fi + LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.buildNumber else "none" end') echo "=========================================" From b73354ba6e995761ef9c868194e955d05316b14a Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:08:00 +0200 Subject: [PATCH 05/13] Update testflight.yml --- .github/workflows/testflight.yml | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index ccd563f..0d8ee52 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -161,12 +161,29 @@ jobs: echo "$PRIVATE_KEY_BASE64" | base64 --decode > /tmp/AuthKey.p8 - NOW=$(date +%s) - EXP=$(($NOW + 1200)) - HEADER=$(echo -n '{"alg":"ES256","kid":"'"$KEY_ID"'","typ":"JWT"}' | base64 -w0 | tr -d '=' | tr '/+' '_-') - PAYLOAD=$(echo -n '{"iss":"'"$ISSUER_ID"'","exp":'"$EXP"',"aud":"appstoreconnect-v1"}' | base64 -w0 | tr -d '=' | tr '/+' '_-') - SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign /tmp/AuthKey.p8 | base64 -w0 | tr -d '=' | tr '/+' '_-') - JWT="$HEADER.$PAYLOAD.$SIGNATURE" + JWT=$(python3 - < Date: Thu, 23 Apr 2026 13:10:44 +0200 Subject: [PATCH 06/13] Update testflight.yml --- .github/workflows/testflight.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 0d8ee52..8a0698e 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -22,6 +22,7 @@ jobs: name: Validate trigger if: > github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'issue_comment' @@ -52,7 +53,7 @@ jobs: if (netbirdMatch) core.setOutput('netbird-ref', netbirdMatch[1]); } - if (context.eventName === 'push') { + if (context.eventName === 'workflow_dispatch' || context.eventName === 'push') { core.setOutput('ref', context.sha); core.setOutput('should-build', 'true'); core.setOutput('upload', 'true'); From a219968c4de82428b36483acb5d042acbcf27f80 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:13:38 +0200 Subject: [PATCH 07/13] Update testflight.yml --- .github/workflows/testflight.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 8a0698e..01fa93e 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -162,29 +162,7 @@ jobs: echo "$PRIVATE_KEY_BASE64" | base64 --decode > /tmp/AuthKey.p8 - JWT=$(python3 - < Date: Thu, 23 Apr 2026 13:14:27 +0200 Subject: [PATCH 08/13] Update testflight.yml --- .github/workflows/testflight.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 01fa93e..5668c89 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -179,7 +179,7 @@ jobs: exit 1 fi - LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.buildNumber else "none" end') + LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.version else "none" end') echo "=========================================" echo " App Store Connect — latest build info" From 885bf717e389ad5343845babff5e1fa51731bb83 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:16:58 +0200 Subject: [PATCH 09/13] Update testflight.yml --- .github/workflows/testflight.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 5668c89..43b777e 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -152,6 +152,7 @@ jobs: - name: Fetch latest build number from App Store Connect if: steps.pre.outputs.should-build == 'true' + id: asc-build env: ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} @@ -181,6 +182,8 @@ jobs: LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.version else "none" end') + echo "latest-build=$LATEST_BUILD" >> "$GITHUB_OUTPUT" + echo "=========================================" echo " App Store Connect — latest build info" echo " Version: $VERSION" @@ -197,9 +200,21 @@ jobs: core.setOutput('ref', '${{ steps.pre.outputs.ref }}'); core.setOutput('should-build', '${{ steps.pre.outputs.should-build }}'); core.setOutput('upload', '${{ steps.pre.outputs.upload }}'); - core.setOutput('build-number', '${{ steps.pre.outputs.build-number }}'); core.setOutput('netbird-ref', '${{ steps.pre.outputs.netbird-ref }}'); + const overrideBuild = '${{ steps.pre.outputs.build-number }}'; + const latestBuild = '${{ steps.asc-build.outputs.latest-build }}'; + let buildNumber; + if (overrideBuild && overrideBuild !== '${{ github.run_number }}') { + buildNumber = overrideBuild; + } else if (latestBuild && latestBuild !== 'none') { + buildNumber = String(parseInt(latestBuild, 10) + 1); + } else { + buildNumber = '1'; + } + core.info(`build-number: ${buildNumber} (latest=${latestBuild}, override=${overrideBuild})`); + core.setOutput('build-number', buildNumber); + const override = '${{ steps.pre.outputs.version-override }}'; const derived = '${{ steps.derive.outputs.version }}'; core.setOutput('version', override || derived); From 2e36846dc626501b76d84e4f672b95fae20f287a Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:25:56 +0200 Subject: [PATCH 10/13] Update testflight.yml --- .github/workflows/testflight.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 43b777e..47cee26 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -1,8 +1,8 @@ name: TestFlight on: - workflow_dispatch: push: + branches: [main] pull_request: branches: [main] types: [opened, edited] From 2933e1239988d11da1b33d30045cab59948646e8 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:28:13 +0200 Subject: [PATCH 11/13] Update testflight.yml --- .github/workflows/testflight.yml | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 47cee26..d295a9d 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -195,17 +195,28 @@ jobs: - name: Finalize outputs id: finalize uses: actions/github-script@v7 + env: + PRE_REF: ${{ steps.pre.outputs.ref }} + PRE_SHOULD_BUILD: ${{ steps.pre.outputs.should-build }} + PRE_UPLOAD: ${{ steps.pre.outputs.upload }} + PRE_NETBIRD_REF: ${{ steps.pre.outputs.netbird-ref }} + PRE_BUILD_NUMBER: ${{ steps.pre.outputs.build-number }} + PRE_VERSION_OVERRIDE: ${{ steps.pre.outputs.version-override }} + DERIVE_VERSION: ${{ steps.derive.outputs.version }} + ASC_LATEST_BUILD: ${{ steps.asc-build.outputs.latest-build }} + GH_RUN_NUMBER: ${{ github.run_number }} with: script: | - core.setOutput('ref', '${{ steps.pre.outputs.ref }}'); - core.setOutput('should-build', '${{ steps.pre.outputs.should-build }}'); - core.setOutput('upload', '${{ steps.pre.outputs.upload }}'); - core.setOutput('netbird-ref', '${{ steps.pre.outputs.netbird-ref }}'); - - const overrideBuild = '${{ steps.pre.outputs.build-number }}'; - const latestBuild = '${{ steps.asc-build.outputs.latest-build }}'; + core.setOutput('ref', process.env.PRE_REF); + core.setOutput('should-build', process.env.PRE_SHOULD_BUILD); + core.setOutput('upload', process.env.PRE_UPLOAD); + core.setOutput('netbird-ref', process.env.PRE_NETBIRD_REF); + + const overrideBuild = process.env.PRE_BUILD_NUMBER || ''; + const latestBuild = process.env.ASC_LATEST_BUILD || ''; + const runNumber = process.env.GH_RUN_NUMBER || ''; let buildNumber; - if (overrideBuild && overrideBuild !== '${{ github.run_number }}') { + if (overrideBuild && overrideBuild !== runNumber) { buildNumber = overrideBuild; } else if (latestBuild && latestBuild !== 'none') { buildNumber = String(parseInt(latestBuild, 10) + 1); @@ -215,8 +226,8 @@ jobs: core.info(`build-number: ${buildNumber} (latest=${latestBuild}, override=${overrideBuild})`); core.setOutput('build-number', buildNumber); - const override = '${{ steps.pre.outputs.version-override }}'; - const derived = '${{ steps.derive.outputs.version }}'; + const override = process.env.PRE_VERSION_OVERRIDE || ''; + const derived = process.env.DERIVE_VERSION || ''; core.setOutput('version', override || derived); core.info(`version: ${override || derived} (override=${override || 'none'}, derived=${derived})`); From ba6dcddeb54ffa53fe32b0156c4dbe057df3f974 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:53:08 +0200 Subject: [PATCH 12/13] feat(ci): auto build number from App Store Connect --- .github/workflows/testflight.yml | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index d295a9d..a88c5eb 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -303,3 +303,43 @@ jobs: issue_number: prNumber, body, }); + + notify-merge: + name: Comment on merge commit + needs: [gate, build, build-tvos] + if: > + always() && + github.event_name == 'push' && + needs.gate.outputs.should-build == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Comment on commit + uses: actions/github-script@v7 + env: + VERSION: ${{ needs.gate.outputs.version }} + BUILD_NUMBER: ${{ needs.gate.outputs.build-number }} + IOS_RESULT: ${{ needs.build.result }} + TVOS_RESULT: ${{ needs.build-tvos.result }} + with: + script: | + const version = process.env.VERSION; + const buildNumber = process.env.BUILD_NUMBER; + const iosResult = process.env.IOS_RESULT; + const tvosResult = process.env.TVOS_RESULT; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + const iosOk = iosResult === 'success'; + const tvosOk = tvosResult === 'success'; + const iosBadge = iosOk ? '✅' : '❌'; + const tvosBadge = tvosOk ? '✅' : '❌'; + + const body = `**TestFlight** \`${version} (${buildNumber})\` — iOS ${iosBadge} tvOS ${tvosBadge}\n\n[View workflow run](${runUrl})`; + + await github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body, + }); From ea5013a0cd94af3ee10ed0dd0a8352ad1232afd7 Mon Sep 17 00:00:00 2001 From: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:55:00 +0200 Subject: [PATCH 13/13] Code refactoring --- .github/workflows/testflight.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index a88c5eb..c48f901 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -161,7 +161,10 @@ jobs: run: | VERSION="${{ steps.pre.outputs.version-override || steps.derive.outputs.version }}" + pip install cryptography --quiet + echo "$PRIVATE_KEY_BASE64" | base64 --decode > /tmp/AuthKey.p8 + trap 'rm -f /tmp/AuthKey.p8' EXIT JWT=$(python3 -c "import base64,json,time,os;from cryptography.hazmat.primitives import hashes,serialization;from cryptography.hazmat.primitives.asymmetric import ec;from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature;key=serialization.load_pem_private_key(open('/tmp/AuthKey.p8','rb').read(),password=None);b64url=lambda d:base64.urlsafe_b64encode(d if isinstance(d,bytes) else d.encode()).rstrip(b'=').decode();now=int(time.time());h=b64url(json.dumps({'alg':'ES256','kid':os.environ['KEY_ID'],'typ':'JWT'},separators=(',',':')));p=b64url(json.dumps({'iss':os.environ['ISSUER_ID'],'exp':now+1200,'aud':'appstoreconnect-v1'},separators=(',',':')));msg=f'{h}.{p}'.encode();sig_der=key.sign(msg,ec.ECDSA(hashes.SHA256()));r,s=decode_dss_signature(sig_der);sig=b64url(r.to_bytes(32,'big')+s.to_bytes(32,'big'));print(f'{h}.{p}.{sig}')") @@ -180,7 +183,7 @@ jobs: exit 1 fi - LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 then .data[0].attributes.version else "none" end') + LATEST_BUILD=$(echo "$RESPONSE" | jq -r 'if (.data | length) > 0 and (.data[0].attributes.version != null) then .data[0].attributes.version else "none" end') echo "latest-build=$LATEST_BUILD" >> "$GITHUB_OUTPUT" @@ -190,7 +193,6 @@ jobs: echo " Latest uploaded build: $LATEST_BUILD" echo "=========================================" - rm -f /tmp/AuthKey.p8 - name: Finalize outputs id: finalize @@ -219,7 +221,8 @@ jobs: if (overrideBuild && overrideBuild !== runNumber) { buildNumber = overrideBuild; } else if (latestBuild && latestBuild !== 'none') { - buildNumber = String(parseInt(latestBuild, 10) + 1); + const parsed = parseInt(latestBuild, 10); + buildNumber = !isNaN(parsed) ? String(parsed + 1) : '1'; } else { buildNumber = '1'; }