Skip to content

feat(session): support local logs and command cancellation #7

feat(session): support local logs and command cancellation

feat(session): support local logs and command cancellation #7

Workflow file for this run

name: release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. v2026.5.22.0)'
required: true
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
# Full history + all tags so the release-body step can diff
# commits against the previous version tag for the changelog.
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Resolve version from tag
id: ver
shell: pwsh
run: |
$tag = "${{ github.event.inputs.tag }}"
if (-not $tag) { $tag = "${{ github.ref_name }}" }
# Strip leading 'v' from the tag to land the family YYYY.M.D.N
# scheme into the binary version string. Tags MUST start with 'v'.
if (-not $tag.StartsWith('v')) {
throw "tag '$tag' does not start with 'v'"
}
$version = $tag.Substring(1)
# Sanity-check the YYYY.M.D.N(-XXXX) shape so a fat-fingered
# tag fails the release rather than shipping a broken version.
if ($version -notmatch '^\d{4}\.\d+\.\d+\.\d+(-[A-Fa-f0-9]{4})?$') {
throw "version '$version' does not match the YYYY.M.D.N(-XXXX) shape"
}
$isPre = $version -like '*-*'
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"is_pre=$($isPre.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "tag=$tag version=$version is_pre=$isPre"
- name: Fetch picotool
shell: pwsh
run: |
./scripts/fetch-picotool.ps1
- name: Build with embedded picotool
shell: pwsh
env:
GOOS: windows
GOARCH: amd64
CGO_ENABLED: '0'
HANDOFF_VERSION: ${{ steps.ver.outputs.version }}
run: |
go build -trimpath -ldflags="-s -w -X main.version=$env:HANDOFF_VERSION" -tags embed_picotool -o handoff.exe .
./handoff.exe version
- name: Write release manifest
id: manifest
shell: pwsh
env:
HANDOFF_VERSION: ${{ steps.ver.outputs.version }}
HANDOFF_TAG: ${{ steps.ver.outputs.tag }}
run: |
$sha = (Get-FileHash -Algorithm SHA256 handoff.exe).Hash.ToLower()
$manifest = [ordered]@{
version = $env:HANDOFF_VERSION
sha256 = $sha
url = "https://github.com/${{ github.repository }}/releases/download/$env:HANDOFF_TAG/handoff.exe"
notes = "Release $env:HANDOFF_TAG"
}
$manifest | ConvertTo-Json -Compress | Out-File -FilePath handoff-version.json -Encoding ASCII
Get-Content handoff-version.json
"sha256=$sha" >> $env:GITHUB_OUTPUT
- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ steps.ver.outputs.tag }}
EXE_SHA256: ${{ steps.manifest.outputs.sha256 }}
shell: pwsh
run: |
# Build the release body. Top section is a conventional-commit
# changelog grouped by type, extracted from `git log $prev..$tag`.
# Bottom section is the static install snippet.
$prevTag = git tag --list 'v*' --sort=-version:refname |
Where-Object { $_ -ne $env:TAG_NAME } |
Select-Object -First 1
$sections = [ordered]@{
feat = 'Features'
fix = 'Bug Fixes'
perf = 'Performance'
refactor = 'Refactors'
docs = 'Documentation'
test = 'Tests'
ci = 'CI'
build = 'Build'
chore = 'Chores'
style = 'Style'
revert = 'Reverts'
other = 'Other'
}
$groups = @{}
$breaking = @()
if ($prevTag) {
$range = "$prevTag..$env:TAG_NAME"
$logLines = git log $range --pretty=format:"%h%x09%s" --no-merges
foreach ($line in $logLines) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split "`t", 2
$sha = $parts[0]
$subject = $parts[1]
if ($subject -match '^(?<type>\w+)(\((?<scope>[^)]+)\))?(?<bang>!)?:\s*(?<desc>.+)$') {
$type = $matches['type'].ToLowerInvariant()
$scope = $matches['scope']
$isBreaking = -not [string]::IsNullOrEmpty($matches['bang'])
$desc = $matches['desc']
if (-not $sections.Contains($type)) { $type = 'other' }
if ($scope) {
$entry = "- ``$sha`` **${scope}**: $desc"
} else {
$entry = "- ``$sha`` $desc"
}
if ($isBreaking) {
if ($scope) {
$breaking += "- ``$sha`` **${scope}**: $desc"
} else {
$breaking += "- ``$sha`` $desc"
}
}
if (-not $groups.ContainsKey($type)) { $groups[$type] = @() }
$groups[$type] += $entry
} else {
if (-not $groups.ContainsKey('other')) { $groups['other'] = @() }
$groups['other'] += "- ``$sha`` $subject"
}
}
}
$out = New-Object System.Collections.Generic.List[string]
if ($prevTag) {
$out.Add("## Changes since $prevTag")
$out.Add("")
if ($breaking.Count -gt 0) {
$out.Add("### Breaking Changes")
$out.Add("")
foreach ($e in $breaking) { $out.Add($e) }
$out.Add("")
}
$emitted = $false
foreach ($key in $sections.Keys) {
if ($groups.ContainsKey($key) -and $groups[$key].Count -gt 0) {
$emitted = $true
$out.Add("### $($sections[$key])")
$out.Add("")
foreach ($e in $groups[$key]) { $out.Add($e) }
$out.Add("")
}
}
if (-not $emitted) {
$out.Add("_No conventional-commit changes since $prevTag._")
$out.Add("")
}
$out.Add("**Full diff:** https://github.com/$env:GITHUB_REPOSITORY/compare/$prevTag...$env:TAG_NAME")
$out.Add("")
} else {
$out.Add("Initial release.")
$out.Add("")
}
$out.Add("## Install")
$out.Add("")
$out.Add("Download ``handoff.exe`` from the assets below and run it from anywhere on PATH (or wherever you keep portable tools).")
$out.Add("")
$out.Add("SHA256: ``$env:EXE_SHA256``")
$out.Add("")
$out.Add("Auto-update clients can poll ``handoff-version.json`` next to the binary in the assets list.")
$out.Add("")
$out.Add("Getting started: https://github.com/$env:GITHUB_REPOSITORY/wiki/Getting-Started")
$notes = $out -join "`n"
$notesPath = Join-Path $env:RUNNER_TEMP "release-notes.md"
Set-Content -LiteralPath $notesPath -Value $notes -Encoding UTF8
# Draft-first: create as draft, upload assets, only then promote
# to published. If anything between create and promote fails the
# release stays as a draft, which is more visible than a live
# release missing one of its assets.
$extra = @('--draft')
if ('${{ steps.ver.outputs.is_pre }}' -eq 'true') { $extra += @('--prerelease','--latest=false') }
gh release create $env:TAG_NAME handoff.exe handoff-version.json --title $env:TAG_NAME --notes-file $notesPath @extra
if ($LASTEXITCODE -ne 0) { throw "gh release create failed (exit $LASTEXITCODE)" }
# Verify the assets actually attached -- gh release create can
# succeed with the metadata but fail mid-upload on the asset
# stage; the draft would then be live with missing files.
$json = gh release view $env:TAG_NAME --json assets,isDraft
if ($LASTEXITCODE -ne 0) { throw "gh release view failed (exit $LASTEXITCODE)" }
$info = $json | ConvertFrom-Json
$expected = @('handoff.exe', 'handoff-version.json')
foreach ($e in $expected) {
if (-not ($info.assets.name -contains $e)) {
throw "Asset '$e' did not attach to draft release $env:TAG_NAME -- leaving as draft for manual inspection."
}
}
Write-Host "Draft release $env:TAG_NAME has all expected assets, promoting to published."
gh release edit $env:TAG_NAME --draft=false
if ($LASTEXITCODE -ne 0) { throw "gh release edit --draft=false failed (exit $LASTEXITCODE)" }