Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 137 additions & 29 deletions .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write

concurrency:
group: testflight-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
group: testflight-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: true

jobs:
Expand All @@ -21,36 +23,137 @@
if: >
github.event_name == 'push' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'issue_comment'
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.resolve.outputs.ref }}
should-build: ${{ steps.resolve.outputs.should-build }}
upload: ${{ steps.resolve.outputs.upload }}
build-number: ${{ steps.resolve.outputs.build-number }}
ref: ${{ steps.finalize.outputs.ref }}
should-build: ${{ steps.finalize.outputs.should-build }}
upload: ${{ steps.finalize.outputs.upload }}
netbird-ref: ${{ steps.finalize.outputs.netbird-ref }}
version: ${{ steps.finalize.outputs.version }}
build-number: ${{ steps.finalize.outputs.build-number }}
steps:
- name: Resolve build ref
id: resolve
- name: Resolve ref and check permissions
id: pre
uses: actions/github-script@v7
with:
script: |
core.setOutput('build-number', '${{ github.run_number }}');
core.setOutput('should-build', 'true');
core.setOutput('upload', 'true');
core.setOutput('should-build', 'false');

if (context.eventName === 'push') {
core.setOutput('ref', context.sha);
} else if (context.eventName === 'pull_request') {
core.setOutput('should-build', 'true');
core.setOutput('upload', 'true');
return;
}

if (context.eventName === 'pull_request') {
core.setOutput('ref', context.payload.pull_request.head.sha);
core.setOutput('should-build', 'true');
core.setOutput('upload', 'true');
return;
}

if (context.eventName === 'issue_comment') {
if (!context.payload.issue.pull_request) {
core.info('Not a PR comment — skipping');
return;
}
const body = context.payload.comment.body || '';
if (!body.includes('/testflight')) {
core.info('No /testflight command — skipping');
Comment thread
evgeniyChepelev marked this conversation as resolved.
return;
}

// Permission check
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const level = perm.permission;
if (level !== 'admin' && level !== 'write') {
core.info(`User ${context.payload.comment.user.login} has '${level}' — skipping`);
return;
}

// Get PR head SHA
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue.number,
});
core.setOutput('ref', pr.data.head.sha);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
core.setOutput('should-build', 'true');
core.setOutput('upload', 'true');
Comment thread
evgeniyChepelev marked this conversation as resolved.

// Parse overrides from /testflight line
const line = body.split('\n').find(l => l.trim().startsWith('/testflight')) || '';
const versionMatch = line.match(/version=(\S+)/);
if (versionMatch) core.setOutput('version-override', versionMatch[1]);
const buildMatch = line.match(/build-number=(\S+)/);
if (buildMatch) core.setOutput('build-number', buildMatch[1]);
const netbirdMatch = line.match(/netbird-ref=(\S+)/);
if (netbirdMatch) core.setOutput('netbird-ref', netbirdMatch[1]);
Comment thread
evgeniyChepelev marked this conversation as resolved.
Outdated
}

- name: Checkout for version derivation
if: steps.pre.outputs.should-build == 'true'
uses: actions/checkout@v4
with:
ref: ${{ steps.pre.outputs.ref }}
fetch-depth: 0
fetch-tags: true
sparse-checkout: .

- name: Derive version from git tags

Check failure

Code scanning / CodeQL

Checkout of untrusted code in trusted context High test

Potential execution of untrusted code on a privileged workflow (
issue_comment
)

Check failure

Code scanning / CodeQL

Untrusted Checkout TOCTOU High test

Insufficient protection against execution of untrusted code on a privileged workflow (
issue_comment
).
Comment thread
evgeniyChepelev marked this conversation as resolved.
Dismissed
Comment thread
evgeniyChepelev marked this conversation as resolved.
Dismissed
if: steps.pre.outputs.should-build == 'true'
id: derive
run: |
LATEST_TAG=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
echo "::error::No v* tags found — cannot derive version"
exit 1
elif ! echo "$LATEST_TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Tag '$LATEST_TAG' is not valid semver (expected vX.Y.Z)"
exit 1
fi
VERSION="${LATEST_TAG#v}"
MAJOR="${VERSION%%.*}"
REST="${VERSION#*.}"
MINOR="${REST%%.*}"
PATCH="${REST#*.}"
NEXT="${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "Tag: $LATEST_TAG → next: $NEXT"

- name: Finalize outputs
id: finalize
uses: actions/github-script@v7
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('version', override || derived);
core.info(`version: ${override || derived} (override=${override || 'none'}, derived=${derived})`);

build:
name: Build and Upload iOS
needs: gate
if: needs.gate.outputs.should-build == 'true'
uses: ./.github/workflows/build-upload.yml
with:
ref: ${{ needs.gate.outputs.ref }}
netbird-ref: ${{ needs.gate.outputs.netbird-ref }}
version: ${{ needs.gate.outputs.version }}
build-number: ${{ needs.gate.outputs.build-number }}
upload: ${{ needs.gate.outputs.upload == 'true' }}
secrets: inherit
Expand All @@ -69,7 +172,10 @@
notify:
name: Notify PR
needs: [gate, build, build-tvos]
if: always() && github.event_name == 'pull_request'
if: >
always() &&
(github.event_name == 'pull_request' || github.event_name == 'issue_comment') &&
needs.gate.outputs.should-build == 'true'
runs-on: ubuntu-latest
permissions:
pull-requests: write
Expand All @@ -78,27 +184,29 @@
uses: actions/github-script@v7
with:
script: |
const iosResult = '${{ needs.build.result }}';
const tvosResult = '${{ needs.build-tvos.result }}';
const ref = '${{ needs.gate.outputs.ref }}';
const shortSha = ref.substring(0, 7);
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const prNumber = context.payload.pull_request.number;
const iosResult = '${{ needs.build.result }}';
const tvosResult = '${{ needs.build-tvos.result }}';
const ref = '${{ needs.gate.outputs.ref }}';
const shortSha = ref.substring(0, 7);
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const buildNumber = '${{ needs.gate.outputs.build-number }}';
const version = '${{ needs.gate.outputs.version }}';

const prNumber = context.eventName === 'pull_request'
? context.payload.pull_request.number
: context.payload.issue.number;

const iosOk = iosResult === 'success';
const tvosOk = tvosResult === 'success';
const iosFailed = iosResult === 'failure';
const tvosFailed = tvosResult === 'failure';
const iosOk = iosResult === 'success';
const tvosOk = tvosResult === 'success';
const iosFail = iosResult === 'failure';
const tvosFail = tvosResult === 'failure';

let body;
if (iosOk && tvosOk) {
body = `**TestFlight builds uploaded** \`(${buildNumber})\` for \`${shortSha}\` — iOS + tvOS\n\n[View workflow run](${runUrl})`;
} else if (iosOk && !tvosFailed) {
body = `**TestFlight build uploaded** \`(${buildNumber})\` for \`${shortSha}\` — iOS only (tvOS skipped)\n\n[View workflow run](${runUrl})`;
} else if (iosFailed || tvosFailed) {
const failed = [iosFailed && 'iOS', tvosFailed && 'tvOS'].filter(Boolean).join(', ');
body = `**Build failed** (${failed}) for \`${shortSha}\`\n\n[View workflow run](${runUrl})`;
body = `**TestFlight builds uploaded** \`${version} (${buildNumber})\` for \`${shortSha}\` — iOS + tvOS\n\n[View workflow run](${runUrl})`;
} else if ((iosFail || tvosFail)) {
const failed = [iosFail && 'iOS', tvosFail && 'tvOS'].filter(Boolean).join(', ');
body = `**Build failed** (${failed}) \`${version} (${buildNumber})\` for \`${shortSha}\`\n\n[View workflow run](${runUrl})`;
} else {
return;
}
Expand All @@ -107,5 +215,5 @@
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body,
body,
});
Loading