ci: reduced build of changed scopes on drafts and manual runs (#7870)

* ci: reduced build of changed scopes on drafts and manual runs

* ci: tests only when selected in reduced builds

* ci: test selection and retries in scripts/ci/run-tests.sh
This commit is contained in:
Evgeny @ SimpleX Chat authored and GitHub committed 2026-10-26 11:19:50 +00:00
1 parent c3b709a807
commit 85ba221d79
11 files changed
+388 -121

No files matched your search

-33
View File
@@ -1,33 +0,0 @@
name: badge web
on:
push:
branches:
- master
- stable
paths:
- "apps/simplex-badge-service/web/**"
- ".github/workflows/badge-web.yml"
pull_request:
paths:
- "apps/simplex-badge-service/web/**"
- ".github/workflows/badge-web.yml"
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/simplex-badge-service/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: apps/simplex-badge-service/web/package-lock.json
- run: npm ci
- run: npm test
+147 -74
View File
@@ -29,10 +29,28 @@ on:
- "scripts/ci/ios-stub-libs.sh"
- "scripts/desktop/**"
- ".github/**"
- "packages/simplex-chat-nodejs/**"
- "packages/simplex-chat-client/types/typescript/**"
- "packages/simplex-chat-python/**"
- "scripts/ci/package-tests.sh"
- "apps/simplex-badge-service/web/**"
- "scripts/ci/packages_release.py"
- "scripts/ci/test_packages_release.py"
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
inputs:
tests:
description: "hspec match patterns, one per line; / runs all tests, empty runs none"
type: string
default: ""
base:
description: the branch that the changes are compared with
type: string
default: master
env:
opt: ${{ github.event_name == 'pull_request' && 'O0' || 'O2' }}
ghc_options: ${{ github.event_name == 'pull_request' && '--ghc-options=-O0' || '' }}
opt: ${{ github.event_name == 'push' && 'O2' || 'O0' }}
ghc_options: ${{ github.event_name != 'push' && '--ghc-options=-O0' || '' }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
branch: ${{ github.base_ref || github.ref_name }}
cache_o0: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable') }}
@@ -104,30 +122,48 @@ jobs:
publish_nodejs: ${{ steps.publish.outputs.nodejs_version }}
publish_python: ${{ steps.publish.outputs.python_version }}
publish: ${{ steps.publish.outputs.nodejs_version || steps.publish.outputs.python_version }}
packages: ${{ steps.changes.outputs.packages }}
scopes: ${{ steps.changes.outputs.scopes }}
reduced: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == true }}
steps:
- name: Reject a manual run on a tag
if: github.event_name == 'workflow_dispatch' && github.ref_type == 'tag'
run: |
echo "::error::A manual run starts on a branch; to retry a release, re-run the jobs of its tag push."
exit 1
- name: Clone project
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Check changes
id: changes
if: github.event_name == 'push' || github.event_name == 'pull_request'
shell: bash
env:
BASE: ${{ github.event.pull_request.base.sha }}
BASE_BRANCH: ${{ inputs.base }}
GH_TOKEN: ${{ github.token }}
run: |
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
git fetch -q --depth=1 origin "$BASE"
python3 scripts/ci/packages_release.py changes "$BASE"
else
echo "haskell=true"
if [ "$GITHUB_REF_TYPE" = branch ]; then
echo "kotlin=true"
echo "ios=true"
fi
fi >> "$GITHUB_OUTPUT"
case "$GITHUB_EVENT_NAME" in
pull_request)
git fetch -q --depth=1 origin "$BASE"
python3 scripts/ci/packages_release.py changes "$BASE"
;;
workflow_dispatch)
base=$(gh api "repos/$GITHUB_REPOSITORY/compare/$BASE_BRANCH...$GITHUB_REF_NAME" --jq .merge_base_commit.sha)
git fetch -q --depth=1 origin "$base"
python3 scripts/ci/packages_release.py changes "$base"
;;
*)
echo "haskell=true"
if [ "$GITHUB_REF_TYPE" = branch ]; then
echo "kotlin=true"
echo "ios=true"
python3 scripts/ci/packages_release.py scopes
fi
;;
esac >> "$GITHUB_OUTPUT"
- name: Build changelog
id: build_changelog
@@ -223,16 +259,17 @@ jobs:
build-linux:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: needs.maybe-release.outputs.haskell == 'true' || needs.maybe-release.outputs.scopes != '' || (needs.maybe-release.outputs.reduced == 'true' && (needs.maybe-release.outputs.kotlin == 'true' || needs.maybe-release.outputs.packages == 'true'))
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
env:
image_cache: ghcr.io/simplex-chat/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
image_cache: ghcr.io/${{ github.repository_owner }}/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
concurrency: &linux-concurrency
group: ${{ github.workflow }}-${{ github.ref }}-linux-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ghc }}-${{ needs.maybe-release.outputs.libs_tag }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
cancel-in-progress: ${{ github.event_name != 'push' }}
strategy:
fail-fast: false
matrix:
@@ -245,17 +282,12 @@ jobs:
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5'
libs: true
- os: 24.04
os_underscore: 24_04
arch: x86_64
runner: "ubuntu-24.04"
should_run: ${{ needs.maybe-release.outputs.haskell == 'true' }}
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237'
kotlin: ${{ needs.maybe-release.outputs.kotlin == 'true' }}
kotlin: ${{ needs.maybe-release.outputs.reduced == 'true' && needs.maybe-release.outputs.kotlin == 'true' }}
packages: ${{ needs.maybe-release.outputs.reduced == 'true' && needs.maybe-release.outputs.packages == 'true' }}
scope_tests: ${{ needs.maybe-release.outputs.scopes != '' }}
steps: &linux-steps
- name: Checkout Code
if: matrix.should_run == true || matrix.kotlin == true
if: matrix.should_run == true || matrix.kotlin == true || matrix.packages == true || matrix.scope_tests == true
uses: actions/checkout@v3
with:
submodules: recursive
@@ -388,7 +420,7 @@ jobs:
- name: Restore library build directory from cache
id: restore-build-lib
if: env.release != 'true' && matrix.libs && needs.maybe-release.outputs.core_changed && matrix.arch == 'x86_64'
if: env.release != 'true' && matrix.libs && needs.maybe-release.outputs.core_changed && matrix.arch == 'x86_64' && (needs.maybe-release.outputs.reduced != 'true' || matrix.packages == true)
uses: actions/cache/restore@v4
with:
path: |
@@ -434,7 +466,7 @@ jobs:
# Also for pull requests that change the chat core, for the package tests.
- name: Build library
id: build-lib
if: matrix.libs && (needs.maybe-release.outputs.libs_tag || (needs.maybe-release.outputs.core_changed && matrix.arch == 'x86_64'))
if: matrix.libs && (needs.maybe-release.outputs.libs_tag || (needs.maybe-release.outputs.core_changed && matrix.arch == 'x86_64' && (needs.maybe-release.outputs.reduced != 'true' || matrix.packages == true)))
shell: docker exec -t builder sh -eu {0}
run: |
scripts/ci/prune-old-builds.sh dist-lib
@@ -618,23 +650,12 @@ jobs:
if: matrix.should_run == true && matrix.arch == 'x86_64'
timeout-minutes: 120
shell: bash
run: |
i=1
attempts=1
${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3
while [ "$i" -le "$attempts" ]; do
if ./simplex-chat-test; then
break
else
echo "Attempt $i failed, retrying..."
i=$((i + 1))
sleep 1
fi
done
if [ "$i" -gt "$attempts" ]; then
echo "All "$attempts" attempts failed."
exit 1
fi
env:
REDUCED: ${{ needs.maybe-release.outputs.reduced }}
TESTS: ${{ github.event_name == 'workflow_dispatch' && inputs.tests || '' }}
PR_BODY: ${{ github.event.pull_request.draft == true && github.event.pull_request.body || '' }}
ATTEMPTS: ${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) && 3 || 1 }}
run: scripts/ci/run-tests.sh ./simplex-chat-test
# Compiling the desktop and Android targets of :common in parallel exceeds the 2 GB Kotlin daemon heap.
# cmakeBuildAndCopy links the JNI library against libsimplex, which this job does not build.
@@ -652,7 +673,7 @@ jobs:
- name: Setup Node.js
id: setup-node
if: ${{ !cancelled() && matrix.should_run == true && matrix.libs && (github.event_name == 'pull_request' || needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
if: ${{ !cancelled() && (matrix.packages == true || matrix.scope_tests == true || (matrix.should_run == true && matrix.libs && ((github.event_name == 'pull_request' && needs.maybe-release.outputs.reduced != 'true') || needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish))) }}
uses: actions/setup-node@v7
with:
node-version: 22
@@ -665,7 +686,7 @@ jobs:
python-version: "3.12"
- name: Package tests (pull request)
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && github.event_name == 'pull_request' && steps.build-lib.outcome != 'failure' }}
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && github.event_name != 'push' && steps.build-lib.outcome != 'failure' && ((needs.maybe-release.outputs.reduced != 'true' && matrix.should_run == true) || matrix.packages == true) }}
timeout-minutes: 30
shell: bash
env:
@@ -701,6 +722,17 @@ jobs:
name: package-tests-linux-${{ matrix.arch }}
path: ${{ runner.temp }}/package-tests
- name: Test changed scopes
if: ${{ !cancelled() && matrix.scope_tests == true && steps.setup-python.outcome == 'success' }}
timeout-minutes: 30
shell: bash
env:
SCOPES: ${{ needs.maybe-release.outputs.scopes }}
run: |
for scope in $SCOPES; do
scripts/ci/scope-tests.sh "$scope"
done
- name: Remove -O2 build directory
id: remove-o2
if: env.cache_o0 == 'true' && !cancelled() && steps.build-cli.outcome == 'success'
@@ -746,16 +778,42 @@ jobs:
!dist-newstyle/src
key: ${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-${{ github.sha }}
build-linux-push:
build-linux-full:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: github.event_name != 'pull_request'
if: needs.maybe-release.outputs.reduced != 'true' && (needs.maybe-release.outputs.haskell == 'true' || needs.maybe-release.outputs.kotlin == 'true')
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
env:
image_cache: ghcr.io/simplex-chat/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
image_cache: ghcr.io/${{ github.repository_owner }}/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
concurrency: *linux-concurrency
strategy:
fail-fast: false
matrix:
include:
- os: 24.04
os_underscore: 24_04
arch: x86_64
runner: "ubuntu-24.04"
should_run: ${{ needs.maybe-release.outputs.haskell == 'true' }}
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237'
kotlin: ${{ needs.maybe-release.outputs.kotlin == 'true' }}
steps: *linux-steps
build-linux-push:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: github.event_name == 'push'
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
env:
image_cache: ghcr.io/${{ github.repository_owner }}/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
cache_o0: false
concurrency: *linux-concurrency
@@ -785,13 +843,13 @@ jobs:
build-linux-ghc8:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: ${{ needs.maybe-release.outputs.haskell == 'true' && !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
if: ${{ needs.maybe-release.outputs.reduced != 'true' && needs.maybe-release.outputs.haskell == 'true' && !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
env:
image_cache: ghcr.io/simplex-chat/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
image_cache: ghcr.io/${{ github.repository_owner }}/simplex-chat-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
concurrency: *linux-concurrency
strategy:
@@ -930,6 +988,7 @@ jobs:
build-macos:
name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: needs.maybe-release.outputs.reduced != 'true' && needs.maybe-release.outputs.haskell == 'true'
runs-on: ${{ matrix.os }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-macos-${{ matrix.os }}-${{ needs.maybe-release.outputs.libs_tag }}
@@ -945,7 +1004,6 @@ jobs:
desktop_asset_name: simplex-desktop-macos-aarch64.dmg
openssl_dir: "/opt/homebrew/opt"
should_run: ${{ needs.maybe-release.outputs.haskell == 'true' }}
ios: ${{ needs.maybe-release.outputs.ios == 'true' }}
- os: macos-15-intel
arch: x86_64
ghc: ${{ needs.variables.outputs.GHC_VER }}
@@ -955,13 +1013,13 @@ jobs:
should_run: ${{ needs.maybe-release.outputs.haskell == 'true' }}
steps:
- name: Checkout Code
if: matrix.should_run == true || matrix.ios == true
if: matrix.should_run == true
uses: actions/checkout@v3
with:
submodules: recursive
- name: Point Command Line Tools SDK to Xcode SDK
if: matrix.should_run == true || matrix.ios == true
if: matrix.should_run == true
shell: bash
run: sudo ln -sfn "$(xcrun --sdk macosx --show-sdk-path)" /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
@@ -1183,23 +1241,6 @@ jobs:
exit 1
fi
- name: Build iOS app
if: ${{ !cancelled() && matrix.ios == true }}
timeout-minutes: 60
shell: bash
run: |
scripts/ci/ios-stub-libs.sh
xcodebuild build -quiet \
-project apps/ios/SimpleX.xcodeproj \
-scheme "SimpleX (iOS)" \
-configuration Debug \
-destination "generic/platform=iOS Simulator" \
-onlyUsePackageVersionsFromResolvedFile \
-skipPackagePluginValidation \
-skipMacroValidation \
ARCHS=arm64 \
CODE_SIGNING_ALLOWED=NO
- name: Build library
id: mac_lib_build
if: ${{ !cancelled() && matrix.should_run == true && needs.maybe-release.outputs.libs_tag && steps.mac_cli_build.outcome == 'success' }}
@@ -1309,6 +1350,38 @@ jobs:
!dist-newstyle/src
key: ${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-${{ github.sha }}
build-ios:
name: "macos-latest (iOS)"
needs: maybe-release
if: needs.maybe-release.outputs.ios == 'true'
runs-on: macos-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-ios
cancel-in-progress: ${{ github.event_name != 'push' }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Point Command Line Tools SDK to Xcode SDK
shell: bash
run: sudo ln -sfn "$(xcrun --sdk macosx --show-sdk-path)" /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
- name: Build iOS app
timeout-minutes: 60
shell: bash
run: |
scripts/ci/ios-stub-libs.sh
xcodebuild build -quiet \
-project apps/ios/SimpleX.xcodeproj \
-scheme "SimpleX (iOS)" \
-configuration Debug \
-destination "generic/platform=iOS Simulator" \
-onlyUsePackageVersionsFromResolvedFile \
-skipPackagePluginValidation \
-skipMacroValidation \
ARCHS=arm64 \
CODE_SIGNING_ALLOWED=NO
# =========================
# Windows Build
# =========================
@@ -1316,7 +1389,7 @@ jobs:
build-windows:
name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
needs: [maybe-release, variables]
if: needs.maybe-release.outputs.haskell == 'true'
if: needs.maybe-release.outputs.reduced != 'true' && needs.maybe-release.outputs.haskell == 'true'
runs-on: ${{ matrix.os }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-windows-${{ matrix.os }}-${{ needs.maybe-release.outputs.libs_tag }}
@@ -1622,7 +1695,7 @@ jobs:
# =========================
releases:
needs: [maybe-release, build-linux, build-linux-push, build-linux-postgres, build-macos, build-windows]
needs: [maybe-release, build-linux, build-linux-full, build-linux-push, build-linux-postgres, build-macos, build-ios, build-windows]
if: ${{ !cancelled() && (needs.maybe-release.outputs.libs_tag || needs.maybe-release.outputs.addon_draft) }}
runs-on: ubuntu-latest
env:
@@ -1646,7 +1719,7 @@ jobs:
# publish under other names.
publish:
needs: [maybe-release, build-linux, build-linux-push, build-macos, build-windows, releases]
needs: [maybe-release, build-linux, build-linux-full, build-linux-push, build-macos, build-ios, build-windows, releases]
if: ${{ !cancelled() && (needs.maybe-release.outputs.publish || needs.maybe-release.outputs.publish_types) }}
runs-on: ubuntu-latest
permissions:
+2
View File
@@ -7,6 +7,7 @@ on:
- "packages/simplex-chat-client/types/typescript/**"
- "packages/simplex-chat-python/**"
- ".github/workflows/packages.yml"
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
# build.yml and this workflow both run for a pull request that changes the core and the packages
@@ -23,6 +24,7 @@ env:
jobs:
addon:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
platforms: ${{ steps.plan.outputs.platforms }}
+144 -2
View File
@@ -1,10 +1,152 @@
# App tests on GitHub's runners
# CI on GitHub's runners
Three workflows build the apps from a branch's own code and test them, only when asked: `ios-sim` (the iOS app in a simulator on a macOS runner), `desktop-linux` (the desktop app on a Linux runner) and `android-emu` (the Android app in an emulator on a Linux runner). Each runs a scripted smoke test and, on request, a Claude session that follows a test plan, and a test backend (relay, directory, names server, peers). Details: [iOS](ios-sim/README.md), [desktop](desktop-linux/README.md), [Android](android/README.md).
## Reduced build
GitHub runs the reduced build of `build.yml` on a dispatch (workflow_dispatch) and on each push to a draft pull request. Once the pull request is marked ready for review, GitHub runs the full matrix.
On a draft pull request, the tests are selected by a fenced block with the info string `ci-tests` in the description, one hspec match pattern per line; the block is read at each push. `/` runs the whole suite; without a block, the reduced build runs no tests.
### Dispatch
```sh
curl -fsS -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/<owner>/simplex-chat/actions/workflows/build.yml/dispatches \
-d '{"ref": "<branch>", "inputs": {"tests": "/SimpleX chat client/group tests/\n/Message batching/"}}'
```
Endpoint: https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event. Inputs:
| Input | Default | Value |
|---|---|---|
| `tests` | empty | hspec match patterns, one per line (`\n` in JSON); `/` runs the whole suite, and an empty value builds without tests |
| `base` | `master` | the branch that the changes are compared with:<br>`stable` for a branch cut from stable<br>the parent branch for a stacked branch |
When a newer dispatch on the same branch starts build-linux or build-ios, GitHub cancels the unfinished job of the older one (`cancel-in-progress`).
Results:
- Run and jobs: `GET /repos/<owner>/simplex-chat/actions/runs?head_sha=<commit>&event=workflow_dispatch` returns the run id; `GET /repos/<owner>/simplex-chat/actions/runs/<run id>/jobs` returns each step with its start, end and conclusion.
- Job log: `GET /repos/<owner>/simplex-chat/actions/jobs/<job id>/logs` returns a redirect to a text file, valid for 1 minute (https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run). The test summary is printed as "Finished in N seconds" and "N examples, N failures".
### Scopes
The reduced build runs one Linux job, build-linux (ubuntu-22.04 x86_64), for every scope except ios, and one macOS job, build-ios (macos-latest), for ios.
#### core
- Globs: `BUILD_CHANGES["haskell"]` in packages_release.py.
- CI, in build-linux:
- Docker image (step "Build and cache Docker image");
- caches (the steps "Restore ... from cache");
- `cabal build -j --enable-tests --ghc-options=-O0` (step "Build CLI");
- simplex-chat-test with the selected tests (step "Run tests").
- Local:
```sh
cp scripts/cabal.project.local.linux cabal.project.local
cabal build --enable-tests --ghc-options=-O0
mkdir -p tests/tmp
cabal test simplex-chat-test --ghc-options=-O0 --test-option=-m --test-option='/SimpleX chat client/group tests/'
```
The selection runs as in CI with scripts/ci/run-tests.sh, which appends the patterns of `TESTS` to the test binary:
```sh
TESTS='/SimpleX chat client/group tests/' scripts/ci/run-tests.sh "$(cabal list-bin simplex-chat-test --ghc-options=-O0)"
```
`--ghc-options=-O0` matches the reduced build; the library and test-suite stanzas of simplex-chat.cabal set -O2. Test directories are created under tests/tmp (`tmpBracket` in tests/Test.hs).
#### kotlin
- Globs: `BUILD_CHANGES["kotlin"]` in packages_release.py.
- CI: Gradle in the Docker container of build-linux (step "Build and test Kotlin apps", matrix field `kotlin`).
- Local:
```sh
cd apps/multiplatform
./gradlew --no-daemon --max-workers=1 :common:desktopTest :desktop:compileKotlinJvm -x :desktop:cmakeBuildAndCopy :android:compileGoogleDebugKotlin :android:testFossDebugUnitTest
```
JDK 17 and an Android SDK in ANDROID_HOME are required, as installed by Dockerfile.build.
#### ios
- Globs:
- apps/ios/**
- scripts/ci/ios-stub-libs.sh
- CI: build-ios: stub libraries, then xcodebuild for the iOS Simulator without signing.
- Local, on macOS: `scripts/ci/ios-stub-libs.sh`, then the xcodebuild command of the step "Build iOS app" of build-ios.
#### packages
- Globs:
- packages/simplex-chat-nodejs/**
- packages/simplex-chat-client/types/typescript/**
- packages/simplex-chat-python/**
- scripts/ci/package-tests.sh
- CI, in build-linux:
- Node 22 and Python 3.12 (steps "Setup Node.js" and "Setup Python");
- libsimplex, when `CORE_SOURCES` of packages_release.py also changed (step "Build library");
- `package-tests.sh build linux-x86_64` and `package-tests.sh test` (step "Package tests (pull request)").
- With libsimplex built, SIMPLEX_LIBS_DIR is set to it for the nodejs tests; the python tests run with it unset (package-tests.sh). Without SIMPLEX_LIBS_DIR, the pinned release libraries are downloaded (packages/simplex-chat-nodejs/src/libs.ts).
- Local:
```sh
scripts/ci/package-tests.sh build linux-x86_64
scripts/ci/package-tests.sh test
```
#### badge-web
- Globs: apps/simplex-badge-service/web/**. These paths are also in the core globs (apps/simplex-badge-service/**), and web/src/catalog.ts is read by core tests (tests/Bots/BadgeService/CatalogTests.hs, WebTests.hs).
- CI, in build-linux: core build and tests, then `scope-tests.sh badge-web` (step "Test changed scopes").
- Selected core tests: `/SimpleX badge service/` and `/SimpleX badge service e2e/`.
- Local:
```sh
cd apps/simplex-badge-service/web
npm ci
npm test
```
#### ci-scripts
- Globs:
- scripts/ci/packages_release.py
- scripts/ci/test_packages_release.py
- These paths are also in the core globs (scripts/ci/**).
- CI, in build-linux: `scope-tests.sh ci-scripts` (step "Test changed scopes").
- Local: `python3 scripts/ci/test_packages_release.py`; Python 3.11 or newer is required for tomllib (packages_release.py).
#### Scopes lacking CI tests
| Scope | Paths | CI |
|---|---|---|
| ios | apps/ios/** | compile only |
| types package | packages/simplex-chat-client/types/typescript/** | compile only |
| website | website/**<br>images/**<br>blog/**<br>docs/**<br>PRIVACY.md | build and deploy on master pushes (web.yml) |
| ts-client | packages/simplex-chat-client/typescript/** | - |
| webrtc | packages/simplex-chat-webrtc/** | - |
| mcp | packages/simplex-chat-mcp/** | - |
| calculator-bot | apps/simplex-calculator-bot/** | - |
| support-bot | apps/simplex-support-bot/** | - |
| support-bot-light | apps/simplex-support-bot-light/** | - |
| prices | scripts/prices/** | - |
| nft | eth/nft/** | - |
CI coverage of such a scope is added in the pull request that changes it (Adding CI coverage).
### Adding CI coverage
For a scope lacking CI tests, in the pull request that changes it:
1. scripts/ci/packages_release.py: an entry `"<scope>": ["<glob>", ...]` in SCOPE_CHANGES.
2. build.yml, `on.pull_request.paths`: the same globs; the two are compared in `test_build_yml_runs_for_the_paths_of_its_builds` of test_packages_release.py.
3. scripts/ci/scope-tests.sh: a `<scope>)` case with the build and test commands, and the scope name in the usage line.
4. scripts/ci/test_packages_release.py, ChangesTest: a case asserting that the scope is printed in `scopes` for a change to one of its files.
5. This file, "Scopes": a subsection for the scope, in place of its row in "Scopes lacking CI tests".
6. Checks: `python3 scripts/ci/test_packages_release.py`, the new case of scope-tests.sh locally, then a dispatch of the reduced build.
The scope commands run in build-linux after the core tests, with Node 22 and Python 3.12 (steps "Setup Node.js" and "Setup Python"). For a scope that needs other versions, a setup step is added before "Test changed scopes", conditioned on the scope name in `needs.maybe-release.outputs.scopes`. A scope that needs macOS is a BUILD_CHANGES entry with a separate maybe-release output, as ios is, and a step in build-ios; the `if` of build-ios includes that output.
## Starting a run
Pushes start nothing: a run is started by hand, on any branch, with inputs that are off by default and combine.
Pushes start none of the app workflows: a run is started by hand, on any branch, with inputs that are off by default and combine.
| Input | iOS | Desktop | Android |
|---|---|---|---|
+1 -1
View File
@@ -4,7 +4,7 @@
## Running it
The workflow runs only when started by hand, on any branch: in the Actions tab, `android-emu` → Run workflow, or `gh workflow run android-emu.yml --ref <branch>` with `-f claude=true`, `-f backend=true` or `-f release=true`. [App tests on GitHub's runners](../README.md) lists the inputs. A newer run on the same branch cancels the older one.
The workflow runs only when started by hand, on any branch: in the Actions tab, `android-emu` → Run workflow, or `gh workflow run android-emu.yml --ref <branch>` with `-f claude=true`, `-f backend=true` or `-f release=true`. [CI on GitHub's runners](../README.md) lists the inputs. A newer run on the same branch cancels the older one.
Every run builds the core library, and the backend when it's asked for, from the branch's own Haskell code. It reuses an earlier build only when the sources are unchanged, as an artifact of the same branch named after a hash of its sources (30 days): for the core `src/**`, `cabal.project`, `simplex-chat.cabal`, `PRIVACY.md`, the Nix files (`flake.nix`, `flake.lock`, `scripts/nix/**`) and `build-core.sh`; the backend is desktop-linux's, under its name and hash, so a backend either workflow built is reused by both. The workflow saves no Actions caches, which this repository evicts within hours and shares with `build.yml`.
+1 -1
View File
@@ -4,7 +4,7 @@
## Running it
The workflow runs only when started by hand, on any branch, from the Actions tab or with `gh workflow run`: [App tests on GitHub's runners](../README.md) says how and lists the inputs.
The workflow runs only when started by hand, on any branch, from the Actions tab or with `gh workflow run`: [CI on GitHub's runners](../README.md) says how and lists the inputs.
Every run builds the core library, and the backend when it's asked for, from the branch's own Haskell code. It reuses an earlier build only when the sources are unchanged, as an artifact of the same branch named after a hash of its sources (30 days): for the core `src/**`, `cabal.project`, `simplex-chat.cabal`, `PRIVACY.md` and the build scripts, for the backend `src/**`, the CLI's and the directory's sources, `cabal.project`, `simplex-chat.cabal` and its build script. So a branch that changes core and app code together tests both. The workflow saves no Actions caches, which this repository evicts within hours and shares with `build.yml`.
+1 -1
View File
@@ -4,7 +4,7 @@
## Running it
The workflow runs only when started by hand, on any branch, from the Actions tab or with `gh workflow run`: [App tests on GitHub's runners](../README.md) says how and lists the inputs.
The workflow runs only when started by hand, on any branch, from the Actions tab or with `gh workflow run`: [CI on GitHub's runners](../README.md) says how and lists the inputs.
Results: the job logs, the Claude job's summary (Claude's usage) and the artifacts `ios-sim-smoke-test` (a screenshot, a 1000-px view and an accessibility tree per step, the app log), `ios-sim-backend-logs` and `ios-sim-claude-test` (the Claude job's smoke test in `smoke-test/`, and per session: `prompt.md`, `report.md`, `shots/`, `transcript.jsonl`, `usage.json`). Read them with `gh run view <id> --log` and `gh run download <id> -n <artifact>`.
+19 -4
View File
@@ -10,7 +10,8 @@ publish-release publish a draft release that has every asset
check-pins check the pins of the packages against the published releases
download download a release asset and check its digest
download-pinned download a pinned asset and check its pin
changes print whether the changes since a commit touch the chat core, the add-on and each build of build.yml
changes print whether the changes since a commit touch the chat core, the add-on, each build of build.yml and each scope of scope-tests.sh
scopes print every scope of scope-tests.sh
build-plan print the platforms to test the packages on, none when build.yml builds the core for the pull request
prepare update pins and versions of the packages
publish-plan print the package versions that are not published yet
@@ -47,7 +48,7 @@ RELEASE_FILE = f"{NODEJS_DIR}/src/release.ts"
PYTHON_VERSION_FILE = f"{PYTHON_DIR}/src/simplex_chat/_version.py"
ADDON_SOURCES = [f"{NODEJS_DIR}/binding.gyp", f"{NODEJS_DIR}/cpp"]
CORE_SOURCES = ["src", "simplex-chat.cabal", "cabal.project"]
# The pull_request paths of build.yml are the union of these globs; ":!" excludes a path from its build.
# The pull_request paths of build.yml are the union of the globs of BUILD_CHANGES and SCOPE_CHANGES; ":!" excludes a path from its build.
BUILD_CHANGES = {
"haskell": [
"src/**",
@@ -68,7 +69,17 @@ BUILD_CHANGES = {
".github/**",
],
"kotlin": ["apps/multiplatform/**", "Dockerfile*", ".github/**"],
"ios": ["apps/ios/**", "scripts/ci/ios-stub-libs.sh", ".github/**"],
"ios": ["apps/ios/**", "scripts/ci/ios-stub-libs.sh"],
"packages": [
"packages/simplex-chat-nodejs/**",
"packages/simplex-chat-client/types/typescript/**",
"packages/simplex-chat-python/**",
"scripts/ci/package-tests.sh",
],
}
SCOPE_CHANGES = {
"badge-web": ["apps/simplex-badge-service/web/**"],
"ci-scripts": ["scripts/ci/packages_release.py", "scripts/ci/test_packages_release.py"],
}
CABAL_FILE = "simplex-chat.cabal"
CABAL_VERSION = re.compile(r"^(version:\s*)(\S+)", re.M)
@@ -496,7 +507,7 @@ def changed_since(base: str, sources: list[str]) -> bool:
def build_pathspecs(build: str) -> list[str]:
return [path if path.startswith(":") else f":(glob){path}" for path in BUILD_CHANGES[build]]
return [path if path.startswith(":") else f":(glob){path}" for path in (BUILD_CHANGES | SCOPE_CHANGES)[build]]
def changes(base: str) -> None:
@@ -504,6 +515,7 @@ def changes(base: str) -> None:
print(f"addon={'true' if changed_since(base, ADDON_SOURCES) else ''}")
for build in BUILD_CHANGES:
print(f"{build}={'true' if changed_since(base, build_pathspecs(build)) else ''}")
print(f"scopes={' '.join(scope for scope in SCOPE_CHANGES if changed_since(base, build_pathspecs(scope)))}")
def print_build_plan(args: argparse.Namespace) -> None:
@@ -959,6 +971,7 @@ def main(argv: list[str]) -> None:
download_pinned_parser.add_argument("directory")
changes_parser = commands.add_parser("changes")
changes_parser.add_argument("base", help="the commit to compare with")
commands.add_parser("scopes")
build_parser = commands.add_parser("build-plan")
build_parser.add_argument("--base", required=True, help="the base commit of the pull request")
prepare_parser = commands.add_parser("prepare")
@@ -996,6 +1009,8 @@ def main(argv: list[str]) -> None:
download_pinned(args.asset, args.directory)
elif args.command == "changes":
changes(args.base)
elif args.command == "scopes":
print(f"scopes={' '.join(SCOPE_CHANGES)}")
elif args.command == "build-plan":
print_build_plan(args)
elif args.command == "prepare":
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
if [ "$#" -eq 0 ]; then
echo "usage: $0 <test command>" >&2
exit 1
fi
if [ -n "${PR_BODY:-}" ]; then
TESTS=$(printf '%s\n' "$PR_BODY" | tr -d '\r' | awk '/^```ci-tests[ \t]*$/ {f = 1; next} /^```[ \t]*$/ {f = 0} f')
fi
args=()
while read -r p; do
if [ -n "$p" ]; then args+=(-m "$p"); fi
done <<< "${TESTS:-}"
if [ "${#args[@]}" -gt 0 ]; then
args+=(--fail-on=empty)
elif [ "${REDUCED:-}" = true ]; then
echo "No tests selected"
exit 0
fi
i=1
attempts=${ATTEMPTS:-1}
while [ "$i" -le "$attempts" ]; do
if "$@" "${args[@]}"; then
break
else
echo "Attempt $i failed, retrying..."
i=$((i + 1))
sleep 1
fi
done
if [ "$i" -gt "$attempts" ]; then
echo "All $attempts attempts failed."
exit 1
fi
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")/../.."
case "${1:-}" in
badge-web)
(cd apps/simplex-badge-service/web && npm ci && npm test)
;;
ci-scripts)
python3 scripts/ci/test_packages_release.py
;;
*)
echo "usage: $0 badge-web | ci-scripts" >&2
exit 1
;;
esac
+17 -5
View File
@@ -27,6 +27,7 @@ from packages_release import (
TYPES_README,
ADDON_PLATFORMS,
BUILD_CHANGES,
SCOPE_CHANGES,
LIBS_ASSETS,
Package,
Pins,
@@ -245,7 +246,7 @@ class BuildPlanTest(unittest.TestCase):
found = re.search(r'^ pull_request:\n paths:\n((?: - ".*"\n)+)', workflow, re.M)
self.assertIsNotNone(found, "no pull_request paths in build.yml")
paths = re.findall(r'^ - "(.*)"$', found[1], re.M)
globs = {path for build in BUILD_CHANGES.values() for path in build if not path.startswith(":")}
globs = {path for build in (BUILD_CHANGES | SCOPE_CHANGES).values() for path in build if not path.startswith(":")}
self.assertEqual(sorted(paths), sorted(globs))
self.assertEqual(len(paths), len(set(paths)), "duplicate pull_request paths in build.yml")
@@ -463,8 +464,8 @@ class ChangesTest(GitRepoTest):
output = io.StringIO()
with mock.patch.object(packages_release, "ROOT", self.repo), contextlib.redirect_stdout(output):
changes(self.base)
values = dict(line.split("=", 1) for line in output.getvalue().splitlines())
return {build for build in BUILD_CHANGES if values[build] == "true"}
self.values = dict(line.split("=", 1) for line in output.getvalue().splitlines())
return {build for build in BUILD_CHANGES if self.values[build] == "true"}
def test_ios_change_builds_only_ios(self):
self.assertEqual(self.builds("apps/ios/Shared/View.swift"), {"ios"})
@@ -487,12 +488,23 @@ class ChangesTest(GitRepoTest):
def test_build_image_change_builds_the_core_and_kotlin(self):
self.assertEqual(self.builds("Dockerfile.build"), {"haskell", "kotlin"})
def test_workflow_change_builds_everything(self):
self.assertEqual(self.builds(".github/workflows/build.yml"), {"haskell", "kotlin", "ios"})
def test_workflow_change_builds_the_core_and_kotlin(self):
self.assertEqual(self.builds(".github/workflows/build.yml"), {"haskell", "kotlin"})
def test_unrelated_change_builds_nothing(self):
self.assertEqual(self.builds("docs/guide.md", "packages/pkg/src.txt"), set())
def test_package_change_builds_only_packages(self):
self.assertEqual(self.builds("packages/simplex-chat-python/src/simplex_chat/client.py"), {"packages"})
def test_badge_web_change_builds_the_core_and_tests_its_scope(self):
self.assertEqual(self.builds("apps/simplex-badge-service/web/src/catalog.ts"), {"haskell"})
self.assertEqual(self.values["scopes"], "badge-web")
def test_release_script_change_tests_the_ci_scripts(self):
self.assertEqual(self.builds("scripts/ci/packages_release.py"), {"haskell"})
self.assertEqual(self.values["scopes"], "ci-scripts")
class PackageStateTest(GitRepoTest):
def setUp(self):