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: dbc99a070dc169ffd9763976a8ea60406eabf268
path: plannotator-tui-source
persist-credentials: false
- name: manifest structure and development parity
run: |
python scripts/test-windows-full-manifest.py
python plannotator-tui-source/herdr/test-manifest.py `
plannotator-tui-source/herdr/herdr-plugin.toml 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
32 changes: 19 additions & 13 deletions herdr-plugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,27 @@ 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.
# Herdr resolves explicit relative programs against $HERDR_PLUGIN_ROOT. A platform release
# binary is fetched into bin/ at build time. Where it opens (overlay | split | popup) remains
# the user's choice in plannotator-tui's config file.

[[build]]
platforms = ["macos", "linux"]
command = ["bash", "scripts/fetch-plannotator-tui.sh"]

[[build]]
platforms = ["windows"]
command = [
"powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", "scripts/fetch-plannotator-tui.ps1",
]

[[panes]]
id = "doc"
title = "Annotate"
placement = "overlay"
platforms = ["macos", "linux"]
command = ["sh", "-c", "exec bash \"$HERDR_PLUGIN_ROOT/scripts/plannotator-tui.sh\" herdr pane"]
platforms = ["macos", "linux", "windows"]
command = ["./bin/plannotator-tui.exe", "herdr", "pane"]

# Both actions run the launcher, which reads Herdr's invocation context: the focused pane's
# folder (open) or the clicked file:// link (open-link), and the focused pane's agent as
Expand All @@ -68,29 +74,29 @@ id = "open"
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"]
platforms = ["macos", "linux", "windows"]
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"]
platforms = ["macos", "linux", "windows"]
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"]
platforms = ["macos", "linux", "windows"]
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]]
id = "markdown-file"
title = "Annotate this file"
pattern = "^file://.*\\.(md|markdown|mdx)$"
action = "open-link"
platforms = ["macos", "linux"]
platforms = ["macos", "linux", "windows"]
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.
# Compatibility wrapper for released and local workflows. The current manifest invokes the
# staged binary directly; remove this wrapper after one compatibility release.
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