Files
amethyst/.github/workflows/build.yml
Claude 8bd5063e10 refactor: consume Quartz from its own repository
Quartz now lives in github.com/vitorpamplona/quartz, split into
quartz-core, quartz-social, quartz-relay-client and the other quartz-*
libraries, with history kept. Amethyst depends on the `quartz` umbrella
artifact at a released version (1.20.0) instead of the :quartz project.

- Remove the quartz/ module, jitpack.yml and the Quartz CI, release and
  pre-push steps; tools/unicode-nfkc moved with it.
- Point every module at libs.quartz.
- Move the device-test fixtures the benchmark and relayBench still read
  to testdata/.
- NostrConnectLoginUseCase parses bunker messages with BunkerJson, which
  now owns NIP-46 JSON.
- Repoint docs and skills at the Quartz repository; the nostr-expert
  scripts read a Quartz checkout (QUARTZ_REPO, default ../quartz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZR5ZBfhFzRiXubJc5Evyv
2026-10-15 02:03:53 +00:00

588 lines
25 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Test/Build
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Orphaned translations (no locale string may outlive its default key)
run: tools/agent-hooks/orphan_strings_check.py
- name: Compose catalog escaping (Android escapes render literally there)
run: tools/agent-hooks/compose_escaping_check.py
- name: Agent config (Claude Code and Codex read the same rules and skills)
run: tools/agent-hooks/agent_config_check.py
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
# Remote Gradle build cache: writes on push to main, reads on PRs and
# other branches. Caches both `~/.gradle/caches/` and individual task
# outputs, so dependency-only changes hit the cache and skip recompiling
# downstream modules / re-merging native libs (~600MB of work on
# :amethyst alone). Replaces the narrower `cache: gradle` previously on
# actions/setup-java, which only cached `modules-2`.
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Linter (gradle)
# The two metadata compiles resolve commonMain against only the
# deps every target shares, which is the Apple classpath — a
# dependency that reaches JVM transitively (okio via OkHttp) but is
# missing for iOS fails here, on Linux, instead of in test-commons-ios.
# (Quartz runs the same gate in its own repository.)
run: |
./gradlew spotlessCheck \
:commons:verifyKmpPurity :commonsUI:verifyKmpPurity \
:commons:compileCommonMainKotlinMetadata \
:commonsUI:compileCommonMainKotlinMetadata
build-desktop:
needs: lint
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
desktop-task: packageDeb
desktop-artifact-name: Desktop Linux DEB
desktop-artifact-path: desktopApp/build/compose/binaries/main/deb/*.deb
- os: macos-latest
desktop-task: packageDmg
desktop-artifact-name: Desktop macOS DMG
desktop-artifact-path: desktopApp/build/compose/binaries/main/dmg/*.dmg
- os: windows-latest
desktop-task: packageMsi
desktop-artifact-name: Desktop Windows MSI
desktop-artifact-path: desktopApp/build/compose/binaries/main/msi/*.msi
runs-on: ${{ matrix.os }}
timeout-minutes: 60
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v7
# Linux packages bundle the JetBrains Runtime, the JDK Gradle runs on, for its native Wayland
# toolkit (see create-release.yml); build and test the .deb the same way.
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: ${{ runner.os == 'Linux' && 'jetbrains' || 'temurin' }}
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# Compose UI smoke test (DesktopLaunchSmokeTest) uses Skiko which needs
# a display server on Linux. xvfb provides a virtual framebuffer.
- name: Install xvfb (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Test + Build Desktop (gradle)
run: |
CMD="./gradlew :commons:jvmTest :commonsUI:jvmTest :quic:jvmTest :nestsClient:jvmTest :marmotQuic:jvmTest :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}"
if [ "${{ runner.os }}" = "Linux" ]; then
xvfb-run --auto-servernum $CMD
else
$CMD
fi
# This job runs the JVM test suites (:commons, :commonsUI, :quic,
# :nestsClient, :marmotQuic, :cli, :desktopApp) but, unlike test-geode /
# test-and-build-android, published nothing when one of them failed. The
# console line names the failing test and the exception class and stops
# there, so the message is lost with the runner. That is how the
# NostrClientNegentropySyncTest failure in run 10540 became
# undiagnosable: NegentropySyncException carries a `detail` naming which
# branch fired (connect timeout / idle silence / NEG-ERR / disconnect),
# and nobody could read it. Same action and pin as the Android job below.
- name: Desktop Test Report
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
if: always()
with:
report_paths: '**/build/test-results/**/TEST-*.xml'
annotate_only: true
detailed_summary: true
fail_on_failure: true
# The HTML reports carry the full stack traces and stdout/stderr the
# annotations truncate. Named per-OS because the three matrix legs upload
# into the same run and artifact names must be unique.
- name: Upload Desktop Test Reports
uses: actions/upload-artifact@v7
if: failure()
with:
name: Desktop Test Reports (${{ matrix.os }})
path: |
commons/build/reports/tests
commonsUI/build/reports/tests
nestsClient/build/reports/tests
cli/build/reports/tests
desktopApp/build/reports/tests
# jpackage pins libicu and the t64-renamed libs (libasound2t64, ...) to the
# build host (ubuntu-24.04). Rewrite the .deb so testers on other Debian/Ubuntu
# releases can install the uploaded artifact.
- name: Relax build-host-pinned deps in .deb
if: matrix.desktop-task == 'packageDeb'
run: |
set -euo pipefail
chmod +x scripts/relax-deb-libicu.sh
scripts/relax-deb-libicu.sh desktopApp/build/compose/binaries/main/deb/*.deb
# The media player drives the system GStreamer from a native library jpackage
# never sees, so the .deb has to be told to depend on it.
- name: Add GStreamer deps to .deb
if: matrix.desktop-task == 'packageDeb'
run: |
set -euo pipefail
chmod +x scripts/add-deb-gstreamer-deps.sh
scripts/add-deb-gstreamer-deps.sh desktopApp/build/compose/binaries/main/deb/*.deb
# The bundled JBR opens libxkbcommon (the keyboard on Wayland) with dlopen.
- name: Add Wayland deps to .deb
if: matrix.desktop-task == 'packageDeb'
run: |
set -euo pipefail
chmod +x scripts/add-deb-wayland-deps.sh
scripts/add-deb-wayland-deps.sh desktopApp/build/compose/binaries/main/deb/*.deb
# Ships the menu entry in /usr/share/applications with StartupWMClass (docks match the
# window's WM_CLASS to it) instead of registering it from postinst with xdg-desktop-menu,
# which fails on systems without menu directories. Also sets the .deb's compression (xz).
- name: Fix desktop entry in .deb
if: matrix.desktop-task == 'packageDeb'
run: |
set -euo pipefail
chmod +x scripts/fix-deb-desktop-entry.sh
scripts/fix-deb-desktop-entry.sh desktopApp/build/compose/binaries/main/deb/*.deb
- name: Upload Desktop Distribution
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.desktop-artifact-name }}
path: ${{ matrix.desktop-artifact-path }}
# geode (the standalone Nostr relay) is JVM-only, so it runs in its own job
# rather than the build-desktop matrix — one runner suffices (no reason to test
# a platform-independent module 3× across the desktop OS matrix). Isolating it
# also keeps its default suite's CPU-heavy throughput benchmarks (a 1M-event
# mirror sync, WireReqFloor, NegentropyServerReconcile) from contending with the
# timing-sensitive relay-client tests of the other suites under org.gradle.parallel — that
# contention flakes tests like NostrClientReqBypassingRelayLimitsTest.
test-geode:
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# -DsyncN shrinks MirrorSyncThroughputTest's corpus from its 1,000,000-event
# default. At 1M the sink cannot keep up with the in-process source, and the
# MirrorWorker's deliberately unbounded intake channel buffers the backlog until
# the runner's heap is gone: throughput collapses (13,800 -> 86 ev/s) and an
# OutOfMemoryError lands on a coroutine thread, where the UncaughtExceptionHandler
# swallows it. JUnit never sees a failure, so the JVM wedges and the job burns to
# the timeout with no signal rather than failing. 100k keeps a real ev/s number
# while bounding the worst-case backlog to a tenth of what died.
#
# Only CI is shrunk: -DsyncN is unset everywhere else, so a local or manual run
# still measures the full 1M — the number written up in
# relayBench/plans/2026-07-04-sync-throughput-1m.md.
- name: Test geode (gradle)
run: ./gradlew :geode:test -DsyncN=100000
- name: Upload geode Test Reports
uses: actions/upload-artifact@v7
if: failure()
with:
name: geode Test Reports
path: geode/build/reports
# The iOS jobs run on macos-latest, the smallest hosted runner there
# is: 3 M1 cores and 7 GB of RAM, and 5 to 10x slower than a current laptop
# on Kotlin/Native compiles and links (:commonsUI:compileKotlinIosSimulatorArm64
# is 48 s on an M5 Pro and 8 to 14 minutes here). Run-to-run speed also
# varies by about 1.5x, so every job needs real headroom under its limit.
# Until 2026-10-12 they were one job, and it hit its 45-minute limit whenever
# :quartz had to be rebuilt (the green runs before that were Gradle
# build-cache hits on an unchanged :quartz: 2.5 min instead of 19). What
# changed:
# - :quartz linked its iOS test binary optimized (an LTO release link with the
# Kotlin/Native dependency cache off): 12 minutes on this runner. The test
# binaries are unoptimized now; only linuxX64 keeps -opt for the benchmark.
# - One job became three, one per module, so each has its own 45 minutes
# and they run side by side: about 15 to 20 min for :quartz and :commons,
# about 25 for :commonsUI.
# - ~/.konan is cached, so a run does not re-download the compiler, LLVM and
# the sysroot, keyed per job because each job populates different
# per-library caches. (:quartz moved to its own repository, which runs its
# iOS and Linux Native suites itself.)
# - gradle.properties asks for a 6 GB daemon heap, more than the host has.
# These jobs cap it at 4 GB with the same `-D` override the Android jobs
# use. That keeps the daemon off swap, but measured on CI it is not what
# makes the difference: the runner's speed is. The Kotlin JVM daemon is not
# involved; every compile and link task here is Kotlin/Native and runs
# inside the Gradle daemon.
test-commons-ios:
# :commons gained iosArm64 + iosSimulatorArm64 targets in Phase 2 of the
# iOS plan. Actual UI / lifecycle wiring will land with the iosApp module
# in Phase 3, but the shared commonMain + commonTest sources are already
# built here against an Apple Native frontend. Two tasks:
# - iosSimulatorArm64Test compiles AND runs the shared commonTest suite
# on the simulator. commonTest is built for every target, so a test
# reaching for a JVM-only API (JUnit, javaClass, @JvmStatic, or a
# jvmAndroid-only symbol) breaks the Apple build even though
# :commons:jvmTest stays green — this is the job that catches it.
# - compileTestKotlinIosArm64 catches device-only compile drift
# (iosArm64 = aarch64-apple-ios) without needing a physical device.
needs: lint
runs-on: macos-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Cache Kotlin/Native toolchain
uses: actions/cache@v6
with:
path: ~/.konan
key: konan-${{ runner.os }}-commons-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: |
konan-${{ runner.os }}-commons-
konan-${{ runner.os }}-
- name: Test Commons on iOS
run: |
./gradlew \
-Dorg.gradle.jvmargs="-Xmx4g -Dfile.encoding=UTF-8" \
:commons:iosSimulatorArm64Test \
:commons:compileTestKotlinIosArm64
- name: Commons iOS Test Report
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
if: always()
with:
report_paths: 'commons/build/test-results/iosSimulatorArm64Test/TEST-*.xml'
annotate_only: true
detailed_summary: true
fail_on_failure: true
- name: Upload Commons iOS Test Reports
uses: actions/upload-artifact@v7
if: failure()
with:
name: Commons iOS Test Reports
path: commons/build/reports
test-commonsui-ios:
# :commonsUI (the Compose half split out of :commons) gets the same
# treatment as :commons so the shared composables keep compiling on Apple
# targets. It is the slowest of the three iOS modules by far (Compose,
# Skiko and the resource pipeline all go through the native compiler), so
# it has a job to itself: together with :commons it ran 35 to 37 minutes
# against the 45-minute limit on back-to-back runs.
needs: lint
runs-on: macos-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Cache Kotlin/Native toolchain
uses: actions/cache@v6
with:
path: ~/.konan
key: konan-${{ runner.os }}-commonsui-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: |
konan-${{ runner.os }}-commonsui-
konan-${{ runner.os }}-
- name: Test CommonsUI on iOS
run: |
./gradlew \
-Dorg.gradle.jvmargs="-Xmx4g -Dfile.encoding=UTF-8" \
:commonsUI:iosSimulatorArm64Test \
:commonsUI:compileTestKotlinIosArm64
- name: CommonsUI iOS Test Report
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
if: always()
with:
report_paths: 'commonsUI/build/test-results/iosSimulatorArm64Test/TEST-*.xml'
annotate_only: true
detailed_summary: true
fail_on_failure: true
- name: Upload CommonsUI iOS Test Reports
uses: actions/upload-artifact@v7
if: failure()
with:
name: CommonsUI iOS Test Reports
path: commonsUI/build/reports
# Android Lint, split out of test-and-build-android.
#
# That job died with "the runner has received a shutdown signal" (SIGTERM,
# exit 143) seven times across this workflow's history — the OOM killer taking
# the runner agent on a 16GB box. Two attempts to fix it by tuning numbers
# have now been spent: capping both daemons to 4g traded the runner OOM for an
# R8/lintAnalyze "Java heap space", and --max-workers=3 survives a warm cache
# but still dies on a cold one.
#
# This is the structural fix rather than a third number. lintAnalyze is the
# single heaviest step in that job — measured at 13 of its 35 minutes on one
# cold run — and it holds a large analysis graph while the Kotlin daemon, a
# forked test JVM and R8 are all still resident. Giving the two lint tasks
# their own runner removes that peak from the critical job instead of trying
# to squeeze everything under one ceiling, and the two now run concurrently.
#
# The cost is honest: both jobs restore the same read-only Gradle cache and so
# repeat some module compilation. That buys back more than it spends here,
# because the duplicated work is parallel while the memory pressure was not.
lint-android:
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# Same daemon cap as the sibling job: lintAnalyze draws on the Gradle
# daemon's heap, which is why the earlier 4g experiment broke it. Only the
# Kotlin daemon is trimmed.
- name: Lint Android (gradle)
run: |
./gradlew \
-Dkotlin.daemon.jvmargs="-Xmx4g -XX:MaxMetaspaceSize=1g" \
--max-workers=3 \
:amethyst:lintFdroidBenchmark \
:amethyst:lintPlayBenchmark
- name: Upload Android Lint Reports
uses: actions/upload-artifact@v7
if: always()
with:
name: Android Lint Reports
path: amethyst/build/reports/lint-results-*.html
test-and-build-android:
needs: lint
runs-on: ubuntu-latest
# 90, not 60. On main this job's Gradle step takes ~46 minutes, which used
# 76% of a 60-minute budget — fine for an incremental run, but PR runs set
# `cache-read-only` (below), so they restore main's Gradle cache and never
# save. A PR that touches `commons` invalidates most of what
# that cache holds for everything downstream, and the job then rebuilds it
# from cold: measured 2-3x the main-branch time across every job in the
# workflow, which puts this one past the cap and gets it killed mid-step
# with no test report. Raising the cap costs nothing on runs that finish
# early — `timeout-minutes` bounds a job, it does not reserve the time.
timeout-minutes: 120
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v6.0.1
with:
distribution: 'temurin'
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# Focused unit tests + benchmark assembly in one Gradle invocation.
# Previously: one invocation for `test` (which compiled all six amethyst
# variants × all flavors) and one for `assembleBenchmark` (re-walking the
# same task graph). Combining them keeps the daemon hot across phases and
# lets task-level dedup (e.g. compileKotlin) only happen once.
#
# Lint used to run here too and now has its own job (lint-android above) —
# see the note there for why. What remains is still the heaviest job in
# the workflow, so the memory notes below continue to apply.
#
# `-PdisableAbiSplits=true` produces a single non-split APK per
# (flavor, buildType) instead of 5 (4 ABIs + universal). The CI only
# uploads the universal-equivalent benchmark APK; per-ABI splits were
# being built and discarded, costing ~600MB of stripped_native_libs
# intermediates and several minutes per run.
#
# Test scope: only Debug unit tests for amethyst. The release/benchmark
# variants are compile-equivalent for unit-test purposes; running all six
# adds ~5× the kotlinc work without catching new defects on PRs. Push to
# main still gets the full test matrix via the production-build path.
# Memory, CI-only. gradle.properties asks for -Xmx6g (Gradle) plus -Xmx8g
# and 2g metaspace (Kotlin daemon) — about 16GB of ceiling on a 16GB
# ubuntu-latest runner, before the launcher JVM, Lint's fork and the KSP
# workers. This job is the only one heavy enough to reach it, and it died
# five times mid-compile with "the runner has received a shutdown signal",
# which is the OOM killer taking the runner agent.
#
# Only the Kotlin daemon is cut. An earlier attempt capped BOTH to 4g and
# traded one OOM for another: the runner survived and the job ran to
# completion, but R8 and lintAnalyze then failed with
# "java.lang.OutOfMemoryError: Java heap space" — they draw on the Gradle
# daemon's heap, and 4g is not enough for them on this app. 6g always was,
# so it stays; 8g + 2g for kotlinc is the part that did not fit.
#
# Overridden here rather than in gradle.properties so local builds on
# bigger machines keep the headroom.
#
# `--max-workers` is the second half, and it is about concurrency rather
# than ceilings. The heap numbers above are per-JVM limits; what actually
# tips a 16GB runner over is how many heavy JVMs are live at once. Gradle
# defaults max-workers to the core count (4 on ubuntu-latest) and
# org.gradle.parallel is on, so a cold run can have several kotlinc
# workers, a lint fork and a forked test JVM resident alongside the two
# daemons.
#
# Cold is the case that matters. The 4g cap was first validated on a run
# that only changed this file, so it restored main's Gradle cache and
# built almost nothing; the next PR run that touched `commons`
# invalidated that cache, rebuilt from cold, and died the same way at
# ~20 minutes. Fewer workers is what makes the cold path fit — dropping
# heap further would start starving R8 again, which is the trade the
# previous attempt already lost.
#
# 3 rather than 2: this job is mostly a chain of single-task module
# compiles, so the parallelism it loses is small, and halving it risks
# the timeout on a cold run. The cap goes to 120 for the same reason.
- name: Test + Build Android (gradle)
run: |
./gradlew \
-Dkotlin.daemon.jvmargs="-Xmx4g -XX:MaxMetaspaceSize=1g" \
--max-workers=3 \
:commons:jvmTest \
:commonsUI:jvmTest \
:commonsUI:testAndroidHostTest \
:nestsClient:jvmTest \
:amethyst:testFdroidDebugUnitTest \
:amethyst:testPlayDebugUnitTest \
:amethyst:assembleBenchmark \
-PdisableAbiSplits=true
# Publishes the JUnit XML produced by the unit-test tasks above as inline
# annotations plus a job summary. Replaces asadmansr/android-test-report-action,
# which was abandoned (last release 2020) and rebuilt an EOL Ubuntu 18.04 +
# Python 2 Docker image on every run — bionic's apt archives have since gone
# unreliable and broke this job. Pinned to a commit SHA (not the movable
# v6.4.2 tag) to close the supply-chain hole. annotate_only avoids needing
# `checks: write`, so it keeps working on pull requests from forks (where the
# GITHUB_TOKEN is read-only). fail_on_failure preserves the old step's
# behavior of marking the job red when a test fails.
- name: Android Test Report
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
if: always()
with:
report_paths: '**/build/test-results/**/TEST-*.xml'
annotate_only: true
detailed_summary: true
fail_on_failure: true
- name: Upload Test Results
uses: actions/upload-artifact@v7
if: failure()
with:
name: Test Reports
path: amethyst/build/reports
# With -PdisableAbiSplits=true the APK is named without the ABI/universal
# suffix: amethyst-<flavor>-benchmark.apk. Glob both forms so this still
# works if a contributor runs CI on a branch that doesn't pass the flag.
- name: Upload Play APK Benchmark
uses: actions/upload-artifact@v7
with:
name: Play Benchmark APK
path: |
amethyst/build/outputs/apk/play/benchmark/amethyst-play-benchmark.apk
amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
- name: Upload FDroid APK Benchmark
uses: actions/upload-artifact@v7
with:
name: FDroid Benchmark APK
path: |
amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-benchmark.apk
amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
- name: Upload Compose Reports
uses: actions/upload-artifact@v7
with:
name: Compose Reports
path: amethyst/build/compose_compiler