Add signed releases and automatic updates (#7)
* feat: prepare stable signed updates * ci: automate signed macOS releases * ci: publish releases atomically * fix: use compatible atomic replacement API --------- Co-authored-by: superdooper86 <[email protected]>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Version from MARKETING_VERSION, without the v prefix
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: meetingnotes-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: macos-15
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||||
|
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Validate release secrets
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
for variable in APPLE_CERTIFICATE_P12 APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_TEAM_ID APPLE_APP_PASSWORD SPARKLE_PRIVATE_KEY; do
|
||||||
|
if [[ -z "${!variable:-}" ]]; then
|
||||||
|
echo "Missing GitHub Actions secret: $variable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Import Developer ID certificate
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
run: |
|
||||||
|
certificate_path="$RUNNER_TEMP/developer-id.p12"
|
||||||
|
keychain_path="$RUNNER_TEMP/release-signing.keychain-db"
|
||||||
|
echo -n "$APPLE_CERTIFICATE_P12" | base64 --decode > "$certificate_path"
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
|
||||||
|
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
|
||||||
|
security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
|
||||||
|
security list-keychain -d user -s "$keychain_path"
|
||||||
|
security default-keychain -d user -s "$keychain_path"
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path"
|
||||||
|
signing_identity=$(security find-identity -v -p codesigning "$keychain_path" | awk -F '"' '/Developer ID Application/{print $2; exit}')
|
||||||
|
if [[ -z "$signing_identity" ]]; then
|
||||||
|
echo "The .p12 does not contain a Developer ID Application identity" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "SIGNING_IDENTITY=$signing_identity" >> "$GITHUB_ENV"
|
||||||
|
echo "RELEASE_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build, sign, notarize, and create appcast
|
||||||
|
run: scripts/package_release.sh
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
tag="v$VERSION"
|
||||||
|
if git rev-parse "$tag" >/dev/null 2>&1; then
|
||||||
|
echo "Tag already exists: $tag" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
gh release create "$tag" \
|
||||||
|
"$RUNNER_TEMP/meetingnotes-release/release/Meetingnotes-$VERSION.zip" \
|
||||||
|
"$RUNNER_TEMP/meetingnotes-release/release/appcast.xml" \
|
||||||
|
--target "$GITHUB_SHA" \
|
||||||
|
--title "Meetingnotes $VERSION" \
|
||||||
|
--generate-notes
|
||||||
|
|
||||||
|
- name: Remove signing keychain
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -n "${RELEASE_KEYCHAIN:-}" ]]; then
|
||||||
|
security delete-keychain "$RELEASE_KEYCHAIN" || true
|
||||||
|
fi
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ML6HYR5LUR;
|
DEVELOPMENT_TEAM = ML6HYR5LUR;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.3;
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ML6HYR5LUR;
|
DEVELOPMENT_TEAM = ML6HYR5LUR;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.3;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = owen.meetingnotes;
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
|
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 2.7.1;
|
minimumVersion = 2.9.4;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
CCC33F7F2E236B6F00EDE382 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
|
CCC33F7F2E236B6F00EDE382 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "df074165274afaa39539c05d57b0832620775b11",
|
"revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7",
|
||||||
"version" : "2.7.1"
|
"version" : "2.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -134,12 +134,25 @@ Open the project in Xcode. Command+R to build it and run it.
|
|||||||
|
|
||||||
## Releasing a New Version
|
## Releasing a New Version
|
||||||
|
|
||||||
Follow these steps to create a new release with auto-updates:
|
Production releases are Developer ID signed, notarized by Apple, published to
|
||||||
|
GitHub Releases, and signed for Sparkle auto-updates.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Homebrew packages: `brew install create-dmg sparkle`
|
- Apple Developer Program membership
|
||||||
- Make scripts executable: `chmod +x scripts/update_version.sh scripts/build_release.sh`
|
- A Developer ID Application certificate exported from Keychain Access as a
|
||||||
|
password-protected `.p12`
|
||||||
|
- An Apple ID app-specific password for notarization
|
||||||
|
- GitHub CLI: `brew install gh`
|
||||||
|
|
||||||
|
Configure the Apple release secrets once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/configure_github_release_secrets.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The Sparkle private key is stored only as the `SPARKLE_PRIVATE_KEY` Actions
|
||||||
|
secret. Its matching public key is committed in `meetingnotes/Info.plist`.
|
||||||
|
|
||||||
### Release Process
|
### Release Process
|
||||||
|
|
||||||
@@ -159,31 +172,19 @@ Follow these steps to create a new release with auto-updates:
|
|||||||
./scripts/update_version.sh custom 1.2.0
|
./scripts/update_version.sh custom 1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Build the release:**
|
2. Commit and push the version change to `main`.
|
||||||
|
|
||||||
```bash
|
3. Run the `Release` workflow from GitHub Actions and enter the version without
|
||||||
./scripts/build_release.sh
|
the `v` prefix. The workflow signs and notarizes the app, generates the
|
||||||
```
|
signed appcast, creates the version tag, and publishes both release assets.
|
||||||
|
|
||||||
This will:
|
The app checks
|
||||||
|
`https://github.com/superdooper86/meetingnotes/releases/latest/download/appcast.xml`
|
||||||
|
and installs later releases automatically through Sparkle.
|
||||||
|
|
||||||
- Clean build the app in Release mode
|
### Recovering Meetings
|
||||||
- Create a signed DMG file
|
|
||||||
- Generate the appcast.xml for auto-updates
|
|
||||||
|
|
||||||
3. **Create GitHub Release:**
|
The first Developer ID signed build may not automatically inherit data from an
|
||||||
|
older ad-hoc signed build. In Settings, use **Import Meetings...** and select the
|
||||||
- Go to [GitHub Releases](https://github.com/owengretzinger/meetingnotes/releases)
|
old `Meetings` folder. After this one-time transition, the stable signing
|
||||||
- Click "Create a new release"
|
identity keeps the same sandbox container across updates.
|
||||||
- Tag: `v1.0.1` (match the version number)
|
|
||||||
- Title: `Meetingnotes v1.0.1`
|
|
||||||
- Upload the DMG and zip files from `releases/` folder
|
|
||||||
- Generate release notes
|
|
||||||
|
|
||||||
4. **Update appcast:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add appcast.xml
|
|
||||||
git commit -m "Update appcast for v1.0.1"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|||||||
+1
-73
@@ -1,78 +1,6 @@
|
|||||||
<?xml version="1.0" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Meetingnotes</title>
|
<title>Meetingnotes</title>
|
||||||
<item>
|
|
||||||
<title>1.1.1</title>
|
|
||||||
<pubDate>Tue, 05 Aug 2025 12:01:01 -0400</pubDate>
|
|
||||||
<sparkle:version>13</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>1.1.1</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes-1.1.1.zip" length="9742223" type="application/octet-stream" sparkle:edSignature="IdBhnJDPglM1H+LxjIDMYvXieGyXK8FLV3o1LSBKgR3SAkbwjieN/dW/kWMw4jOL7ousjZMssvL7AVs+4OoeAw==" />
|
|
||||||
<sparkle:deltas>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes13-12.delta" sparkle:deltaFrom="12" length="1495358" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="JHUS+WLJjdvNP+GGj25pUTXbVL5uzc9IO1eLtkFhSvl1+yb7l7btuRTnCPDVmvX/c8rF+MQraNRamrKVcB2eAw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes13-11.delta" sparkle:deltaFrom="11" length="2386250" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="sSZug40z/SSNzUROZFM36FyyaJ6LvTqVGZtpYnvBITkd2/fqrbGaKUXCgBKY/vzvdq+jNduKlArSsQUIkRemAA==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes13-10.delta" sparkle:deltaFrom="10" length="2381262" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="ODG+JexsG6BGjbOyPMTnt9Gfpd+iHHqSPSQBsCJTIMIJ0/RcKa39iZfZ7rkL996sQllFbwdAAph3bDQCYa0NCg==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes13-9.delta" sparkle:deltaFrom="9" length="2384498" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="66GFKhxIVI5jzrV40NKywvn3Lx8cdxsQEHYqiWWw1IeqSqH4lF00qE1giPJgKdZkEbL6neMjIqofcvqkIbPRAw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.1/Meetingnotes13-8.delta" sparkle:deltaFrom="8" length="2970002" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="C+KVv/usN2Vmg8tkJmYRBWrT7bBrvQY2GbTgXuVljvz/HbgnA1fyrsrhIrkXTbWJ8pdH76pn/Lfzw2oYtvkGDQ==" />
|
|
||||||
</sparkle:deltas>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>1.1.0</title>
|
|
||||||
<pubDate>Fri, 25 Jul 2025 17:39:07 -0400</pubDate>
|
|
||||||
<sparkle:version>12</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>1.1.0</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes-1.1.0.zip" length="9715895" type="application/octet-stream" sparkle:edSignature="Htx9FYb58yGYIJc+xJd/W2PHgbjsVXY+TL2WsGa8gx4J4f0Uyk+1Uizl8c2lDC4+hIra+b9lIrjG6l69NjU4Cg==" />
|
|
||||||
<sparkle:deltas>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes12-11.delta" sparkle:deltaFrom="11" length="2319634" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="L2IQPoN19GQfVV8leQCCh4U7/1/Oe3SyclBWbT+58VFKI+FH3KGj2vZM1x+v3X3pmyPdSy/S0N9sg+B7jFSUBg==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes12-10.delta" sparkle:deltaFrom="10" length="2301554" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="+GKgyT7MBDTqot6SFnvQax1il9p3ZEDG5SWg0gCOwMkKPrp6v48GrJY/VAF8tow2RbeVY2Q2HP6DNVPTm1jeDQ==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes12-9.delta" sparkle:deltaFrom="9" length="2342546" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="lPAYUzZGPYYgb+UWGCv237QvKTH8l3jZVXxyXHsWOtYcDzuGRYiZ39eMYTAF9OMEKbvfm5vQiXXxu42CKnj3DA==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes12-8.delta" sparkle:deltaFrom="8" length="2940458" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="iL16jVnWBp5Ws/F1B8goazEcGCrrMK9XwH2imslbJiAWlwG5sXAaN2N8XHcHA7xuysQkway/R1vCONjmm/DaDg==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.1.0/Meetingnotes12-7.delta" sparkle:deltaFrom="7" length="2923010" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="LQHXBknrE6nFFdpDW4ZWP9f/BBhU3PIUZ7DihMLh+Iu46/8cCv8vDaH7/doEJGoUfa1oEstYsWX0bCdHkKp9Cw==" />
|
|
||||||
</sparkle:deltas>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>1.0.6</title>
|
|
||||||
<pubDate>Mon, 21 Jul 2025 16:21:51 -0400</pubDate>
|
|
||||||
<sparkle:version>11</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>1.0.6</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes-1.0.6.zip" length="9530065" type="application/octet-stream" sparkle:edSignature="IxQ4qvkMsUjHaIn4g4/wH1MR1HtpPXJ/N1XaBtCpyeEK45J1UjoIn1zZae8SrHj8zNDglzK+XvV9vZM3iw1nAA==" />
|
|
||||||
<sparkle:deltas>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes11-9.delta" sparkle:deltaFrom="9" length="1081770" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="uvLxEKq4bGpYvCqPPcp3mDal9z7DI3FPwpBMsdqZFUjmnBWXvtha7Pk6Rv+Ua6OuxjsNqOGg5qiwkp9z3J41Aw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes11-10.delta" sparkle:deltaFrom="10" length="1192342" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="+u8c+Qeen3ILEKiPtOpW3KINmsBBk6nqBsCr5aZ4i/WqHHVbG/kTe6oWPCWhiq5XFlzNaRpXguDl3PdEhvafCQ==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes11-8.delta" sparkle:deltaFrom="8" length="2083034" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="uQinSWneeR1R9ad5GaE3Vr2pZI0vO9KGVVf/y5xw31iKvwe6S1fpZTdZpujlE1NZzl2R07ZOPt9mDYu6uMiXAA==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes11-7.delta" sparkle:deltaFrom="7" length="2075294" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="9u3ldVNYRuIIVGN0zG4T76yQ7JviM39Rl2cnnfUCpDwJ/+KjXgsjwpbelZa/5AWFeKffDth04D6xVRJB5tWfAw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.6/Meetingnotes11-6.delta" sparkle:deltaFrom="6" length="2075930" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="AgHLB01YstVzOTxXfief9M2rfxCNcya/w7N6QevalSI3Uxdw8CH5MVF2UGFWy1kdIiuQC7YBsftGmL20L9KzBA==" />
|
|
||||||
</sparkle:deltas>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>1.0.5</title>
|
|
||||||
<pubDate>Thu, 17 Jul 2025 08:36:59 -0400</pubDate>
|
|
||||||
<sparkle:version>10</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>1.0.5</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.5/Meetingnotes-1.0.5.zip" length="9519029" type="application/octet-stream" sparkle:edSignature="XBVJM2eh02aEQ6aNotS/3DNYXaOXA7956k/6GWEcfpcDVpJmUMDKcTIX0mtDWJl5YXin6YTA9qpIZDUJmkitBg==" />
|
|
||||||
<sparkle:deltas>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.5/Meetingnotes10-9.delta" sparkle:deltaFrom="9" length="358186" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="WCLkjV3zZwBgy8QEpQVcvI8mLeZVVwHkYMCis3GROY+TlX3o9P12tJfB/Mb0bRI5MHaXgl+6PelbdBC2SLL9Dg==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.5/Meetingnotes10-8.delta" sparkle:deltaFrom="8" length="2026862" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="wjQTre8hcYt6MS3gIXnXUpv+GnQ7tZALOAajn1jEgY7nNxk5P30v9qESknlDzLNCcOpb9iy/4aFTtv+aDqb1DQ==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.5/Meetingnotes10-7.delta" sparkle:deltaFrom="7" length="2033094" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="hUqgwwoswcKVUOA4RUs8DVriGh4BXFXZ15gV2TSaCw8C4TX83qgrDVFa6ZjmpFPO23ck5+lY4chyfAzTFHq8Bw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.5/Meetingnotes10-6.delta" sparkle:deltaFrom="6" length="2026918" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="L8Y1rn8/8Kh7mVf9WNbYWIrXJo8jNtQ0MK14qWQ+PKzImSUc8nqVZ1uWgI7cClBRaPUhpqcL8CnWGWfI4BgQBg==" />
|
|
||||||
</sparkle:deltas>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>1.0.4</title>
|
|
||||||
<pubDate>Wed, 16 Jul 2025 09:02:52 -0400</pubDate>
|
|
||||||
<sparkle:version>9</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>1.0.4</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.4/Meetingnotes-1.0.4.zip" length="9518446" type="application/octet-stream" sparkle:edSignature="aWBAf2cnvhAf1WBEa9oUHzL+N5RBph8Xbsh/XXBhR7wo9Vkeh51CqBh+3pjWJqxaDGxb3aimgydjIKrCVVpyAw==" />
|
|
||||||
<sparkle:deltas>
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.4/Meetingnotes9-8.delta" sparkle:deltaFrom="8" length="2038454" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="AUNQbGbKk3a7Phzxfm1yEQQ6KuaqvX0NASF7u657DtdkcTbxvBqPBon/JKaZIEVtlnq+qDyjfcUpTY1S1Cj0Cw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.4/Meetingnotes9-7.delta" sparkle:deltaFrom="7" length="2027690" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="+libxIpQFVA0lsctoJzmH8dNGCcHZLE8agqRKMzbCN1+fRA8Zq0hrvIAxvrpyCim1StmOtvh/zWKz+483SyJBw==" />
|
|
||||||
<enclosure url="https://github.com/owengretzinger/meetingnotes/releases/download/v1.0.4/Meetingnotes9-6.delta" sparkle:deltaFrom="6" length="2022146" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="864832" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,en" sparkle:edSignature="LLFyZQK3TLSNvD8RCpQKNmKfefg8mp9Z7J1uw09zaYhahjSa+hV1jbzkSMIEG6P6ncBScBfmOrhhuimAG6OWBw==" />
|
|
||||||
</sparkle:deltas>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -12,9 +12,13 @@
|
|||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Meetingnotes needs access to your microphone for transcription.</string>
|
<string>Meetingnotes needs access to your microphone for transcription.</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>https://raw.githubusercontent.com/superdooper86/meetingnotes/main/appcast.xml</string>
|
<string>https://github.com/superdooper86/meetingnotes/releases/latest/download/appcast.xml</string>
|
||||||
<key>SUPublicEDKey</key>
|
<key>SUPublicEDKey</key>
|
||||||
<string>BVXHOV8ZxPxKZ1swhFndymzew9nyd3si7849JA9cqsg=</string>
|
<string>9ZuN9G9ERB3Qoyyd/4FsF+6LMUv5jzAGP26OXAHBiW0=</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<true/>
|
||||||
|
<key>SUAutomaticallyUpdate</key>
|
||||||
|
<true/>
|
||||||
<key>SUEnableInstallerLauncherService</key>
|
<key>SUEnableInstallerLauncherService</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import Foundation
|
|||||||
class LocalStorageManager {
|
class LocalStorageManager {
|
||||||
static let shared = LocalStorageManager()
|
static let shared = LocalStorageManager()
|
||||||
|
|
||||||
|
struct MeetingImportResult {
|
||||||
|
let importedCount: Int
|
||||||
|
let skippedCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
private let documentsDirectory: URL
|
private let documentsDirectory: URL
|
||||||
private let meetingsDirectory: URL
|
private let meetingsDirectory: URL
|
||||||
private let templatesDirectory: URL
|
private let templatesDirectory: URL
|
||||||
@@ -47,7 +52,17 @@ class LocalStorageManager {
|
|||||||
// Write atomically using a temp file then replace
|
// Write atomically using a temp file then replace
|
||||||
let tmpURL = fileURL.appendingPathExtension("tmp")
|
let tmpURL = fileURL.appendingPathExtension("tmp")
|
||||||
try data.write(to: tmpURL, options: .atomic)
|
try data.write(to: tmpURL, options: .atomic)
|
||||||
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL, backupItemName: nil, options: [], resultingItemURL: nil)
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||||
|
try FileManager.default.replaceItem(
|
||||||
|
at: fileURL,
|
||||||
|
withItemAt: tmpURL,
|
||||||
|
backupItemName: nil,
|
||||||
|
options: [],
|
||||||
|
resultingItemURL: nil
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
try FileManager.default.moveItem(at: tmpURL, to: fileURL)
|
||||||
|
}
|
||||||
|
|
||||||
print("✅ Saved meeting: \(meeting.id)")
|
print("✅ Saved meeting: \(meeting.id)")
|
||||||
return true
|
return true
|
||||||
@@ -129,6 +144,51 @@ class LocalStorageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Imports meeting JSON files from a folder selected by the user.
|
||||||
|
func importMeetings(from directory: URL) throws -> MeetingImportResult {
|
||||||
|
let didStartAccess = directory.startAccessingSecurityScopedResource()
|
||||||
|
defer {
|
||||||
|
if didStartAccess {
|
||||||
|
directory.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
let existingIDs = Set(loadMeetings().map(\.id))
|
||||||
|
var importedIDs = Set<UUID>()
|
||||||
|
var skippedCount = 0
|
||||||
|
|
||||||
|
guard let enumerator = FileManager.default.enumerator(
|
||||||
|
at: directory,
|
||||||
|
includingPropertiesForKeys: [.isRegularFileKey],
|
||||||
|
options: [.skipsHiddenFiles, .skipsPackageDescendants]
|
||||||
|
) else {
|
||||||
|
throw CocoaError(.fileReadUnknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "json" {
|
||||||
|
do {
|
||||||
|
let values = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
|
||||||
|
guard values.isRegularFile == true else { continue }
|
||||||
|
let data = try Data(contentsOf: fileURL)
|
||||||
|
let meeting = try decoder.decode(Meeting.self, from: data)
|
||||||
|
guard meeting.dataVersion <= Meeting.currentDataVersion,
|
||||||
|
!existingIDs.contains(meeting.id),
|
||||||
|
!importedIDs.contains(meeting.id),
|
||||||
|
saveMeeting(meeting) else {
|
||||||
|
skippedCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
importedIDs.insert(meeting.id)
|
||||||
|
} catch {
|
||||||
|
skippedCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MeetingImportResult(importedCount: importedIDs.count, skippedCount: skippedCount)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Template Management
|
// MARK: - Template Management
|
||||||
|
|
||||||
/// Saves a note template to local storage
|
/// Saves a note template to local storage
|
||||||
@@ -146,7 +206,17 @@ class LocalStorageManager {
|
|||||||
// Write atomically using a temp file then replace
|
// Write atomically using a temp file then replace
|
||||||
let tmpURL = fileURL.appendingPathExtension("tmp")
|
let tmpURL = fileURL.appendingPathExtension("tmp")
|
||||||
try data.write(to: tmpURL, options: .atomic)
|
try data.write(to: tmpURL, options: .atomic)
|
||||||
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL, backupItemName: nil, options: [], resultingItemURL: nil)
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||||
|
try FileManager.default.replaceItem(
|
||||||
|
at: fileURL,
|
||||||
|
withItemAt: tmpURL,
|
||||||
|
backupItemName: nil,
|
||||||
|
options: [],
|
||||||
|
resultingItemURL: nil
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
try FileManager.default.moveItem(at: tmpURL, to: fileURL)
|
||||||
|
}
|
||||||
|
|
||||||
print("✅ Saved template: \(template.id)")
|
print("✅ Saved template: \(template.id)")
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@ObservedObject var viewModel: SettingsViewModel
|
@ObservedObject var viewModel: SettingsViewModel
|
||||||
@StateObject private var localAPIServer = LocalAPIServer.shared
|
@StateObject private var localAPIServer = LocalAPIServer.shared
|
||||||
@State private var showingTemplateManager = false
|
@State private var showingTemplateManager = false
|
||||||
@State private var confirmingTokenRegeneration = false
|
@State private var confirmingTokenRegeneration = false
|
||||||
|
@State private var showingMeetingImporter = false
|
||||||
|
@State private var meetingImportMessage = ""
|
||||||
|
@State private var showingMeetingImportResult = false
|
||||||
@Binding var navigationPath: NavigationPath
|
@Binding var navigationPath: NavigationPath
|
||||||
|
|
||||||
init(viewModel: SettingsViewModel, navigationPath: Binding<NavigationPath> = .constant(NavigationPath())) {
|
init(viewModel: SettingsViewModel, navigationPath: Binding<NavigationPath> = .constant(NavigationPath())) {
|
||||||
@@ -120,6 +124,17 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Meeting Storage")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showingMeetingImporter = true
|
||||||
|
} label: {
|
||||||
|
Label("Import Meetings...", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note Templates Section: only the Manage Templates button
|
// Note Templates Section: only the Manage Templates button
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Note Templates")
|
Text("Note Templates")
|
||||||
@@ -276,6 +291,29 @@ struct SettingsView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.saveMessage)
|
Text(viewModel.saveMessage)
|
||||||
}
|
}
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $showingMeetingImporter,
|
||||||
|
allowedContentTypes: [.folder],
|
||||||
|
allowsMultipleSelection: false
|
||||||
|
) { result in
|
||||||
|
do {
|
||||||
|
guard let directory = try result.get().first else { return }
|
||||||
|
let importResult = try LocalStorageManager.shared.importMeetings(from: directory)
|
||||||
|
meetingImportMessage = "Imported \(importResult.importedCount) meeting\(importResult.importedCount == 1 ? "" : "s")."
|
||||||
|
if importResult.skippedCount > 0 {
|
||||||
|
meetingImportMessage += " Skipped \(importResult.skippedCount) existing or invalid file\(importResult.skippedCount == 1 ? "" : "s")."
|
||||||
|
}
|
||||||
|
NotificationCenter.default.post(name: .meetingSaved, object: nil)
|
||||||
|
} catch {
|
||||||
|
meetingImportMessage = "Meeting import failed: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
showingMeetingImportResult = true
|
||||||
|
}
|
||||||
|
.alert("Meeting Import", isPresented: $showingMeetingImportResult) {
|
||||||
|
Button("OK") { }
|
||||||
|
} message: {
|
||||||
|
Text(meetingImportMessage)
|
||||||
|
}
|
||||||
.confirmationDialog("Regenerate API token?", isPresented: $confirmingTokenRegeneration, titleVisibility: .visible) {
|
.confirmationDialog("Regenerate API token?", isPresented: $confirmingTokenRegeneration, titleVisibility: .visible) {
|
||||||
Button("Regenerate", role: .destructive) {
|
Button("Regenerate", role: .destructive) {
|
||||||
viewModel.regenerateMuteDeckAPIToken()
|
viewModel.regenerateMuteDeckAPIToken()
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>group.owen.meetingnotes</string>
|
|
||||||
</array>
|
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.screen-capture</key>
|
<key>com.apple.security.device.screen-capture</key>
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPOSITORY="${REPOSITORY:-superdooper86/meetingnotes}"
|
||||||
|
|
||||||
|
if ! command -v gh >/dev/null 2>&1; then
|
||||||
|
echo "Install GitHub CLI first: brew install gh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh auth status >/dev/null
|
||||||
|
|
||||||
|
read -r -p "Developer ID certificate (.p12) path: " certificate_path
|
||||||
|
if [[ ! -f "$certificate_path" ]]; then
|
||||||
|
echo "Certificate not found: $certificate_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -s -p "Certificate export password: " certificate_password
|
||||||
|
printf '\n'
|
||||||
|
read -r -p "Apple ID email: " apple_id
|
||||||
|
read -r -p "Apple Developer Team ID: " team_id
|
||||||
|
read -r -s -p "Apple app-specific password: " app_password
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
base64 < "$certificate_path" | gh secret set APPLE_CERTIFICATE_P12 -R "$REPOSITORY"
|
||||||
|
printf '%s' "$certificate_password" | gh secret set APPLE_CERTIFICATE_PASSWORD -R "$REPOSITORY"
|
||||||
|
printf '%s' "$apple_id" | gh secret set APPLE_ID -R "$REPOSITORY"
|
||||||
|
printf '%s' "$team_id" | gh secret set APPLE_TEAM_ID -R "$REPOSITORY"
|
||||||
|
printf '%s' "$app_password" | gh secret set APPLE_APP_PASSWORD -R "$REPOSITORY"
|
||||||
|
|
||||||
|
unset certificate_password app_password
|
||||||
|
echo "Apple release secrets configured for $REPOSITORY."
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="Meetingnotes"
|
||||||
|
PROJECT="Meetingnotes.xcodeproj"
|
||||||
|
SCHEME="meetingnotes"
|
||||||
|
RUNNER_TEMP="${RUNNER_TEMP:-/tmp}"
|
||||||
|
BUILD_ROOT="${BUILD_ROOT:-$RUNNER_TEMP/meetingnotes-release}"
|
||||||
|
DERIVED_DATA="$BUILD_ROOT/DerivedData"
|
||||||
|
RELEASE_DIR="$BUILD_ROOT/release"
|
||||||
|
APP_PATH="$DERIVED_DATA/Build/Products/Release/$APP_NAME.app"
|
||||||
|
|
||||||
|
required_variables=(
|
||||||
|
VERSION
|
||||||
|
SIGNING_IDENTITY
|
||||||
|
APPLE_ID
|
||||||
|
APPLE_TEAM_ID
|
||||||
|
APPLE_APP_PASSWORD
|
||||||
|
SPARKLE_PRIVATE_KEY
|
||||||
|
GITHUB_REPOSITORY
|
||||||
|
)
|
||||||
|
|
||||||
|
for variable in "${required_variables[@]}"; do
|
||||||
|
if [[ -z "${!variable:-}" ]]; then
|
||||||
|
echo "Missing required environment variable: $variable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
project_version=$(grep -m1 'MARKETING_VERSION' "$PROJECT/project.pbxproj" | sed 's/.*= \(.*\);/\1/')
|
||||||
|
if [[ "$project_version" != "$VERSION" ]]; then
|
||||||
|
echo "Release version $VERSION does not match project version $project_version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$BUILD_ROOT"
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
|
||||||
|
xcodebuild \
|
||||||
|
-project "$PROJECT" \
|
||||||
|
-scheme "$SCHEME" \
|
||||||
|
-configuration Release \
|
||||||
|
-destination 'generic/platform=macOS' \
|
||||||
|
-derivedDataPath "$DERIVED_DATA" \
|
||||||
|
ARCHS="arm64 x86_64" \
|
||||||
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
clean build
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_PATH" ]]; then
|
||||||
|
echo "Built app not found at $APP_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_component "$SPARKLE_CONTENTS/XPCServices/Installer.xpc"
|
||||||
|
if [[ -d "$SPARKLE_CONTENTS/XPCServices/Downloader.xpc" ]]; then
|
||||||
|
codesign --force --timestamp --options runtime \
|
||||||
|
--preserve-metadata=entitlements \
|
||||||
|
--sign "$SIGNING_IDENTITY" \
|
||||||
|
"$SPARKLE_CONTENTS/XPCServices/Downloader.xpc"
|
||||||
|
fi
|
||||||
|
sign_component "$SPARKLE_CONTENTS/Autoupdate"
|
||||||
|
sign_component "$SPARKLE_CONTENTS/Updater.app"
|
||||||
|
sign_component "$SPARKLE_FRAMEWORK"
|
||||||
|
|
||||||
|
codesign --force --timestamp --options runtime \
|
||||||
|
--entitlements meetingnotes/meetingnotes.entitlements \
|
||||||
|
--sign "$SIGNING_IDENTITY" \
|
||||||
|
"$APP_PATH"
|
||||||
|
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
||||||
|
codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q 'com.apple.security.app-sandbox'
|
||||||
|
|
||||||
|
PRE_NOTARY_ZIP="$BUILD_ROOT/$APP_NAME-pre-notary.zip"
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$PRE_NOTARY_ZIP"
|
||||||
|
|
||||||
|
xcrun notarytool submit "$PRE_NOTARY_ZIP" \
|
||||||
|
--apple-id "$APPLE_ID" \
|
||||||
|
--team-id "$APPLE_TEAM_ID" \
|
||||||
|
--password "$APPLE_APP_PASSWORD" \
|
||||||
|
--wait
|
||||||
|
|
||||||
|
xcrun stapler staple "$APP_PATH"
|
||||||
|
xcrun stapler validate "$APP_PATH"
|
||||||
|
|
||||||
|
ARCHIVE_NAME="$APP_NAME-$VERSION.zip"
|
||||||
|
ARCHIVE_PATH="$RELEASE_DIR/$ARCHIVE_NAME"
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ARCHIVE_PATH"
|
||||||
|
spctl --assess --type execute --verbose=2 "$APP_PATH"
|
||||||
|
|
||||||
|
GENERATE_APPCAST=$(find "$DERIVED_DATA/SourcePackages/artifacts" -type f -name generate_appcast -print -quit)
|
||||||
|
if [[ -z "$GENERATE_APPCAST" ]]; then
|
||||||
|
echo "Sparkle generate_appcast tool was not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOWNLOAD_URL="https://github.com/$GITHUB_REPOSITORY/releases/download/v$VERSION/"
|
||||||
|
printf '%s' "$SPARKLE_PRIVATE_KEY" | "$GENERATE_APPCAST" "$RELEASE_DIR" \
|
||||||
|
--ed-key-file - \
|
||||||
|
--download-url-prefix "$DOWNLOAD_URL" \
|
||||||
|
--maximum-deltas 0 \
|
||||||
|
-o "$RELEASE_DIR/appcast.xml"
|
||||||
|
|
||||||
|
grep -q "$DOWNLOAD_URL$ARCHIVE_NAME" "$RELEASE_DIR/appcast.xml"
|
||||||
|
grep -q 'sparkle:edSignature=' "$RELEASE_DIR/appcast.xml"
|
||||||
|
|
||||||
|
echo "Release artifacts are ready in $RELEASE_DIR"
|
||||||
Reference in New Issue
Block a user