Repository navigation
193 lines (178 loc) · 8.31 KB
/
Copy pathrelease.yml
File metadata and controls
193 lines (178 loc) · 8.31 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
# Every push to main ships: build the NSIS installer and publish it as a GitHub
# Release named after package.json's version, with generated notes from the
# merged PRs. Existing versions are immutable: a release requires a NEW version
# in package.json (bump it inside the PR), and CI refuses to overwrite one.
#
# The bundled tools (ImageMagick, CaesiumCLT, 7-Zip, ffmpeg, mutool,
# LibreOffice, Ghostscript, Real-ESRGAN) are not in git, and a runner has none
# of the local installs fetch-binaries copies from. `--pinned` stages the same
# versions from official downloads, each checked against a pinned SHA-256
# (scripts/pinned-tools.mjs), and verify-bundle fails the build if any tool is
# missing or does not run, both before and after electron-builder packs it.
#
# Pull requests that touch the release machinery, and manual dispatches, run a
# DRY RUN: everything up to and including the verified installer, uploaded as a
# workflow artifact, but never published.
#
# Gates: typecheck, lint, prettier, unit tests. The e2e suite stays the local
# pre-PR gate (it launches the built app with real tools, CLAUDE.md).
#
# Unsigned: no certificate is configured.
name: release
on:
push:
branches: [main]
# Docs and licence text cannot change the installer. Without this a docs
# push would fail on "Require a new version", or, bumped, publish an
# identical installer under a new name for nothing.
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.github/ISSUE_TEMPLATE/**'
# Tests cannot change the installer either.
- 'test/**'
- 'e2e/**'
pull_request:
paths:
- '.github/workflows/release.yml'
- 'scripts/fetch-binaries.mjs'
- 'scripts/pinned-tools.mjs'
- 'scripts/verify-bundle.mjs'
- 'electron-builder.yml'
- 'package.json'
- 'src/cli/**'
- 'resources/cli/**'
- 'resources/skill/**'
- 'build/installer.nsh'
- 'build/installer/**'
workflow_dispatch:
concurrency:
# Releases from main queue rather than race; a PR's newer push replaces its
# older dry run.
group: release-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
release:
runs-on: windows-latest
timeout-minutes: 75
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npx prettier --check .
- run: npm test
- name: Read version
id: ver
shell: pwsh
run: |
$v = (Get-Content package.json -Raw | ConvertFrom-Json).version
if (-not $v) { throw 'could not read version from package.json' }
"version=$v" >> $env:GITHUB_OUTPUT
# Runs on dry runs too, so a PR that forgot the bump fails here, not
# after it is merged.
- name: Require a new version
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
# Actions runs pwsh with $ErrorActionPreference='Stop', and PowerShell
# 7.4 can turn a non-zero NATIVE exit into a throw. `gh release view`
# exits 1 when the tag does not exist, which is the expected path, so
# the exit code is checked by hand.
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$v = '${{ steps.ver.outputs.version }}'
gh release view "v$v" *> $null
if ($LASTEXITCODE -eq 0) { throw "Release v$v already exists. Bump package.json in the PR." }
Write-Host "v$v is new"
$global:LASTEXITCODE = 0
- name: Fetch pinned tools
run: node scripts/fetch-binaries.mjs --pinned
- name: Verify bundled tools (resources/)
run: node scripts/verify-bundle.mjs resources --manifest dist/resources-manifest.txt
# --publish never: on CI electron-builder tries to publish ITSELF and
# fails wanting a token; the Publish step below owns publishing.
- run: npm run build
- run: npx electron-builder --win --publish never
# electron-builder only WARNS when an extraResources source is missing, so
# check what actually got packed.
- name: Verify bundled tools (packed app)
run: node scripts/verify-bundle.mjs dist/win-unpacked/resources --manifest dist/packed-manifest.txt
# The command line in the packed app (spec 7.3): the shim must run the
# app's own exe in Node mode, print the package.json version, pass
# doctor's core checks and convert one file. This also fails loudly if the
# runAsNode Electron fuse is ever turned off.
- name: Smoke-test the packed CLI
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
$shim = 'dist\win-unpacked\resources\cli\filesmith.cmd'
$v = (& $shim --version | Out-String).Trim()
if ($v -ne '${{ steps.ver.outputs.version }}') { throw "filesmith --version printed '$v'" }
$env:FILESMITH_USER_DATA = Join-Path $env:RUNNER_TEMP 'fs-ud'
$events = & $shim doctor --json | ForEach-Object { $_ | ConvertFrom-Json }
$bad = @($events | Where-Object { $_.event -eq 'check' -and $_.group -eq 'core' -and $_.status -eq 'fail' })
if ($bad.Count) { throw "doctor core checks failed: $($bad.id -join ', ')" }
$png = Join-Path $env:RUNNER_TEMP 'smoke.png'
[IO.File]::WriteAllBytes($png, [Convert]::FromBase64String('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='))
& $shim convert $png --to webp --json | Out-Host
if ($LASTEXITCODE -ne 0) { throw "filesmith convert exited $LASTEXITCODE" }
if (-not (Test-Path (Join-Path $env:RUNNER_TEMP 'smoke.webp'))) { throw 'smoke.webp was not written' }
"- packed CLI ``$v``: doctor core checks ok, convert ok" >> $env:GITHUB_STEP_SUMMARY
$global:LASTEXITCODE = 0
- name: Check installer
id: exe
shell: pwsh
run: |
$v = '${{ steps.ver.outputs.version }}'
$exe = "dist/Filesmith-Setup-x64-$v.exe"
if (-not (Test-Path $exe)) { throw "installer not found: $exe" }
$item = Get-Item $exe
$sha = (Get-FileHash $exe -Algorithm SHA256).Hash
$mb = [math]::Round($item.Length / 1MB, 1)
"path=$exe" >> $env:GITHUB_OUTPUT
"## Filesmith $v installer" >> $env:GITHUB_STEP_SUMMARY
"" >> $env:GITHUB_STEP_SUMMARY
"- ``$($item.Name)``: $mb MB ($($item.Length) bytes)" >> $env:GITHUB_STEP_SUMMARY
"- sha256 ``$sha``" >> $env:GITHUB_STEP_SUMMARY
"- unsigned (no certificate configured)" >> $env:GITHUB_STEP_SUMMARY
"- event ``${{ github.event_name }}``" >> $env:GITHUB_STEP_SUMMARY
- name: Upload dry-run installer
if: github.event_name != 'push'
uses: actions/upload-artifact@v4
with:
name: Filesmith-Setup-x64-${{ steps.ver.outputs.version }}-dryrun
path: |
${{ steps.exe.outputs.path }}
dist/resources-manifest.txt
dist/packed-manifest.txt
retention-days: 7
# The installer is already LZMA-compressed.
compression-level: 0
- name: Publish
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$v = '${{ steps.ver.outputs.version }}'
$exe = '${{ steps.exe.outputs.path }}'
# A stable-named copy of the same installer, so
# https://github.com/<repo>/releases/latest/download/Filesmith-Setup-x64.exe
# always serves the newest release. The versioned name stays primary.
$stable = 'dist/Filesmith-Setup-x64.exe'
Copy-Item $exe $stable -Force
gh release create "v$v" $exe $stable --title "Filesmith $v" --generate-notes --latest
if ($LASTEXITCODE -ne 0) { throw 'Release publication failed' }
"- published ``v$v``" >> $env:GITHUB_STEP_SUMMARY