diff --git a/.github/workflows/build-release.yml b/.github/workflows/android.yml similarity index 72% rename from .github/workflows/build-release.yml rename to .github/workflows/android.yml index acb2fb1e..a52950e5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/android.yml @@ -1,4 +1,16 @@ -name: Build & Release APK +name: Android CI & Release + +# Android pipeline for the ft8af/ app: unit tests + coverage, instrumented +# emulator tests, static analysis (lint/detekt/ktlint), and the signed +# APK/AAB build + GitHub Release + Play internal publish. +# +# PATH-FILTERED: on pull requests and dev pushes this whole pipeline only runs +# when Android-relevant files change (ft8af/** — which includes the shared +# native C core at ft8af/app/src/main/cpp/ — or this workflow file). Pushes to +# main and v* tags always build (a full per-version release snapshot, +# regardless of what changed). The always-run `android-gate` job is the single +# required status check so branch protection never gets stuck "pending" when the +# build jobs are path-skipped. on: push: @@ -31,9 +43,44 @@ concurrency: cancel-in-progress: false jobs: + detect: + name: Detect Android changes + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decide.outputs.run }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Filter paths + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + android: + - 'ft8af/**' + - '.github/workflows/android.yml' + + - name: Decide whether to run + id: decide + env: + CHANGED: ${{ steps.filter.outputs.android }} + run: | + set -euo pipefail + # main pushes and v* tags always build the full release; otherwise + # build only when Android-relevant files changed. + if [[ "$GITHUB_EVENT_NAME" == "push" && ( "$GITHUB_REF" == "refs/heads/main" || "$GITHUB_REF" == refs/tags/v* ) ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [[ "$CHANGED" == "true" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + test: name: Unit tests & coverage - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + needs: detect + if: ${{ needs.detect.outputs.run == 'true' && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) }} runs-on: ubuntu-latest steps: @@ -110,10 +157,12 @@ jobs: name: Instrumented tests (emulator) # Exercises the androidTest suite (AppSmokeTest launches the real # ComposeMainActivity) on an emulator. Same trigger as the unit-test job: - # every PR plus pushes to main. This job is intentionally NOT a dependency - # of `build` and is left out of branch protection's required checks while it - # stabilises (issue #60) — a flaky emulator must never block a release. - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + # every Android-affected PR plus pushes to main. This job is intentionally + # NOT part of android-gate and is left out of branch protection's required + # checks while it stabilises (issue #60) — a flaky emulator must never block + # a release. + needs: detect + if: ${{ needs.detect.outputs.run == 'true' && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) }} runs-on: ubuntu-latest steps: @@ -209,12 +258,85 @@ jobs: ft8af/app/build/reports/androidTests/ ft8af/app/build/outputs/androidTest-results/ + static-analysis: + name: lint / detekt / ktlint + # Issue #63: Android lint, detekt, and ktlint on PRs. + # + # ROLLOUT IS PHASED — every analysis step below is currently WARN-ONLY + # (`continue-on-error: true`). CI runs the tool and uploads its report, but a + # violation does NOT fail the check. To FLIP A TOOL TO BLOCKING: delete its + # `continue-on-error: true` line. Do them one at a time. + # + # Notes on what already gates new code even in warn-only mode: + # - detekt runs against config/detekt/baseline.xml, so it only reports NEW + # correctness findings; the baseline holds the existing ones. + # - ktlint has no baseline yet — the existing codebase isn't formatted, so + # it will report many violations until a formatting pass lands. + needs: detect + if: ${{ needs.detect.outputs.run == 'true' && github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('ft8af/**/*.gradle*', 'ft8af/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: Grant execute permission for gradlew + run: chmod +x ft8af/gradlew + + # --- WARN-ONLY during rollout: remove continue-on-error to make blocking --- + + - name: Android lint (debug) + id: lint + continue-on-error: true + working-directory: ft8af + run: ./gradlew lintDebug --stacktrace + + - name: detekt + id: detekt + continue-on-error: true + working-directory: ft8af + run: ./gradlew detekt --stacktrace + + - name: ktlint + id: ktlint + continue-on-error: true + working-directory: ft8af + run: ./gradlew ktlintCheck --stacktrace + + - name: Upload analysis reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: static-analysis-reports + path: | + ft8af/app/build/reports/lint-results-debug.html + ft8af/app/build/reports/lint-results-debug.xml + ft8af/app/build/reports/detekt/ + ft8af/app/build/reports/ktlint/ + if-no-files-found: ignore + build: name: Build APK - needs: test - # Run when tests pass (PR/main push), or when tests were skipped (push to main/dev/tags). - # Block only on actual test failure or cancellation. - if: ${{ always() && needs.test.result != 'failure' && needs.test.result != 'cancelled' }} + needs: [detect, test] + # Run when Android is in scope (detect.run) AND tests passed (PR/main push) or + # were skipped (push to dev/tags). Block only on actual test failure/cancel. + if: ${{ always() && needs.detect.outputs.run == 'true' && needs.test.result != 'failure' && needs.test.result != 'cancelled' }} runs-on: ubuntu-latest steps: @@ -442,3 +564,34 @@ jobs: track: internal status: completed releaseName: ${{ steps.tag.outputs.release_tag }} + + android-gate: + name: android-gate + # Always-run aggregator: the single required status check for Android. + # Because the build jobs above are path-skipped on unrelated changes, this + # job (which always runs) reports the overall Android result so branch + # protection never gets stuck "pending". It fails only if a REQUIRED job + # actually failed/cancelled. `instrumented` and `static-analysis` are + # deliberately excluded — instrumented is flaky (issue #60) and static + # analysis is warn-only during rollout — so neither can block a merge. + # `detect` is included: a `detect` failure skips `test`/`build`, and a skip + # alone would otherwise be read as "nothing to do" and pass the gate — so + # its result is checked directly. + needs: [detect, test, build] + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify required Android jobs succeeded or were skipped + env: + DETECT_RESULT: ${{ needs.detect.result }} + TEST_RESULT: ${{ needs.test.result }} + BUILD_RESULT: ${{ needs.build.result }} + run: | + set -euo pipefail + for r in "$DETECT_RESULT" "$TEST_RESULT" "$BUILD_RESULT"; do + if [[ "$r" == "failure" || "$r" == "cancelled" ]]; then + echo "::error::A required Android job did not pass (detect=$DETECT_RESULT test=$TEST_RESULT build=$BUILD_RESULT)" + exit 1 + fi + done + echo "Android gate OK (detect=$DETECT_RESULT test=$TEST_RESULT build=$BUILD_RESULT)." diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 00000000..4fff0e1c --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,266 @@ +name: Desktop CI & Release + +# Desktop pipeline for the desktop/ Tauri port (Rust + Vite/React). On PRs and +# non-release pushes it does a compile check (`tauri build --no-bundle`) across +# Windows + macOS + Linux. On main pushes and desktop-v* tags it builds native +# bundles on all three OSes and publishes them to a per-platform GitHub Release. +# +# Release scheme is namespaced so it sits separately from the Android v* / dev-N +# releases on the Releases page: +# desktop-v* tag -> production desktop release +# push to main -> auto-bumped desktop-v* release +# push to dev -> desktop-dev- PRERELEASE +# +# PATH-FILTERED: PRs and dev pushes only build when desktop/** or the shared +# native C core (ft8af/app/src/main/cpp/**) changes, or this workflow file. +# main pushes and desktop-v* tags always build. The always-run `desktop-gate` +# job is the required status check. + +on: + push: + branches: [main, dev] + tags: + - 'desktop-v*' + pull_request: + branches: [main, dev] + +permissions: + contents: write + +concurrency: + group: >- + ${{ + (github.event_name == 'push' + && (github.ref == 'refs/heads/main' + || github.ref == 'refs/heads/dev' + || startsWith(github.ref, 'refs/tags/desktop-v'))) + && 'desktop-release' + || format('desktop-{0}', github.run_id) + }} + cancel-in-progress: false + +jobs: + detect: + name: Detect desktop changes + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decide.outputs.run }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Filter paths + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + desktop: + - 'ft8af/app/src/main/cpp/**' + - 'desktop/**' + - '.github/workflows/desktop.yml' + + - name: Decide whether to run + id: decide + env: + CHANGED: ${{ steps.filter.outputs.desktop }} + run: | + set -euo pipefail + # main pushes and desktop-v* tags always build the full release; + # otherwise build only when desktop-relevant files changed. + if [[ "$GITHUB_EVENT_NAME" == "push" && ( "$GITHUB_REF" == "refs/heads/main" || "$GITHUB_REF" == refs/tags/desktop-v* ) ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [[ "$CHANGED" == "true" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + + version: + name: Determine desktop release tag + needs: detect + if: ${{ needs.detect.outputs.run == 'true' }} + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.tag.outputs.release_tag }} + version_name: ${{ steps.tag.outputs.version_name }} + should_release: ${{ steps.tag.outputs.should_release }} + is_prerelease: ${{ steps.tag.outputs.is_prerelease }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --tags --force + + - name: Determine release tag + id: tag + run: | + set -euo pipefail + # Mirrors the Android lane logic but in the desktop-v* / desktop-dev-N + # namespace so desktop releases sort separately from the app's. + if [[ "${GITHUB_REF}" == refs/tags/desktop-v* ]]; then + echo "release_tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + echo "version_name=${GITHUB_REF_NAME#desktop-v}" >> "$GITHUB_OUTPUT" + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + elif [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/main" ]]; then + # Auto-bump the last component from the latest desktop-v* tag. + latest=$(git tag --list 'desktop-v*' --sort=-v:refname | head -n1) + if [[ -z "$latest" ]]; then + new_tag="desktop-v0.1.0" + else + version="${latest#desktop-v}" + IFS='.' read -ra parts <<< "$version" + last_idx=$((${#parts[@]} - 1)) + parts[$last_idx]=$((parts[$last_idx] + 1)) + new_tag="desktop-v$(IFS=.; echo "${parts[*]}")" + fi + echo "Latest tag: ${latest:-} -> new tag: $new_tag" + echo "release_tag=$new_tag" >> "$GITHUB_OUTPUT" + echo "version_name=${new_tag#desktop-v}" >> "$GITHUB_OUTPUT" + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + elif [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/dev" ]]; then + new_tag="desktop-dev-${GITHUB_RUN_NUMBER}" + latest=$(git tag --list 'desktop-v*' --sort=-v:refname | head -n1) + base_ver="${latest:+${latest#desktop-v}}" + base_ver="${base_ver:-0.1.0}" + echo "release_tag=$new_tag" >> "$GITHUB_OUTPUT" + echo "version_name=${base_ver}-dev.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT" + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + # PR or other branch push — compile-check only, no release. + echo "release_tag=" >> "$GITHUB_OUTPUT" + echo "version_name=" >> "$GITHUB_OUTPUT" + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + + build: + name: Build (${{ matrix.os }}) + needs: [detect, version] + if: ${{ needs.detect.outputs.run == 'true' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri + + # Tauri v2 Linux system dependencies (webkit2gtk-4.1 + GTK + packaging), + # plus the native libs our crates link against on Linux: + # - libudev-dev: serialport's libudev-sys (CAT rig control) + # - libasound2-dev: alsa-sys (audio capture/playback) + # Without these the Linux build fails resolving the `libudev` / `alsa` + # pkg-config packages. + # + # ubuntu-latest is Ubuntu 24.04, which dropped libappindicator3-dev in + # favour of the Ayatana fork — use libayatana-appindicator3-dev (Tauri's + # current documented prerequisite). + # + # DEBIAN_FRONTEND=noninteractive + DPkg::Lock::Timeout guard against the two + # ways this step hangs forever on hosted runners: a debconf prompt that -y + # doesn't suppress, and apt blocking on the dpkg lock held by a background + # apt-daily job (instead, wait up to 10 min then fail loudly). + - name: Install Linux dependencies + if: runner.os == 'Linux' + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update -o DPkg::Lock::Timeout=600 + sudo apt-get install -y --no-install-recommends \ + -o DPkg::Lock::Timeout=600 \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + librsvg2-dev \ + libayatana-appindicator3-dev \ + libudev-dev \ + libasound2-dev \ + patchelf \ + file \ + build-essential \ + curl \ + wget + + # build.rs compiles the C core with clang-cl on the windows-msvc target + # (MSVC's cl.exe rejects ft8_lib's C99 VLAs). LLVM/clang-cl is preinstalled + # on the GitHub windows runners; this just confirms it's resolvable. + - name: Verify clang-cl (Windows) + if: runner.os == 'Windows' + shell: bash + run: clang-cl --version || echo "::warning::clang-cl not on PATH; build.rs falls back to C:\\Program Files\\LLVM\\bin" + + - name: Install frontend dependencies + working-directory: desktop + run: npm ci + + # --- Non-release: compile check only (no bundles), fastest path. --- + - name: Compile check (no bundle) + if: ${{ needs.version.outputs.should_release != 'true' }} + working-directory: desktop + run: npm run tauri build -- --no-bundle + + # --- Release: build native bundles and upload to the per-platform release. + # tauri.conf.json has bundle.active=false (keeps local dev builds bundle- + # free), so enable bundling for the release build via an inline --config + # merge. Each OS produces its native targets (dmg/app, msi/nsis, + # deb/appimage) from the base config's targets:"all". All matrix legs share + # one tagName; tauri-action creates the release once and attaches to it. --- + - name: Build & publish release bundles + if: ${{ needs.version.outputs.should_release == 'true' }} + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: desktop + tagName: ${{ needs.version.outputs.release_tag }} + releaseName: ${{ needs.version.outputs.release_tag }} + releaseBody: 'FT8AF desktop build ${{ needs.version.outputs.version_name }}.' + prerelease: ${{ needs.version.outputs.is_prerelease == 'true' }} + args: --config '{"bundle":{"active":true}}' + + desktop-gate: + name: desktop-gate + # Always-run aggregator: the single required status check for desktop. + # Reports success when every build leg passed OR the build was path-skipped, + # so branch protection never gets stuck "pending" on an unrelated change. + # Depends on `detect` and `version` too: a failure in either skips `build`, + # and a skip alone would otherwise be read as "nothing to do" and pass the + # gate — so their results are checked directly. + needs: [detect, version, build] + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify desktop jobs succeeded or were skipped + env: + DETECT_RESULT: ${{ needs.detect.result }} + VERSION_RESULT: ${{ needs.version.result }} + BUILD_RESULT: ${{ needs.build.result }} + run: | + set -euo pipefail + for r in "$DETECT_RESULT" "$VERSION_RESULT" "$BUILD_RESULT"; do + if [[ "$r" == "failure" || "$r" == "cancelled" ]]; then + echo "::error::A desktop job did not pass (detect=$DETECT_RESULT version=$VERSION_RESULT build=$BUILD_RESULT)" + exit 1 + fi + done + echo "Desktop gate OK (detect=$DETECT_RESULT version=$VERSION_RESULT build=$BUILD_RESULT)." diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 00000000..15ff4d94 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,130 @@ +name: iOS CI + +# iOS pipeline for the ios/ port. Runs the FT8AFKit host test suites +# (FT8DSP/FT8Audio/FT8Engine/FT8Rig — the encode/decode/golden gate compiled +# under Apple clang) and an unsigned iOS-Simulator build of the FT8AF app to +# prove it compiles. No release artifact: a distributable .ipa needs an Apple +# Developer cert + provisioning profile, which aren't wired up yet. +# +# PATH-FILTERED: only runs when ios/** or the shared native C core +# (ft8af/app/src/main/cpp/**) changes, or this workflow file. Pushes to main +# always verify. The always-run `ios-gate` job is the required status check. + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +concurrency: + group: ios-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + detect: + name: Detect iOS changes + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decide.outputs.run }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Filter paths + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + ios: + - 'ft8af/app/src/main/cpp/**' + - 'ios/**' + - '.github/workflows/ios.yml' + + - name: Decide whether to run + id: decide + env: + CHANGED: ${{ steps.filter.outputs.ios }} + run: | + set -euo pipefail + # main pushes always verify; otherwise only when iOS-relevant files + # (or the shared C core) changed. + if [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [[ "$CHANGED" == "true" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + + verify: + name: swift test + simulator build + needs: detect + if: ${{ needs.detect.outputs.run == 'true' }} + # macos-15 ships Xcode 16. Current XcodeGen (2.45.x) always writes the + # pbxproj in the Xcode 16 object format (objectVersion 77), which Xcode 15 + # refuses to open ("future Xcode project file format (77)"), so the + # simulator build needs Xcode 16 to read the generated project. The app's + # iOS 17 deployment target still builds fine under the newer toolchain. + runs-on: macos-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # FT8AFKit declares a macOS platform so its pure-logic suites run on the + # host with no simulator. This is the real DSP gate: green here means the + # shared C core (#included in place from ../../ft8af/app/src/main/cpp) is + # bit-identical under Apple clang. Surfaces the ios/README.md + # header-search-path / non-modular-include caveats if any regress. + - name: Run FT8AFKit tests + working-directory: ios/FT8AFKit + run: swift test + + # The app's .xcodeproj is generated from project.yml via XcodeGen and no + # shared scheme is committed, so regenerate it to get a buildable scheme. + - name: Install XcodeGen + run: brew install xcodegen + + - name: Generate Xcode project + working-directory: ios/FT8AF + run: xcodegen generate + + # Unsigned simulator build — proves the SwiftUI app + FT8AFKit link and + # compile. CODE_SIGNING_ALLOWED=NO so no certs/provisioning are needed. + - name: Build app for iOS Simulator (unsigned) + working-directory: ios/FT8AF + run: | + set -euo pipefail + xcodebuild build \ + -project FT8AF.xcodeproj \ + -scheme FT8AF \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -configuration Debug \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO + + ios-gate: + name: ios-gate + # Always-run aggregator: the single required status check for iOS. Reports + # success when `verify` passed OR was path-skipped, so branch protection + # never gets stuck "pending" on an unrelated change. Depends on `detect` + # too: a `detect` failure skips `verify`, and a skip alone would otherwise + # be read as "nothing to do" and pass the gate — so its result is checked. + needs: [detect, verify] + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify iOS jobs succeeded or were skipped + env: + DETECT_RESULT: ${{ needs.detect.result }} + VERIFY_RESULT: ${{ needs.verify.result }} + run: | + set -euo pipefail + for r in "$DETECT_RESULT" "$VERIFY_RESULT"; do + if [[ "$r" == "failure" || "$r" == "cancelled" ]]; then + echo "::error::An iOS job did not pass (detect=$DETECT_RESULT verify=$VERIFY_RESULT)" + exit 1 + fi + done + echo "iOS gate OK (detect=$DETECT_RESULT verify=$VERIFY_RESULT)." diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml deleted file mode 100644 index 9da514e5..00000000 --- a/.github/workflows/static-analysis.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Static analysis - -# Issue #63: run Android lint, detekt, and ktlint on PRs. -# -# ROLLOUT IS PHASED — every analysis step below is currently WARN-ONLY -# (`continue-on-error: true`). CI runs the tool and uploads its report, but a -# violation does NOT fail the check. This lets the team see a few PRs' worth of -# output before any of these gate merges. -# -# To FLIP A TOOL TO BLOCKING: delete its `continue-on-error: true` line. Do them -# one at a time, not all three at once. -# -# Notes on what already gates new code even in warn-only mode: -# - detekt runs against config/detekt/baseline.xml, so it only reports NEW -# correctness findings; the baseline holds the existing ones. -# - ktlint has no baseline yet — the existing codebase isn't formatted, so it -# will report many violations until a formatting pass lands. That's why it -# stays warn-only here. - -on: - pull_request: - branches: [main, dev] - -jobs: - static-analysis: - name: lint / detekt / ktlint - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('ft8af/**/*.gradle*', 'ft8af/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Grant execute permission for gradlew - run: chmod +x ft8af/gradlew - - # --- WARN-ONLY during rollout: remove continue-on-error to make blocking --- - - - name: Android lint (debug) - id: lint - continue-on-error: true - working-directory: ft8af - run: ./gradlew lintDebug --stacktrace - - - name: detekt - id: detekt - continue-on-error: true - working-directory: ft8af - run: ./gradlew detekt --stacktrace - - - name: ktlint - id: ktlint - continue-on-error: true - working-directory: ft8af - run: ./gradlew ktlintCheck --stacktrace - - - name: Upload analysis reports - if: always() - uses: actions/upload-artifact@v7 - with: - name: static-analysis-reports - path: | - ft8af/app/build/reports/lint-results-debug.html - ft8af/app/build/reports/lint-results-debug.xml - ft8af/app/build/reports/detekt/ - ft8af/app/build/reports/ktlint/ - if-no-files-found: ignore diff --git a/ft8af/app/src/main/cpp/ft8_lib/ft8/text.c b/ft8af/app/src/main/cpp/ft8_lib/ft8/text.c index 869f6fc1..557ecfeb 100644 --- a/ft8af/app/src/main/cpp/ft8_lib/ft8/text.c +++ b/ft8af/app/src/main/cpp/ft8_lib/ft8/text.c @@ -90,7 +90,7 @@ bool equals(const char* string1, const char* string2) // Text message formatting: // - replaces lowercase letters with uppercase // - merges consecutive spaces into single space -void fmtmsg(char* msg_out, const char* msg_in) +void ft8_fmtmsg(char* msg_out, const char* msg_in) { char c; char last_out = 0; diff --git a/ft8af/app/src/main/cpp/ft8_lib/ft8/text.h b/ft8af/app/src/main/cpp/ft8_lib/ft8/text.h index dd90e700..7a24386d 100644 --- a/ft8af/app/src/main/cpp/ft8_lib/ft8/text.h +++ b/ft8af/app/src/main/cpp/ft8_lib/ft8/text.h @@ -35,7 +35,10 @@ bool equals(const char* string1, const char* string2); // Text message formatting: // - replaces lowercase letters with uppercase // - merges consecutive spaces into single space -void fmtmsg(char* msg_out, const char* msg_in); +// Named ft8_fmtmsg (not the upstream `fmtmsg`) to avoid colliding with the +// POSIX `fmtmsg(long, ...)` declared in , which is visible when this +// header is compiled under the macOS/Apple SDK (e.g. FT8AFKit `swift test`). +void ft8_fmtmsg(char* msg_out, const char* msg_in); /// Extract and copy a space-delimited token from a string. /// When the last token has been extracted, the return value points to the terminating zero character.