Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 0 additions & 2 deletions Sources/LinguistMacCore/AppFeature.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

public struct AppFeature: Identifiable, Equatable, Sendable {
public let id: String
public let title: String
Expand Down
2 changes: 1 addition & 1 deletion Tests/LinguistMacCoreTests/AppFeatureTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import XCTest
@testable import LinguistMacCore
import XCTest

final class AppFeatureTests: XCTestCase {
func testStarterFeaturesAreNotEmpty() {
Expand Down
45 changes: 45 additions & 0 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
@@ -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
```
34 changes: 24 additions & 10 deletions script/build_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" <<PLIST
cat >"$INFO_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand All @@ -67,33 +67,47 @@ cat >"$INFO_PLIST" <<PLIST
</dict>
</plist>
PLIST
}

open_app() {
/usr/bin/open -n "$APP_BUNDLE"
}

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
Loading