diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 932b69f..c48f901 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'); @@ -149,19 +150,87 @@ 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' + id: asc-build + 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 }}" + + 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}')") + + 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 and (.data[0].attributes.version != null) 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" + echo " Latest uploaded build: $LATEST_BUILD" + echo "=========================================" + + - 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('build-number', '${{ steps.pre.outputs.build-number }}'); - core.setOutput('netbird-ref', '${{ steps.pre.outputs.netbird-ref }}'); - - const override = '${{ steps.pre.outputs.version-override }}'; - const derived = '${{ steps.derive.outputs.version }}'; + 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 !== runNumber) { + buildNumber = overrideBuild; + } else if (latestBuild && latestBuild !== 'none') { + const parsed = parseInt(latestBuild, 10); + buildNumber = !isNaN(parsed) ? String(parsed + 1) : '1'; + } else { + buildNumber = '1'; + } + core.info(`build-number: ${buildNumber} (latest=${latestBuild}, override=${overrideBuild})`); + core.setOutput('build-number', buildNumber); + + 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})`); @@ -237,3 +306,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, + });