Skip to content

Commit 9b069db

Browse files
committed
initial commit
0 parents  commit 9b069db

File tree

7 files changed

+780
-0
lines changed

7 files changed

+780
-0
lines changed

.github/workflows/release.yml

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# =============================================================================
2+
# Osaurus Plugin Release Workflow
3+
# =============================================================================
4+
# This workflow automatically builds, signs, and releases your Osaurus plugin
5+
# when you push a version tag (e.g., 1.0.0). It also generates the registry
6+
# entry JSON for submitting to the plugin registry.
7+
#
8+
# SETUP:
9+
# 1. Update the configuration below for your plugin
10+
# 2. Add the required secrets to your repository:
11+
# - MINISIGN_SECRET_KEY: Your minisign private key
12+
# - MINISIGN_PUBLIC_KEY: Your minisign public key
13+
# - MINISIGN_PASSWORD: Key password (optional, can be empty)
14+
# - DEVELOPER_ID_CERTIFICATE_P12_BASE64: Base64-encoded .p12 certificate (for code signing)
15+
# - DEVELOPER_ID_CERTIFICATE_PASSWORD: Password for the .p12 certificate
16+
#
17+
# USAGE:
18+
# git tag 1.0.0
19+
# git push origin 1.0.0
20+
#
21+
# After the workflow completes, follow the instructions in the workflow summary
22+
# to submit your plugin to the registry.
23+
# =============================================================================
24+
25+
name: Build and Release
26+
27+
on:
28+
push:
29+
tags:
30+
- "[0-9]*"
31+
32+
permissions:
33+
contents: write
34+
35+
# =============================================================================
36+
# CONFIGURATION - Update these values for your plugin
37+
# =============================================================================
38+
env:
39+
# Your plugin ID
40+
PLUGIN_ID: osaurus.contacts
41+
42+
# Display name for the plugin
43+
PLUGIN_NAME: Contacts
44+
45+
# Brief description of what the plugin does
46+
PLUGIN_DESCRIPTION: Interact with macOS Contacts.app - get phone numbers, find contacts by name or phone
47+
48+
# The built dynamic library name (without lib prefix and .dylib extension)
49+
DYLIB_NAME: osaurus-contacts
50+
51+
# License (e.g., MIT, Apache-2.0)
52+
LICENSE: MIT
53+
54+
# Minimum macOS version required
55+
MIN_MACOS: "13.0"
56+
57+
# Minimum Osaurus version required (optional)
58+
MIN_OSAURUS: "0.5.0"
59+
60+
# Tool definitions (JSON array of {name, description} objects)
61+
TOOLS: '[{"name": "get_all_numbers", "description": "Get all contacts and their phone numbers"}, {"name": "find_number", "description": "Find phone numbers for a contact by name"}, {"name": "find_contact_by_phone", "description": "Find a contact name by their phone number"}]'
62+
63+
# =============================================================================
64+
# JOBS - No changes needed below this line
65+
# =============================================================================
66+
jobs:
67+
build:
68+
runs-on: macos-14
69+
outputs:
70+
version: ${{ steps.version.outputs.VERSION }}
71+
sha256: ${{ steps.sha256.outputs.SHA256 }}
72+
size: ${{ steps.size.outputs.SIZE }}
73+
signature_b64: ${{ steps.minisign.outputs.SIGNATURE_B64 }}
74+
75+
steps:
76+
- name: Checkout
77+
uses: actions/checkout@v4
78+
79+
- name: Setup Swift
80+
uses: swift-actions/setup-swift@v2
81+
with:
82+
swift-version: "6.0"
83+
84+
- name: Build Release
85+
run: swift build -c release -Xswiftc -swift-version -Xswiftc 5
86+
87+
- name: Import Code Signing Certificate
88+
id: codesign
89+
env:
90+
CERTIFICATE_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_P12_BASE64 }}
91+
CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}
92+
run: |
93+
if [ -z "$CERTIFICATE_P12_BASE64" ]; then
94+
echo "⚠️ No DEVELOPER_ID_CERTIFICATE_P12_BASE64 configured, will use ad-hoc signing"
95+
echo " Note: Code signing is REQUIRED for distributed plugins"
96+
echo "identity=" >> $GITHUB_OUTPUT
97+
exit 0
98+
fi
99+
100+
# Create a temporary keychain
101+
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
102+
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
103+
104+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
105+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
106+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
107+
108+
# Import certificate
109+
CERT_PATH="$RUNNER_TEMP/certificate.p12"
110+
echo "$CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH"
111+
112+
security import "$CERT_PATH" \
113+
-P "$CERTIFICATE_PASSWORD" \
114+
-A \
115+
-t cert \
116+
-f pkcs12 \
117+
-k "$KEYCHAIN_PATH"
118+
119+
# Add keychain to search list
120+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
121+
122+
# Allow codesign to access the key without prompting
123+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
124+
125+
# Extract signing identity from certificate
126+
IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)".*/\1/')
127+
128+
if [ -z "$IDENTITY" ]; then
129+
echo "::error::No Developer ID Application certificate found in keychain"
130+
exit 1
131+
fi
132+
133+
echo "Found signing identity: $IDENTITY"
134+
echo "identity=$IDENTITY" >> $GITHUB_OUTPUT
135+
136+
# Store keychain path for cleanup
137+
echo "keychain_path=$KEYCHAIN_PATH" >> $GITHUB_OUTPUT
138+
139+
# Clean up temp cert file
140+
rm -f "$CERT_PATH"
141+
142+
- name: Code Sign dylib
143+
env:
144+
CODESIGN_IDENTITY: ${{ steps.codesign.outputs.identity }}
145+
run: |
146+
DYLIB_PATH=".build/release/lib${{ env.DYLIB_NAME }}.dylib"
147+
148+
if [ -z "$CODESIGN_IDENTITY" ]; then
149+
echo "⚠️ No signing identity available, using ad-hoc signing"
150+
codesign --force --sign - "$DYLIB_PATH"
151+
else
152+
echo "Signing with: $CODESIGN_IDENTITY"
153+
codesign --force --options runtime --timestamp \
154+
--sign "$CODESIGN_IDENTITY" \
155+
"$DYLIB_PATH"
156+
fi
157+
158+
# Verify signature
159+
codesign -dv --verbose=4 "$DYLIB_PATH"
160+
echo "✅ Code signing completed successfully"
161+
162+
- name: Get version from tag
163+
id: version
164+
run: echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
165+
166+
- name: Package artifact
167+
run: |
168+
mkdir -p dist
169+
cp ".build/release/lib${{ env.DYLIB_NAME }}.dylib" dist/
170+
cd dist
171+
zip -r "../${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip" .
172+
173+
- name: Calculate SHA256
174+
id: sha256
175+
run: |
176+
SHA=$(shasum -a 256 "${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip" | cut -d ' ' -f 1)
177+
echo "SHA256=$SHA" >> $GITHUB_OUTPUT
178+
179+
- name: Get artifact size
180+
id: size
181+
run: |
182+
SIZE=$(stat -f%z "${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip")
183+
echo "SIZE=$SIZE" >> $GITHUB_OUTPUT
184+
185+
- name: Install minisign
186+
run: brew install minisign
187+
188+
- name: Sign artifact with minisign
189+
id: minisign
190+
env:
191+
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
192+
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
193+
run: |
194+
if [ -z "$MINISIGN_SECRET_KEY" ]; then
195+
echo "No MINISIGN_SECRET_KEY configured, skipping signing"
196+
echo "SIGNATURE_B64=" >> $GITHUB_OUTPUT
197+
exit 0
198+
fi
199+
200+
echo "$MINISIGN_SECRET_KEY" > $RUNNER_TEMP/minisign.key
201+
202+
ARTIFACT="${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip"
203+
if [ -n "$MINISIGN_PASSWORD" ]; then
204+
echo "$MINISIGN_PASSWORD" | minisign -Slm "$ARTIFACT" -s $RUNNER_TEMP/minisign.key -x "${ARTIFACT}.minisig"
205+
else
206+
minisign -Slm "$ARTIFACT" -s $RUNNER_TEMP/minisign.key -x "${ARTIFACT}.minisig" -W
207+
fi
208+
209+
SIGNATURE_B64=$(base64 -i "${ARTIFACT}.minisig")
210+
echo "SIGNATURE_B64=$SIGNATURE_B64" >> $GITHUB_OUTPUT
211+
212+
rm -f $RUNNER_TEMP/minisign.key
213+
214+
- name: Create GitHub Release
215+
uses: softprops/action-gh-release@v2
216+
with:
217+
name: "${{ env.PLUGIN_NAME }} v${{ steps.version.outputs.VERSION }}"
218+
files: |
219+
${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip
220+
${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip.minisig
221+
fail_on_unmatched_files: false
222+
generate_release_notes: true
223+
224+
- name: Upload registry JSON artifact
225+
uses: actions/upload-artifact@v4
226+
with:
227+
name: build-outputs
228+
path: |
229+
${{ env.PLUGIN_ID }}-${{ steps.version.outputs.VERSION }}.zip
230+
231+
- name: Cleanup Keychain
232+
if: always()
233+
run: |
234+
KEYCHAIN_PATH="${{ steps.codesign.outputs.keychain_path }}"
235+
if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then
236+
security delete-keychain "$KEYCHAIN_PATH" || true
237+
fi
238+
239+
registry-entry:
240+
needs: build
241+
runs-on: ubuntu-latest
242+
243+
steps:
244+
- name: Checkout
245+
uses: actions/checkout@v4
246+
247+
- name: Download build outputs
248+
uses: actions/download-artifact@v4
249+
with:
250+
name: build-outputs
251+
252+
- name: Generate registry JSON
253+
env:
254+
VERSION: ${{ needs.build.outputs.version }}
255+
SHA256: ${{ needs.build.outputs.sha256 }}
256+
SIZE: ${{ needs.build.outputs.size }}
257+
SIGNATURE_B64: ${{ needs.build.outputs.signature_b64 }}
258+
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
259+
REPO_OWNER: ${{ github.repository_owner }}
260+
TAG_NAME: ${{ github.ref_name }}
261+
MINISIGN_PUBLIC_KEY: ${{ secrets.MINISIGN_PUBLIC_KEY }}
262+
run: |
263+
mkdir -p release
264+
265+
# Decode signature if present
266+
if [ -n "$SIGNATURE_B64" ]; then
267+
MINISIGN_SIG=$(echo "$SIGNATURE_B64" | base64 -d)
268+
else
269+
MINISIGN_SIG=""
270+
fi
271+
272+
echo "$MINISIGN_SIG" > /tmp/minisign_sig.txt
273+
274+
python3 << 'PYTHON_SCRIPT'
275+
import json
276+
import os
277+
from datetime import datetime
278+
279+
plugin_id = os.environ["PLUGIN_ID"]
280+
plugin_name = os.environ["PLUGIN_NAME"]
281+
plugin_desc = os.environ["PLUGIN_DESCRIPTION"]
282+
version = os.environ["VERSION"]
283+
sha256 = os.environ["SHA256"]
284+
size = int(os.environ["SIZE"])
285+
repo_url = os.environ["REPO_URL"]
286+
repo_owner = os.environ["REPO_OWNER"]
287+
tag_name = os.environ["TAG_NAME"]
288+
license_type = os.environ["LICENSE"]
289+
min_macos = os.environ["MIN_MACOS"]
290+
minisign_public_key = os.environ.get("MINISIGN_PUBLIC_KEY", "")
291+
292+
try:
293+
with open('/tmp/minisign_sig.txt', 'r') as f:
294+
minisign_sig = f.read().strip()
295+
except:
296+
minisign_sig = ""
297+
298+
# Get tools from TOOLS env var
299+
tools = json.loads(os.environ.get("TOOLS", "[]"))
300+
301+
artifact = {
302+
"os": "macos",
303+
"arch": "arm64",
304+
"min_macos": min_macos,
305+
"url": f"{repo_url}/releases/download/{tag_name}/{plugin_id}-{version}.zip",
306+
"sha256": sha256,
307+
"size": size
308+
}
309+
310+
if minisign_sig:
311+
artifact["minisign"] = {"signature": minisign_sig}
312+
313+
data = {
314+
"plugin_id": plugin_id,
315+
"name": plugin_name,
316+
"description": plugin_desc,
317+
"homepage": repo_url,
318+
"license": license_type,
319+
"authors": [repo_owner],
320+
"capabilities": {"tools": tools},
321+
"public_keys": {"minisign": minisign_public_key} if minisign_public_key else {},
322+
"versions": [{
323+
"version": version,
324+
"release_date": datetime.utcnow().strftime("%Y-%m-%d"),
325+
"artifacts": [artifact]
326+
}]
327+
}
328+
329+
# Remove empty public_keys
330+
if not data["public_keys"]:
331+
del data["public_keys"]
332+
333+
with open(f'release/{plugin_id}.json', 'w') as f:
334+
json.dump(data, f, indent=2)
335+
336+
print(f"Generated release/{plugin_id}.json for v{version}")
337+
PYTHON_SCRIPT
338+
339+
- name: Commit registry entry
340+
run: |
341+
git config user.name "github-actions[bot]"
342+
git config user.email "github-actions[bot]@users.noreply.github.com"
343+
git add release/
344+
git commit -m "Update registry entry for ${{ needs.build.outputs.version }}"
345+
git push origin HEAD:master

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
/*.xcworkspace
6+
/xcuserdata
7+
/.swiftpm
8+
/*.zip
9+
*.dylib
10+

Package.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "osaurus-contacts",
6+
platforms: [.macOS(.v13)],
7+
products: [
8+
.library(name: "osaurus-contacts", type: .dynamic, targets: ["osaurus_contacts"])
9+
],
10+
targets: [
11+
.target(
12+
name: "osaurus_contacts",
13+
path: "Sources/osaurus_contacts"
14+
)
15+
]
16+
)

0 commit comments

Comments
 (0)