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
61 changes: 61 additions & 0 deletions .github/workflows/windows-full-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: windows-full-ci

on:
pull_request:
paths:
- "herdr-plugin.toml"
- "plannotator-tui.version"
- "scripts/fetch-plannotator-tui.*"
- "scripts/plannotator-tui.sh"
- "scripts/smoke.sh"
- "scripts/test-fetch-plannotator-tui.*"
- "scripts/test-herdr-windows-plugin.ps1"
- "scripts/test-http-server.py"
- "scripts/test-windows-full-manifest.py"
- ".github/workflows/windows-full-ci.yml"
merge_group:

concurrency:
group: windows-full-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
windows-full:
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: local override and idempotence
run: ./scripts/test-fetch-plannotator-tui.ps1 -Case LocalOverride
- name: loopback download and checksum preservation
run: ./scripts/test-fetch-plannotator-tui.ps1 -Case Download
- uses: actions/checkout@v4
with:
repository: plannotator/plannotator-tui
ref: 3d0f671e50613eb2354226a76209df995c5b2437
path: plannotator-tui-source
persist-credentials: false
- name: manifest structure and development parity
run: |
python plannotator-tui-source/herdr/test-manifest.py
python scripts/test-windows-full-manifest.py `
plannotator-tui-source/herdr/herdr-plugin.toml
- name: pinned Herdr link and list
run: ./scripts/test-herdr-windows-plugin.ps1

unix-regression:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: bash scripts/test-fetch-plannotator-tui.sh
14 changes: 7 additions & 7 deletions herdr-plugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ command = ["bun", "src/manager.ts"]

# ---------------------------------------------------------------------------------------
# Document review with plannotator-tui (https://github.com/plannotator/plannotator-tui).
# Commands resolve through $HERDR_PLUGIN_ROOT: the review pane is opened with --cwd set to
# the folder under review, so a path relative to the plugin root would not resolve there.
# A prebuilt binary is fetched into bin/ at build time; macOS and Linux for now. Where it
# opens (overlay | split | popup) is the user's choice in ~/.config/plannotator-tui/config.toml.
# Actions resolve explicit relative programs against $HERDR_PLUGIN_ROOT. The review pane is
# opened with --cwd set to the folder under review, so it retains the plugin-root wrapper.
# A prebuilt binary is fetched into bin/ at build time on macOS and Linux. Where it opens
# (overlay | split | popup) remains the user's choice in plannotator-tui's config file.

[[build]]
platforms = ["macos", "linux"]
Expand All @@ -69,23 +69,23 @@ title = "Annotate: open here"
description = "Review the focused pane's folder in plannotator-tui and send feedback to its agent."
contexts = ["workspace", "pane"]
platforms = ["macos", "linux"]
command = ["sh", "-c", "exec bash \"$HERDR_PLUGIN_ROOT/scripts/plannotator-tui.sh\" herdr open"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

[[actions]]
id = "open-link"
title = "Annotate this file"
description = "Open a Ctrl-clicked Markdown file in plannotator-tui."
contexts = ["pane"]
platforms = ["macos", "linux"]
command = ["sh", "-c", "exec bash \"$HERDR_PLUGIN_ROOT/scripts/plannotator-tui.sh\" herdr open"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

[[actions]]
id = "last"
title = "Annotate: agent's last message"
description = "Review the focused agent's most recent message in plannotator-tui and send feedback back."
contexts = ["pane"]
platforms = ["macos", "linux"]
command = ["sh", "-c", "exec bash \"$HERDR_PLUGIN_ROOT/scripts/plannotator-tui.sh\" herdr last"]
command = ["./bin/plannotator-tui.exe", "herdr", "last"]

# Ctrl-click on a file:// Markdown link. Anchored on the scheme so web links never match.
[[link_handlers]]
Expand Down
2 changes: 1 addition & 1 deletion plannotator-tui.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.0
0.6.0
157 changes: 157 additions & 0 deletions scripts/fetch-plannotator-tui.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
$ErrorActionPreference = "Stop"
Set-Location (Join-Path $PSScriptRoot "..")

$versionContents = Get-Content -LiteralPath "plannotator-tui.version" -Raw
$version = if ($null -eq $versionContents) { "" } else { [string]$versionContents }
$version = $version.Trim()
if (-not $version) { throw "plannotator-tui.version is empty" }

$destinationDirectory = Join-Path (Get-Location).Path "bin"
$destination = Join-Path $destinationDirectory "plannotator-tui.exe"
$stamp = Join-Path $destinationDirectory "plannotator-tui.version"
New-Item -ItemType Directory -Force $destinationDirectory | Out-Null

$localOverride = [Environment]::GetEnvironmentVariable("PLANNOTATOR_TUI_BIN", "Process")
$hasLocalOverride = $null -ne $localOverride
$installed = if (Test-Path -LiteralPath $stamp -PathType Leaf) {
([string](Get-Content -LiteralPath $stamp -Raw)).Trim()
} else {
""
}

if ((Test-Path -LiteralPath $destination -PathType Leaf) -and
$installed -eq $version -and -not $hasLocalOverride) {
Write-Output "plannotator-tui $version already installed"
exit 0
}

function Install-PlannotatorTui {
param([Parameter(Mandatory = $true)][string]$Source)

$candidate = Join-Path $destinationDirectory ("plannotator-tui-" + [guid]::NewGuid() + ".tmp")
$backup = Join-Path $destinationDirectory ("plannotator-tui-" + [guid]::NewGuid() + ".bak")
$stampBackup = Join-Path $destinationDirectory ("plannotator-tui-version-" + [guid]::NewGuid() + ".bak")
$hadDestination = Test-Path -LiteralPath $destination -PathType Leaf
$hadStamp = Test-Path -LiteralPath $stamp -PathType Leaf
$replacementCompleted = $false
$keepRecoveryFiles = $false
try {
if ($hadStamp) {
Copy-Item -LiteralPath $stamp -Destination $stampBackup
}
Copy-Item -LiteralPath $Source -Destination $candidate
if ($hadDestination) {
try {
[System.IO.File]::Replace(
[System.IO.Path]::GetFullPath($candidate),
[System.IO.Path]::GetFullPath($destination),
[System.IO.Path]::GetFullPath($backup)
)
} catch {
throw "failed to replace ${destination}: $($_.Exception.Message)"
}
} else {
Move-Item -LiteralPath $candidate -Destination $destination
}
$replacementCompleted = $true
Set-Content -LiteralPath $stamp -NoNewline -Value $version
} catch {
$installFailure = $_
if ($replacementCompleted) {
try {
if ($hadDestination) {
Remove-Item -LiteralPath $destination -Force
Move-Item -LiteralPath $backup -Destination $destination
} else {
Remove-Item -LiteralPath $destination -Force
}
if ($hadStamp) {
Remove-Item -LiteralPath $stamp -Force -ErrorAction SilentlyContinue
Move-Item -LiteralPath $stampBackup -Destination $stamp
} else {
Remove-Item -LiteralPath $stamp -Force -ErrorAction SilentlyContinue
}
$replacementCompleted = $false
} catch {
$keepRecoveryFiles = $true
throw (
"$($installFailure.Exception.Message); rollback also failed: " +
$_.Exception.Message
)
}
}
throw $installFailure
} finally {
Remove-Item -LiteralPath $candidate -Force -ErrorAction SilentlyContinue
if (-not $keepRecoveryFiles) {
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $stampBackup -Force -ErrorAction SilentlyContinue
}
}
}

if ($hasLocalOverride) {
if (-not (Test-Path -LiteralPath $localOverride -PathType Leaf)) {
throw "PLANNOTATOR_TUI_BIN is not a file: $localOverride"
}
Install-PlannotatorTui -Source $localOverride
Write-Output "installed plannotator-tui from $localOverride (local build, stamped $version)"
exit 0
}

try {
$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
$target = switch ($architecture) {
"X64" { "x86_64-pc-windows-msvc" }
"Arm64" { "aarch64-pc-windows-msvc" }
default { throw "no plannotator-tui release target for Windows/$architecture" }
}

$asset = "plannotator-tui-$target.exe"
$releaseBaseOverride = [Environment]::GetEnvironmentVariable(
"PLANNOTATOR_TUI_RELEASE_BASE",
"Process"
)
# PLANNOTATOR_TUI_RELEASE_BASE is a test-only seam for a loopback fixture server.
$base = if ($null -ne $releaseBaseOverride) {
$releaseBaseOverride.TrimEnd([char]"/")
} else {
"https://github.com/plannotator/plannotator-tui/releases/download/v$version"
}

$temporary = Join-Path ([System.IO.Path]::GetTempPath()) ("plannotator-tui-" + [guid]::NewGuid())
try {
New-Item -ItemType Directory $temporary | Out-Null
$downloadedAsset = Join-Path $temporary $asset
$checksumFile = Join-Path $temporary "SHA256SUMS"
Invoke-WebRequest -UseBasicParsing "$base/$asset" -OutFile $downloadedAsset
Invoke-WebRequest -UseBasicParsing "$base/SHA256SUMS" -OutFile $checksumFile

$matches = @(
Get-Content -LiteralPath $checksumFile | Where-Object {
$fields = @($_ -split "\s+")
$fields.Count -ge 2 -and $fields[-1] -ceq $asset
}
)
if ($matches.Count -ne 1) {
throw "expected exactly one checksum for $asset in $base/SHA256SUMS; found $($matches.Count)"
}
$checksumFields = $matches[0].Trim() -split "\s+"
$expected = $checksumFields[0].ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $downloadedAsset).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "sha256 mismatch for ${asset}: expected $expected, got $actual"
}

Install-PlannotatorTui -Source $downloadedAsset
Write-Output "installed plannotator-tui $version ($target)"
} finally {
Remove-Item -LiteralPath $temporary -Recurse -Force -ErrorAction SilentlyContinue
}
} catch {
Write-Warning (
"Full review is unavailable until the plugin is reinstalled or updated: " +
$_.Exception.Message
)
exit 0
}
20 changes: 12 additions & 8 deletions scripts/fetch-plannotator-tui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# (cwd = plugin root) and by hand for local testing.
#
# plannotator-tui.version the release to install (one line, e.g. 0.1.0)
# bin/plannotator-tui the binary
# bin/plannotator-tui.exe the binary
# bin/plannotator-tui.version what is installed; matching the pin means nothing to do
#
# Modes, in order:
Expand All @@ -19,19 +19,21 @@ cd "$(dirname "$0")/.."
version="$(tr -d '[:space:]' < plannotator-tui.version)"
[ -n "$version" ] || { echo "plannotator-tui.version is empty" >&2; exit 1; }
mkdir -p bin
destination="bin/plannotator-tui.exe"
stamp="bin/plannotator-tui.version"
installed="$(cat bin/plannotator-tui.version 2>/dev/null || true)"

if [ -x bin/plannotator-tui ] && [ "$installed" = "$version" ] && [ -z "${PLANNOTATOR_TUI_BIN:-}" ]; then
if [ -x "$destination" ] && [ "$installed" = "$version" ] && [ -z "${PLANNOTATOR_TUI_BIN:-}" ]; then
echo "plannotator-tui $version already installed"
exit 0
fi

if [ -n "${PLANNOTATOR_TUI_BIN:-}" ]; then
[ -x "$PLANNOTATOR_TUI_BIN" ] || { echo "PLANNOTATOR_TUI_BIN is not an executable: $PLANNOTATOR_TUI_BIN" >&2; exit 1; }
cp "$PLANNOTATOR_TUI_BIN" bin/plannotator-tui.tmp
chmod +x bin/plannotator-tui.tmp
mv bin/plannotator-tui.tmp bin/plannotator-tui
echo "$version" > bin/plannotator-tui.version
rm -f "$destination"
cp "$PLANNOTATOR_TUI_BIN" "$destination"
chmod +x "$destination"
printf '%s' "$version" > "$stamp"
echo "installed plannotator-tui from $PLANNOTATOR_TUI_BIN (local build, stamped $version)"
exit 0
fi
Expand Down Expand Up @@ -74,6 +76,8 @@ fi
[ "$actual" = "$expected" ] || give_up "sha256 mismatch for $asset: expected $expected, got $actual"

chmod +x "$tmp/$asset"
mv "$tmp/$asset" bin/plannotator-tui
echo "$version" > bin/plannotator-tui.version
rm -f "$destination"
cp "$tmp/$asset" "$destination"
chmod +x "$destination"
printf '%s' "$version" > "$stamp"
echo "installed plannotator-tui $version ($target)"
9 changes: 4 additions & 5 deletions scripts/plannotator-tui.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
#!/usr/bin/env bash
# Run the bundled plannotator-tui, or say clearly why it cannot run. Herdr invokes this for
# the review pane and actions; the pane's cwd is the folder under review, so the binary is
# located relative to this script, never to the cwd.
# The review pane runs with the folder under review as cwd. Resolve the staged binary from this
# script's plugin-root location instead.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
if [ -x "$root/bin/plannotator-tui" ]; then
exec "$root/bin/plannotator-tui" "$@"
if [ -x "$root/bin/plannotator-tui.exe" ]; then
exec "$root/bin/plannotator-tui.exe" "$@"
fi
msg="plannotator-tui is not installed. Reinstall the plugin: herdr plugin install plannotator/herdr-annotate"
echo "$msg" >&2
Expand Down
2 changes: 1 addition & 1 deletion scripts/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ actions() { herdr plugin action list --plugin annotate | python3 -c "
import json,sys; print(','.join(sorted(a['action_id'] for a in json.load(sys.stdin)['result']['actions'])))"; }
bin_version() {
local root; root="$(plugin_json | field "p['plugin_root']")"
local bin="$root/bin/plannotator-tui"; [ -x "$bin" ] && "$bin" --version | awk '{print $2}' || echo none
local bin="$root/bin/plannotator-tui.exe"; [ -x "$bin" ] && "$bin" --version | awk '{print $2}' || echo none
}
pin() { local root; root="$(plugin_json | field "p['plugin_root']")"; tr -d '[:space:]' < "$root/plannotator-tui.version" 2>/dev/null || echo none; }
check() { if [ "$2" = "$3" ]; then echo " ok $1: $2"; else echo " FAIL $1: got '$2', want '$3'" >&2; failures=$((failures+1)); fi; }
Expand Down
Loading
Loading