hammer-editor/.github/workflows/build.yml
Adam Brown 4e3629c0b2
Add self-contained static-analysis CI gate (Semgrep + gitleaks) (#747)
Adds a static-analysis job to Build CI running two self-contained scanners
as a gate, replacing reliance on out-of-band Codacy:

- Semgrep against hand-written rules in .semgrep/ (raw SQL to
  prepareStatement), with --error; honors // nosemgrep.
- gitleaks for committed secrets, pinned binary; fixtures allowlisted in
  .gitleaks.toml.

Both tool versions are pinned for reproducible gating. Also fixes and
extends the migrator's SQL suppression (correct placement, both rule
ids), and suppresses the parity-check COUNT false positive.

Supersedes #746.
2026-07-17 14:02:44 -07:00

344 lines
14 KiB
YAML

name: Build CI
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew buildDebug
# Compiles + packages the Android instrumented (androidTest) source set without a device.
# buildDebug does not touch androidTest, so stale instrumented tests rot silently otherwise.
- name: Compile Android instrumented tests
run: ./gradlew :android:assembleDebugAndroidTest
- name: Run Kover
run: ./gradlew koverXmlReport
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Explicit gate for the server unit tests (incl. EntityHashSensitivityTest). These otherwise
# only run as a side-effect of koverXmlReport, which is fragile - a kover config change would
# silently stop running them.
- name: Run Server Unit Tests
id: server-tests
run: ./gradlew :server:test
- name: Upload server test report on failure
if: failure() && steps.server-tests.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: server-tests-report
path: |
server/build/reports/tests/test/
server/build/test-results/test/
- name: Run JavaScript Unit Tests
run: ./gradlew :server:jsTest
- name: Run Server Migration Tests
run: ./gradlew :server:verifySqlDelightMigration
- name: Run Round-Trip Sync Integration Tests
id: integration-tests
run: ./gradlew :integrationTests:jvmTest
- name: Upload integration test report on failure
if: failure() && steps.integration-tests.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: integration-tests-report
path: |
integrationTests/build/reports/tests/jvmTest/
integrationTests/build/test-results/jvmTest/
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Semgrep
run: pip install semgrep==1.170.0
# Self-contained code-pattern rules under .semgrep/ (no registry fetch, no account).
# Honors `// nosemgrep` comments. `--error` makes an unsuppressed finding fail the build.
- name: Run Semgrep (custom rules)
run: semgrep scan --config .semgrep --error --metrics off
# Pinned for reproducibility — a tool auto-update must never spontaneously fail an
# unrelated PR's gate. Bump deliberately to pick up new secret-detection rules.
- name: Install gitleaks
env:
GITLEAKS_VERSION: 8.30.1
run: |
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| sudo tar -xz -C /usr/local/bin gitleaks
gitleaks version
# Fails the build on any committed secret. Known test fixtures are allowlisted in
# .gitleaks.toml; the JSON report is parsed so failures list rule + file:line.
- name: Run gitleaks
run: |
gitleaks dir . --config .gitleaks.toml --no-banner --redact \
--report-format json --report-path gitleaks-report.json --exit-code 2 || true
count=$(python3 -c "import json; print(len(json.load(open('gitleaks-report.json'))))")
if [ "$count" -gt 0 ]; then
echo "::error::gitleaks found $count secret(s):"
python3 -c "import json; [print(f\" {x['RuleID']} {x['File']}:{x['StartLine']}\") for x in json.load(open('gitleaks-report.json'))]"
exit 1
fi
echo "gitleaks: no leaks"
android-instrumented-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# The hardware-accelerated emulator needs KVM, which ubuntu-latest exposes once permissioned.
- name: Enable KVM
run: |
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
# Cache the emulator system image + AVD snapshot so subsequent runs skip the slow cold
# boot. Keyed on every parameter that defines the AVD - bump the key if any `with:` value
# below changes, or the cached snapshot won't match.
- name: AVD cache
uses: actions/cache@v6
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api34-x86_64-google_apis-pixel_6
# On a cache miss, cold-boot once and let the action save a snapshot for the cache. No tests here.
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 34
arch: x86_64
target: google_apis
profile: pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: echo "Generated AVD snapshot for caching."
- name: Run instrumented tests on emulator
id: instrumented-tests
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 34
arch: x86_64
target: google_apis
profile: pixel_6
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: ./gradlew :android:connectedDebugAndroidTest
- name: Upload instrumented test report on failure
if: failure() && steps.instrumented-tests.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: instrumented-tests-report
path: |
android/build/reports/androidTests/connected/
android/build/outputs/androidTest-results/connected/
# The ubuntu `build` job never touches the iOS targets, so Native-only breakage
# in commonMain slips through otherwise. This job both compiles the iOS targets
# and runs the Kotlin/Native unit tests on the simulator.
ios-compile:
name: iOS compile & test
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Select Xcode 26
# macos-latest still defaults to Xcode 16.4 (iOS 18.5 SDK), but linking the
# iOS test executable pulls in Compose's ui-uikit, which references symbols
# (e.g. UIViewLayoutRegion) that only exist in the iOS 26 SDK. Without this
# the link fails non-deterministically depending on the runner image. Glob
# keeps us off a pinned patch version that could vanish on a future image.
run: |
XCODE_APP="$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)"
echo "Using $XCODE_APP"
sudo xcode-select -s "$XCODE_APP/Contents/Developer"
xcodebuild -version
- name: set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Compile iOS Kotlin targets
run: ./gradlew :composeUi:compileKotlinIosArm64 :composeUi:compileKotlinIosSimulatorArm64
- name: Run iOS unit tests
id: ios-tests
run: ./gradlew :common:iosSimulatorArm64Test
- name: Upload iOS test report on failure
if: failure() && steps.ios-tests.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: ios-tests-report
path: |
common/build/reports/tests/iosSimulatorArm64Test/
common/build/test-results/iosSimulatorArm64Test/
# Runs the real iOS app on a simulator and drives it through XCUITest (the iOS analogue of the
# android-instrumented-tests job). The whole UI is Compose Multiplatform, so the tests target
# Compose testTags, which the app exposes to the iOS accessibility tree when launched with
# --uitesting (see composeUi .../MainViewController.kt and ios/iosUITests).
ios-ui-tests:
name: iOS UI tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Select Xcode 26
# Match ios-compile: linking Compose's ui-uikit needs the iOS 26 SDK.
run: |
XCODE_APP="$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)"
echo "Using $XCODE_APP"
sudo xcode-select -s "$XCODE_APP/Contents/Developer"
xcodebuild -version
- name: set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Pick the newest available iPhone simulator rather than pinning a device that a future
# Xcode image might drop, then boot it so the test run doesn't race first-boot.
- name: Select & boot a simulator
id: sim
run: |
UDID=$(xcrun simctl list devices available --json \
| jq -r '[.devices[][] | select(.name | startswith("iPhone"))] | last | .udid')
if [ -z "$UDID" ] || [ "$UDID" = "null" ]; then
echo "No iPhone simulator available"; xcrun simctl list devices available; exit 1
fi
echo "Using simulator $UDID"
# Disable the hardware keyboard before boot so the software keyboard shows — Compose
# text fields need it for XCUITest text entry (see ios/scripts).
bash ios/scripts/disable_sim_hardware_keyboard.sh
xcrun simctl boot "$UDID" || true
echo "udid=$UDID" >> "$GITHUB_OUTPUT"
- name: Run iOS UI tests
id: ios-ui-tests
run: |
set -o pipefail
xcodebuild test \
-project ios/ios.xcodeproj \
-scheme iosUITests \
-destination "id=${{ steps.sim.outputs.udid }}" \
-configuration Debug \
-resultBundlePath ios-ui-tests.xcresult \
CODE_SIGNING_ALLOWED=NO
- name: Upload iOS UI test results on failure
if: failure() && steps.ios-ui-tests.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: ios-ui-tests-report
path: ios-ui-tests.xcresult
# ios:
# runs-on: macos-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: check Xcode version
# run: /usr/bin/xcodebuild -version
#
# - name: Get swift version
# run: swift --version
#
# - name: set up JDK 21
# uses: actions/setup-java@v4
# with:
# java-version: '21'
# distribution: 'temurin'
# cache: gradle
#
# - name: Grant execute permission for gradlew
# run: chmod +x gradlew
#
# - name: Install the Apple certificate and provisioning profile
# env:
# BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
# P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
# BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
# KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
# run: |
# # create variables
# CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
# PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
# KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
#
# # import certificate and provisioning profile from secrets
# echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH"
# echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o "$PP_PATH"
#
# # create temporary keychain
# security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
# security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
#
# # import certificate to keychain
# security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
# security list-keychain -d user -s "$KEYCHAIN_PATH"
#
# # apply provisioning profile
# mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# cp "$PP_PATH" ~/Library/MobileDevice/Provisioning\ Profiles
#
# - name: Set Default Scheme
# run: |
# cd ios
# default="ios"
# echo $default | cat >default
# echo Using default scheme: $default
#
# - name: Build
# env:
# scheme: ${{ 'default' }}
# platform: ${{ 'iOS Simulator' }}
# run: |
# cd ios
# # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
# device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
# if [ $scheme = default ]; then scheme=$(cat default); fi
# if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
# file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
# xcodebuild build-for-testing -scheme "$scheme" -sdk iphoneos -allowProvisioningUpdates