diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index c9732cf..78e7b00 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,17 +1,4 @@ -# Runs the Go tests, and builds the macOS disk image (signed and notarized) and the Windows installer -# (signed with Artifact Signing) and keeps them as run artifacts. Signing takes the secrets and -# variables of the "release" environment, and is required in the merge queue and for releases. -# -# - on every pull request: application Go tests, tool tests when tools change, and the apps compiled -# and tested unsigned ("macOS app", "Windows app"); the signed builds are skipped (which counts -# as passed). One that changes only docs/ skips the Go tests too, unless it goes into main, and one -# that changes docs/ gets the docs check (tools/docslint) -# - in the merge queue: everything, named with the version in .release-please-manifest.json. These -# are the checks that must pass before a PR (the release PR) is merged, and release-stable.yml attaches -# these very files to the release. -# - by hand from the Actions tab (any branch): unsigned, named dev- -# - from release-stable.yml, only when it cannot find the merge queue's builds -# - from release-beta.yml, for a beta (named with its version, e.g. 1.1.0-beta.2) +# Runs shared checks and delegates platform work while keeping one build run for release artifacts. name: Build on: @@ -25,7 +12,7 @@ on: type: string default: "" version: - description: Version to stamp into the builds (e.g. 1.2.0). + description: Version to stamp into the builds (for example, 1.2.0). type: string default: "" @@ -33,7 +20,6 @@ permissions: contents: read concurrency: - # A newer push to a PR replaces the tests of the older one. group: build-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true @@ -57,7 +43,6 @@ jobs: if [ -n "$INPUT_VERSION" ]; then version="$INPUT_VERSION" elif [ "$EVENT" = merge_group ]; then - # The release PR's new version; any other PR keeps the last released one. version=$(jq -r '."." // empty' .release-please-manifest.json) version="${version:-dev-$(git rev-parse --short HEAD)}" else @@ -68,9 +53,6 @@ jobs: fi echo "version=$version" >> "$GITHUB_OUTPUT" - # What a pull request changes: "code" unless it's only docs/, "docs" if any of docs/. Into main - # it's always "code", so everything that's released has had the Go tests run on it. - # (With git, not the API: release-beta.yml and release-stable.yml call this workflow with contents: read only.) changes: runs-on: ubuntu-latest outputs: @@ -91,13 +73,13 @@ jobs: BASE: ${{ github.event.pull_request.base.ref }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} + #language=bash run: | if [ "$EVENT" != pull_request ]; then echo "code=true" >> "$GITHUB_OUTPUT" echo "tooling=true" >> "$GITHUB_OUTPUT" exit 0 fi - # Since the PR's branch left its base; --no-renames lists both sides of a rename. files=$(git diff --name-only --no-renames "$BASE_SHA...$HEAD_SHA") code=false if [ "$BASE" = main ] || grep -v '^docs/' <<< "$files" | grep -q .; then code=true; fi @@ -109,7 +91,6 @@ jobs: echo "docs=$docs" >> "$GITHUB_OUTPUT" echo "tooling=$tooling" >> "$GITHUB_OUTPUT" - # The pages, navigation.json, links and images in docs/, as flockphp.com reads them. docs: name: Docs needs: changes @@ -119,7 +100,6 @@ jobs: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-go@v7 with: go-version-file: go.mod @@ -127,9 +107,6 @@ jobs: - name: Check run: go run ./tools/docslint - # The application Go tests on both platforms Flock supports (the race detector needs cgo, so macOS only). - # Without code changes the steps are skipped, not the job: a skipped matrix job reports under its - # unexpanded name, so the required "Go tests (...)" checks would never pass. go-test: name: Go tests (${{ matrix.name }}) needs: [ version, changes ] @@ -137,10 +114,18 @@ jobs: fail-fast: false matrix: include: - - { name: macOS, os: macos-26, flags: -race } - - { name: Windows, os: windows-2025, flags: "" } - - { name: Linux, os: ubuntu-24.04, flags: -race } - - { name: Linux arm64, os: ubuntu-24.04-arm, flags: -race } + - name: macOS + os: macos-26 + flags: -race + - name: Windows + os: windows-2025 + flags: "" + - name: Linux + os: ubuntu-24.04 + flags: -race + - name: Linux arm64 + os: ubuntu-24.04-arm + flags: -race runs-on: ${{ matrix.os }} env: CODE: ${{ needs.changes.outputs.code }} @@ -165,13 +150,10 @@ jobs: shell: bash run: go test ${{ matrix.flags }} $(go list ./... | grep -v '^flock/tools/') - # The licenses that ship with the builds; run `make notices` after changing dependencies. - name: Third-party notices if: env.CODE == 'true' && matrix.name == 'macOS' run: go run ./tools/notices -check - # Tools run on Linux when their source or shared Go module metadata changes. Product-only PRs do - # not need to run these tests, but non-PR builds run the full suite. tool-test: name: Tool tests needs: changes @@ -192,340 +174,31 @@ jobs: - name: Test run: go test ./tools/... - # On pull requests the apps are only compiled and tested (unsigned, no secrets, so forks get it - # too); the signed builds below run in the merge queue and for releases. - macos-check: - name: macOS app - needs: changes - if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' - runs-on: macos-26 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - - # Every target, FlockScreenshots too: it reaches the app's views through @testable import. - - name: Build - run: swift build --package-path macos - - - name: Test - run: swift test --package-path macos - - windows-check: - name: Windows app - needs: changes - if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' - runs-on: windows-2025 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: 10.0.x - - - name: Test - run: dotnet test windows/Flock.Kit.Tests - - # FlockScreenshots compiles the app's sources with SCREENSHOTS defined, so a change can break - # it without touching it. - - name: Build - run: | - dotnet build windows/Flock.Tray -c Release -p:Platform=x64 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - dotnet build windows/Flock.Screenshots -c Release -p:Platform=x64 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - macos: - name: macOS (Apple silicon) - needs: version - if: github.event_name != 'pull_request' - runs-on: macos-26 - # Holds the signing secrets; only what may be released runs in it (builds by hand stay unsigned). - environment: ${{ (github.event_name == 'merge_group' || inputs.version != '') && 'release' || '' }} - env: - VERSION: ${{ needs.version.outputs.version }} - SIGNING: ${{ secrets.MACOS_CERTIFICATE_P12 != '' }} - # build-app.sh builds it in as SUPublicEDKey; Sparkle installs only updates signed with its - # private half (SPARKLE_PRIVATE_KEY, used by tools/release). - SPARKLE_PUBLIC_KEY: ${{ vars.SPARKLE_PUBLIC_KEY }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.event.pull_request.head.sha }} - - - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - - - name: Test - run: swift test --package-path macos - - # Builds by hand may be ad-hoc signed; what gets released may not. - - name: Require signing - if: env.SIGNING != 'true' && (github.event_name == 'merge_group' || inputs.version != '') - run: echo "::error::the MACOS_* signing secrets are not set" && exit 1 - - # Without the key, the app would never update itself. - - name: Require the update key - if: env.SPARKLE_PUBLIC_KEY == '' && (github.event_name == 'merge_group' || inputs.version != '') - run: echo "::error::the SPARKLE_PUBLIC_KEY variable is not set (go run ./tools/release keygen)" && exit 1 - - # Into a keychain of its own; build-app.sh signs with $SIGN_IDENTITY and notarizes with the - # API key at $NOTARY_KEY_PATH. - - name: Set up signing - if: env.SIGNING == 'true' - env: - CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} - CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} - NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - run: | - keychain="$RUNNER_TEMP/signing.keychain-db" - password=$(uuidgen) - security create-keychain -p "$password" "$keychain" - security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$password" "$keychain" - echo "$CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12" - security import "$RUNNER_TEMP/certificate.p12" -k "$keychain" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign - rm "$RUNNER_TEMP/certificate.p12" - # The intermediate certificate, in case the runner image lacks it. - curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" - security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain" || true - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$password" "$keychain" > /dev/null - security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') - - identity=$(security find-identity -v -p codesigning "$keychain" | grep -o '"Developer ID Application: [^"]*"' | head -1 | tr -d '"') - [ -n "$identity" ] || { echo "::error::no Developer ID Application identity in MACOS_CERTIFICATE_P12"; exit 1; } - echo "SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" - - printf '%s\n' "$NOTARY_KEY" > "$RUNNER_TEMP/notary-key.p8" - { - echo "NOTARY_KEY_PATH=$RUNNER_TEMP/notary-key.p8" - echo "NOTARY_KEY_ID=$NOTARY_KEY_ID" - echo "NOTARY_ISSUER_ID=$NOTARY_ISSUER_ID" - } >> "$GITHUB_ENV" - - - name: Build app and disk image - run: | - macos/build-app.sh - mv build/macos/Flock.dmg "build/macos/Flock-$VERSION-macOS.dmg" - - - uses: actions/upload-artifact@v7 - with: - name: flock-macos - path: build/macos/Flock-*-macOS.dmg - if-no-files-found: error - - - name: Clean up signing - if: always() && env.SIGNING == 'true' - run: | - security delete-keychain "$RUNNER_TEMP/signing.keychain-db" || true - rm -f "$RUNNER_TEMP/notary-key.p8" + needs: [ version, changes ] + uses: ./.github/workflows/ci-macos.yml + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha }} + version: ${{ needs.version.outputs.version }} + code: ${{ needs.changes.outputs.code }} + secrets: inherit windows: - name: Windows (x64) - needs: version - if: github.event_name != 'pull_request' - runs-on: windows-2025 - # As for macOS: the signing variables, and what Azure's federated credential trusts. - environment: ${{ (github.event_name == 'merge_group' || inputs.version != '') && 'release' || '' }} + needs: [ version, changes ] permissions: contents: read id-token: write - env: - FLOCK_VERSION: ${{ needs.version.outputs.version }} - SIGNING: ${{ vars.ARTIFACT_SIGNING_ACCOUNT != '' }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.event.pull_request.head.sha }} - - - uses: actions/setup-go@v7 - with: - go-version-file: go.mod + uses: ./.github/workflows/ci-windows.yml + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha }} + version: ${{ needs.version.outputs.version }} + code: ${{ needs.changes.outputs.code }} + secrets: inherit - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: 10.0.x - - - name: Install Inno Setup - run: | - if (-not (Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe")) { - choco install innosetup --no-progress -y - } - - - name: Require signing - if: env.SIGNING != 'true' && (github.event_name == 'merge_group' || inputs.version != '') - run: echo "::error::the ARTIFACT_SIGNING_* variables are not set" && exit 1 - - # No secret: Azure trusts this repository's "release" environment (a federated credential). - - uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 - if: env.SIGNING == 'true' - with: - client-id: ${{ vars.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - allow-no-subscriptions: true - - # SignTool's Artifact Signing plugin and its settings; windows/sign.ps1 signs with them. - - name: Set up signing - if: env.SIGNING == 'true' - env: - ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT }} - ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT }} - PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE }} - run: | - $client = "$env:RUNNER_TEMP\artifact-signing" - Invoke-WebRequest https://api.nuget.org/v3-flatcontainer/microsoft.artifactsigning.client/1.0.146/microsoft.artifactsigning.client.1.0.146.nupkg -OutFile "$client.zip" - if ((Get-FileHash "$client.zip" -Algorithm SHA256).Hash -ne "7761E5D8F31E54B764DA21D33CD8B0A2940E1B05526C7DFCB5E94B0431426F70") { - throw "unexpected Microsoft.ArtifactSigning.Client package" - } - Expand-Archive "$client.zip" $client - # Signs as the Azure CLI login above, nothing else. - @{ - Endpoint = $env:ENDPOINT; CodeSigningAccountName = $env:ACCOUNT; CertificateProfileName = $env:PROFILE - ExcludeCredentials = @("EnvironmentCredential", "ManagedIdentityCredential", "WorkloadIdentityCredential", - "SharedTokenCacheCredential", "VisualStudioCredential", "VisualStudioCodeCredential", - "AzurePowerShellCredential", "AzureDeveloperCliCredential", "InteractiveBrowserCredential") - } | ConvertTo-Json | Set-Content "$client\metadata.json" - # Takes the token now, while GitHub's short-lived ID token behind the login is still valid; - # the CLI keeps it for the signing after the build. - az account get-access-token --resource https://codesigning.azure.net --output none - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - "SIGN_DLIB=$client\bin\x64\Azure.CodeSigning.Dlib.dll" >> $env:GITHUB_ENV - "SIGN_METADATA=$client\metadata.json" >> $env:GITHUB_ENV - - - name: Build installer - run: | - ./windows/build-installer.ps1 - Move-Item windows/dist/Flock-Setup-x64.exe "windows/dist/Flock-Setup-$env:FLOCK_VERSION-x64.exe" - - - name: Verify signature - if: env.SIGNING == 'true' - run: | - $signature = Get-AuthenticodeSignature windows/dist/Flock-Setup-*-x64.exe - $signature | Format-List Status, StatusMessage, SignerCertificate, TimeStamperCertificate - if ($signature.Status -ne "Valid") { throw "the installer's signature is $($signature.Status)" } - - - uses: actions/upload-artifact@v7 - with: - name: flock-windows - path: windows/dist/Flock-Setup-*-x64.exe - if-no-files-found: error - - # The Linux app (linux/, Rust with GTK 4 and libadwaita) against Ubuntu 24.04's GTK 4.14 and - # libadwaita 1.5, the oldest Flock supports. - linux-app: - name: Linux app - needs: [ version, changes ] - runs-on: ubuntu-24.04 - env: - CODE: ${{ needs.changes.outputs.code }} - steps: - - uses: actions/checkout@v7 - if: env.CODE == 'true' - with: - ref: ${{ inputs.ref || github.event.pull_request.head.sha }} - - - name: GTK, libadwaita and WebKitGTK - if: env.CODE == 'true' - # The runner image's package lists go stale: updated packages 404. - run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev - - - name: Format - if: env.CODE == 'true' - run: cargo fmt --manifest-path linux/Cargo.toml --check - - - name: Clippy - if: env.CODE == 'true' - run: cargo clippy --manifest-path linux/Cargo.toml --all-targets --all-features --locked -- -D warnings - - - name: Test - if: env.CODE == 'true' - run: cargo test --manifest-path linux/Cargo.toml --all-features --locked - - # The flockphp .deb and .rpm per architecture (linux/build-packages.sh): the CLI and the app, - # built on Ubuntu 24.04 (the oldest GTK and glibc Flock supports), then installed, run and - # removed again on the distributions Flock supports. Unsigned packages: the repository signs - # its index, not the packages. linux: - name: Linux (${{ matrix.arch }}) - needs: version - strategy: - fail-fast: false - matrix: - include: - - { arch: amd64, os: ubuntu-24.04 } - - { arch: arm64, os: ubuntu-24.04-arm } - runs-on: ${{ matrix.os }} - env: - VERSION: ${{ needs.version.outputs.version }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.event.pull_request.head.sha }} - - - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - - - name: GTK, libadwaita and WebKitGTK - # The runner image's package lists go stale: updated packages 404. - run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev - - - name: Build packages - run: linux/build-packages.sh "${{ matrix.arch }}" - - - name: Install, run and remove - run: | - for image in ubuntu:24.04 ubuntu:26.04 fedora:latest; do - echo "::group::$image" - case "$image" in - fedora*) install='dnf install -y -q /pkgs/flockphp*.rpm'; remove='dnf remove -y -q flockphp' ;; - *) install='apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq /pkgs/flockphp*.deb' - remove='DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq flockphp' ;; - esac - docker run --rm -v "$PWD/build/linux:/pkgs:ro" "$image" sh -ec " - $install - test -x /usr/libexec/flockphp/flock && test -f /usr/lib/systemd/system/flock@.service - test -f /usr/share/applications/dev.flock.Flock.desktop - ! ldd /usr/libexec/flockphp/flock-app | grep 'not found' - flk version - flk --help >/dev/null - $remove - ! test -e /usr/bin/flk - " - echo "::endgroup::" - done - - - name: Install, run and remove on Arch (AUR package) - # The AUR's flockphp-bin repackages the release's .deb: built here from this one with - # makepkg (as a user, as AUR helpers do). Arch has no arm64 image. - if: matrix.arch == 'amd64' - run: | - docker run --rm -e VERSION -v "$PWD/build/linux:/pkgs:ro" -v "$PWD/linux/packaging/aur/flockphp:/aur:ro" archlinux:latest sh -ec ' - pacman -Syu --noconfirm --needed base-devel >/dev/null - useradd -m builder - cp -r /aur /home/builder/pkg && cp /pkgs/flockphp_*_amd64.deb /home/builder/pkg/ - cd /home/builder/pkg - sed -i -e "s|^_tag=.*|_tag=$VERSION|" -e "s|\"https://[^\"]*/|\"|" PKGBUILD - chown -R builder . && su builder -c "makepkg --nodeps --noconfirm" - pacman -U --noconfirm flockphp-bin-*.pkg.tar.zst - test -x /usr/libexec/flockphp/flock && test -f /usr/lib/systemd/system/flock@.service - test -f /usr/share/applications/dev.flock.Flock.desktop && test -f /usr/share/polkit-1/rules.d/50-flock.rules - ! ldd /usr/libexec/flockphp/flock-app | grep "not found" - flk version - flk --help >/dev/null - pacman -R --noconfirm flockphp-bin - ! test -e /usr/bin/flk - ' - - - uses: actions/upload-artifact@v7 - with: - name: flock-linux-${{ matrix.arch }} - path: | - build/linux/flockphp*.deb - build/linux/flockphp*.rpm - if-no-files-found: error + needs: [ version, changes ] + uses: ./.github/workflows/ci-linux.yml + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha }} + version: ${{ needs.version.outputs.version }} + code: ${{ needs.changes.outputs.code }} diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml new file mode 100644 index 0000000..bf51305 --- /dev/null +++ b/.github/workflows/ci-linux.yml @@ -0,0 +1,148 @@ +name: Linux build + +on: + workflow_call: + inputs: + ref: + type: string + required: true + version: + type: string + required: true + code: + type: string + required: true + +jobs: + linux-app: + name: Linux app + runs-on: ubuntu-24.04 + env: + CODE: ${{ inputs.code }} + steps: + - uses: actions/checkout@v7 + if: env.CODE == 'true' + with: + ref: ${{ inputs.ref }} + + - name: GTK, libadwaita and WebKitGTK + if: env.CODE == 'true' + run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev + + - name: Format + if: env.CODE == 'true' + run: cargo fmt --manifest-path linux/Cargo.toml --check + + - name: Clippy + if: env.CODE == 'true' + run: cargo clippy --manifest-path linux/Cargo.toml --all-targets --all-features --locked -- -D warnings + + - name: Test + if: env.CODE == 'true' + run: cargo test --manifest-path linux/Cargo.toml --all-features --locked + + linux: + name: Linux (${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + os: ubuntu-24.04 + - arch: arm64 + os: ubuntu-24.04-arm + runs-on: ${{ matrix.os }} + env: + VERSION: ${{ inputs.version }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + + - name: GTK, libadwaita and WebKitGTK + run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev + + - name: Build packages + run: linux/build-packages.sh "${{ matrix.arch }}" + + - uses: actions/upload-artifact@v7 + with: + name: flock-linux-${{ matrix.arch }} + path: | + build/linux/flockphp*.deb + build/linux/flockphp*.rpm + if-no-files-found: error + + linux-package-validation: + name: Linux package validation (${{ matrix.name }}) + needs: linux + strategy: + fail-fast: false + matrix: + include: + - name: Ubuntu 24.04 + arch: amd64 + runner: ubuntu-24.04 + image: ubuntu:24.04 + package: deb + - name: Ubuntu 26.04 + arch: amd64 + runner: ubuntu-24.04 + image: ubuntu:26.04 + package: deb + - name: Fedora + arch: amd64 + runner: ubuntu-24.04 + image: fedora:latest + package: rpm + - name: Ubuntu 24.04 ARM64 + arch: arm64 + runner: ubuntu-24.04-arm + image: ubuntu:24.04 + package: deb + - name: Ubuntu 26.04 ARM64 + arch: arm64 + runner: ubuntu-24.04-arm + image: ubuntu:26.04 + package: deb + - name: Fedora ARM64 + arch: arm64 + runner: ubuntu-24.04-arm + image: fedora:latest + package: rpm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/download-artifact@v8 + with: + name: flock-linux-${{ matrix.arch }} + path: build/linux + + - name: Install, run and remove + run: bash linux/validate-package.sh "${{ matrix.package }}" "${{ matrix.image }}" build/linux + + linux-aur-validation: + name: Linux AUR validation + needs: linux + runs-on: ubuntu-24.04 + env: + VERSION: ${{ inputs.version }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/download-artifact@v8 + with: + name: flock-linux-amd64 + path: build/linux + + - name: Build, install, run and remove + run: bash linux/validate-package.sh aur archlinux:latest build/linux diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml new file mode 100644 index 0000000..1004c34 --- /dev/null +++ b/.github/workflows/ci-macos.yml @@ -0,0 +1,118 @@ +name: macOS build + +on: + workflow_call: + inputs: + ref: + type: string + required: true + version: + type: string + required: true + code: + type: string + required: true + +jobs: + macos-check: + name: macOS app + if: github.event_name == 'pull_request' && inputs.code == 'true' + runs-on: macos-26 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + # Every target, FlockScreenshots too: it reaches the app's views through @testable import. + - name: Build + run: swift build --package-path macos + + - name: Test + run: swift test --package-path macos + + macos: + name: macOS (Apple silicon) + if: github.event_name != 'pull_request' + runs-on: macos-26 + # Holds the signing secrets; only what may be released runs in it (builds by hand stay unsigned). + environment: ${{ (github.event_name == 'merge_group' || inputs.version != '') && 'release' || '' }} + env: + VERSION: ${{ inputs.version }} + SIGNING: ${{ secrets.MACOS_CERTIFICATE_P12 != '' }} + # build-app.sh builds it in as SUPublicEDKey; Sparkle installs only updates signed with its + # private half (SPARKLE_PRIVATE_KEY, used by tools/release). + SPARKLE_PUBLIC_KEY: ${{ vars.SPARKLE_PUBLIC_KEY }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + + - name: Test + run: swift test --package-path macos + + # Builds by hand may be ad-hoc signed; what gets released may not. + - name: Require signing + if: env.SIGNING != 'true' && (github.event_name == 'merge_group' || inputs.version != '') + run: echo "::error::the MACOS_* signing secrets are not set" && exit 1 + + # Without the key, the app would never update itself. + - name: Require the update key + if: env.SPARKLE_PUBLIC_KEY == '' && (github.event_name == 'merge_group' || inputs.version != '') + run: echo "::error::the SPARKLE_PUBLIC_KEY variable is not set (go run ./tools/release keygen)" && exit 1 + + # Into a keychain of its own; build-app.sh signs with $SIGN_IDENTITY and notarizes with the + # API key at $NOTARY_KEY_PATH. + - name: Set up signing + if: env.SIGNING == 'true' + env: + CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} + #language=bash + run: | + keychain="$RUNNER_TEMP/signing.keychain-db" + password=$(uuidgen) + security create-keychain -p "$password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$password" "$keychain" + echo "$CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12" + security import "$RUNNER_TEMP/certificate.p12" -k "$keychain" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm "$RUNNER_TEMP/certificate.p12" + curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" + security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain" || true + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$password" "$keychain" > /dev/null + security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') + identity=$(security find-identity -v -p codesigning "$keychain" | grep -o '"Developer ID Application: [^"]*"' | head -1 | tr -d '"') + [ -n "$identity" ] || { echo "::error::no Developer ID Application identity in MACOS_CERTIFICATE_P12"; exit 1; } + echo "SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" + printf '%s\n' "$NOTARY_KEY" > "$RUNNER_TEMP/notary-key.p8" + { + echo "NOTARY_KEY_PATH=$RUNNER_TEMP/notary-key.p8" + echo "NOTARY_KEY_ID=$NOTARY_KEY_ID" + echo "NOTARY_ISSUER_ID=$NOTARY_ISSUER_ID" + } >> "$GITHUB_ENV" + + - name: Build app and disk image + #language=bash + run: | + macos/build-app.sh + mv build/macos/Flock.dmg "build/macos/Flock-$VERSION-macOS.dmg" + + - uses: actions/upload-artifact@v7 + with: + name: flock-macos + path: build/macos/Flock-*-macOS.dmg + if-no-files-found: error + + - name: Clean up signing + if: always() && env.SIGNING == 'true' + #language=bash + run: | + security delete-keychain "$RUNNER_TEMP/signing.keychain-db" || true + rm -f "$RUNNER_TEMP/notary-key.p8" diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..4ee8474 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,129 @@ +name: Windows build + +on: + workflow_call: + inputs: + ref: + type: string + required: true + version: + type: string + required: true + code: + type: string + required: true + +jobs: + windows-check: + name: Windows app (${{ matrix.name }}) + if: github.event_name == 'pull_request' && inputs.code == 'true' + strategy: + fail-fast: false + matrix: + include: + - name: Kit tests + command: 'dotnet test windows/Flock.Kit.Tests' + - name: Tray build + command: 'dotnet build windows/Flock.Tray -c Release -p:Platform=x64' + # FlockScreenshots compiles the app's sources with SCREENSHOTS defined, so a change can break + # it without touching it. + - name: Screenshot build + command: 'dotnet build windows/Flock.Screenshots -c Release -p:Platform=x64' + runs-on: windows-2025 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: ${{ matrix.name }} + run: ${{ matrix.command }} + + windows: + name: Windows (x64) + if: github.event_name != 'pull_request' + runs-on: windows-2025 + environment: ${{ (github.event_name == 'merge_group' || inputs.version != '') && 'release' || '' }} + permissions: + contents: read + id-token: write + env: + FLOCK_VERSION: ${{ inputs.version }} + SIGNING: ${{ vars.ARTIFACT_SIGNING_ACCOUNT != '' }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Install Inno Setup + #language=powershell + run: | + if (-not (Test-Path "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe")) { + choco install innosetup --no-progress -y + } + + - name: Require signing + if: env.SIGNING != 'true' && (github.event_name == 'merge_group' || inputs.version != '') + run: echo "::error::the ARTIFACT_SIGNING_* variables are not set" && exit 1 + + - uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 + if: env.SIGNING == 'true' + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + allow-no-subscriptions: true + + - name: Set up signing + if: env.SIGNING == 'true' + env: + ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT }} + ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT }} + PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE }} + #language=powershell + run: | + $client = "$env:RUNNER_TEMP\artifact-signing" + Invoke-WebRequest https://api.nuget.org/v3-flatcontainer/microsoft.artifactsigning.client/1.0.146/microsoft.artifactsigning.client.1.0.146.nupkg -OutFile "$client.zip" + if ((Get-FileHash "$client.zip" -Algorithm SHA256).Hash -ne "7761E5D8F31E54B764DA21D33CD8B0A2940E1B05526C7DFCB5E94B0431426F70") { + throw "unexpected Microsoft.ArtifactSigning.Client package" + } + Expand-Archive "$client.zip" $client + @{ + Endpoint = $env:ENDPOINT; CodeSigningAccountName = $env:ACCOUNT; CertificateProfileName = $env:PROFILE + ExcludeCredentials = @("EnvironmentCredential", "ManagedIdentityCredential", "WorkloadIdentityCredential", + "SharedTokenCacheCredential", "VisualStudioCredential", "VisualStudioCodeCredential", + "AzurePowerShellCredential", "AzureDeveloperCliCredential", "InteractiveBrowserCredential") + } | ConvertTo-Json | Set-Content "$client\metadata.json" + az account get-access-token --resource https://codesigning.azure.net --output none + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + "SIGN_DLIB=$client\bin\x64\Azure.CodeSigning.Dlib.dll" >> $env:GITHUB_ENV + "SIGN_METADATA=$client\metadata.json" >> $env:GITHUB_ENV + + - name: Build installer + #language=powershell + run: | + ./windows/build-installer.ps1 + Move-Item windows/dist/Flock-Setup-x64.exe "windows/dist/Flock-Setup-$env:FLOCK_VERSION-x64.exe" + + - name: Verify signature + if: env.SIGNING == 'true' + run: | + $signature = Get-AuthenticodeSignature windows/dist/Flock-Setup-*-x64.exe + $signature | Format-List Status, StatusMessage, SignerCertificate, TimeStamperCertificate + if ($signature.Status -ne "Valid") { throw "the installer's signature is $($signature.Status)" } + + - uses: actions/upload-artifact@v7 + with: + name: flock-windows + path: windows/dist/Flock-Setup-*-x64.exe + if-no-files-found: error diff --git a/linux/validate-package.sh b/linux/validate-package.sh new file mode 100644 index 0000000..fbe365d --- /dev/null +++ b/linux/validate-package.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Installs, checks, and removes a Flock package in its target container. +# +# Usage: linux/validate-package.sh +# +# VERSION is required for an AUR validation. The AUR package repackages the AMD64 .deb. +set -euo pipefail + +package_type="${1:?package type is required}" +image="${2:?container image is required}" +package_dir="$(cd "${3:?package directory is required}" && pwd)" +repo="$(cd "$(dirname "$0")/.." && pwd)" + +case "$package_type" in + deb) + install='apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq /pkgs/flockphp*.deb' + remove='DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq flockphp' + check_polkit=false + ;; + rpm) + install='dnf install -y -q /pkgs/flockphp*.rpm' + remove='dnf remove -y -q flockphp' + check_polkit=false + ;; + aur) + : "${VERSION:?VERSION is required for AUR validation}" + docker run --rm -e VERSION -v "$package_dir:/pkgs:ro" -v "$repo/linux/packaging/aur/flockphp:/aur:ro" "$image" sh -ec ' + pacman -Syu --noconfirm --needed base-devel >/dev/null + useradd -m builder + cp -r /aur /home/builder/pkg && cp /pkgs/flockphp_*_amd64.deb /home/builder/pkg/ + cd /home/builder/pkg + sed -i -e "s|^_tag=.*|_tag=$VERSION|" -e "s|\"https://[^\"]*/|\"|" PKGBUILD + chown -R builder . && su builder -c "makepkg --nodeps --noconfirm" + pacman -U --noconfirm flockphp-bin-*.pkg.tar.zst + test -x /usr/libexec/flockphp/flock && test -f /usr/lib/systemd/system/flock@.service + test -f /usr/share/applications/dev.flock.Flock.desktop && test -f /usr/share/polkit-1/rules.d/50-flock.rules + ! ldd /usr/libexec/flockphp/flock-app | grep "not found" + flk version + flk --help >/dev/null + pacman -R --noconfirm flockphp-bin + ! test -e /usr/bin/flk + ' + exit 0 + ;; + *) + echo "Unsupported package type: $package_type" >&2 + exit 2 + ;; +esac + +docker run --rm -v "$package_dir:/pkgs:ro" "$image" sh -ec " + $install + test -x /usr/libexec/flockphp/flock && test -f /usr/lib/systemd/system/flock@.service + test -f /usr/share/applications/dev.flock.Flock.desktop + ! ldd /usr/libexec/flockphp/flock-app | grep 'not found' + flk version + flk --help >/dev/null + $remove + ! test -e /usr/bin/flk +"