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
14 changes: 11 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ jobs:
build-and-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
dotnet-version: 10.0.x
persist-credentials: false
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Verify release scripts
shell: pwsh
run: |
./scripts/Test-Publish-ReleaseHardening.ps1
./scripts/Test-ReleaseMechanics.ps1
./scripts/Test-ReleaseArtifactVerifier.ps1
- name: Restore locked dependencies
run: dotnet restore WinBulkTranscript.sln --locked-mode
- name: Build
Expand Down
196 changes: 196 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: Release

on:
push:
tags: ['v*']

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

permissions:
contents: read

env:
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
DOTNET_NOLOGO: '1'

jobs:
validate-tag:
runs-on: windows-latest
outputs:
version: ${{ steps.tag.outputs.version }}
prerelease: ${{ steps.tag.outputs.prerelease }}
steps:
- name: Check out the tagged source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Parse and validate tag
id: tag
shell: pwsh
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
$pattern = '^v(?<version>(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?)$'
$tagMatch = [regex]::Match($env:RELEASE_TAG, $pattern)
if (-not $tagMatch.Success) {
throw "Release tag '$env:RELEASE_TAG' must be canonical SemVer prefixed by v, for example v1.2.3 or v1.2.3-rc.1."
}

$version = $tagMatch.Groups['version'].Value
"version=$version" >> $env:GITHUB_OUTPUT
"prerelease=$($version.Contains('-').ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT

- name: Require the tagged commit on the default branch
shell: pwsh
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
git merge-base --is-ancestor HEAD "origin/$env:DEFAULT_BRANCH"
if ($LASTEXITCODE -ne 0) {
throw "Release tag must point to a commit reachable from origin/$env:DEFAULT_BRANCH."
}

build:
name: Build win-${{ matrix.architecture }}
needs: validate-tag
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [x64, arm64]
steps:
- name: Check out the tagged source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Set up the pinned .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json

- name: Verify release scripts
shell: pwsh
run: |
./scripts/Test-Publish-ReleaseHardening.ps1
./scripts/Test-ReleaseMechanics.ps1
./scripts/Test-ReleaseArtifactVerifier.ps1

- name: Restore locked dependencies
run: dotnet restore WinBulkTranscript.sln --locked-mode

- name: Build
run: dotnet build WinBulkTranscript.sln --configuration Release --no-restore

- name: Run deterministic Core tests
run: dotnet test tests/WinBulkTranscript.Core.Tests/WinBulkTranscript.Core.Tests.csproj --configuration Release --no-build --logger "console;verbosity=normal"

- name: Build release artifact from committed evidence
shell: pwsh
env:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_ARCHITECTURE: ${{ matrix.architecture }}
run: |
$artifactsRoot = Join-Path $env:RUNNER_TEMP 'release-artifacts'
./scripts/Invoke-TagRelease.ps1 `
-Tag $env:RELEASE_TAG `
-Architecture $env:RELEASE_ARCHITECTURE `
-ArtifactsRoot $artifactsRoot

- name: Stage immutable release files
shell: pwsh
env:
RELEASE_VERSION: ${{ needs.validate-tag.outputs.version }}
RELEASE_ARCHITECTURE: ${{ matrix.architecture }}
run: |
$artifactName = "WinBulkTranscript-$env:RELEASE_VERSION-win-$env:RELEASE_ARCHITECTURE"
$sourceRoot = Join-Path $env:RUNNER_TEMP 'release-artifacts'
$stagingRoot = Join-Path $env:RUNNER_TEMP 'release-staging'
[IO.Directory]::CreateDirectory($stagingRoot) | Out-Null

foreach ($suffix in @('.zip', '.zip.sha256', '.release-record.json')) {
$sourcePath = Join-Path $sourceRoot ($artifactName + $suffix)
if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
throw "Expected release file was not created: $sourcePath"
}

[IO.File]::Copy($sourcePath, (Join-Path $stagingRoot ([IO.Path]::GetFileName($sourcePath))), $false)
}

- name: Upload architecture release files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-win-${{ matrix.architecture }}
path: ${{ runner.temp }}/release-staging/*
if-no-files-found: error
retention-days: 7
compression-level: 0

publish:
name: Publish reviewed draft
needs: [validate-tag, build]
runs-on: windows-latest
environment: release
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
steps:
- name: Check out the tagged source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Download architecture release files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-win-*
path: ${{ runner.temp }}/release-assets
merge-multiple: true

- name: Verify assembled release and create combined checksums
shell: pwsh
env:
RELEASE_VERSION: ${{ needs.validate-tag.outputs.version }}
run: |
$repositoryRevision = (& git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $repositoryRevision -notmatch '^[0-9a-fA-F]{40}$') {
throw 'Could not resolve the checked-out release commit.'
}

./scripts/Test-ReleaseArtifacts.ps1 `
-ArtifactsRoot (Join-Path $env:RUNNER_TEMP 'release-assets') `
-Version $env:RELEASE_VERSION `
-RepositoryRevision $repositoryRevision `
-ChecksumManifestPath (Join-Path $env:RUNNER_TEMP 'release-assets\SHA256SUMS.txt')

- name: Attest release assets
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ runner.temp }}/release-assets/*

- name: Create draft GitHub Release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
IS_PRERELEASE: ${{ needs.validate-tag.outputs.prerelease }}
run: |
$releaseOptions = @(
'--verify-tag',
'--draft',
'--title', "WinBulkTranscript $env:RELEASE_TAG",
'--notes-file', (Join-Path $env:GITHUB_WORKSPACE "release-inputs\$env:RELEASE_TAG\release-notes.md")
)
if ($env:IS_PRERELEASE -eq 'true') {
$releaseOptions += '--prerelease'
}

gh release create $env:RELEASE_TAG (Join-Path $env:RUNNER_TEMP 'release-assets\*') @releaseOptions
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,33 @@

WinBulkTranscript by Jamieson Lab is a Windows 11 desktop application that recursively transcribes MP4 files into WebVTT (`.vtt`) files.

> **Development/UAT status:** local x64 implementation evidence is green, but this repository is not yet a supported release. See [UAT evidence status](docs/validation/uat-evidence-status.md) and the [release test matrix](docs/release/release-test-matrix.md).
> **Development/UAT status:** local x64 implementation evidence is green, but this repository does not yet have a supported release. When a release is approved, installable ZIPs will appear on the [GitHub Releases page](https://github.com/JamiesonLabUTSW/win-bulk-transcript/releases). See [UAT evidence status](docs/validation/uat-evidence-status.md) and the [release test matrix](docs/release/release-test-matrix.md).

## Install a release ZIP

Approved releases provide two self-contained downloads:

- `WinBulkTranscript-<version>-win-x64.zip` for ordinary Intel/AMD Windows 11 PCs.
- `WinBulkTranscript-<version>-win-arm64.zip` for Windows on Arm PCs.

Download the matching archive and `SHA256SUMS.txt` from [GitHub Releases](https://github.com/JamiesonLabUTSW/win-bulk-transcript/releases), verify it, extract the **entire** ZIP to a normal folder, and launch `WinBulkTranscript.exe` from that extracted folder. Do not run it inside the ZIP or copy the EXE alone. The bundle includes .NET and Windows App SDK dependencies; the speech model is downloaded separately on first use.

```powershell
$version = '1.2.3'
$architecture = 'x64' # Use arm64 on Windows on Arm.
$zip = "WinBulkTranscript-$version-win-$architecture.zip"
$matchingLines = @(Get-Content .\SHA256SUMS.txt |
Where-Object { $_ -match "^[0-9a-fA-F]{64} \*$([regex]::Escape($zip))$" })
if ($matchingLines.Count -ne 1) { throw 'Checksum manifest is missing or ambiguous.' }
$expected = ($matchingLines[0] -split ' ', 2)[0].ToLowerInvariant()
$actual = (Get-FileHash ".\$zip" -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw 'Checksum verification failed.' }
Expand-Archive ".\$zip" -DestinationPath ".\WinBulkTranscript-$version-win-$architecture"
```

Release ZIPs are currently unsigned. Windows SmartScreen may warn, and organizational policy may block them; obtain files only from the Releases page and do not disable or bypass security policy. GitHub build provenance can additionally be checked with `gh attestation verify $zip --repo JamiesonLabUTSW/win-bulk-transcript`.

We use ZIP rather than an unsigned MSIX because [Microsoft reserves unsigned-MSIX installation for testing](https://learn.microsoft.com/en-us/windows/msix/package/unsigned-package): executable packages generally need elevated PowerShell with `Add-AppxPackage -AllowUnsigned`, while self-signed MSIX packages require each device to trust a certificate first. A signed MSIX is a future option once a trusted signing/Store channel is available.

## Quick start: run from source

Expand Down Expand Up @@ -34,7 +60,7 @@ dotnet publish .\src\WinBulkTranscript.App\WinBulkTranscript.App.csproj `
& "$publish\WinBulkTranscript.exe"
```

Replace `win-x64` with `win-arm64` on an ARM64 Windows machine. Keep every file in the published folder beside `WinBulkTranscript.exe`; do not copy or run the EXE by itself. Version 1 uses a folder/ZIP deployment rather than a single-file executable.
Replace `win-x64` with `win-arm64` on an ARM64 Windows machine. Keep every file in the published folder beside `WinBulkTranscript.exe`; do not copy or run the EXE by itself. Version 1 uses a folder/ZIP deployment rather than a single-file executable or MSIX.

## Use the application

Expand All @@ -53,7 +79,7 @@ Use **Cancel** to stop a batch cooperatively and wait for cleanup to finish befo
- **Start is disabled:** both folders must still exist, and the input tree must contain at least one readable `.mp4` file. Reparse points are not traversed.
- **First model load fails:** connect to the internet and retry. Cached-offline behavior is intended but is not yet release-validated on a clean machine.
- **Existing transcript files:** choose the batch-wide collision policy in the dialog; do not force-kill the app while it is processing.
- **Supported package:** do not treat `artifacts\publish-smoke` as a release. A final release ZIP does not exist yet; see the [release process](docs/release/README.md).
- **Supported package:** do not treat `artifacts\publish-smoke` or an Actions run artifact as a release. Only assets attached to an approved entry on the [GitHub Releases page](https://github.com/JamiesonLabUTSW/win-bulk-transcript/releases) are release downloads; if the page has no release, no supported package exists yet. See the [release process](docs/release/README.md).

## License and permitted use

Expand Down
Loading