From 4f03c3714e2ca3974f102f35e6d83472afc54a58 Mon Sep 17 00:00:00 2001 From: Peerapat_J Date: Sun, 24 May 2026 02:02:54 +0700 Subject: [PATCH 1/4] Add CI and packaging workflow --- .github/workflows/ci.yml | 78 ++++++++++++++++++++++++++++++++++++++++ docs/ci-cd.md | 30 ++++++++++++++++ script/build_and_run.sh | 34 ++++++++++++------ 3 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/ci-cd.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..407da62 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build and test + runs-on: macos-15 + timeout-minutes: 20 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Print tool versions + run: | + swift --version + xcodebuild -version + + - name: Build Swift package + run: swift build --product LinguistMac + + - name: Run Swift package tests + run: swift test + + - name: Build app scheme with xcodebuild + run: | + xcodebuild \ + -scheme LinguistMac \ + -destination 'platform=macOS' \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedData" \ + CODE_SIGNING_ALLOWED=NO \ + build + + - name: Test app scheme with xcodebuild + run: | + xcodebuild \ + -scheme LinguistMac \ + -destination 'platform=macOS' \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedDataTests" \ + CODE_SIGNING_ALLOWED=NO \ + test + + package-unsigned-app: + name: Package unsigned app + needs: build-and-test + runs-on: macos-15 + timeout-minutes: 15 + if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build app bundle + run: ./script/build_and_run.sh --package + + - name: Archive unsigned app bundle + run: ditto -c -k --keepParent dist/LinguistMac.app dist/LinguistMac-unsigned.zip + + - name: Upload unsigned app artifact + uses: actions/upload-artifact@v4 + with: + name: LinguistMac-unsigned + path: dist/LinguistMac-unsigned.zip + if-no-files-found: error diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..2250ba6 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,30 @@ +# CI/CD + +LinguistMac uses GitHub Actions to keep the clean-room macOS rewrite buildable as each feature lands. + +## Continuous integration + +The `CI` workflow runs on pull requests, pushes to `main`, and manual dispatches. It checks: + +- `swift build --product LinguistMac` +- `swift test` +- `xcodebuild` build for the `LinguistMac` scheme +- `xcodebuild` test for the `LinguistMac` scheme + +## Delivery artifact + +The workflow also builds an unsigned `.app` artifact on pushes to `main` and manual dispatches. This is only a development artifact, not a signed or notarized release. + +Signed distribution should wait until the app identity, entitlements, Developer ID signing, and notarization flow are defined. + +## Local parity + +Run the same checks locally before pushing: + +```sh +swift build --product LinguistMac +swift test +xcodebuild -scheme LinguistMac -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived CODE_SIGNING_ALLOWED=NO build +xcodebuild -scheme LinguistMac -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived-test CODE_SIGNING_ALLOWED=NO test +./script/build_and_run.sh --package +``` diff --git a/script/build_and_run.sh b/script/build_and_run.sh index ac0390b..4740126 100755 --- a/script/build_and_run.sh +++ b/script/build_and_run.sh @@ -37,17 +37,17 @@ APPLESCRIPT pkill -f "$APP_BINARY" >/dev/null 2>&1 || true } -quit_existing_app +build_app_bundle() { + swift build --product "$APP_NAME" + local build_binary + build_binary="$(swift build --show-bin-path)/$APP_NAME" -swift build --product "$APP_NAME" -BUILD_BINARY="$(swift build --show-bin-path)/$APP_NAME" + rm -rf "$APP_BUNDLE" + mkdir -p "$APP_MACOS" + cp "$build_binary" "$APP_BINARY" + chmod +x "$APP_BINARY" -rm -rf "$APP_BUNDLE" -mkdir -p "$APP_MACOS" -cp "$BUILD_BINARY" "$APP_BINARY" -chmod +x "$APP_BINARY" - -cat >"$INFO_PLIST" <"$INFO_PLIST" < @@ -67,6 +67,7 @@ cat >"$INFO_PLIST" < PLIST +} open_app() { /usr/bin/open -n "$APP_BUNDLE" @@ -74,26 +75,39 @@ open_app() { case "$MODE" in run) + quit_existing_app + build_app_bundle open_app ;; + --package|package) + build_app_bundle + ;; --debug|debug) + quit_existing_app + build_app_bundle lldb -- "$APP_BINARY" ;; --logs|logs) + quit_existing_app + build_app_bundle open_app /usr/bin/log stream --info --style compact --predicate "process == \"$APP_NAME\"" ;; --telemetry|telemetry) + quit_existing_app + build_app_bundle open_app /usr/bin/log stream --info --style compact --predicate "subsystem == \"$BUNDLE_ID\"" ;; --verify|verify) + quit_existing_app + build_app_bundle open_app sleep 1 pgrep -x "$APP_NAME" >/dev/null ;; *) - echo "usage: $0 [run|--debug|--logs|--telemetry|--verify]" >&2 + echo "usage: $0 [run|--package|--debug|--logs|--telemetry|--verify]" >&2 exit 2 ;; esac From d8f025cee319d513570676b801510a77c0d1f8ea Mon Sep 17 00:00:00 2001 From: Peerapat_J Date: Sun, 24 May 2026 02:09:18 +0700 Subject: [PATCH 2/4] Use package scheme for xcodebuild tests --- .github/workflows/ci.yml | 4 ++-- docs/ci-cd.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407da62..d6ccf25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,10 @@ jobs: CODE_SIGNING_ALLOWED=NO \ build - - name: Test app scheme with xcodebuild + - name: Test package scheme with xcodebuild run: | xcodebuild \ - -scheme LinguistMac \ + -scheme LinguistMac-Package \ -destination 'platform=macOS' \ -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedDataTests" \ CODE_SIGNING_ALLOWED=NO \ diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 2250ba6..232da97 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -9,7 +9,7 @@ The `CI` workflow runs on pull requests, pushes to `main`, and manual dispatches - `swift build --product LinguistMac` - `swift test` - `xcodebuild` build for the `LinguistMac` scheme -- `xcodebuild` test for the `LinguistMac` scheme +- `xcodebuild` test for the `LinguistMac-Package` scheme ## Delivery artifact @@ -25,6 +25,6 @@ Run the same checks locally before pushing: swift build --product LinguistMac swift test xcodebuild -scheme LinguistMac -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived CODE_SIGNING_ALLOWED=NO build -xcodebuild -scheme LinguistMac -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived-test CODE_SIGNING_ALLOWED=NO test +xcodebuild -scheme LinguistMac-Package -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived-test CODE_SIGNING_ALLOWED=NO test ./script/build_and_run.sh --package ``` From 1ce7e4f2164e38a780bd3fb3ced0c40fd05d1bd3 Mon Sep 17 00:00:00 2001 From: Peerapat_J Date: Sun, 24 May 2026 02:37:09 +0700 Subject: [PATCH 3/4] Expand CI quality checks --- .github/workflows/ci.yml | 84 +++++++++++++++++-- .swiftformat | 8 ++ .swiftlint.yml | 36 ++++++++ .../AppFeatureTests.swift | 2 +- docs/ci-cd.md | 28 +++++-- 5 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 .swiftformat create mode 100644 .swiftlint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6ccf25..b9b99d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,46 @@ concurrency: cancel-in-progress: true jobs: + lint-and-format: + name: Lint and format + runs-on: macos-15 + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install lint tools + run: | + if ! command -v swiftlint >/dev/null; then + brew install swiftlint + fi + + if ! command -v swiftformat >/dev/null; then + brew install swiftformat + fi + + swiftlint version + swiftformat --version + + - name: Run SwiftLint + run: swiftlint lint --strict --no-cache + + - name: Check SwiftFormat + run: swiftformat --lint . --config .swiftformat --cache ignore + build-and-test: - name: Build and test + name: Build and test (${{ matrix.configuration }}) runs-on: macos-15 timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - configuration: Debug + swift-configuration: debug + - configuration: Release + swift-configuration: release steps: - name: Check out repository @@ -30,17 +66,18 @@ jobs: xcodebuild -version - name: Build Swift package - run: swift build --product LinguistMac + run: swift build -c ${{ matrix.swift-configuration }} --product LinguistMac - name: Run Swift package tests - run: swift test + run: swift test -c ${{ matrix.swift-configuration }} - name: Build app scheme with xcodebuild run: | xcodebuild \ -scheme LinguistMac \ + -configuration ${{ matrix.configuration }} \ -destination 'platform=macOS' \ - -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedData" \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedData-${{ matrix.configuration }}" \ CODE_SIGNING_ALLOWED=NO \ build @@ -48,14 +85,49 @@ jobs: run: | xcodebuild \ -scheme LinguistMac-Package \ + -configuration ${{ matrix.configuration }} \ -destination 'platform=macOS' \ - -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedDataTests" \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacDerivedDataTests-${{ matrix.configuration }}" \ CODE_SIGNING_ALLOWED=NO \ + ENABLE_TESTABILITY=YES \ test + strict-analysis: + name: Strict analysis + runs-on: macos-15 + timeout-minutes: 20 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build with strict Swift compiler flags + run: | + swift build \ + -c debug \ + --product LinguistMac \ + -Xswiftc -warnings-as-errors \ + -Xswiftc -strict-concurrency=complete + + - name: Run Xcode static analyzer + run: | + xcodebuild \ + -scheme LinguistMac \ + -configuration Debug \ + -destination 'platform=macOS' \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacAnalyzerDerivedData" \ + CODE_SIGNING_ALLOWED=NO \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=YES \ + GCC_TREAT_WARNINGS_AS_ERRORS=YES \ + CLANG_ANALYZER_NONNULL=YES \ + analyze + package-unsigned-app: name: Package unsigned app - needs: build-and-test + needs: + - lint-and-format + - build-and-test + - strict-analysis runs-on: macos-15 timeout-minutes: 15 if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..9443fe3 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,8 @@ +--swiftversion 6.0 +--indent 4 +--linebreaks lf +--semicolons never +--stripunusedargs closure-only +--trailing-commas never +--trimwhitespace always +--exclude .build,.git,.swiftpm,DerivedData,dist diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..e659a40 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,36 @@ +included: + - Package.swift + - Sources + - Tests + +excluded: + - .build + - .git + - .swiftpm + - DerivedData + - dist + +analyzer_rules: + - unused_declaration + - unused_import + +line_length: + warning: 120 + error: 160 + +identifier_name: + min_length: + warning: 2 + error: 1 + +file_length: + warning: 500 + error: 800 + +type_body_length: + warning: 300 + error: 500 + +function_body_length: + warning: 50 + error: 100 diff --git a/Tests/LinguistMacCoreTests/AppFeatureTests.swift b/Tests/LinguistMacCoreTests/AppFeatureTests.swift index e11615f..3c1059a 100644 --- a/Tests/LinguistMacCoreTests/AppFeatureTests.swift +++ b/Tests/LinguistMacCoreTests/AppFeatureTests.swift @@ -1,5 +1,5 @@ -import XCTest @testable import LinguistMacCore +import XCTest final class AppFeatureTests: XCTestCase { func testStarterFeaturesAreNotEmpty() { diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 232da97..c6f1f19 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -6,10 +6,14 @@ LinguistMac uses GitHub Actions to keep the clean-room macOS rewrite buildable a The `CI` workflow runs on pull requests, pushes to `main`, and manual dispatches. It checks: -- `swift build --product LinguistMac` -- `swift test` -- `xcodebuild` build for the `LinguistMac` scheme -- `xcodebuild` test for the `LinguistMac-Package` scheme +- SwiftLint in strict mode +- SwiftFormat in lint mode +- `swift build` for Debug and Release +- `swift test` for Debug and Release +- `xcodebuild` build for the `LinguistMac` scheme in Debug and Release +- `xcodebuild` test for the `LinguistMac-Package` scheme in Debug and Release +- strict Swift compiler checks with warnings treated as errors +- Xcode static analyzer checks with warnings treated as errors ## Delivery artifact @@ -22,9 +26,17 @@ Signed distribution should wait until the app identity, entitlements, Developer Run the same checks locally before pushing: ```sh -swift build --product LinguistMac -swift test -xcodebuild -scheme LinguistMac -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived CODE_SIGNING_ALLOWED=NO build -xcodebuild -scheme LinguistMac-Package -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-derived-test CODE_SIGNING_ALLOWED=NO test +swiftlint lint --strict --no-cache +swiftformat --lint . --config .swiftformat --cache ignore +swift build -c debug --product LinguistMac +swift build -c release --product LinguistMac +swift test -c debug +swift test -c release +xcodebuild -scheme LinguistMac -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-debug CODE_SIGNING_ALLOWED=NO build +xcodebuild -scheme LinguistMac -configuration Release -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-release CODE_SIGNING_ALLOWED=NO build +xcodebuild -scheme LinguistMac-Package -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-debug-test CODE_SIGNING_ALLOWED=NO ENABLE_TESTABILITY=YES test +xcodebuild -scheme LinguistMac-Package -configuration Release -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-release-test CODE_SIGNING_ALLOWED=NO ENABLE_TESTABILITY=YES test +swift build -c debug --product LinguistMac -Xswiftc -warnings-as-errors -Xswiftc -strict-concurrency=complete +xcodebuild -scheme LinguistMac -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-analyze CODE_SIGNING_ALLOWED=NO SWIFT_TREAT_WARNINGS_AS_ERRORS=YES GCC_TREAT_WARNINGS_AS_ERRORS=YES CLANG_ANALYZER_NONNULL=YES analyze ./script/build_and_run.sh --package ``` From 15f1458cb8841761c9296f9b5995f5bebfd2053e Mon Sep 17 00:00:00 2001 From: Peerapat_J Date: Sun, 24 May 2026 13:02:28 +0700 Subject: [PATCH 4/4] Address CI review findings --- .github/workflows/ci.yml | 33 +++++++++++++++++++----- Sources/LinguistMacCore/AppFeature.swift | 2 -- docs/ci-cd.md | 3 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9b99d7..ae1b1da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,9 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Install lint tools run: | @@ -37,9 +39,22 @@ jobs: swiftlint version swiftformat --version - - name: Run SwiftLint + - name: Run SwiftLint lint run: swiftlint lint --strict --no-cache + - name: Capture compiler log for SwiftLint analyzer + run: | + xcodebuild \ + -scheme LinguistMac \ + -configuration Debug \ + -destination 'platform=macOS' \ + -derivedDataPath "$RUNNER_TEMP/LinguistMacSwiftLintDerivedData" \ + CODE_SIGNING_ALLOWED=NO \ + clean build > "$RUNNER_TEMP/swiftlint-analyze.log" 2>&1 + + - name: Run SwiftLint + run: swiftlint analyze --strict --compiler-log-path "$RUNNER_TEMP/swiftlint-analyze.log" + - name: Check SwiftFormat run: swiftformat --lint . --config .swiftformat --cache ignore @@ -58,7 +73,9 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Print tool versions run: | @@ -99,7 +116,9 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Build with strict Swift compiler flags run: | @@ -134,7 +153,9 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false - name: Build app bundle run: ./script/build_and_run.sh --package @@ -143,7 +164,7 @@ jobs: run: ditto -c -k --keepParent dist/LinguistMac.app dist/LinguistMac-unsigned.zip - name: Upload unsigned app artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: LinguistMac-unsigned path: dist/LinguistMac-unsigned.zip diff --git a/Sources/LinguistMacCore/AppFeature.swift b/Sources/LinguistMacCore/AppFeature.swift index 9dfba01..e0d5bad 100644 --- a/Sources/LinguistMacCore/AppFeature.swift +++ b/Sources/LinguistMacCore/AppFeature.swift @@ -1,5 +1,3 @@ -import Foundation - public struct AppFeature: Identifiable, Equatable, Sendable { public let id: String public let title: String diff --git a/docs/ci-cd.md b/docs/ci-cd.md index c6f1f19..0ca63e3 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -7,6 +7,7 @@ LinguistMac uses GitHub Actions to keep the clean-room macOS rewrite buildable a The `CI` workflow runs on pull requests, pushes to `main`, and manual dispatches. It checks: - SwiftLint in strict mode +- SwiftLint analyzer rules with compiler-log input - SwiftFormat in lint mode - `swift build` for Debug and Release - `swift test` for Debug and Release @@ -27,6 +28,8 @@ Run the same checks locally before pushing: ```sh swiftlint lint --strict --no-cache +xcodebuild -scheme LinguistMac -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/linguistmac-swiftlint CODE_SIGNING_ALLOWED=NO clean build > /tmp/linguistmac-swiftlint-analyze.log 2>&1 +swiftlint analyze --strict --compiler-log-path /tmp/linguistmac-swiftlint-analyze.log swiftformat --lint . --config .swiftformat --cache ignore swift build -c debug --product LinguistMac swift build -c release --product LinguistMac