Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
push:
branches:
- master
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify-windows:
runs-on: windows-latest
timeout-minutes: 90

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: npm

- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"

- name: Setup uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
with:
enable-cache: true
cache-dependency-glob: backend/uv.lock

- name: Install frontend dependencies
run: npm ci

- name: Install locked backend dependencies
run: uv sync --project backend --locked

- name: Audit dependencies
run: npm audit

- name: Verify source
run: npm run verify

- name: Package application
run: npm run package

- name: Verify packaged layout
run: npm run verify:package
172 changes: 127 additions & 45 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,158 @@ on:
- "v*"

permissions:
contents: write
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build-windows:
runs-on: windows-latest
timeout-minutes: 90
permissions:
attestations: write
contents: write
id-token: write
env:
RELEASE_TAG: ${{ github.ref_name }}
WINDOWS_CERTIFICATE_FILE: ${{ runner.temp }}/trainkit-signing.pfx
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # zizmor: ignore[cache-poisoning] v4 caching is off when the cache input is omitted
with:
node-version: "22"
cache: "npm"

- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Setup uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
with:
enable-cache: true
enable-cache: false

- name: Validate tag and signing configuration
id: metadata
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
shell: pwsh
run: |
$version = (Get-Content package.json -Raw | ConvertFrom-Json).version
if ($env:RELEASE_TAG -ne "v$version") {
throw "Tag $env:RELEASE_TAG does not match package version v$version"
}
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_BASE64)) {
throw "WINDOWS_CERTIFICATE_BASE64 is required for release signing"
}
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)) {
throw "WINDOWS_CERTIFICATE_PASSWORD is required for release signing"
}
[IO.File]::WriteAllBytes(
$env:WINDOWS_CERTIFICATE_FILE,
[Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64)
)
"version=$version" >> $env:GITHUB_ENV
"version=$version" >> $env:GITHUB_OUTPUT

- name: Install frontend dependencies
run: npm ci

- name: Install backend dependencies
run: |
cd backend
uv sync
shell: pwsh

- name: Get version
id: get_version
run: |
$version = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$version" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: Install locked backend dependencies
run: uv sync --project backend --locked

- name: Lint
run: npm run lint
- name: Verify source
run: npm run verify

- name: Build and package
- name: Build signed Windows artifacts
run: npm run make

- name: Zip installer folder
- name: Verify packaged layout
run: npm run verify:package

- name: Verify Authenticode signatures
shell: pwsh
run: |
Compress-Archive -Path "out/make/squirrel.windows/x64/*" -DestinationPath "out/TrainKit-Windows-Installer.zip"
$targets = @(
"out/TrainKit-win32-x64/TrainKit.exe"
Get-ChildItem "out/make/squirrel.windows/x64" -Filter "*.exe" |
Select-Object -ExpandProperty FullName
)
if ($targets.Count -lt 2) {
throw "Expected the application and installer executables"
}
foreach ($target in $targets) {
$signature = Get-AuthenticodeSignature $target
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for ${target}: $($signature.Status)"
}
}

- name: Prepare release artifacts and checksums
shell: pwsh

- name: Create Release
uses: softprops/action-gh-release@v2
run: |
$installer = "out/TrainKit-Windows-Installer-$env:version.zip"
Compress-Archive -Path "out/make/squirrel.windows/x64/*" -DestinationPath $installer -Force
$portable = "out/make/zip/win32/x64/TrainKit-win32-x64-$env:version.zip"
if (!(Test-Path $portable)) {
throw "Portable archive was not produced: $portable"
}
@($installer, $portable) | ForEach-Object {
$hash = (Get-FileHash $_ -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $(Split-Path $_ -Leaf)"
} | Set-Content "out/SHA256SUMS.txt" -Encoding utf8

- name: Prepare changelog release notes
shell: pwsh
run: |
$changelog = Get-Content "CHANGELOG.md" -Raw
$escapedVersion = [regex]::Escape($env:version)
$match = [regex]::Match(
$changelog,
"(?ms)^## \[$escapedVersion\].*?(?=^## \[|\z)"
)
if (!$match.Success) {
throw "CHANGELOG.md does not contain a section for $env:version"
}
$provenance = @"
---

Windows artifacts are Authenticode-signed and accompanied by SHA-256 checksums and a GitHub build-provenance attestation.
"@
"$($match.Value.Trim())`n`n$($provenance.Trim())" |
Set-Content "out/RELEASE_NOTES.md" -Encoding utf8

- name: Attest release provenance
uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
with:
tag_name: ${{ github.ref_name }}
name: TrainKit ${{ github.ref_name }}
draft: false
prerelease: false
files: |
out/TrainKit-Windows-Installer.zip
out/make/zip/win32/x64/TrainKit-win32-x64-${{ steps.get_version.outputs.version }}.zip
body: |
## TrainKit ${{ github.ref_name }}

### Downloads
- **TrainKit-Windows-Installer.zip** - Windows installer (extract, run Setup.exe)
- **TrainKit-win32-x64-${{ steps.get_version.outputs.version }}.zip** - Portable version (extract, run TrainKit.exe)

### Requirements
- [uv](https://docs.astral.sh/uv/) must be installed for first-time setup
- Python 3.12 will be automatically downloaded during setup
subject-path: |
out/TrainKit-Windows-Installer-${{ steps.metadata.outputs.version }}.zip
out/make/zip/win32/x64/TrainKit-win32-x64-${{ steps.metadata.outputs.version }}.zip
out/SHA256SUMS.txt

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
shell: pwsh
run: |
gh release create $env:RELEASE_TAG `
"out/TrainKit-Windows-Installer-$env:version.zip" `
"out/make/zip/win32/x64/TrainKit-win32-x64-$env:version.zip" `
"out/SHA256SUMS.txt" `
--verify-tag `
--title "TrainKit $env:RELEASE_TAG" `
--notes-file "out/RELEASE_NOTES.md"

- name: Remove signing certificate
if: always()
shell: pwsh
run: Remove-Item -LiteralPath $env:WINDOWS_CERTIFICATE_FILE -Force -ErrorAction SilentlyContinue
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ out/

# Python virtual environments
backend/.venv/
backend/.python/
backend/.tmp/
backend/.model-cache/
backend/.setup_complete.json
backend/.pytest_cache/
backend/.ruff_cache/
.venv/
__pycache__/
*.py[cod]
Expand Down
Loading
Loading