ci: release notarized Meetingnotes updates through Gitea
Build / macos (push) Failing after 57s

This commit is contained in:
2026-09-09 23:49:37 +02:00
parent 2508c77f17
commit df23c84474
9 changed files with 171 additions and 15 deletions
+7 -5
View File
@@ -14,11 +14,13 @@ APP_PATH="$DERIVED_DATA/Build/Products/Release/$APP_NAME.app"
required_variables=(
VERSION
SIGNING_IDENTITY
SIGNING_KEYCHAIN
APPLE_ID
APPLE_TEAM_ID
APPLE_APP_PASSWORD
SPARKLE_PRIVATE_KEY
GITHUB_REPOSITORY
RELEASE_BASE_URL
)
for variable in "${required_variables[@]}"; do
@@ -57,12 +59,12 @@ SPARKLE_FRAMEWORK="$APP_PATH/Contents/Frameworks/Sparkle.framework"
SPARKLE_CONTENTS="$SPARKLE_FRAMEWORK/Versions/B"
sign_component() {
codesign --force --timestamp --options runtime --sign "$SIGNING_IDENTITY" "$1"
codesign --force --timestamp --options runtime --keychain "$SIGNING_KEYCHAIN" --sign "$SIGNING_IDENTITY" "$1"
}
sign_component "$SPARKLE_CONTENTS/XPCServices/Installer.xpc"
if [[ -d "$SPARKLE_CONTENTS/XPCServices/Downloader.xpc" ]]; then
codesign --force --timestamp --options runtime \
codesign --force --timestamp --options runtime --keychain "$SIGNING_KEYCHAIN" \
--preserve-metadata=entitlements \
--sign "$SIGNING_IDENTITY" \
"$SPARKLE_CONTENTS/XPCServices/Downloader.xpc"
@@ -71,7 +73,7 @@ sign_component "$SPARKLE_CONTENTS/Autoupdate"
sign_component "$SPARKLE_CONTENTS/Updater.app"
sign_component "$SPARKLE_FRAMEWORK"
codesign --force --timestamp --options runtime \
codesign --force --timestamp --options runtime --keychain "$SIGNING_KEYCHAIN" \
--entitlements meetingnotes/meetingnotes.entitlements \
--sign "$SIGNING_IDENTITY" \
"$APP_PATH"
@@ -116,7 +118,7 @@ if [[ -z "$GENERATE_APPCAST" ]]; then
exit 1
fi
DOWNLOAD_URL="https://github.com/$GITHUB_REPOSITORY/releases/download/v$VERSION/"
DOWNLOAD_URL="$RELEASE_BASE_URL/releases/download/v$VERSION/"
printf '%s' "$SPARKLE_PRIVATE_KEY" | "$GENERATE_APPCAST" "$RELEASE_DIR" \
--ed-key-file - \
--download-url-prefix "$DOWNLOAD_URL" \
@@ -127,7 +129,7 @@ grep -q "$DOWNLOAD_URL$ARCHIVE_NAME" "$RELEASE_DIR/appcast.xml"
grep -q 'sparkle:edSignature=' "$RELEASE_DIR/appcast.xml"
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
printf 'Built, Developer ID-signed, notarized, and stapled Meetingnotes %s. The GitHub release is ready to publish.\n' \
printf 'Built, Developer ID-signed, notarized, and stapled Meetingnotes %s. The Gitea release is ready to publish.\n' \
"$VERSION" >> "$GITHUB_STEP_SUMMARY"
fi
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Publish both signed assets together, keeping incomplete uploads as a draft."""
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
base = os.environ["GITEA_SERVER_URL"].rstrip("/") + "/api/v1/repos/" + os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITEA_TOKEN"]
version = os.environ["VERSION"]
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
def request(path, method="GET", data=None, binary=False):
body = data if binary else None if data is None else json.dumps(data).encode()
req = urllib.request.Request(base + path, method=method, data=body, headers={
"Authorization": "token " + token,
"Content-Type": "application/octet-stream" if binary else "application/json",
"User-Agent": "Meetingnotes-release",
})
with urllib.request.build_opener(NoRedirect()).open(req, timeout=120) as response:
raw = response.read()
return json.loads(raw) if raw else None
release_dir = Path(os.environ["RUNNER_TEMP"]) / "meetingnotes-release/release"
assets = [release_dir / f"Meetingnotes-{version}.zip", release_dir / "appcast.xml"]
for asset in assets:
if not asset.is_file() or not asset.stat().st_size:
raise RuntimeError(f"Missing release artifact: {asset.name}")
release = request("/releases", "POST", {
"tag_name": "v" + version, "target_commitish": os.environ["GITHUB_SHA"],
"name": "Meetingnotes " + version, "draft": True, "prerelease": False,
"body": "Developer ID signed, Apple notarized, and signed for Sparkle updates.\n\nBuilt from main at `" + os.environ["GITHUB_SHA"] + "`.",
})
for asset in assets:
request(f"/releases/{release['id']}/assets?name=" + urllib.parse.quote(asset.name), "POST", asset.read_bytes(), binary=True)
request(f"/releases/{release['id']}", "PATCH", {"draft": False})
print("Published Meetingnotes " + version)
+5 -1
View File
@@ -16,7 +16,7 @@ with socket.socket() as listener:
port = listener.getsockname()[1]
with tempfile.TemporaryFile() as log:
process = subprocess.Popen(
[str(app), "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port)],
[str(app), "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"],
stdout=log, stderr=subprocess.STDOUT,
)
try:
@@ -39,6 +39,10 @@ with tempfile.TemporaryFile() as log:
else:
raise RuntimeError("Recording status allowed an unauthenticated request")
print("Local API readiness and authentication checks passed")
except Exception:
log.seek(0)
print(log.read().decode(errors="replace")[-8000:], file=sys.stderr)
raise
finally:
if process.poll() is None:
process.terminate()
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Import the CI certificate into an isolated keychain for one release command."""
import base64
import os
import re
import secrets
import subprocess
import sys
import tempfile
from pathlib import Path
def security(*args):
result = subprocess.run(["security", *args], capture_output=True, text=True)
if result.returncode:
raise RuntimeError(f"security {args[0]} failed (output withheld to protect credentials)")
return result.stdout
with tempfile.TemporaryDirectory(prefix="meetingnotes-signing-", dir=os.environ["RUNNER_TEMP"]) as directory:
keychain = str(Path(directory) / "release.keychain-db")
certificate = Path(directory) / "certificate.p12"
certificate.write_bytes(base64.b64decode(os.environ["APPLE_CERTIFICATE_P12"]))
certificate.chmod(0o600)
password = secrets.token_urlsafe(32)
created = False
try:
security("create-keychain", "-p", password, keychain)
created = True
security("set-keychain-settings", "-lut", "21600", keychain)
security("unlock-keychain", "-p", password, keychain)
security("import", str(certificate), "-k", keychain, "-P", os.environ["APPLE_CERTIFICATE_PASSWORD"], "-T", "/usr/bin/codesign", "-T", "/usr/bin/security")
security("set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychain)
identities = security("find-identity", "-v", "-p", "codesigning", keychain)
match = re.search(r'([0-9A-F]{40}) "Developer ID Application:', identities)
if not match:
raise RuntimeError("The certificate contains no valid Developer ID Application identity")
environment = dict(os.environ, SIGNING_IDENTITY=match[1], SIGNING_KEYCHAIN=keychain)
status = subprocess.run(sys.argv[1:], env=environment).returncode
finally:
if created:
security("delete-keychain", keychain)
sys.exit(status)