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
182 changes: 169 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
name: Release DMG
name: Release

# Buduje i publikuje CloudMachine-<wersja>.dmg jako zalacznik GitHub Release
# przy kazdym pushu tagu w formacie vX.Y.Z. Podpis pozostaje ad-hoc (bez
# certyfikatu Apple Developer) - Gatekeeper nadal pokaze ostrzezenie
# "niezidentyfikowany deweloper" przy pierwszym uruchomieniu, patrz README.
# Tag vX.Y.Z -> uniwersalny CloudMachine.app (Apple Silicon + Intel) podpisany
# STALYM certyfikatem, .dmg w GitHub Release i zaktualizowany cask w
# RenaCode/homebrew-tap (`brew install --cask renacode/tap/cloudmachine`).
#
# TODO (po zalozeniu konta Apple Developer): dodac krok `codesign` z realnym
# Developer ID certyfikatem (zaimportowanym z sekretow repo) i `notarytool`
# przed `make-dmg`, zeby usunac to ostrzezenie.
# Pull request zmieniajacy budowanie albo cask przechodzi ten sam potok na
# sucho: podpis ad-hoc, bez wydania i bez tapu, artefakty do pobrania z runu.
#
# Sekrety (tylko dla tagu):
# CM_SIGNING_P12_BASE64, CM_SIGNING_P12_PASSWORD - certyfikat self-signed
# "CloudMachine Release Signing" (patrz packaging/README.md). Bez niego
# wydanie sie NIE buduje: podpis ad-hoc zmienia tozsamosc appki przy
# kazdym wydaniu i macOS cofa Pelny dostep do dysku po kazdym upgradzie.
# HOMEBREW_TAP_TOKEN - token z prawem zapisu do RenaCode/homebrew-tap.
#
# Nadal brak Developer ID i notaryzacji - Gatekeeper nie zna wydawcy; cask
# zdejmuje kwarantanne w postflight.

on:
push:
tags:
- "v*.*.*"
pull_request:
paths:
- ".github/workflows/release.yml"
- "packaging/**"
- "mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift"
- "mac-app/Sources/CloudMachineAgent/MakeDmgCommand.swift"
- "mac-app/Resources/Info.plist"

env:
CM_SIGNING_CERT_NAME: CloudMachine Release Signing
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') }}

jobs:
build-and-release:
name: Build DMG and publish release
release:
name: Build, release, update tap
runs-on: macos-14
permissions:
contents: write
Expand All @@ -25,16 +44,153 @@ jobs:
with:
fetch-depth: 0

- name: Build .app (ad-hoc signed)
- name: Tag matches mac-app/VERSION
id: version
run: |
version="$(tr -d '[:space:]' < mac-app/VERSION)"
if [ "$IS_RELEASE" = "true" ] && [ "${GITHUB_REF_NAME}" != "v${version}" ]; then
echo "::error::Tag ${GITHUB_REF_NAME} != v${version} z mac-app/VERSION. Podbij VERSION albo popraw tag."
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"

- name: swift test
working-directory: mac-app
run: swift test

- name: Import signing certificate
if: env.IS_RELEASE == 'true'
env:
P12_BASE64: ${{ secrets.CM_SIGNING_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.CM_SIGNING_P12_PASSWORD }}
run: |
if [ -z "$P12_BASE64" ] || [ -z "$P12_PASSWORD" ]; then
echo "::error::Brak sekretow CM_SIGNING_P12_*. Wydanie podpisane ad-hoc cofaloby Pelny dostep do dysku po kazdym upgradzie - przerywam. Patrz packaging/README.md."
exit 1
fi
keychain="$RUNNER_TEMP/signing.keychain-db"
keychain_password="$(openssl rand -hex 16)"
printf '%s' "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
/usr/bin/openssl pkcs12 -in "$RUNNER_TEMP/cert.p12" -nokeys \
-passin "pass:$P12_PASSWORD" -out "$RUNNER_TEMP/cert.pem"

security create-keychain -p "$keychain_password" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$P12_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain"
# Dopisujemy do listy wyszukiwania, bo `build-app` szuka certyfikatu
# przez `security find-certificate -c` bez wskazania keychaina.
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
sudo security add-trusted-cert -d -r trustRoot -p codeSign \
-k /Library/Keychains/System.keychain "$RUNNER_TEMP/cert.pem"
rm -f "$RUNNER_TEMP/cert.p12"

- name: Build CloudMachine.app (universal)
working-directory: mac-app
run: swift run cloudmachine-agent build-app --universal

- name: Verify architectures and signature
working-directory: mac-app
run: swift run cloudmachine-agent build-app
run: |
for bin in CloudMachine cloudmachine-agent; do
archs="$(lipo -archs "build/CloudMachine.app/Contents/MacOS/$bin")"
echo "$bin: $archs"
case "$archs" in *arm64*x86_64*|*x86_64*arm64*) ;; *)
echo "::error::$bin nie jest uniwersalny ($archs)"; exit 1 ;;
esac
done
codesign --verify --deep --strict build/CloudMachine.app
signature="$(codesign -dv --verbose=2 build/CloudMachine.app 2>&1)"
echo "$signature"
# `build-app` po cichu spada do ad-hoc, gdy nie znajdzie certyfikatu -
# tu to musi byc blad, nie ostrzezenie.
if [ "$IS_RELEASE" = "true" ] && ! grep -qF "Authority=$CM_SIGNING_CERT_NAME" <<<"$signature"; then
echo "::error::Wydanie nie jest podpisane certyfikatem '$CM_SIGNING_CERT_NAME'."
exit 1
fi

- name: Package .dmg
working-directory: mac-app
run: swift run cloudmachine-agent make-dmg

- name: Render cask
id: cask
run: |
version="${{ steps.version.outputs.version }}"
dmg="mac-app/build/CloudMachine-${version}.dmg"
sha256="$(shasum -a 256 "$dmg" | cut -d' ' -f1)"
sed -e "s/__VERSION__/${version}/" -e "s/__SHA256__/${sha256}/" \
packaging/homebrew/cloudmachine.rb.in > mac-app/build/cloudmachine.rb
if grep -q '__[A-Z0-9]*__' mac-app/build/cloudmachine.rb; then
echo "::error::W casku zostal niewypelniony znacznik."; exit 1
fi
echo "${sha256} CloudMachine-${version}.dmg" > "mac-app/build/CloudMachine-${version}.dmg.sha256"
echo "dmg=${dmg}" >> "$GITHUB_OUTPUT"

# Reguly dla caskow `brew style` stosuje tylko do plikow w Casks/ tapu -
# na luznym pliku sprawdza go jak zwykly Ruby i przepuszcza bledy caska.
- name: brew style + audit
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
run: |
brew tap-new --no-git renacode/ci-check
tap="$(brew --repository renacode/ci-check)"
mkdir -p "$tap/Casks"
cp mac-app/build/cloudmachine.rb "$tap/Casks/cloudmachine.rb"
brew style --cask renacode/ci-check/cloudmachine
brew audit --cask --strict renacode/ci-check/cloudmachine

- name: Upload artifacts (dry run)
if: env.IS_RELEASE != 'true'
uses: actions/upload-artifact@v4
with:
name: cloudmachine-${{ steps.version.outputs.version }}-dry-run
path: |
mac-app/build/*.dmg
mac-app/build/*.sha256
mac-app/build/cloudmachine.rb

- name: Publish GitHub Release
if: env.IS_RELEASE == 'true'
uses: softprops/action-gh-release@v2
with:
files: mac-app/build/*.dmg
files: |
mac-app/build/*.dmg
mac-app/build/*.sha256
generate_release_notes: true

- name: Check tap token
if: env.IS_RELEASE == 'true'
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
if [ -z "$TAP_TOKEN" ]; then
echo "::error::Wydanie opublikowane, ale brak HOMEBREW_TAP_TOKEN - cask w tapie NIE zostal zaktualizowany. Zawartosc do recznego wstawienia:"
cat mac-app/build/cloudmachine.rb
exit 1
fi

- name: Check out tap
if: env.IS_RELEASE == 'true'
uses: actions/checkout@v4
with:
repository: RenaCode/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap

- name: Push cask to tap
if: env.IS_RELEASE == 'true'
working-directory: homebrew-tap
run: |
mkdir -p Casks
cp ../mac-app/build/cloudmachine.rb Casks/cloudmachine.rb
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Casks/cloudmachine.rb
if git diff --cached --quiet; then
echo "Cask bez zmian."; exit 0
fi
git commit -m "cloudmachine ${{ steps.version.outputs.version }}"
git push
30 changes: 26 additions & 4 deletions mac-app/Sources/CloudMachineAgent/BuildAppCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ struct BuildApp: AsyncParsableCommand {
"Buduje CloudMachine.app (Release) - GUI + cloudmachine-agent w Contents/MacOS/, plus launchd/config jako Resources."
)

@Flag(
name: .long,
help:
"Binarki dla Apple Silicon i Intela naraz (tak buduje wydanie w CI; lokalnie zbedne).")
var universal = false

func run() async throws {
let macAppRoot = BuildPaths.macAppRoot
let projectRoot = BuildPaths.projectRoot
Expand All @@ -41,15 +47,31 @@ struct BuildApp: AsyncParsableCommand {
// swift.org installer, TOOLCHAINS env var) moga miec inny `swift` niz
// ten domyslny z Xcode. Oryginalny bash robil to samo (`swift build`
// bez sciezki, resolved przez PATH powloki).
let buildStatus = try await InteractiveProcess.run(
"/usr/bin/env", ["swift", "build", "-c", "release", "--package-path", macAppRoot.path])
let swiftArgs =
["build", "-c", "release", "--package-path", macAppRoot.path]
+ (universal ? ["--arch", "arm64", "--arch", "x86_64"] : [])
let buildStatus = try await InteractiveProcess.run("/usr/bin/env", ["swift"] + swiftArgs)
guard buildStatus == 0 else {
print("BLAD: swift build zakonczyl sie kodem \(buildStatus).")
throw ExitCode.failure
}

let appBinPath = macAppRoot.appendingPathComponent(".build/release/\(appName)App")
let agentBinPath = macAppRoot.appendingPathComponent(".build/release/cloudmachine-agent")
// Katalog z binarkami podaje sam SwiftPM: przy kilku architekturach to
// nie `.build/release`, tylko katalog zalezny od wersji narzedzi
// (`.build/apple/...` albo `.build/out/...`) - zgadywanie go zepsuloby
// sie przy pierwszej aktualizacji Xcode.
guard
let binPathResult = try? await ProcessRunner.run(
"/usr/bin/env", ["swift"] + swiftArgs + ["--show-bin-path"]),
binPathResult.succeeded
else {
print("BLAD: swift build --show-bin-path nie podal katalogu z binarkami.")
throw ExitCode.failure
}
let binDir = URL(
fileURLWithPath: binPathResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines))
let appBinPath = binDir.appendingPathComponent("\(appName)App")
let agentBinPath = binDir.appendingPathComponent("cloudmachine-agent")
for path in [appBinPath, agentBinPath] {
guard fm.fileExists(atPath: path.path) else {
print("BLAD: nie znaleziono zbudowanej binarki pod \(path.path)")
Expand Down
11 changes: 9 additions & 2 deletions mac-app/Sources/CloudMachineAgent/SetupSigningCertCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ struct SetupSigningCert: AsyncParsableCommand {
// nie rozumie - bez tej flagi import konczy sie mylacym "MAC
// verification failed (wrong password?)" mimo poprawnego hasla. -legacy
// wraca do 3DES/RC2, ktore macOS poprawnie parsuje.
//
// Ale `/usr/bin/openssl` na macOS to LibreSSL, ktory flagi -legacy NIE
// ZNA i konczy sie bledem (sprawdzone na LibreSSL 3.3.6) - a 3DES/RC2 ma
// juz domyslnie. Flage dokladamy wiec tylko prawdziwemu OpenSSL 3.
let versionOutput =
(try? await ProcessRunner.run("/usr/bin/openssl", ["version"]))?.stdout ?? ""
let legacyFlag = versionOutput.hasPrefix("OpenSSL 3") ? ["-legacy"] : []
let pkcs12Status = try await InteractiveProcess.run(
"/usr/bin/openssl",
[
"pkcs12", "-export", "-legacy", "-out", p12File.path, "-inkey", keyFile.path,
["pkcs12", "-export"] + legacyFlag + [
"-out", p12File.path, "-inkey", keyFile.path,
"-in", certFile.path, "-passout", "pass:cloudmachine-local",
])
guard pkcs12Status == 0 else {
Expand Down
2 changes: 1 addition & 1 deletion mac-app/Sources/CloudMachineCore/AppVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public enum AppVersionReader {
/// Przy `swift run` zadnego bundla nie ma i to nie jest blad - zwracamy
/// `nil`, a wolajacy mowi wprost, ze to build z drzewa roboczego.
public static func current(
executable: URL = URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath()
executable: URL = CMPaths.runningExecutable
) -> AppVersion? {
let infoPlist =
executable
Expand Down
29 changes: 26 additions & 3 deletions mac-app/Sources/CloudMachineCore/CMPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import Foundation
/// Jedno miejsce prawdy dla GUI i CLI - wczesniej ta sama logika byla
/// zduplikowana (raz jako common.sh, raz czesciowo w CloudMachineController).
public enum CMPaths {
/// Prawdziwa sciezka do dzialajacej binarki, po rozwinieciu dowiazan.
///
/// NIE `CommandLine.arguments[0]`: przy wywolaniu z PATH (dowiazanie
/// `/usr/local/bin/cloudmachine-agent`, binarka z Homebrew) powloka podaje
/// tam sama nazwe, a `URL(fileURLWithPath:)` dokleja ja do biezacego
/// katalogu. `cd /tmp && cloudmachine-agent version` meldowal wtedy "Build
/// z drzewa roboczego", a `install-launchd` wskazalby launchd binarke
/// `/tmp/cloudmachine-agent`, ktorej nie ma. `Bundle.main.executableURL`
/// bierze sciezke od jadra, niezaleznie od tego, jak polecenie wpisano.
public static var runningExecutable: URL {
(Bundle.main.executableURL ?? URL(fileURLWithPath: CommandLine.arguments[0]))
.resolvingSymlinksInPath()
}

/// Katalog z zasobami projektu (`launchd/`, `config/`) - w .app to
/// `Contents/Resources`, w checkoutcie deweloperskim to korzen repo
/// (rodzic `mac-app/`). `nil`, jesli zaden z tych katalogow nie istnieje
Expand All @@ -23,7 +37,16 @@ public enum CMPaths {
// ta binarka siedzi pod mac-app/.build/<triple>/<config>/, wiec korzen
// repo to 5 poziomow wyzej. Sprawdzamy tez plytsza sciezke na wypadek
// uruchomienia bezposrednio z katalogu mac-app.
let exeDir = URL(fileURLWithPath: CommandLine.arguments[0]).deletingLastPathComponent()
let exeDir = runningExecutable.deletingLastPathComponent()
// Agent wolany przez dowiazanie: `Bundle.main` bywa wtedy liczony od
// katalogu dowiazania, nie od .app - wiec Resources szukamy tez obok
// prawdziwej binarki (Contents/MacOS -> Contents/Resources).
let bundleResources = exeDir.deletingLastPathComponent().appendingPathComponent("Resources")
if FileManager.default.fileExists(
atPath: bundleResources.appendingPathComponent("launchd").path)
{
return bundleResources
}
var candidate = exeDir
for _ in 0..<6 {
if FileManager.default.fileExists(atPath: candidate.appendingPathComponent("launchd").path) {
Expand Down Expand Up @@ -70,9 +93,9 @@ public enum CMPaths {
/// 3. Fallback dla GUI uruchomionego przez `swift run` w drzewie repo -
/// szukamy `cloudmachine-agent` w `.build/*/{release,debug}/` obok binarki GUI.
public static var agentBinaryPath: URL? {
let selfURL = URL(fileURLWithPath: CommandLine.arguments[0])
let selfURL = runningExecutable
if selfURL.lastPathComponent == "cloudmachine-agent" {
return selfURL.standardizedFileURL
return selfURL
}
if let bundled = Bundle.main.executableURL?.deletingLastPathComponent().appendingPathComponent(
"cloudmachine-agent"),
Expand Down
Loading
Loading