-
Notifications
You must be signed in to change notification settings - Fork 128
217 lines (197 loc) · 8.24 KB
/
Copy pathrelease.yml
File metadata and controls
217 lines (197 loc) · 8.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
name: Build desktop releases
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
release_tag:
description: 'Existing version tag to publish (leave empty for a build-only run)'
required: false
type: string
permissions:
contents: write
concurrency:
group: release-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false
jobs:
prepare:
name: Prepare release
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
tag: ${{ steps.target.outputs.tag }}
source_sha: ${{ steps.target.outputs.source_sha }}
publish: ${{ steps.target.outputs.publish }}
steps:
- name: Check out source
uses: actions/checkout@v4
with:
ref: ${{ inputs.release_tag || github.ref }}
- name: Resolve and validate release target
id: target
env:
TAG_NAME: ${{ inputs.release_tag || (startsWith(github.ref, 'refs/tags/') && github.ref_name) || '' }}
run: |
node --input-type=module <<'NODE'
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { join } from 'node:path'
const tag = process.env.TAG_NAME
const sha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
if (tag) {
const { version } = JSON.parse(readFileSync('package.json', 'utf8'))
if (!/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(tag) || tag !== `v${version}`) {
throw new Error(`Release tag ${tag} does not match package version ${version}`)
}
const tagSha = execFileSync('git', ['rev-parse', `refs/tags/${tag}^{commit}`], { encoding: 'utf8' }).trim()
if (tagSha !== sha) throw new Error('The checked-out commit does not match the release tag')
const lines = readFileSync('CHANGELOG.md', 'utf8').split(/\r?\n/)
const start = lines.indexOf(`## ${tag}`)
if (start < 0) throw new Error(`No changelog entry found for ${tag}`)
const rest = lines.slice(start + 1)
const end = rest.findIndex(line => line.startsWith('## '))
const notes = rest.slice(0, end < 0 ? undefined : end).join('\n').trim()
if (!notes) throw new Error(`Empty changelog entry for ${tag}`)
writeFileSync(join(process.env.RUNNER_TEMP, 'release-notes.md'), `${notes}\n`)
}
appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\nsource_sha=${sha}\npublish=${Boolean(tag)}\n`)
NODE
- name: Prepare draft Release
if: steps.target.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ steps.target.outputs.tag }}
run: |
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
gh release edit "$TAG_NAME" --title "$TAG_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
else
gh release create "$TAG_NAME" --verify-tag --draft \
--title "$TAG_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
fi
build:
name: Build ${{ matrix.name }}
needs: prepare
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: Windows
id: windows
platform: windows
runner: windows-latest
arch: x64
artifact_path: dist/*.exe
- name: Ubuntu
id: linux
platform: linux
runner: ubuntu-latest
arch: x64
artifact_path: dist/*.AppImage
- name: macOS Apple Silicon
id: macos-arm64
platform: macos
runner: macos-15
arch: arm64
artifact_path: |
dist/*-arm64-mac.dmg
dist/*-arm64-mac.zip
- name: macOS Intel
id: macos-x64
platform: macos
runner: macos-15-intel
arch: x64
artifact_path: |
dist/*-x64-mac.dmg
dist/*-x64-mac.zip
steps:
- name: Check out source
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.source_sha }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
# Keep CI and packaging on a runtime that provides node:sqlite.
node-version: 24.16.0
cache: npm
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Build application
run: npm run dist:ci -- --${{ matrix.arch }}
env:
# Unsigned Windows and macOS artifacts are intentional for this release flow.
CSC_IDENTITY_AUTO_DISCOVERY: false
- name: Upload platform assets to Release
if: needs.prepare.outputs.publish == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ needs.prepare.outputs.tag }}
BUILD_PLATFORM: ${{ matrix.platform }}
BUILD_ARCH: ${{ matrix.arch }}
run: |
node --input-type=module <<'NODE'
import { execFileSync } from 'node:child_process'
import { ARTIFACTS, collectArtifacts } from './scripts/upload-client-release.mjs'
const { TAG_NAME: tag, BUILD_PLATFORM: platform, BUILD_ARCH: architecture } = process.env
const { found } = await collectArtifacts('dist', false, tag.slice(1))
const expected = ARTIFACTS.filter(item => item.platform === platform && item.architecture === architecture)
const assets = found.filter(item => item.platform === platform && item.architecture === architecture)
if (expected.length === 0 || assets.length !== expected.length) {
throw new Error(`Missing release assets for ${platform}/${architecture}`)
}
execFileSync('gh', ['release', 'upload', tag, ...assets.map(item => item.path), '--clobber'], { stdio: 'inherit' })
NODE
- name: Upload build-only artifacts
if: needs.prepare.outputs.publish != 'true'
uses: actions/upload-artifact@v4
with:
name: nextcowork-${{ matrix.id }}
path: ${{ matrix.artifact_path }}
if-no-files-found: error
retention-days: 1
publish:
name: Publish GitHub Release
if: needs.prepare.outputs.publish == 'true'
needs: [prepare, build]
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ needs.prepare.outputs.tag }}
steps:
- name: Check out release tag
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.source_sha }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24.16.0
- name: Download platform artifacts
run: gh release download "$TAG_NAME" --dir release-assets --pattern '*.exe' --pattern '*.AppImage' --pattern '*-mac.dmg' --pattern '*-mac.zip'
- name: Verify client artifacts before publishing
run: node scripts/upload-client-release.mjs --dir release-assets --version "${TAG_NAME#v}" --require-all --dry-run
- name: Publish complete Release
shell: bash
run: |
set -euo pipefail
awk -v tag="$TAG_NAME" '
$0 == "## " tag { found = 1; next }
found && /^## / { exit }
found { print }
' CHANGELOG.md > "$RUNNER_TEMP/release-notes.md"
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/release-notes.md"; then
echo "No changelog entry found for $TAG_NAME" >&2
exit 1
fi
gh release edit "$TAG_NAME" --draft=false --title "$TAG_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
- name: Upload client artifacts to NextCoWork API
env:
CLIENT_UPLOAD_TOKEN: ${{ secrets.CLIENT_UPLOAD_TOKEN }}
CLIENT_UPDATE_BASE_URL: https://nextco.work
# The API stores notes from an HTTP header, which cannot carry multiline Chinese text.
run: node scripts/upload-client-release.mjs --dir release-assets --version "${TAG_NAME#v}" --base-url "$CLIENT_UPDATE_BASE_URL" --notes "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG_NAME" --require-all --ignore-duplicates