CI #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI + Release | |
| # Triggers: push to main (releases), pull requests (checks only) | |
| # Builds binaries for all platforms, signs/notarizes, creates pre-release, updates homebrew-tap | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '_kos/**' | |
| - 'schema/**' | |
| - 'LICENSE' | |
| - '.gitignore' | |
| - '.gitattributes' | |
| pull_request: | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '_kos/**' | |
| - 'schema/**' | |
| - 'LICENSE' | |
| - '.gitignore' | |
| - '.gitattributes' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| check-fmt: | |
| name: Rustfmt | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| components: rustfmt | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - run: cargo +nightly fmt --all -- --check | |
| check-clippy: | |
| name: Clippy | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - run: cargo clippy --all-targets --all-features -- -D warnings | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - run: cargo test --all-targets | |
| deny: | |
| name: Cargo Deny | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| - uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| command: check advisories licenses bans | |
| build-release: | |
| name: Build (${{ matrix.target }}) | |
| needs: [check-fmt, check-clippy, test, deny] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| strategy: | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| os: macos-latest | |
| artifact_name: kos-darwin-arm64 | |
| - target: x86_64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| artifact_name: kos-linux-amd64 | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-24.04-arm | |
| artifact_name: kos-linux-arm64 | |
| runs-on: ${{ matrix.os }} | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - name: Extract version | |
| id: version | |
| run: | | |
| VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') | |
| TAG="main-$(date -u +%Y%m%d-%H%M%S)-$(echo ${{ github.sha }} | head -c 7)" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Build release binary | |
| env: | |
| KOS_CHANNEL: main | |
| KOS_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| cargo build --release --target ${{ matrix.target }} | |
| cp target/${{ matrix.target }}/release/kos ${{ matrix.artifact_name }} | |
| - name: Verify binary | |
| run: ./${{ matrix.artifact_name }} --version | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: binaries-${{ matrix.artifact_name }} | |
| path: ${{ matrix.artifact_name }} | |
| sign-and-notarize: | |
| name: Sign & Notarize | |
| needs: build-release | |
| if: vars.SIGNING_ENABLED == 'true' | |
| environment: release | |
| strategy: | |
| matrix: | |
| arch: [arm64] | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download binaries | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: binaries-kos-darwin-${{ matrix.arch }} | |
| - name: Import certificates | |
| env: | |
| APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_INSTALLER_CERTIFICATE_P12: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_P12 }} | |
| APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} | |
| run: | | |
| security create-keychain -p "" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "" build.keychain | |
| echo "$APPLE_CERTIFICATE_P12" | base64 --decode > cert.p12 | |
| security import cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| rm cert.p12 | |
| echo "$APPLE_INSTALLER_CERTIFICATE_P12" | base64 --decode > installer-cert.p12 | |
| security import installer-cert.p12 -k build.keychain -P "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/pkgbuild -T /usr/bin/productbuild -T /usr/bin/productsign | |
| rm installer-cert.p12 | |
| curl -sfo /tmp/DeveloperIDG2CA.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer | |
| security add-certificates -k build.keychain /tmp/DeveloperIDG2CA.cer | |
| rm /tmp/DeveloperIDG2CA.cer | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain | |
| - name: Sign binary | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| run: | | |
| codesign --force --options runtime \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --timestamp \ | |
| kos-darwin-${{ matrix.arch }} | |
| - name: Verify signature | |
| run: codesign --verify --deep --strict kos-darwin-${{ matrix.arch }} | |
| - name: Build app bundle | |
| run: | | |
| VERSION="${{ needs.build-release.outputs.version }}" | |
| chmod +x scripts/create-app.sh | |
| ./scripts/create-app.sh kos-darwin-${{ matrix.arch }} "$VERSION" . | |
| mv Kos.app Kos-${{ matrix.arch }}.app | |
| - name: Sign app bundle | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| run: | | |
| codesign --force --deep --options runtime \ | |
| --sign "$APPLE_SIGNING_IDENTITY" \ | |
| --timestamp \ | |
| Kos-${{ matrix.arch }}.app | |
| - name: Build dmg | |
| run: | | |
| VERSION="${{ needs.build-release.outputs.version }}" | |
| chmod +x scripts/create-dmg.sh | |
| ./scripts/create-dmg.sh Kos-${{ matrix.arch }}.app "$VERSION" kos-${{ matrix.arch }}.dmg | |
| - name: Build pkg | |
| env: | |
| APPLE_INSTALLER_IDENTITY: ${{ secrets.APPLE_INSTALLER_IDENTITY }} | |
| run: | | |
| if ! security find-identity -v build.keychain | grep -q "$APPLE_INSTALLER_IDENTITY"; then | |
| echo "::error::APPLE_INSTALLER_IDENTITY not found in keychain" | |
| security find-identity -v build.keychain | |
| exit 1 | |
| fi | |
| VERSION="${{ needs.build-release.outputs.version }}" | |
| chmod +x scripts/create-pkg.sh | |
| ./scripts/create-pkg.sh kos-darwin-${{ matrix.arch }} "$VERSION" "$APPLE_INSTALLER_IDENTITY" kos-${{ matrix.arch }}.pkg | |
| - name: Notarize and staple | |
| env: | |
| APPLE_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }} | |
| APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} | |
| APPLE_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_NOTARIZATION_TEAM_ID }} | |
| run: | | |
| for ARTIFACT in kos-${{ matrix.arch }}.pkg kos-${{ matrix.arch }}.dmg; do | |
| echo "Notarizing $ARTIFACT..." | |
| xcrun notarytool submit "$ARTIFACT" \ | |
| --apple-id "$APPLE_NOTARIZATION_APPLE_ID" \ | |
| --password "$APPLE_NOTARIZATION_PASSWORD" \ | |
| --team-id "$APPLE_NOTARIZATION_TEAM_ID" \ | |
| --wait --timeout 14400 | |
| xcrun stapler staple "$ARTIFACT" | |
| done | |
| - name: Upload signed artifacts | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: signed-binaries-darwin-${{ matrix.arch }} | |
| path: | | |
| kos-darwin-${{ matrix.arch }} | |
| kos-${{ matrix.arch }}.pkg | |
| kos-${{ matrix.arch }}.dmg | |
| retention-days: 14 | |
| - name: Cleanup keychain | |
| if: always() | |
| run: security delete-keychain build.keychain || true | |
| create-release: | |
| name: Create Release | |
| needs: [build-release, sign-and-notarize] | |
| if: | | |
| always() && | |
| needs.build-release.result == 'success' && | |
| (needs.sign-and-notarize.result == 'success' || needs.sign-and-notarize.result == 'skipped') | |
| environment: release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download macOS arm64 | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: ${{ needs.sign-and-notarize.result == 'success' && 'signed-binaries-darwin-arm64' || 'binaries-kos-darwin-arm64' }} | |
| path: dist/ | |
| - name: Download Linux amd64 | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: binaries-kos-linux-amd64 | |
| path: dist/ | |
| - name: Download Linux arm64 | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: binaries-kos-linux-arm64 | |
| path: dist/ | |
| - name: List release files | |
| run: ls -la dist/ | |
| - name: Create pre-release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="${{ needs.build-release.outputs.tag }}" | |
| gh release create "$TAG" \ | |
| --repo "${{ github.repository }}" \ | |
| --prerelease \ | |
| --title "$TAG" \ | |
| --notes "$(cat <<NOTES | |
| Automated release from main. | |
| **Version:** ${{ needs.build-release.outputs.version }} | |
| **Commit:** ${{ github.sha }} | |
| **Built with:** Rust (cargo) | |
| **Signed:** ${{ needs.sign-and-notarize.result == 'success' && 'Yes (Apple Developer ID)' || 'No (unsigned)' }} | |
| **Binaries:** | |
| - \`kos-darwin-arm64\` — macOS Apple Silicon | |
| - \`kos-linux-amd64\` — Linux x86_64 | |
| - \`kos-linux-arm64\` — Linux ARM64 | |
| NOTES | |
| )" \ | |
| dist/* | |
| - name: Clean old releases (keep 30) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release list --repo "${{ github.repository }}" \ | |
| --json tagName,isPrerelease \ | |
| --jq '[.[] | select(.isPrerelease)] | sort_by(.tagName) | reverse | .[30:] | .[].tagName' \ | |
| | while read -r tag; do | |
| echo "Deleting old release: $tag" | |
| gh release delete "$tag" --repo "${{ github.repository }}" --yes --cleanup-tag | |
| done | |
| update-homebrew: | |
| name: Update Homebrew Tap | |
| needs: [build-release, sign-and-notarize, create-release] | |
| if: needs.create-release.result == 'success' && needs.sign-and-notarize.result == 'success' | |
| environment: release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download signed darwin binaries | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: signed-binaries-darwin-arm64 | |
| path: dist/ | |
| - name: Update Homebrew formula | |
| env: | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| TAG="${{ needs.build-release.outputs.tag }}" | |
| VERSION="${{ needs.build-release.outputs.version }}" | |
| SHA256_ARM64=$(shasum -a 256 dist/kos-darwin-arm64 | cut -d' ' -f1) | |
| git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/arcavenae/homebrew-tap.git" homebrew-tap-repo | |
| cd homebrew-tap-repo | |
| mkdir -p Formula | |
| cp ../Formula/kos.rb Formula/kos.rb | |
| sed -i "s/VERSION_PLACEHOLDER/$TAG/g" Formula/kos.rb | |
| sed -i "s/TAG_PLACEHOLDER/$TAG/g" Formula/kos.rb | |
| sed -i "s/SHA256_ARM64_PLACEHOLDER/$SHA256_ARM64/g" Formula/kos.rb | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/kos.rb | |
| git diff --cached --quiet || git commit -m "Update kos to $TAG" | |
| git push |