Skip to content

Commit 027c3da

Browse files
committed
Add GitHub release and Pages setup
1 parent 5da4be4 commit 027c3da

6 files changed

Lines changed: 369 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Release macOS DMG
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: "Release tag, for example v0.1.0"
11+
required: true
12+
default: "v0.1.0"
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
build-release:
19+
name: Build and publish DMG
20+
runs-on: macos-14
21+
22+
env:
23+
APP_NAME: stayawake
24+
25+
steps:
26+
- name: Check out repository
27+
uses: actions/checkout@v4
28+
29+
- name: Resolve release version
30+
id: version
31+
shell: bash
32+
run: |
33+
set -euo pipefail
34+
35+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
36+
VERSION_TAG="${{ inputs.version }}"
37+
else
38+
VERSION_TAG="${GITHUB_REF_NAME}"
39+
fi
40+
41+
if [[ ! "$VERSION_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
42+
echo "Version tag must look like v0.1.0, got: $VERSION_TAG" >&2
43+
exit 1
44+
fi
45+
46+
APP_VERSION="${VERSION_TAG#v}"
47+
echo "tag=$VERSION_TAG" >> "$GITHUB_OUTPUT"
48+
echo "app_version=$APP_VERSION" >> "$GITHUB_OUTPUT"
49+
echo "dmg_name=${APP_NAME}-${APP_VERSION}-macos.dmg" >> "$GITHUB_OUTPUT"
50+
51+
- name: Verify app version
52+
shell: bash
53+
run: |
54+
set -euo pipefail
55+
APP_VERSION="${{ steps.version.outputs.app_version }}"
56+
SCRIPT_VERSION="$(awk '
57+
/<key>CFBundleShortVersionString<\\/key>/ { getline; gsub(/.*<string>|<\\/string>.*/, ""); print; exit }
58+
' build-app.sh)"
59+
60+
if [ "$SCRIPT_VERSION" != "$APP_VERSION" ]; then
61+
echo "build-app.sh version is $SCRIPT_VERSION, but release tag is v$APP_VERSION" >&2
62+
exit 1
63+
fi
64+
65+
- name: Show toolchain
66+
run: |
67+
swift --version
68+
xcodebuild -version
69+
70+
- name: Import Developer ID certificate
71+
if: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 != '' && secrets.APPLE_CERTIFICATE_PASSWORD != '' }}
72+
env:
73+
APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
74+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
75+
KEYCHAIN_PASSWORD: ${{ github.run_id }}-${{ github.run_attempt }}
76+
shell: bash
77+
run: |
78+
set -euo pipefail
79+
80+
CERTIFICATE_PATH="$RUNNER_TEMP/developer-id.p12"
81+
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
82+
83+
echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
84+
85+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
86+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
87+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
88+
security import "$CERTIFICATE_PATH" \
89+
-P "$APPLE_CERTIFICATE_PASSWORD" \
90+
-A \
91+
-t cert \
92+
-f pkcs12 \
93+
-k "$KEYCHAIN_PATH"
94+
security list-keychains -d user -s "$KEYCHAIN_PATH"
95+
security set-key-partition-list \
96+
-S apple-tool:,apple: \
97+
-s \
98+
-k "$KEYCHAIN_PASSWORD" \
99+
"$KEYCHAIN_PATH"
100+
101+
CODESIGN_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)"
102+
if [ -z "$CODESIGN_IDENTITY" ]; then
103+
echo "No Developer ID Application identity found in the imported certificate." >&2
104+
security find-identity -v -p codesigning "$KEYCHAIN_PATH" >&2
105+
exit 1
106+
fi
107+
108+
echo "CODESIGN_IDENTITY=$CODESIGN_IDENTITY" >> "$GITHUB_ENV"
109+
echo "IS_SIGNED=true" >> "$GITHUB_ENV"
110+
111+
- name: Run tests
112+
run: swift test
113+
114+
- name: Build app bundle
115+
run: ./build-app.sh
116+
117+
- name: Sign app bundle
118+
if: ${{ env.CODESIGN_IDENTITY != '' }}
119+
shell: bash
120+
run: |
121+
set -euo pipefail
122+
123+
codesign \
124+
--force \
125+
--deep \
126+
--options runtime \
127+
--timestamp \
128+
--sign "$CODESIGN_IDENTITY" \
129+
"build/${APP_NAME}.app"
130+
131+
codesign --verify --deep --strict --verbose=2 "build/${APP_NAME}.app"
132+
133+
- name: Create DMG
134+
id: dmg
135+
shell: bash
136+
run: |
137+
set -euo pipefail
138+
139+
APP_VERSION="${{ steps.version.outputs.app_version }}"
140+
DMG_NAME="${{ steps.version.outputs.dmg_name }}"
141+
DMG_PATH="$PWD/build/$DMG_NAME"
142+
DMG_ROOT="$PWD/build/dmg-root"
143+
144+
rm -rf "$DMG_ROOT" "$DMG_PATH"
145+
mkdir -p "$DMG_ROOT"
146+
cp -R "build/${APP_NAME}.app" "$DMG_ROOT/${APP_NAME}.app"
147+
ln -s /Applications "$DMG_ROOT/Applications"
148+
149+
hdiutil create \
150+
-volname "${APP_NAME} ${APP_VERSION}" \
151+
-srcfolder "$DMG_ROOT" \
152+
-ov \
153+
-format UDZO \
154+
"$DMG_PATH"
155+
156+
echo "path=$DMG_PATH" >> "$GITHUB_OUTPUT"
157+
158+
- name: Sign DMG
159+
if: ${{ env.CODESIGN_IDENTITY != '' }}
160+
shell: bash
161+
run: |
162+
set -euo pipefail
163+
164+
codesign \
165+
--force \
166+
--timestamp \
167+
--sign "$CODESIGN_IDENTITY" \
168+
"${{ steps.dmg.outputs.path }}"
169+
170+
codesign --verify --verbose=2 "${{ steps.dmg.outputs.path }}"
171+
172+
- name: Notarize and staple DMG
173+
if: ${{ env.CODESIGN_IDENTITY != '' && secrets.APPLE_ID != '' && secrets.APPLE_APP_SPECIFIC_PASSWORD != '' && secrets.APPLE_TEAM_ID != '' }}
174+
env:
175+
APPLE_ID: ${{ secrets.APPLE_ID }}
176+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
177+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
178+
shell: bash
179+
run: |
180+
set -euo pipefail
181+
182+
xcrun notarytool submit "${{ steps.dmg.outputs.path }}" \
183+
--apple-id "$APPLE_ID" \
184+
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
185+
--team-id "$APPLE_TEAM_ID" \
186+
--wait
187+
188+
xcrun stapler staple "${{ steps.dmg.outputs.path }}"
189+
xcrun stapler validate "${{ steps.dmg.outputs.path }}"
190+
spctl --assess --type open --context context:primary-signature --verbose "${{ steps.dmg.outputs.path }}"
191+
192+
echo "IS_NOTARIZED=true" >> "$GITHUB_ENV"
193+
194+
- name: Write SHA-256 checksum
195+
id: checksum
196+
shell: bash
197+
run: |
198+
set -euo pipefail
199+
200+
SHA256_PATH="${{ steps.dmg.outputs.path }}.sha256"
201+
shasum -a 256 "${{ steps.dmg.outputs.path }}" > "$SHA256_PATH"
202+
echo "sha256_path=$SHA256_PATH" >> "$GITHUB_OUTPUT"
203+
204+
- name: Upload workflow artifact
205+
uses: actions/upload-artifact@v4
206+
with:
207+
name: ${{ steps.version.outputs.dmg_name }}
208+
path: |
209+
${{ steps.dmg.outputs.path }}
210+
${{ steps.checksum.outputs.sha256_path }}
211+
212+
- name: Publish GitHub Release
213+
env:
214+
GH_TOKEN: ${{ github.token }}
215+
VERSION_TAG: ${{ steps.version.outputs.tag }}
216+
APP_VERSION: ${{ steps.version.outputs.app_version }}
217+
DMG_PATH: ${{ steps.dmg.outputs.path }}
218+
SHA256_PATH: ${{ steps.checksum.outputs.sha256_path }}
219+
shell: bash
220+
run: |
221+
set -euo pipefail
222+
223+
SIGNING_NOTE="Unsigned DMG. macOS Gatekeeper may warn until you sign and notarize a release."
224+
if [ "${IS_NOTARIZED:-false}" = "true" ]; then
225+
SIGNING_NOTE="Signed with Developer ID and notarized by Apple."
226+
elif [ "${IS_SIGNED:-false}" = "true" ]; then
227+
SIGNING_NOTE="Signed with Developer ID, but notarization secrets were not configured."
228+
fi
229+
230+
RELEASE_NOTES="$(mktemp)"
231+
cat > "$RELEASE_NOTES" <<EOF
232+
stayawake ${APP_VERSION}
233+
234+
macOS menu-bar app packaged as a DMG.
235+
236+
${SIGNING_NOTE}
237+
238+
Install:
239+
1. Download the DMG.
240+
2. Open it.
241+
3. Drag stayawake.app into Applications.
242+
243+
SHA-256 checksum is attached as a separate .sha256 file.
244+
EOF
245+
246+
if gh release view "$VERSION_TAG" >/dev/null 2>&1; then
247+
gh release upload "$VERSION_TAG" "$DMG_PATH" "$SHA256_PATH" --clobber
248+
else
249+
gh release create "$VERSION_TAG" \
250+
"$DMG_PATH" \
251+
"$SHA256_PATH" \
252+
--title "$VERSION_TAG" \
253+
--notes-file "$RELEASE_NOTES"
254+
fi

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ Screenshots are kept out of the main README so the project page stays compact.
2020

2121
See [Screenshots](docs/SCREENSHOTS.md) for the menu, settings tabs, and logs window.
2222

23+
## GitHub Pages
24+
25+
The project site lives in `docs/` and can be published with GitHub Pages.
26+
27+
To enable it after pushing the repository:
28+
29+
1. Open the repository on GitHub.
30+
2. Go to **Settings** -> **Pages**.
31+
3. Set **Source** to **Deploy from a branch**.
32+
4. Select the default branch and the `/docs` folder.
33+
5. Save the setting.
34+
35+
The landing page is `docs/index.md`; screenshots are linked from `docs/SCREENSHOTS.md`.
36+
2337
## Features
2438

2539
- Automatic sleep control with no manual toggle required for normal use
@@ -141,6 +155,9 @@ To add another language:
141155
.
142156
├── Package.swift
143157
├── build-app.sh
158+
├── docs/_config.yml
159+
├── docs/index.md
160+
├── docs/SCREENSHOTS.md
144161
├── docs/assets
145162
├── docs/screenshots
146163
├── Sources/stayawake
@@ -176,6 +193,39 @@ The test suite covers policy decisions, signal derivation, log deduplication, bl
176193

177194
This repository builds a local, unsigned `.app` bundle. If you plan to distribute binaries, sign and notarize the app with your Apple Developer account.
178195

196+
GitHub Actions can build and publish a DMG automatically:
197+
198+
```bash
199+
git tag v0.1.0
200+
git push origin v0.1.0
201+
```
202+
203+
The release workflow runs tests, builds `stayawake.app`, packages `stayawake-0.1.0-macos.dmg`, writes a SHA-256 checksum, and uploads both files to the GitHub Release. You can also run **Release macOS DMG** manually from the GitHub Actions tab with `v0.1.0`.
204+
205+
### Signing and notarization
206+
207+
For public distribution outside the Mac App Store, configure these GitHub Actions repository secrets before creating the release tag:
208+
209+
| Secret | Value |
210+
| --- | --- |
211+
| `APPLE_CERTIFICATE_P12_BASE64` | Base64-encoded `Developer ID Application` `.p12` certificate |
212+
| `APPLE_CERTIFICATE_PASSWORD` | Password used when exporting the `.p12` certificate |
213+
| `APPLE_ID` | Apple ID email used for notarization |
214+
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password for that Apple ID |
215+
| `APPLE_TEAM_ID` | Apple Developer Team ID |
216+
217+
On your Mac, export the certificate from Keychain Access, then encode it:
218+
219+
```bash
220+
base64 -i DeveloperIDApplication.p12 | pbcopy
221+
```
222+
223+
Paste the copied value into `APPLE_CERTIFICATE_P12_BASE64`.
224+
225+
When these secrets are present, the workflow signs the app, signs the DMG, submits the DMG to Apple notarization, staples the notarization ticket, and then uploads the final DMG. Without these secrets, the workflow still builds an unsigned DMG.
226+
227+
This workflow uses Developer ID distribution. Mac App Store distribution is a separate path and requires App Sandbox, App Store signing, provisioning, and App Store Connect submission.
228+
179229
Before publishing your own fork, review:
180230

181231
- `CFBundleIdentifier` in `build-app.sh`

docs/SCREENSHOTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
---
2+
layout: default
3+
title: Screenshots
4+
---
5+
16
# Screenshots
27

8+
[Back to stayawake](./)
9+
310
## Menu Bar
411

512
The menu opens directly to the current state, reason, next check timing, and the latest 5 meaningful logs.

docs/_config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
title: stayawake
2+
description: Lightweight automatic sleep control for macOS.
3+
theme: jekyll-theme-cayman
4+
show_downloads: false

docs/assets/app-icon.png

23.6 KB
Loading

docs/index.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
layout: default
3+
title: stayawake
4+
---
5+
6+
<p align="center">
7+
<img src="assets/app-icon.png" width="128" alt="stayawake app icon">
8+
</p>
9+
10+
# stayawake
11+
12+
Lightweight automatic sleep control for macOS.
13+
14+
stayawake keeps your Mac awake during long-running work and lets macOS sleep normally when useful activity is gone.
15+
16+
<p>
17+
<a href="{{ site.github.repository_url }}/releases/latest">Download latest DMG</a>
18+
·
19+
<a href="SCREENSHOTS.html">Screenshots</a>
20+
·
21+
<a href="{{ site.github.repository_url }}">Source code</a>
22+
</p>
23+
24+
![Awake and Sleep status icons](assets/status-preview.png)
25+
26+
## What It Handles
27+
28+
- Builds, downloads, renders, scripts, and other long-running tasks
29+
- CPU, network, disk, audio, fullscreen, foreground-app, and idle signals
30+
- Manual keep-awake for 30 minutes, 1 hour, or until turned off
31+
- Recent decision logs directly from the menu
32+
- English and Simplified Chinese
33+
34+
## Status
35+
36+
The menu-bar icon changes with the current decision.
37+
38+
- Awake: stayawake is actively keeping the Mac awake.
39+
- Sleep: macOS is allowed to sleep normally.
40+
41+
## Privacy
42+
43+
stayawake runs locally. It does not require an account, cloud service, telemetry, Accessibility permission, or content upload.
44+
45+
## Requirements
46+
47+
- macOS 13 or later
48+
- Apple silicon or Intel Mac
49+
50+
## More
51+
52+
- [Screenshots](SCREENSHOTS.html)
53+
- [Release downloads]({{ site.github.repository_url }}/releases/latest)
54+
- [README]({{ site.github.repository_url }}#readme)

0 commit comments

Comments
 (0)