Skip to content

CI

CI #20

Workflow file for this run

# 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