mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
ci(release): sign and notarize the macOS bundle, and bump the Homebrew cask
An unsigned bundle on a current macOS opens to a malware warning with no obvious way past it, and the way past it that does exist teaches people to click through exactly the warning worth reading. The build now signs with a Developer ID certificate and notarizes with an App Store Connect API key, which is also what does the App Store upload, so there is one credential to rotate. The verification step is the point. codesign only says a signature is internally consistent; spctl is what a person double-clicking the file actually meets, and it does not pass until the notarization ticket is stapled. A final job rewrites the version and sha256 in the tap's cask, using an SSH deploy key rather than a token so a leak from a release job cannot reach the app repositories. Also fixes Cargo.lock drifting a version behind on every release: the bump wrote Cargo.toml but never staged the lock, so any fresh build dirtied the tree. Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
This commit is contained in:
1 parent
d8c47bb6f0
commit
39d4097773
4 files changed
+101
-5
No files matched your search
@@ -44,6 +44,12 @@ jobs:
|
|||||||
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > "$tmp" && mv "$tmp" src-tauri/tauri.conf.json
|
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > "$tmp" && mv "$tmp" src-tauri/tauri.conf.json
|
||||||
jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
|
jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
|
||||||
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" src-tauri/Cargo.toml
|
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||||
|
# Cargo.lock records margin-app's own version, so bumping only Cargo.toml leaves the
|
||||||
|
# lock a release behind and the next build rewrites it under whoever checked it out.
|
||||||
|
awk -v v="$VERSION" '
|
||||||
|
/^name = "margin-app"$/ { print; getline; sub(/^version = ".*"/, "version = \"" v "\""); print; next }
|
||||||
|
{ print }
|
||||||
|
' src-tauri/Cargo.lock > "$tmp" && mv "$tmp" src-tauri/Cargo.lock
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
env:
|
env:
|
||||||
@@ -51,7 +57,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add src-tauri/tauri.conf.json package.json src-tauri/Cargo.toml
|
git add src-tauri/tauri.conf.json package.json src-tauri/Cargo.toml src-tauri/Cargo.lock
|
||||||
git commit -m "chore(release): $TAG"
|
git commit -m "chore(release): $TAG"
|
||||||
for attempt in 1 2 3 4 5; do
|
for attempt in 1 2 3 4 5; do
|
||||||
git fetch origin main
|
git fetch origin main
|
||||||
@@ -147,19 +153,49 @@ jobs:
|
|||||||
echo "Wrote google-credentials.json from GOOGLE_CREDENTIALS secret."
|
echo "Wrote google-credentials.json from GOOGLE_CREDENTIALS secret."
|
||||||
else
|
else
|
||||||
cp google-credentials.example.json google-credentials.json
|
cp google-credentials.example.json google-credentials.json
|
||||||
echo "::warning::GOOGLE_CREDENTIALS secret not set — embedding placeholder credentials; Google Drive backup will be disabled in this release."
|
echo "::warning::GOOGLE_CREDENTIALS secret not set, embedding placeholder credentials; Google Drive backup will be disabled in this release."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Provision Apple notarization key
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$KEY_P8" ]; then
|
||||||
|
echo "::warning::APPLE_API_KEY_P8 is not set, so the macOS bundle will be ad-hoc signed and Gatekeeper will refuse to open it."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf '%s' "$KEY_P8" | base64 --decode > "$RUNNER_TEMP/apple-api-key.p8"
|
||||||
|
chmod 600 "$RUNNER_TEMP/apple-api-key.p8"
|
||||||
|
echo "APPLE_API_KEY_PATH=$RUNNER_TEMP/apple-api-key.p8" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Build and upload
|
- name: Build and upload
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
with:
|
with:
|
||||||
releaseId: ${{ needs.prepare.outputs.release_id }}
|
releaseId: ${{ needs.prepare.outputs.release_id }}
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
|
- name: Verify the bundle is signed and notarized
|
||||||
|
if: runner.os == 'macOS' && env.APPLE_API_KEY_PATH != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
app="src-tauri/target/universal-apple-darwin/release/bundle/macos/Margin.app"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app"
|
||||||
|
# Gatekeeper only says "accepted" once the notarization ticket is stapled to the bundle,
|
||||||
|
# so this is the check that a user double-clicking the dmg will actually get past.
|
||||||
|
spctl --assess --type execute --verbose=4 "$app"
|
||||||
|
xcrun stapler validate "$app"
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: [prepare, build]
|
needs: [prepare, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -175,8 +211,49 @@ jobs:
|
|||||||
jq '.platforms | keys' latest.json
|
jq '.platforms | keys' latest.json
|
||||||
for key in darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64; do
|
for key in darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64; do
|
||||||
if ! jq -e ".platforms[\"$key\"].url" latest.json > /dev/null; then
|
if ! jq -e ".platforms[\"$key\"].url" latest.json > /dev/null; then
|
||||||
echo "::error::latest.json is missing platform '$key' — refusing to publish a partial update manifest. Re-run the release."
|
echo "::error::latest.json is missing platform '$key'. Refusing to publish a partial update manifest; re-run the release."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
gh release edit "$TAG" --repo "$REPO" --draft=false --latest
|
gh release edit "$TAG" --repo "$REPO" --draft=false --latest
|
||||||
|
|
||||||
|
homebrew:
|
||||||
|
needs: [prepare, publish]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Point the cask at the release that just went out
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ needs.prepare.outputs.tag }}
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
TAP: priyanshujain/homebrew-margin
|
||||||
|
run: |
|
||||||
|
if [ -z "$DEPLOY_KEY" ]; then
|
||||||
|
echo "::warning::HOMEBREW_TAP_DEPLOY_KEY is not set, so $TAG is published but the Homebrew cask still points at the previous version."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
dmg="Margin_${VERSION}_universal.dmg"
|
||||||
|
gh release download "$TAG" --repo "$REPO" --pattern "$dmg" --output "$dmg"
|
||||||
|
sha=$(sha256sum "$dmg" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# A deploy key rather than a token: it reaches the tap and nothing else, so a leak from
|
||||||
|
# this job cannot touch the app repos.
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/tap_key
|
||||||
|
chmod 600 ~/.ssh/tap_key
|
||||||
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ~/.ssh/tap_key -o IdentitiesOnly=yes"
|
||||||
|
|
||||||
|
git clone --depth 1 "[email protected]:$TAP.git" tap
|
||||||
|
cd tap
|
||||||
|
sed -i -E "s|^ version \".*\"| version \"$VERSION\"|" Casks/margin.rb
|
||||||
|
sed -i -E "s|^ sha256 \".*\"| sha256 \"$sha\"|" Casks/margin.rb
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add Casks/margin.rb
|
||||||
|
git commit -m "margin $VERSION"
|
||||||
|
git push
|
||||||
Generated
+1
-1
@@ -4713,7 +4713,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "margin-app"
|
name = "margin-app"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- Margin's only cryptography is TLS for the Google Drive API and the SHA-256 that PKCE
|
||||||
|
needs, both of which are exempt. Declaring it here rather than in App Store Connect means
|
||||||
|
the question is not asked again on every submission. -->
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -36,6 +36,14 @@
|
|||||||
"icons/[email protected]",
|
"icons/[email protected]",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
],
|
||||||
|
"category": "Productivity",
|
||||||
|
"shortDescription": "A calm, offline writing studio for authors",
|
||||||
|
"longDescription": "Write your book, set it in real book typography, and export print-ready PDF and ebook files for every store. Everything happens on your machine.",
|
||||||
|
"copyright": "Copyright © 2026 Priyanshu Jain",
|
||||||
|
"macOS": {
|
||||||
|
"minimumSystemVersion": "10.15",
|
||||||
|
"hardenedRuntime": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user