Skip to content

docs: user and developer docs, repo cleanup (#45) #34

docs: user and developer docs, repo cleanup (#45)

docs: user and developer docs, repo cleanup (#45) #34

Workflow file for this run

# 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