diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ae1b1da --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,171 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + 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@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - 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 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 + + 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 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - name: Print tool versions + run: | + swift --version + xcodebuild -version + + - name: Build Swift package + run: swift build -c ${{ matrix.swift-configuration }} --product LinguistMac + + - name: Run Swift package tests + 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-${{ matrix.configuration }}" \ + CODE_SIGNING_ALLOWED=NO \ + build + + - name: Test package scheme with xcodebuild + run: | + xcodebuild \ + -scheme LinguistMac-Package \ + -configuration ${{ matrix.configuration }} \ + -destination 'platform=macOS' \ + -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@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - 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: + - 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' + + steps: + - name: Check out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + + - 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@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: LinguistMac-unsigned + path: dist/LinguistMac-unsigned.zip + if-no-files-found: error 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/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/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 new file mode 100644 index 0000000..0ca63e3 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,45 @@ +# 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: + +- 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 +- `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 + +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 +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 +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 +``` 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