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
1 change: 0 additions & 1 deletion .claude/worktrees/specs-beta-refresh
Submodule specs-beta-refresh deleted from 4f4b81
203 changes: 203 additions & 0 deletions .github/workflows/bump-terminal-gui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
name: Bump Terminal.Gui

# Keeps <TerminalGuiVersion> in Directory.Build.props tracking Terminal.Gui's
# NuGet publishes. Policy: develop tracks TG's develop pre-releases (Editor is
# a continuous canary for TG API churn); a stable Editor release requires a
# stable pin (enforced by prepare-release.yml).
#
# Flow:
# - Resolve the newest TG version on NuGet for the requested channel
# (default: prerelease = newest overall; stable = newest without a `-`).
# - If it differs from the current pin: bump, build, run the test suites.
# - Green → commit directly to develop (push uses RELEASE_PAT so the push
# triggers release.yml, publishing a new Editor pre-release and dispatching
# downstream to clet).
# - Red → push a `bump/terminal-gui-<version>` branch, open a PR so the
# breakage is visible, and fail this run.
#
# Triggers:
# - repository_dispatch `terminal-gui-published` (sent by gui-cs/Terminal.Gui's
# publish workflow; payload: { "version": "2.4.6-develop.10" })
# - schedule: fallback poll, in case the dispatch is missing/not configured
# - workflow_dispatch: manual, with channel selection (use channel=stable to
# pin a stable TG ahead of an Editor stable release)

on:
repository_dispatch:
types: [terminal-gui-published]
schedule:
- cron: '23 */6 * * *'
workflow_dispatch:
inputs:
channel:
description: 'Which TG stream to pin'
required: true
type: choice
options:
- prerelease
- stable
default: prerelease
version:
description: 'Explicit Terminal.Gui version (optional; overrides channel)'
required: false
type: string

permissions:
contents: write
pull-requests: write

concurrency:
group: bump-terminal-gui
cancel-in-progress: false

jobs:
bump:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
# Terminal.Gui drivers skip real-terminal probing/IO on TTY-less runners.
DisableRealDriverIO: "1"
steps:
- name: Checkout develop
uses: actions/checkout@v5
with:
ref: develop
token: ${{ secrets.RELEASE_PAT }}

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Resolve target Terminal.Gui version
id: resolve
shell: bash
run: |
CURRENT=$(sed -n 's|.*<TerminalGuiVersion[^>]*>\(.*\)</TerminalGuiVersion>.*|\1|p' Directory.Build.props | head -1)
if [ -z "$CURRENT" ]; then
echo "::error::Could not read <TerminalGuiVersion> from Directory.Build.props."
exit 1
fi

EXPLICIT="${{ github.event.inputs.version || github.event.client_payload.version }}"
CHANNEL="${{ github.event.inputs.channel || 'prerelease' }}"

if [ -n "$EXPLICIT" ]; then
TARGET="$EXPLICIT"
else
Comment on lines +85 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject stale Terminal.Gui dispatch versions

When this workflow is triggered by repository_dispatch, the payload version is accepted as TARGET solely because it is present. If Terminal.Gui publishes multiple versions close together or GitHub queues/delivers dispatch runs out of order, a later run for an older payload (for example 2.4.6-develop.9 after the pin has already advanced to 2.4.6-develop.17) will pass the TARGET != CURRENT check and commit a downgrade to develop, publishing Editor against an older dependency. Please compare TARGET and CURRENT semver order for dispatch/manual values and skip non-advancing targets.

Useful? React with 👍 / 👎.

# Flat-container index is sorted ascending by NuGet semver.
INDEX=$(curl -fsS https://api.nuget.org/v3-flatcontainer/terminal.gui/index.json)
if [ "$CHANNEL" = "stable" ]; then
TARGET=$(echo "$INDEX" | jq -r '[.versions[] | select(contains("-") | not)] | last')
else
TARGET=$(echo "$INDEX" | jq -r '.versions | last')
fi
fi

if [ -z "$TARGET" ] || [ "$TARGET" = "null" ]; then
echo "::error::Could not resolve a target Terminal.Gui version."
exit 1
fi

echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "target=$TARGET" >> "$GITHUB_OUTPUT"

if [ "$TARGET" = "$CURRENT" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Pin already at ${CURRENT}; nothing to do."
elif git ls-remote --heads origin "bump/terminal-gui-${TARGET}" | grep -q .; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "::warning::bump/terminal-gui-${TARGET} already exists (a previous bump to ${TARGET} failed CI). Skipping."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "Bumping TerminalGuiVersion: ${CURRENT} → ${TARGET}"
fi

- name: Wait for NuGet flat-container to index the target
if: steps.resolve.outputs.changed == 'true'
shell: bash
run: |
TARGET="${{ steps.resolve.outputs.target }}"
# A dispatch from TG's publish workflow can arrive before NuGet has
# indexed the version; restore would fail and mis-report breakage.
for i in $(seq 1 60); do
if curl -fsS https://api.nuget.org/v3-flatcontainer/terminal.gui/index.json \
| jq -r '.versions[]' | grep -qx "$TARGET"; then
echo "Indexed on flat-container: $TARGET"
exit 0
fi
echo "Waiting for Terminal.Gui $TARGET on NuGet flat-container ($i/60, 10s each)..."
sleep 10
done
echo "::error::Timed out waiting for Terminal.Gui $TARGET on flat-container."
exit 1

- name: Apply pin
if: steps.resolve.outputs.changed == 'true'
shell: bash
run: |
TARGET="${{ steps.resolve.outputs.target }}"
sed -i "s|\(<TerminalGuiVersion[^>]*>\)[^<]*\(</TerminalGuiVersion>\)|\1${TARGET}\2|" Directory.Build.props
grep TerminalGuiVersion Directory.Build.props

- name: Setup .NET
if: steps.resolve.outputs.changed == 'true'
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'

- name: Validate (restore, build, test)
if: steps.resolve.outputs.changed == 'true'
id: validate
continue-on-error: true
shell: bash
run: |
set -euo pipefail
dotnet restore Terminal.Gui.Editor.slnx
dotnet build Terminal.Gui.Editor.slnx --no-restore -c Release
dotnet run --project tests/Terminal.Gui.Editor.Tests --no-build -c Release
dotnet run --project tests/Terminal.Gui.Editor.IntegrationTests --no-build -c Release
dotnet run --project tests/Terminal.Gui.Editor.ConfigTests --no-build -c Release

- name: Commit to develop (green)
if: steps.resolve.outputs.changed == 'true' && steps.validate.outcome == 'success'
shell: bash
run: |
TARGET="${{ steps.resolve.outputs.target }}"
git add Directory.Build.props
git commit -m "Bump TerminalGuiVersion to ${TARGET}"
# develop may have moved while tests ran; replay the bump on top.
git pull --rebase origin develop
git push origin develop
echo "## Bumped TerminalGuiVersion" >> "$GITHUB_STEP_SUMMARY"
echo "- ${{ steps.resolve.outputs.current }} → ${TARGET} (pushed to develop)" >> "$GITHUB_STEP_SUMMARY"

- name: Open PR (red)
if: steps.resolve.outputs.changed == 'true' && steps.validate.outcome != 'success'
shell: bash
run: |
TARGET="${{ steps.resolve.outputs.target }}"
CURRENT="${{ steps.resolve.outputs.current }}"
BRANCH="bump/terminal-gui-${TARGET}"

git checkout -b "$BRANCH"
git add Directory.Build.props
git commit -m "Bump TerminalGuiVersion to ${TARGET}"
git push origin "$BRANCH"

cat > /tmp/pr_body.md << EOF
Automated bump of \`TerminalGuiVersion\` from \`${CURRENT}\` to \`${TARGET}\` **failed validation** (build or tests).

Editor needs source changes to absorb this Terminal.Gui update. See the
[failed workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
EOF

gh pr create \
--base develop \
--head "$BRANCH" \
--title "Bump TerminalGuiVersion to ${TARGET} (needs fixes)" \
--body-file /tmp/pr_body.md

echo "::error::TG ${TARGET} broke the build/tests; opened PR from ${BRANCH}."
exit 1
32 changes: 29 additions & 3 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ on:
- stable
default: stable
version_override:
description: 'Version override (optional, e.g., 2.2.4). Defaults to Directory.Build.props without -develop.'
description: 'Version override (optional, e.g., 2.6.0). Defaults to GitVersion''s next patch (latest tag + 1).'
required: false
type: string

Expand Down Expand Up @@ -46,15 +46,41 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v4.5.0
with:
versionSpec: '6.x'

- name: Run GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v4.5.0
with:
useConfigFile: true

- name: Require a stable Terminal.Gui pin for stable releases
if: github.event.inputs.release_type == 'stable'
shell: bash
run: |
TG_PIN=$(sed -n 's|.*<TerminalGuiVersion[^>]*>\(.*\)</TerminalGuiVersion>.*|\1|p' Directory.Build.props | head -1)
if [ -z "$TG_PIN" ]; then
echo "::error::Could not read <TerminalGuiVersion> from Directory.Build.props."
exit 1
fi
if echo "$TG_PIN" | grep -q -- '-'; then
echo "::error::TerminalGuiVersion is '${TG_PIN}', a pre-release. A stable Editor release must depend on a stable Terminal.Gui (NuGet rejects stable→prerelease dependencies, NU5104). Pin a stable TG on develop first (e.g. via the Bump Terminal.Gui workflow with channel=stable)."
exit 1
fi
echo "TerminalGuiVersion pin is stable: ${TG_PIN}"

- name: Compute release version
id: version
shell: bash
run: |
if [ -n "${{ github.event.inputs.version_override }}" ]; then
VERSION="${{ github.event.inputs.version_override }}"
else
VERSION=$(sed -n 's|.*<Version>\(.*\)</Version>.*|\1|p' Directory.Build.props | head -1)
VERSION="${VERSION%%-*}"
# Next patch over the latest tag reachable from develop (GitVersion.yml: increment Patch).
VERSION="${{ steps.gitversion.outputs.MajorMinorPatch }}"
fi

RELEASE_TYPE="${{ github.event.inputs.release_type }}"
Expand Down
39 changes: 25 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ name: Release
# Publishes Terminal.Gui.Editor to NuGet.
#
# Triggers:
# 1. Push of a `v*` tag (canonical release path, e.g. v2.1.0)
# 1. Push of a `v*` tag (canonical release path, e.g. v2.5.3)
# → Version = tag with leading `v` stripped.
# 2. Push to `develop` (rolling pre-release smoke test)
# → Version = <Version> from Directory.Build.props + ".<github.run_number>"
# e.g. 2.1.1-develop.7
# 2. Push to `develop` (rolling pre-release)
# → Version computed by GitVersion from git history + GitVersion.yml,
# e.g. 2.5.3-develop.7 (latest reachable tag + Patch, develop label).
#
# The package version is declared in Directory.Build.props.
# The computed value overrides that base via `-p:Version=...`.
# No version is stored in the repo; tags are the source of truth.
# The computed value is injected into builds via `-p:Version=...`.

on:
push:
Expand All @@ -31,22 +31,33 @@ jobs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v5
with:
# GitVersion needs full history + tags.
fetch-depth: 0

- name: Install GitVersion
if: github.ref_type != 'tag'
uses: gittools/actions/gitversion/setup@v4.5.0
with:
versionSpec: '6.x'

- name: Run GitVersion
if: github.ref_type != 'tag'
id: gitversion
uses: gittools/actions/gitversion/execute@v4.5.0
with:
useConfigFile: true

- name: Compute version
id: v
shell: bash
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
# Tag form: v2.1.0 → 2.1.0
# Tag form: v2.5.3 → 2.5.3 (the tag is canonical; no GitVersion needed)
VERSION="${GITHUB_REF_NAME#v}"
elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then
# Read base from Directory.Build.props (e.g. "2.1.1-develop"), append run number.
BASE=$(sed -n 's|.*<Version>\(.*\)</Version>.*|\1|p' Directory.Build.props | head -1)
if [ -z "$BASE" ]; then
echo "::error::Could not read <Version> from Directory.Build.props."
exit 1
fi
VERSION="${BASE}.${GITHUB_RUN_NUMBER}"
# GitVersion: latest reachable tag + Patch, develop label, commit count.
VERSION="${{ steps.gitversion.outputs.SemVer }}"
else
echo "::error::Unsupported trigger: event=${{ github.event_name }} ref=${{ github.ref }}"
exit 1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ Thumbs.db
.env.*
!.env.example
BenchmarkDotNet.Artifacts/

# Claude Code session worktrees
.claude/worktrees/
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ Active development happens on **`develop`**. `main` is the release/stable branch
- Work on `develop`. During pre-alpha, direct commits and pushes to `develop` are allowed — no PRs required for routine work.
- Do not push directly to `main`. Promotion from `develop` to `main` is a deliberate release step.
- Two paths trigger `.github/workflows/release.yml`, which builds + tests cross-platform, then packs and pushes the NuGet package:
- **Push a `v*` tag** (e.g. `v2.1.0`) — canonical stable release; version = tag minus leading `v`.
- **Push to `develop`** — rolling pre-release; version = `<Version>` from `Directory.Build.props` + `.${github.run_number}`. With base `2.1.1-develop`, the first run publishes `2.1.1-develop.1`, etc.
- **Push a `v*` tag** (e.g. `v2.5.3`) — canonical stable release; version = tag minus leading `v`.
- **Push to `develop`** — rolling pre-release; version computed by GitVersion (e.g. `2.5.3-develop.7`).
- Stable releases are created through **Prepare Release** (`.github/workflows/prepare-release.yml`), which opens a release PR from `develop` to `main`. Merging that PR triggers **Finalize Release** (`.github/workflows/finalize-release.yml`) to create the `v*` tag, GitHub Release, and back-merge PR to `develop`; the tag push triggers NuGet publishing.

## Versioning

`Directory.Build.props` holds a single `<Version>` shared by both packages. Track Terminal.Gui's version stream — when the latest stable Terminal.Gui is `X.Y.Z`, our develop base is the next-patch pre-release (e.g. TG 2.1.0 → our base `2.1.1-develop`). Bump the base when TG ships a new stable, not on every commit. The `.${run_number}` suffix is the per-build counter, applied automatically by the workflow.
See `specs/decisions.md` DEC-010. Two independent axes, never conflated:

`<TerminalGuiVersion>` (also in `Directory.Build.props`) pins the Terminal.Gui dependency. Bump it when the project is ready to consume a new TG release; CI/release workflows can override via `-p:TerminalGuiVersion=<x>` if needed.
- **Editor's own version** — computed from git tags by **GitVersion 6** (`GitVersion.yml`, the same GitFlow model Terminal.Gui uses). No version lives in the repo; do not add one. Stable releases are `v*` tags on `main`; develop builds are `<latest-tag+patch>-develop.<commits-since-tag>` (always sorts above the latest stable). Local builds without `-p:Version` get the `0.0.0-local` placeholder. Editor's version is **not** coupled to Terminal.Gui's — do not "track TG's stream".
- **`<TerminalGuiVersion>`** (`Directory.Build.props`) — the minimum supported Terminal.Gui, i.e. the NuGet dependency floor. On `develop` it tracks TG's develop pre-releases and is bumped automatically by `.github/workflows/bump-terminal-gui.yml` (triggered by TG's publish dispatch, a fallback schedule, or manually): the bump is validated with a full build + all test suites, then committed directly to `develop` on green or opened as a PR from `bump/terminal-gui-<version>` on red. A **stable** Editor release requires a **stable** TG pin — `prepare-release.yml` gates on this (run the bump workflow with `channel=stable` first). Override per-build via `-p:TerminalGuiVersion=<x>`.

For the inner dev loop against a local TG enlistment, `dotnet build -p:UseLocalTerminalGui=true` swaps the Terminal.Gui PackageReference for a ProjectReference into the sibling `../Terminal.Gui` clone (see `Directory.Build.targets`). It is blocked in CI and for `pack`, and restore assets are mode-specific — re-restore after toggling.

## Build and test

Expand Down Expand Up @@ -258,4 +261,4 @@ Don't accidentally do these — they were considered and rejected:

## Open decisions

`specs/00-plan.md` §10 lists open design questions (line-ending policy, xshd vs TextMate for first highlighter, async I/O placement, read-only ranges, completion item shape). Resolutions go in `specs/05-decisions.md` (not yet created). If a task touches one of these, surface the decision rather than picking unilaterally.
`specs/00-plan.md` §10 lists open design questions (line-ending policy, xshd vs TextMate for first highlighter, async I/O placement, read-only ranges, completion item shape). Resolutions go in `specs/decisions.md`. If a task touches one of these, surface the decision rather than picking unilaterally.
28 changes: 18 additions & 10 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,30 @@
<Copyright>Copyright (c) gui-cs and contributors</Copyright>

<!--
Base version for both NuGet packages. Tracks Terminal.Gui's stream:
when latest TG stable is X.Y.Z, our develop base is X.(Y+1).0-develop or X.Y.(Z+1)-develop.
Release workflow overrides via -p:Version=<computed>:
- tag push v1.2.3 → 1.2.3
- develop branch push → 2.1.1-develop.<github.run_number>
Stable releases are prepared with .github/workflows/prepare-release.yml,
which opens a release PR. Merging that PR creates the v* tag.
No version lives in the repo. Versions are computed from git tags by
GitVersion (see GitVersion.yml) and injected by CI via -p:Version=<computed>:
- tag push v2.5.3 → 2.5.3
- develop branch push → 2.5.3-develop.<commits-since-tag>
Editor's version is independent of Terminal.Gui's; TG compatibility is
expressed only by <TerminalGuiVersion> below. Local builds that don't pass
-p:Version get an obviously-non-releasable placeholder.
-->
<Version>2.4.1-develop</Version>
<Version Condition="'$(Version)' == ''">0.0.0-local</Version>
<PackageProjectUrl>https://github.com/gui-cs/Editor</PackageProjectUrl>
<RepositoryUrl>https://github.com/gui-cs/Editor</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>

<!-- Pinned Terminal.Gui version. CI / release workflows can override via -p:TerminalGuiVersion=<x>. -->
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.0</TerminalGuiVersion>
<!--
Minimum supported Terminal.Gui (the NuGet dependency floor). On develop this
tracks TG's develop pre-releases and is bumped automatically by
.github/workflows/bump-terminal-gui.yml when TG publishes; a stable Editor
release requires a stable value here (prepare-release.yml gates on it,
and NuGet forbids stable→prerelease dependencies anyway).
Override per-build via -p:TerminalGuiVersion=<x>; use -p:UseLocalTerminalGui=true
to build against the ../Terminal.Gui enlistment instead (see Directory.Build.targets).
-->
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.6</TerminalGuiVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading