|
| 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 |
0 commit comments