diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..3c9d444 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,73 @@ +name: macoc CI (Docker) + +on: + push: + branches: + - main + tags: + - 'v*' + workflow_dispatch: + +jobs: + swift-core: + # This is the ONLY currently available runner label. + runs-on: docker + # The runner may have a Node/Alpine default image. Override it with + # official Swift on Linux so we can run the real platform-neutral XCTest. + container: + image: swift:6.2-noble + steps: + - name: Check out commit without Node.js actions + env: + MACOC_SERVER_URL: "${{ forgejo.server_url }}" + MACOC_REPOSITORY: "${{ forgejo.repository }}" + MACOC_SHA: "${{ forgejo.sha }}" + MACOC_TOKEN: "${{ forgejo.token }}" + run: | + set -eu + test "$MACOC_REPOSITORY" = "catsona/macoc" + case "$MACOC_SHA" in + ''|*[!0-9a-f]*) echo "Invalid Forgejo commit SHA" >&2; exit 1 ;; + esac + + # Checkout actions depend on Node.js, which the official Swift + # image does not provide. Forgejo's own archive API returns an + # archive for the exact triggering commit (also for private repos). + workspace="${FORGEJO_WORKSPACE:-${GITHUB_WORKSPACE:-$PWD}}" + mkdir -p "$workspace" + archive="$(mktemp)" + config="$(mktemp)" + chmod 600 "$config" + trap 'rm -f "$archive" "$config"' EXIT + printf 'header = "Authorization: token %s"\n' "$MACOC_TOKEN" > "$config" + curl --config "$config" --fail --silent --show-error \ + "$MACOC_SERVER_URL/api/v1/repos/$MACOC_REPOSITORY/archive/$MACOC_SHA.tar.gz" \ + --output "$archive" + tar -xzf "$archive" --strip-components=1 -C "$workspace" + cd "$workspace" + + # The API tarball has no .git directory. Create a temporary local + # index so git diff --cached --check still catches whitespace errors. + git init -q + git config --global --add safe.directory "$workspace" + git add -A + + - name: Lint and run Linux SwiftCore XCTest + env: + MACOC_REQUIRE_SWIFT: '1' + run: | + swift --version + bash scripts/verify.sh + + - name: Verify release tag matches Info.plist + if: startsWith(forgejo.ref, 'refs/tags/v') + env: + MACOC_TAG: "${{ forgejo.ref_name }}" + run: | + set -eu + version="$(sed -n '/CFBundleShortVersionString<\/key>/{n;s/.*\([^<]*\)<\/string>.*/\1/p;}' Resources/Info.plist)" + if [ -z "$version" ] || [ "$MACOC_TAG" != "v$version" ]; then + echo "Tag $MACOC_TAG does not match app version v$version" >&2 + exit 1 + fi + echo "Validated source tag $MACOC_TAG (no macOS binary built in Docker)." diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml deleted file mode 100644 index d38bae5..0000000 --- a/.forgejo/workflows/macos.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: macoc macOS - -on: - push: - branches: - - main - tags: - - 'v*' - workflow_dispatch: - -jobs: - build: - # A dedicated macOS Forgejo runner is required; register label macos:host. - # It must have Command Line Tools. There is no Linux/macOS cross-build. - runs-on: macos - steps: - - name: Checkout - uses: https://code.forgejo.org/actions/checkout@v4 - with: - persist-credentials: false - - - name: Run tests and static checks - run: bash scripts/verify.sh - - - name: Build and package macOS app - run: bash scripts/package.sh - env: - MACOC_CODESIGN_IDENTITY: "${{ vars.MACOC_CODESIGN_IDENTITY }}" - - - name: Publish Forgejo release on version tags - if: startsWith(forgejo.ref, 'refs/tags/v') - env: - FORGEJO_TOKEN: "${{ secrets.FORGEJO_TOKEN }}" - FORGEJO_SERVER_URL: "${{ forgejo.server_url }}" - FORGEJO_REPOSITORY: "${{ forgejo.repository }}" - FORGEJO_REF_NAME: "${{ forgejo.ref_name }}" - run: bash scripts/publish-release.sh diff --git a/README.md b/README.md index 90f3e6d..37a485e 100644 --- a/README.md +++ b/README.md @@ -102,17 +102,23 @@ assigned its normal modifier role rather than No Action. ## Forgejo CI and releases -The workflow at .forgejo/workflows/macos.yml builds and tests on pushes to -main. A pushed tag such as v0.3.0 also creates a Forgejo release with a -macOS .app ZIP and SHA-256 checksum, through the built-in scoped token. +CI workflow: .forgejo/workflows/ci.yml. It uses the **existing Docker +runner** and the official Linux Swift image. Pushes to main run shell +validation and **actual XCTest for pure Swift core logic**. On version +tags it also checks the tag against the application version. -**A real Mac Forgejo runner labelled macos is required.** The Linux server -cannot build AppKit or IOKit. See [CI setup](docs/ci.md). Until a Mac runner -is registered and its job succeeds, no release build has been produced. +**Linux Docker cannot compile macOS .app binaries** because Apple AppKit, +IOKit and macOS SDK are unavailable there. Docker CI does not publish +nonexistent macOS binaries. -A release archive signed ad-hoc is intended for local evaluation. -To distribute broadly without Gatekeeper warnings, use Developer ID -signing and Apple's notarization service (not yet configured here). +For a release archive, check out the tag on a real Mac and run +bash scripts/package.sh (or task package), then execute +bash scripts/publish-release.sh with a repository-scoped Forgejo token. +See [CI and release instructions](docs/ci.md). Fully automated macOS +binary releases require a real Mac runner or another Mac build service. + +Ad-hoc artifacts are suited to local evaluation; public distribution +requires Developer ID signing and Apple notarization. ## Diagnostics and recovery diff --git a/docs/ci.md b/docs/ci.md index f6234be..b41fa17 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,48 +1,66 @@ -# Forgejo Actions CI and macOS releases +# Forgejo Actions CI: existing Docker runner -The workflow is in .forgejo/workflows/macos.yml. +The only registered Forgejo runner has the **docker** label. Workflow: +.forgejo/workflows/ci.yml. It uses runs-on: docker with the official +swift:6.2-noble Linux container image. -- Push to main: Swift tests, static checks, macOS app build and ZIP archive. -- Push a version tag like v0.3.0: same checks plus a Forgejo release with - macoc-vX.Y.Z-macos-.zip and a SHA-256 checksum file. -- Manual workflow run: test and build only. +## CI checks -Artifacts are for the runner's CPU architecture, not universal. +- Main branch / manual dispatch: obtain the exact triggering commit, validate + source and shell scripts, and run the **real** XCTest suite for the pure + Swift CapswitchCore module. +- Tags v*: same tests plus Info.plist-version/tag consistency checking. + The v0.3.0 tag must match CFBundleShortVersionString 0.3.0. -## One-time macOS runner setup +scripts/test-linux-core.sh creates a temporary SwiftPM package containing +the actual Sources/Capswitch source files and Tests/CapswitchCoreTests suite. +The standard Package.swift is macOS-specific and cannot be compiled on Linux. -1. Enable Actions under repository Settings -> Units. -2. Register a dedicated runner on a real Mac with Command Line Tools, Swift, - iconutil, sips, codesign, ditto, curl, Git, Python 3 and Node for checkout. -3. Under Settings -> Actions -> Runners obtain the registration token. -4. Configure the runner label macos:host; workflow uses runs-on: macos. -5. Push a main-branch commit to check the workflow. Then push an exact version - tag matching Resources/Info.plist when the build is green. +## Why Docker CI does not produce a macOS binary -Host runners have no isolation; allow only trusted contributors to edit -the workflow, protect release tags, and avoid running arbitrary PR code. +Linux Docker does not include the Apple macOS SDK, AppKit, Carbon or IOKit. +It cannot generate a signed functional macoc.app. This workflow does not run +scripts/package.sh on Linux and does not create a misleading binary release. -Official documentation: -https://forgejo.org/docs/latest/user/actions/overview/ +For a release with the current infrastructure: + +1. Push tag v0.3.0 (or the version in Info.plist); verify Docker CI passed. +2. On a real Mac with Command Line Tools, check out that precise tag. +3. Run bash scripts/package.sh (equivalent to task package). This creates + dist/macoc-v0.3.0-macos-.zip and the SHA-256 checksum file. +4. Set FORGEJO_SERVER_URL, FORGEJO_REPOSITORY=catsona/macoc, + FORGEJO_REF_NAME=v0.3.0, and FORGEJO_TOKEN with repository release write + access; then run bash scripts/publish-release.sh to publish the Mac + generated ZIP/checksum to Forgejo. + +That is a manual macOS packaging step. For fully automated builds of +macOS binaries, a real Mac runner or a trusted Mac build service is needed. + +Code signing / Developer ID notarization is separate; ad-hoc builds may +require explicit Gatekeeper approval. + +## No Node checkout dependency + +Forgejo Runner's default Docker image might supply Node, but the official +Swift image does not promise Node.js, so actions/checkout is not used. +The workflow obtains an authenticated archive at the triggering SHA using +Forgejo's own API, with the short-lived repository-scoped forgejo.token. +The token is only placed in a mode-0600 temporary curl configuration, +not in clone URLs, source files or process arguments. + +As tar archives have no Git index, the job creates a local Git index for +git diff --cached --check. No CI commit or push occurs. + +## Prerequisites + +The registered runner must advertise the docker label to this repository. +It must be allowed to pull swift:6.2-noble from Docker Hub and reach the +Forgejo API. No Docker socket inside the job is necessary. + +Forgejo documentation: https://forgejo.org/docs/latest/admin/actions/configuration/ -https://forgejo.org/docs/latest/user/actions/basic-concepts/ +https://forgejo.org/docs/latest/user/actions/reference/ +https://forgejo.org/docs/latest/user/actions/security/ -## Signing and publishing - -The optional Forgejo repository variable MACOC_CODESIGN_IDENTITY selects -a code-signing identity already available in the macOS runner keychain. -Otherwise builds use ad-hoc signing. Ad-hoc releases are useful for local -testing but are not notarized and may trigger Gatekeeper warnings. - -For public distribution use Developer ID Application signing plus Apple -notarization. Do not commit private keys or notarization credentials. -Notarization has not been automated in this version. - -scripts/package.sh builds the archive and SHA-256 checksum. -scripts/publish-release.sh creates the release and uploads artifacts over -Forgejo REST using the workflow's short-lived FORGEJO_TOKEN. No long-lived -access token is stored in this repository. - -The workflow requires a separately provisioned macOS runner. It cannot -be completed with the current Linux development host alone, and merely -committing it does not prove that a Mac build/release has run. +Swift Docker: +https://www.swift.org/install/linux/docker/ diff --git a/scripts/lint.sh b/scripts/lint.sh index 5fcb6a0..c87489a 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -18,8 +18,8 @@ grep -q 'TISSelectInputSource' Sources/CapswitchApp/InputSourceManager.swift grep -q 'IOHIDServiceClientSetProperty' Sources/CapswitchHID/CapswitchRemapping.c grep -q '/usr/bin/open' Sources/CapswitchApp/ApplicationRelauncher.swift grep -q 'SMAppService.mainApp' Sources/CapswitchApp/LoginItemManager.swift -test -f .forgejo/workflows/macos.yml -for name in build bundle test install doctor clean lint verify icon sign package publish-release setup-signing; do +test -f .forgejo/workflows/ci.yml +for name in build bundle test test-linux-core install doctor clean lint verify icon sign package publish-release setup-signing; do bash -n "scripts/$name.sh" done echo "lint: PASS" diff --git a/scripts/test-linux-core.sh b/scripts/test-linux-core.sh new file mode 100755 index 0000000..afbe0e7 --- /dev/null +++ b/scripts/test-linux-core.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +source "$(dirname "$0")/common.sh" + +if [[ "$IS_MACOS" == 1 ]]; then + echo "Linux-only test fixture must not be used on macOS." >&2 + exit 1 +fi + +if ! command -v swift >/dev/null 2>&1; then + echo "Swift toolchain is required for Linux core tests." >&2 + exit 1 +fi + +# The normal Package.swift targets AppKit, Carbon and IOKit. None of those +# frameworks exist in Linux containers. Build only the pure Swift core and +# its real XCTest suite in a temporary, independent SwiftPM package. +temporary_package="$(mktemp -d)" +trap 'rm -rf "$temporary_package"' EXIT +mkdir -p \ + "$temporary_package/Sources/CapswitchCore" \ + "$temporary_package/Tests/CapswitchCoreTests" + +cp "$ROOT"/Sources/Capswitch/*.swift \ + "$temporary_package/Sources/CapswitchCore/" +cp "$ROOT"/Tests/CapswitchCoreTests/*.swift \ + "$temporary_package/Tests/CapswitchCoreTests/" + +cat > "$temporary_package/Package.swift" <<'SWIFT' +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "macoc-LinuxCoreTests", + products: [ + .library(name: "CapswitchCore", targets: ["CapswitchCore"]), + ], + targets: [ + .target(name: "CapswitchCore"), + .testTarget( + name: "CapswitchCoreTests", + dependencies: ["CapswitchCore"] + ), + ] +) +SWIFT + +swift test --package-path "$temporary_package" --jobs 2 diff --git a/scripts/test.sh b/scripts/test.sh index 278c540..46266a7 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,5 +1,14 @@ #!/usr/bin/env bash source "$(dirname "$0")/common.sh" -macos_only -cd "$ROOT" -swift test + +if [[ "$IS_MACOS" == 1 ]]; then + cd "$ROOT" + swift test +elif command -v swift >/dev/null 2>&1; then + bash "$ROOT/scripts/test-linux-core.sh" +elif [[ "${MACOC_REQUIRE_SWIFT:-0}" == 1 ]]; then + echo "ERROR: the CI requires Swift for Linux XCTest." >&2 + exit 1 +else + echo "SKIP: Swift is not installed on this Linux development host." +fi