feat(tunnel): forward host local ports to operators #8
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)" } |