Skip to content

Commit 9a6b32f

Browse files
tukuyomil032claude
andcommitted
feat(release): refactor to 2-job workflow with version.env, skip guard, and appcast.xml auto-update
check: version validation vs version.env, skip if GitHub Release already exists release: create-dmg DMG, CURRENT_PROJECT_VERSION from BUILD_NUMBER, appcast.xml via Contents API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3a2e227 commit 9a6b32f

1 file changed

Lines changed: 137 additions & 39 deletions

File tree

.github/workflows/release.yml

Lines changed: 137 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,94 @@ on:
77
workflow_dispatch:
88
inputs:
99
version:
10-
description: 'Release tag (e.g. v0.3.0 or v0.3.0-beta.1)'
10+
description: 'Release tag (e.g. v0.3.1 or v0.3.1-beta.1)'
1111
required: true
1212
type: string
1313

1414
permissions:
1515
contents: write
1616

1717
jobs:
18+
# ─── Job 1: Skip guard + version resolution (cheap, ubuntu-latest) ────────
19+
check:
20+
runs-on: ubuntu-latest
21+
outputs:
22+
skip: ${{ steps.gate.outputs.skip }}
23+
version: ${{ steps.version.outputs.version }}
24+
base_version: ${{ steps.version.outputs.base_version }}
25+
build_number: ${{ steps.version.outputs.build_number }}
26+
tag: ${{ steps.version.outputs.tag }}
27+
is_beta: ${{ steps.version.outputs.is_beta }}
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Read version.env
33+
id: ver_env
34+
run: |
35+
set -a; source version.env; set +a
36+
echo "marketing_version=$MARKETING_VERSION" >> "$GITHUB_OUTPUT"
37+
echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT"
38+
39+
- name: Resolve and validate version
40+
id: version
41+
run: |
42+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
43+
TAG="${{ github.event.inputs.version }}"
44+
else
45+
TAG="${GITHUB_REF_NAME}"
46+
fi
47+
48+
VERSION="${TAG#v}"
49+
BASE_VERSION="${VERSION%%-*}" # strips -beta.1, -rc.1, etc.
50+
51+
IS_BETA="false"
52+
[[ "$TAG" == *"-beta"* ]] && IS_BETA="true"
53+
54+
MVER="${{ steps.ver_env.outputs.marketing_version }}"
55+
if [ "$BASE_VERSION" != "$MVER" ]; then
56+
echo "::error::Tag base version ($BASE_VERSION) ≠ version.env MARKETING_VERSION ($MVER)"
57+
echo "Update version.env (MARKETING_VERSION=$BASE_VERSION) before tagging."
58+
exit 1
59+
fi
60+
61+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
62+
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
63+
echo "build_number=${{ steps.ver_env.outputs.build_number }}" >> "$GITHUB_OUTPUT"
64+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
65+
echo "is_beta=$IS_BETA" >> "$GITHUB_OUTPUT"
66+
67+
- name: Check if release already exists
68+
id: gate
69+
env:
70+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71+
run: |
72+
TAG="${{ steps.version.outputs.tag }}"
73+
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" 2>/dev/null; then
74+
echo "Release $TAG already exists — skipping."
75+
echo "skip=true" >> "$GITHUB_OUTPUT"
76+
else
77+
echo "skip=false" >> "$GITHUB_OUTPUT"
78+
fi
79+
80+
# ─── Job 2: Build, package, publish (macos-26) ───────────────────────────
1881
release:
82+
needs: check
83+
if: needs.check.outputs.skip != 'true'
1984
runs-on: macos-26
2085
env:
21-
SCHEME: perch
22-
APP_NAME: perch
86+
SCHEME: perch
87+
APP_NAME: perch
88+
VERSION: ${{ needs.check.outputs.version }}
89+
BASE_VERSION: ${{ needs.check.outputs.base_version }}
90+
BUILD_NUMBER: ${{ needs.check.outputs.build_number }}
91+
TAG: ${{ needs.check.outputs.tag }}
92+
IS_BETA: ${{ needs.check.outputs.is_beta }}
2393

2494
steps:
2595
- uses: actions/checkout@v4
96+
with:
97+
fetch-depth: 0 # needed for GitHub Contents API push of appcast.xml
2698

2799
- name: Cache DerivedData
28100
uses: actions/cache@v4
@@ -32,27 +104,13 @@ jobs:
32104
restore-keys: |
33105
${{ runner.os }}-dd-release-
34106
35-
- name: Resolve version
36-
id: version
37-
run: |
38-
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
39-
TAG="${{ github.event.inputs.version }}"
40-
else
41-
TAG="${GITHUB_REF_NAME}"
42-
fi
43-
VERSION="${TAG#v}"
44-
BASE_VERSION="${VERSION%%-*}"
45-
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
46-
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
47-
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
48-
49107
- name: Build universal archive
50108
run: |
51109
set -o pipefail
52110
xcodebuild archive \
53111
-scheme "$SCHEME" \
54112
-configuration Release \
55-
-archivePath build/$APP_NAME.xcarchive \
113+
-archivePath "build/$APP_NAME.xcarchive" \
56114
-destination 'generic/platform=macOS' \
57115
-derivedDataPath build/DerivedData \
58116
-skipPackagePluginValidation \
@@ -63,33 +121,30 @@ jobs:
63121
CODE_SIGN_IDENTITY="-" \
64122
CODE_SIGNING_REQUIRED=NO \
65123
CODE_SIGNING_ALLOWED=NO \
66-
MARKETING_VERSION="${{ steps.version.outputs.base_version }}" \
124+
MARKETING_VERSION="$BASE_VERSION" \
125+
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
67126
SKIP_INSTALL=NO 2>&1 | tail -100
68127
69128
- name: Ad-hoc sign app bundle
70129
run: |
71130
APP="build/$APP_NAME.xcarchive/Products/Applications/$APP_NAME.app"
72-
codesign --force --deep --sign - --timestamp=none "${APP}"
73-
echo "Signed: ${APP}"
131+
codesign --force --deep --sign - --timestamp=none "$APP"
132+
echo "Signed: $APP"
133+
134+
- name: Install create-dmg
135+
run: brew install create-dmg
74136

75137
- name: Create DMG
76138
run: |
77-
VERSION="${{ steps.version.outputs.version }}"
78139
mkdir -p build/dmg
79-
STAGING="build/dmg-staging"
80-
mkdir -p "$STAGING"
81-
cp -R "build/$APP_NAME.xcarchive/Products/Applications/$APP_NAME.app" "$STAGING/$APP_NAME.app"
82-
hdiutil create \
83-
-volname "Perch" \
84-
-srcfolder "$STAGING" \
85-
-ov -format UDZO \
86-
"build/dmg/$APP_NAME-${VERSION}.dmg"
87-
echo "Created: $APP_NAME-${VERSION}.dmg"
140+
bash scripts/build-dmg.sh \
141+
"$VERSION" \
142+
"build/$APP_NAME.xcarchive/Products/Applications/$APP_NAME.app" \
143+
"build/dmg"
88144
89145
- name: Compose release body
90146
id: body
91147
run: |
92-
VERSION="${{ steps.version.outputs.version }}"
93148
{
94149
echo "body<<__BODY__"
95150
echo "## Perch ${VERSION}"
@@ -110,10 +165,53 @@ jobs:
110165
- name: Create GitHub Release
111166
uses: softprops/action-gh-release@v2
112167
with:
113-
tag_name: ${{ steps.version.outputs.tag }}
114-
name: "Perch ${{ steps.version.outputs.version }}"
115-
body: ${{ steps.body.outputs.body }}
116-
files: |
117-
build/dmg/${{ env.APP_NAME }}-${{ steps.version.outputs.version }}.dmg
118-
draft: false
119-
prerelease: ${{ contains(steps.version.outputs.tag, 'beta') }}
168+
tag_name: ${{ env.TAG }}
169+
name: "Perch ${{ env.VERSION }}"
170+
body: ${{ steps.body.outputs.body }}
171+
files: build/dmg/${{ env.APP_NAME }}-${{ env.VERSION }}.dmg
172+
draft: false
173+
prerelease: ${{ env.IS_BETA == 'true' }}
174+
175+
- name: Update appcast.xml
176+
env:
177+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178+
run: |
179+
DMG_PATH="build/dmg/${APP_NAME}-${VERSION}.dmg"
180+
DMG_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${APP_NAME}-${VERSION}.dmg"
181+
DMG_SIZE=$(stat -f%z "$DMG_PATH")
182+
183+
CHANNEL_ARG=""
184+
[[ "$IS_BETA" == "true" ]] && CHANNEL_ARG="beta"
185+
186+
# Always fetch appcast.xml from main branch (not from tag checkout)
187+
if gh api "repos/${GITHUB_REPOSITORY}/contents/appcast.xml" \
188+
--jq '.content' 2>/dev/null | base64 -d > appcast.xml; then
189+
echo "Fetched existing appcast.xml from main"
190+
else
191+
echo "appcast.xml not found on main — using template from checkout"
192+
fi
193+
194+
if [[ -n "$CHANNEL_ARG" ]]; then
195+
python3 scripts/update-appcast.py \
196+
"$VERSION" "$BUILD_NUMBER" "$DMG_URL" "$DMG_SIZE" "$CHANNEL_ARG"
197+
else
198+
python3 scripts/update-appcast.py \
199+
"$VERSION" "$BUILD_NUMBER" "$DMG_URL" "$DMG_SIZE"
200+
fi
201+
202+
# Push updated appcast.xml to main branch via GitHub Contents API
203+
SHA=$(gh api "repos/${GITHUB_REPOSITORY}/contents/appcast.xml" \
204+
--jq '.sha' 2>/dev/null || echo "")
205+
206+
ENCODED=$(base64 -i appcast.xml | tr -d '\n')
207+
jq -n \
208+
--arg message "appcast: add v${VERSION} (build ${BUILD_NUMBER})" \
209+
--arg content "$ENCODED" \
210+
--arg sha "$SHA" \
211+
'{"message": $message, "content": $content}
212+
+ (if $sha != "" then {"sha": $sha} else {} end)' \
213+
| gh api "repos/${GITHUB_REPOSITORY}/contents/appcast.xml" \
214+
--method PUT \
215+
--input -
216+
217+
echo "appcast.xml updated for v${VERSION}"

0 commit comments

Comments
 (0)