Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
72 changes: 72 additions & 0 deletions .github/workflows/android-device.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Android device tests

on:
workflow_call:
inputs:
api:
required: true
type: number

permissions:
contents: read

jobs:
device:
name: Device tests
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
ANDROID_SERIAL: emulator-5554
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
- uses: gradle/actions/setup-gradle@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.10"
bundler-cache: true
- name: Enable hardware acceleration
run: |
test -e /dev/kvm
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Install build tools
run: '"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-36" "build-tools;36.0.0"'
- name: Build test APKs
run: ./gradlew assembleDebug assembleDebugAndroidTest --no-daemon
- name: Run on disposable Android emulator
uses: reactivecircus/android-emulator-runner@v2.38.0
with:
api-level: ${{ inputs.api }}
target: google_apis
arch: x86_64
profile: pixel_5
avd-name: message487-tests
disable-animations: true
emulator-options: -accel on -no-window -no-snapshot -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none
script: |
mkdir -p test-results/android-api${{ inputs.api }}
adb logcat -v threadtime > test-results/android-api${{ inputs.api }}/logcat.txt &
bundle exec fastlane android device_tests
- name: Upload Android evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: android-api${{ inputs.api }}-results
path: |
test-results/android-api${{ inputs.api }}/
app/build/reports/androidTests/connected/
retention-days: 14
- name: Upload emulator JUnit reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: junit-Android API ${{ inputs.api }}
path: app/build/outputs/androidTest-results/connected/**/*.xml
retention-days: 14
133 changes: 128 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,34 @@ jobs:
app/build/reports/lint-results-*
if-no-files-found: warn
retention-days: 14
- name: Upload Android JUnit reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: junit-Android JVM
path: app/build/test-results/testDebugUnitTest/*.xml
retention-days: 14
- uses: actions/upload-artifact@v7
id: apk
with:
name: message487-builds
path: |
app/build/outputs/apk/**/*.apk
name: message487-pr-${{ github.event.pull_request.number || github.run_id }}-debug-apk
path: app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
retention-days: 14
- uses: actions/upload-artifact@v7
id: release_apk
with:
name: message487-pr-${{ github.event.pull_request.number || github.run_id }}-unsigned-release-apk
path: app/build/outputs/apk/release/app-release-unsigned.apk
if-no-files-found: error
retention-days: 14
- name: Link build artifacts
env:
ARTIFACT_URL: ${{ steps.apk.outputs.artifact-url }}
run: echo "[Download debug APK and unsigned release APK]($ARTIFACT_URL)" >> "$GITHUB_STEP_SUMMARY"
DEBUG_URL: ${{ steps.apk.outputs.artifact-url }}
RELEASE_URL: ${{ steps.release_apk.outputs.artifact-url }}
run: |
echo "[Download debug APK]($DEBUG_URL)" >> "$GITHUB_STEP_SUMMARY"
echo "[Download unsigned release APK]($RELEASE_URL)" >> "$GITHUB_STEP_SUMMARY"

python:
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
Expand All @@ -122,6 +138,13 @@ jobs:
cache-dependency-path: requirements-dev.txt
- run: python -m pip install -r requirements-dev.txt
- run: bundle exec fastlane android python_checks
- name: Upload Python JUnit reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: junit-Python
path: test-results/python/**/*.xml
retention-days: 14

dev-server:
name: Development server tests
Expand All @@ -137,10 +160,110 @@ jobs:
with:
ruby-version: "3.4.10"
bundler-cache: true
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: python -m pip install -r requirements-dev.txt
- run: bundle exec fastlane android server_tests
- name: Upload n8n JUnit reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: junit-n8n
path: test-results/server/*.xml
retention-days: 14
- name: Server logs
if: failure()
run: docker compose -f DevServer/compose.yaml logs --tail 150
- name: Stop server
if: always()
run: docker compose -f DevServer/compose.yaml down

emulator26:
name: Android emulator API 26
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
uses: ./.github/workflows/android-device.yml
with:
api: 26

emulator35:
name: Android emulator API 35
if: github.event_name != 'workflow_dispatch' || inputs.release_dry_run_version == ''
uses: ./.github/workflows/android-device.yml
with:
api: 35

report-android:
name: Publish Android JVM results
needs: android
if: >-
!cancelled() && needs.android.result != 'skipped' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(Android JVM)$/'

report-python:
name: Publish Python results
needs: python
if: >-
!cancelled() && needs.python.result != 'skipped' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(Python)$/'

report-dev-server:
name: Publish n8n results
needs: dev-server
if: >-
!cancelled() && needs.dev-server.result != 'skipped' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(n8n)$/'

report-emulator26:
name: Publish Android API 26 results
needs: emulator26
if: >-
!cancelled() && needs.emulator26.result != 'skipped' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(Android API 26)$/'

report-emulator35:
name: Publish Android API 35 results
needs: emulator35
if: >-
!cancelled() && needs.emulator35.result != 'skipped' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(Android API 35)$/'
107 changes: 107 additions & 0 deletions .github/workflows/pr-artifacts-description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: PR artifact links

on:
workflow_run:
workflows:
- CI
types:
- completed

permissions:
actions: read
contents: read
pull-requests: write

jobs:
description:
name: Add APK links to the pull request
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: pr-apk-links-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.id }}
cancel-in-progress: false
steps:
- name: Create or update the APK artifact description block
uses: actions/github-script@v9
with:
script: |
const run = context.payload.workflow_run;
const pullRequests = run.pull_requests || [];

if (pullRequests.length !== 1) {
core.notice(`Expected one pull request for workflow run ${run.id}, found ${pullRequests.length}`);
return;
}

const pullNumber = pullRequests[0].number;
const expectedArtifacts = {
debug: `message487-pr-${pullNumber}-debug-apk`,
release: `message487-pr-${pullNumber}-unsigned-release-apk`,
};
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
per_page: 100,
}
);

const findArtifact = (name) => artifacts.find(
(artifact) => artifact.name === name && !artifact.expired
);
const debugArtifact = findArtifact(expectedArtifacts.debug);
const releaseArtifact = findArtifact(expectedArtifacts.release);

if (!debugArtifact || !releaseArtifact) {
core.setFailed(`Expected APK artifacts were not found for workflow run ${run.id}`);
return;
}

const artifactUrl = (artifact) =>
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
`/actions/runs/${run.id}/artifacts/${artifact.id}`;
const marker = '<!-- message487-pr-apks -->';
const body = [
marker,
`<!-- message487-pr-apks-run: ${run.run_number}:${run.run_attempt || 1} -->`,
'## APK artifacts',
'',
`- [Download debug APK](${artifactUrl(debugArtifact)}) — signed with the disposable Android debug key`,
`- [Download unsigned release APK](${artifactUrl(releaseArtifact)}) — verified unsigned`,
'',
`Built from commit \`${run.head_sha.slice(0, 8)}\` by [CI run #${run.run_number}](${run.html_url}).`,
'Artifacts expire after 14 days. Neither APK uses the Message487 release key.',
'<!-- /message487-pr-apks -->',
].join('\n');

const { data: pull } = await github.rest.pulls.get({
...context.repo, pull_number: pullNumber,
});
if (pull.state !== 'open' || pull.head.sha !== run.head_sha) {
core.notice('PR is closed or its head changed; keeping the existing description');
return;
}
const description = pull.body || '';
const previousRun = description.match(/<!-- message487-pr-apks-run: (\d+):(\d+) -->/);
if (previousRun && (
Number(previousRun[1]) > run.run_number ||
(Number(previousRun[1]) === run.run_number && Number(previousRun[2]) > (run.run_attempt || 1))
)) {
core.notice('A newer CI run already published its artifacts');
return;
}
const block = /<!-- message487-pr-apks -->[\s\S]*?<!-- \/message487-pr-apks -->/g;
const authorBody = description.replace(block, '').trimEnd();
if (authorBody.includes(marker) || authorBody.includes('<!-- /message487-pr-apks -->')) {
core.setFailed('Malformed APK block markers; refusing to overwrite the PR description');
return;
}
await github.rest.pulls.update({
...context.repo, pull_number: pullNumber,
body: authorBody ? `${authorBody}\n\n${body}` : body,
});
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ jobs:
app/build/reports/lint-results-*
retention-days: 14

- name: Upload release JUnit reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: junit-Release Android JVM
path: app/build/test-results/testDebugUnitTest/*.xml
retention-days: 14

report-tests:
name: Publish release Android JVM results
needs: release
if: ${{ !cancelled() && needs.release.result != 'skipped' }}
permissions:
contents: read
actions: read
checks: write
uses: ./.github/workflows/test-reports.yml
with:
artifact: '/^junit-(Release Android JVM)$/'

publish:
name: Publish GitHub Release
needs: release
Expand Down
Loading
Loading