Skip to content

Commit c7f0a00

Browse files
tigclaude
andcommitted
Decouple versioning from Terminal.Gui; automate TG pin (DEC-010)
Editor's package version is now computed from git tags by GitVersion 6 (same GitFlow model TG uses) instead of a hand-maintained <Version> base that had already gone stale (develop prereleases 2.4.1-develop.N sorted below the published 2.5.2 stable). - GitVersion.yml: tag-driven versions; develop = 2.5.3-develop.N - release.yml: resolve-version via GitVersion (tags still strip 'v') - prepare-release.yml: default version from GitVersion MajorMinorPatch; gate stable releases on a stable TerminalGuiVersion pin (NU5104) - bump-terminal-gui.yml: auto-bump the TG pin on TG publishes (dispatch/schedule/manual); green -> commit to develop, red -> PR - Directory.Build.targets: -p:UseLocalTerminalGui=true swaps the TG PackageReference for a ProjectReference into ../Terminal.Gui (blocked in CI and for pack) - TerminalGuiVersion 2.4.0 -> 2.4.6-develop.9; tests now use Glyphs.CheckStateChecked/UnChecked instead of hardcoded glyphs (TG changed the checked-menu glyph upstream) - CLAUDE.md + specs/decisions.md DEC-010 document the model Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f6da564 commit c7f0a00

10 files changed

Lines changed: 391 additions & 35 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
name: Bump Terminal.Gui
2+
3+
# Keeps <TerminalGuiVersion> in Directory.Build.props tracking Terminal.Gui's
4+
# NuGet publishes. Policy: develop tracks TG's develop pre-releases (Editor is
5+
# a continuous canary for TG API churn); a stable Editor release requires a
6+
# stable pin (enforced by prepare-release.yml).
7+
#
8+
# Flow:
9+
# - Resolve the newest TG version on NuGet for the requested channel
10+
# (default: prerelease = newest overall; stable = newest without a `-`).
11+
# - If it differs from the current pin: bump, build, run the test suites.
12+
# - Green → commit directly to develop (push uses RELEASE_PAT so the push
13+
# triggers release.yml, publishing a new Editor pre-release and dispatching
14+
# downstream to clet).
15+
# - Red → push a `bump/terminal-gui-<version>` branch, open a PR so the
16+
# breakage is visible, and fail this run.
17+
#
18+
# Triggers:
19+
# - repository_dispatch `terminal-gui-published` (sent by gui-cs/Terminal.Gui's
20+
# publish workflow; payload: { "version": "2.4.6-develop.10" })
21+
# - schedule: fallback poll, in case the dispatch is missing/not configured
22+
# - workflow_dispatch: manual, with channel selection (use channel=stable to
23+
# pin a stable TG ahead of an Editor stable release)
24+
25+
on:
26+
repository_dispatch:
27+
types: [terminal-gui-published]
28+
schedule:
29+
- cron: '23 */6 * * *'
30+
workflow_dispatch:
31+
inputs:
32+
channel:
33+
description: 'Which TG stream to pin'
34+
required: true
35+
type: choice
36+
options:
37+
- prerelease
38+
- stable
39+
default: prerelease
40+
version:
41+
description: 'Explicit Terminal.Gui version (optional; overrides channel)'
42+
required: false
43+
type: string
44+
45+
permissions:
46+
contents: write
47+
pull-requests: write
48+
49+
concurrency:
50+
group: bump-terminal-gui
51+
cancel-in-progress: false
52+
53+
jobs:
54+
bump:
55+
runs-on: ubuntu-latest
56+
env:
57+
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
58+
# Terminal.Gui drivers skip real-terminal probing/IO on TTY-less runners.
59+
DisableRealDriverIO: "1"
60+
steps:
61+
- name: Checkout develop
62+
uses: actions/checkout@v5
63+
with:
64+
ref: develop
65+
token: ${{ secrets.RELEASE_PAT }}
66+
67+
- name: Configure Git
68+
run: |
69+
git config user.name "github-actions[bot]"
70+
git config user.email "github-actions[bot]@users.noreply.github.com"
71+
72+
- name: Resolve target Terminal.Gui version
73+
id: resolve
74+
shell: bash
75+
run: |
76+
CURRENT=$(sed -n 's|.*<TerminalGuiVersion[^>]*>\(.*\)</TerminalGuiVersion>.*|\1|p' Directory.Build.props | head -1)
77+
if [ -z "$CURRENT" ]; then
78+
echo "::error::Could not read <TerminalGuiVersion> from Directory.Build.props."
79+
exit 1
80+
fi
81+
82+
EXPLICIT="${{ github.event.inputs.version || github.event.client_payload.version }}"
83+
CHANNEL="${{ github.event.inputs.channel || 'prerelease' }}"
84+
85+
if [ -n "$EXPLICIT" ]; then
86+
TARGET="$EXPLICIT"
87+
else
88+
# Flat-container index is sorted ascending by NuGet semver.
89+
INDEX=$(curl -fsS https://api.nuget.org/v3-flatcontainer/terminal.gui/index.json)
90+
if [ "$CHANNEL" = "stable" ]; then
91+
TARGET=$(echo "$INDEX" | jq -r '[.versions[] | select(contains("-") | not)] | last')
92+
else
93+
TARGET=$(echo "$INDEX" | jq -r '.versions | last')
94+
fi
95+
fi
96+
97+
if [ -z "$TARGET" ] || [ "$TARGET" = "null" ]; then
98+
echo "::error::Could not resolve a target Terminal.Gui version."
99+
exit 1
100+
fi
101+
102+
echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
103+
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
104+
105+
if [ "$TARGET" = "$CURRENT" ]; then
106+
echo "changed=false" >> "$GITHUB_OUTPUT"
107+
echo "Pin already at ${CURRENT}; nothing to do."
108+
elif git ls-remote --heads origin "bump/terminal-gui-${TARGET}" | grep -q .; then
109+
echo "changed=false" >> "$GITHUB_OUTPUT"
110+
echo "::warning::bump/terminal-gui-${TARGET} already exists (a previous bump to ${TARGET} failed CI). Skipping."
111+
else
112+
echo "changed=true" >> "$GITHUB_OUTPUT"
113+
echo "Bumping TerminalGuiVersion: ${CURRENT} → ${TARGET}"
114+
fi
115+
116+
- name: Apply pin
117+
if: steps.resolve.outputs.changed == 'true'
118+
shell: bash
119+
run: |
120+
TARGET="${{ steps.resolve.outputs.target }}"
121+
sed -i "s|\(<TerminalGuiVersion[^>]*>\)[^<]*\(</TerminalGuiVersion>\)|\1${TARGET}\2|" Directory.Build.props
122+
grep TerminalGuiVersion Directory.Build.props
123+
124+
- name: Setup .NET
125+
if: steps.resolve.outputs.changed == 'true'
126+
uses: actions/setup-dotnet@v5
127+
with:
128+
dotnet-version: '10.0.x'
129+
dotnet-quality: 'preview'
130+
131+
- name: Validate (restore, build, test)
132+
if: steps.resolve.outputs.changed == 'true'
133+
id: validate
134+
continue-on-error: true
135+
shell: bash
136+
run: |
137+
set -euo pipefail
138+
dotnet restore Terminal.Gui.Editor.slnx
139+
dotnet build Terminal.Gui.Editor.slnx --no-restore -c Release
140+
dotnet run --project tests/Terminal.Gui.Editor.Tests --no-build -c Release
141+
dotnet run --project tests/Terminal.Gui.Editor.IntegrationTests --no-build -c Release
142+
dotnet run --project tests/Terminal.Gui.Editor.ConfigTests --no-build -c Release
143+
144+
- name: Commit to develop (green)
145+
if: steps.resolve.outputs.changed == 'true' && steps.validate.outcome == 'success'
146+
shell: bash
147+
run: |
148+
TARGET="${{ steps.resolve.outputs.target }}"
149+
git add Directory.Build.props
150+
git commit -m "Bump TerminalGuiVersion to ${TARGET}"
151+
# develop may have moved while tests ran; replay the bump on top.
152+
git pull --rebase origin develop
153+
git push origin develop
154+
echo "## Bumped TerminalGuiVersion" >> "$GITHUB_STEP_SUMMARY"
155+
echo "- ${{ steps.resolve.outputs.current }} → ${TARGET} (pushed to develop)" >> "$GITHUB_STEP_SUMMARY"
156+
157+
- name: Open PR (red)
158+
if: steps.resolve.outputs.changed == 'true' && steps.validate.outcome != 'success'
159+
shell: bash
160+
run: |
161+
TARGET="${{ steps.resolve.outputs.target }}"
162+
CURRENT="${{ steps.resolve.outputs.current }}"
163+
BRANCH="bump/terminal-gui-${TARGET}"
164+
165+
git checkout -b "$BRANCH"
166+
git add Directory.Build.props
167+
git commit -m "Bump TerminalGuiVersion to ${TARGET}"
168+
git push origin "$BRANCH"
169+
170+
cat > /tmp/pr_body.md << EOF
171+
Automated bump of \`TerminalGuiVersion\` from \`${CURRENT}\` to \`${TARGET}\` **failed validation** (build or tests).
172+
173+
Editor needs source changes to absorb this Terminal.Gui update. See the
174+
[failed workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
175+
EOF
176+
177+
gh pr create \
178+
--base develop \
179+
--head "$BRANCH" \
180+
--title "Bump TerminalGuiVersion to ${TARGET} (needs fixes)" \
181+
--body-file /tmp/pr_body.md
182+
183+
echo "::error::TG ${TARGET} broke the build/tests; opened PR from ${BRANCH}."
184+
exit 1

.github/workflows/prepare-release.yml

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ on:
1818
- stable
1919
default: stable
2020
version_override:
21-
description: 'Version override (optional, e.g., 2.2.4). Defaults to Directory.Build.props without -develop.'
21+
description: 'Version override (optional, e.g., 2.6.0). Defaults to GitVersion''s next patch (latest tag + 1).'
2222
required: false
2323
type: string
2424

@@ -46,15 +46,41 @@ jobs:
4646
git config user.name "github-actions[bot]"
4747
git config user.email "github-actions[bot]@users.noreply.github.com"
4848
49+
- name: Install GitVersion
50+
uses: gittools/actions/gitversion/setup@v4.5.0
51+
with:
52+
versionSpec: '6.x'
53+
54+
- name: Run GitVersion
55+
id: gitversion
56+
uses: gittools/actions/gitversion/execute@v4.5.0
57+
with:
58+
useConfigFile: true
59+
60+
- name: Require a stable Terminal.Gui pin for stable releases
61+
if: github.event.inputs.release_type == 'stable'
62+
shell: bash
63+
run: |
64+
TG_PIN=$(sed -n 's|.*<TerminalGuiVersion[^>]*>\(.*\)</TerminalGuiVersion>.*|\1|p' Directory.Build.props | head -1)
65+
if [ -z "$TG_PIN" ]; then
66+
echo "::error::Could not read <TerminalGuiVersion> from Directory.Build.props."
67+
exit 1
68+
fi
69+
if echo "$TG_PIN" | grep -q -- '-'; then
70+
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)."
71+
exit 1
72+
fi
73+
echo "TerminalGuiVersion pin is stable: ${TG_PIN}"
74+
4975
- name: Compute release version
5076
id: version
5177
shell: bash
5278
run: |
5379
if [ -n "${{ github.event.inputs.version_override }}" ]; then
5480
VERSION="${{ github.event.inputs.version_override }}"
5581
else
56-
VERSION=$(sed -n 's|.*<Version>\(.*\)</Version>.*|\1|p' Directory.Build.props | head -1)
57-
VERSION="${VERSION%%-*}"
82+
# Next patch over the latest tag reachable from develop (GitVersion.yml: increment Patch).
83+
VERSION="${{ steps.gitversion.outputs.MajorMinorPatch }}"
5884
fi
5985
6086
RELEASE_TYPE="${{ github.event.inputs.release_type }}"

.github/workflows/release.yml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ name: Release
33
# Publishes Terminal.Gui.Editor to NuGet.
44
#
55
# Triggers:
6-
# 1. Push of a `v*` tag (canonical release path, e.g. v2.1.0)
6+
# 1. Push of a `v*` tag (canonical release path, e.g. v2.5.3)
77
# → Version = tag with leading `v` stripped.
8-
# 2. Push to `develop` (rolling pre-release smoke test)
9-
# → Version = <Version> from Directory.Build.props + ".<github.run_number>"
10-
# e.g. 2.1.1-develop.7
8+
# 2. Push to `develop` (rolling pre-release)
9+
# → Version computed by GitVersion from git history + GitVersion.yml,
10+
# e.g. 2.5.3-develop.7 (latest reachable tag + Patch, develop label).
1111
#
12-
# The package version is declared in Directory.Build.props.
13-
# The computed value overrides that base via `-p:Version=...`.
12+
# No version is stored in the repo; tags are the source of truth.
13+
# The computed value is injected into builds via `-p:Version=...`.
1414

1515
on:
1616
push:
@@ -31,22 +31,33 @@ jobs:
3131
version: ${{ steps.v.outputs.version }}
3232
steps:
3333
- uses: actions/checkout@v5
34+
with:
35+
# GitVersion needs full history + tags.
36+
fetch-depth: 0
37+
38+
- name: Install GitVersion
39+
if: github.ref_type != 'tag'
40+
uses: gittools/actions/gitversion/setup@v4.5.0
41+
with:
42+
versionSpec: '6.x'
43+
44+
- name: Run GitVersion
45+
if: github.ref_type != 'tag'
46+
id: gitversion
47+
uses: gittools/actions/gitversion/execute@v4.5.0
48+
with:
49+
useConfigFile: true
3450

3551
- name: Compute version
3652
id: v
3753
shell: bash
3854
run: |
3955
if [ "${{ github.ref_type }}" = "tag" ]; then
40-
# Tag form: v2.1.0 → 2.1.0
56+
# Tag form: v2.5.3 → 2.5.3 (the tag is canonical; no GitVersion needed)
4157
VERSION="${GITHUB_REF_NAME#v}"
4258
elif [ "${{ github.ref }}" = "refs/heads/develop" ]; then
43-
# Read base from Directory.Build.props (e.g. "2.1.1-develop"), append run number.
44-
BASE=$(sed -n 's|.*<Version>\(.*\)</Version>.*|\1|p' Directory.Build.props | head -1)
45-
if [ -z "$BASE" ]; then
46-
echo "::error::Could not read <Version> from Directory.Build.props."
47-
exit 1
48-
fi
49-
VERSION="${BASE}.${GITHUB_RUN_NUMBER}"
59+
# GitVersion: latest reachable tag + Patch, develop label, commit count.
60+
VERSION="${{ steps.gitversion.outputs.SemVer }}"
5061
else
5162
echo "::error::Unsupported trigger: event=${{ github.event_name }} ref=${{ github.ref }}"
5263
exit 1

CLAUDE.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ Active development happens on **`develop`**. `main` is the release/stable branch
1313
- Work on `develop`. During pre-alpha, direct commits and pushes to `develop` are allowed — no PRs required for routine work.
1414
- Do not push directly to `main`. Promotion from `develop` to `main` is a deliberate release step.
1515
- Two paths trigger `.github/workflows/release.yml`, which builds + tests cross-platform, then packs and pushes the NuGet package:
16-
- **Push a `v*` tag** (e.g. `v2.1.0`) — canonical stable release; version = tag minus leading `v`.
17-
- **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.
16+
- **Push a `v*` tag** (e.g. `v2.5.3`) — canonical stable release; version = tag minus leading `v`.
17+
- **Push to `develop`** — rolling pre-release; version computed by GitVersion (e.g. `2.5.3-develop.7`).
1818
- 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.
1919

2020
## Versioning
2121

22-
`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.
22+
See `specs/decisions.md` DEC-010. Two independent axes, never conflated:
2323

24-
`<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.
24+
- **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".
25+
- **`<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>`.
26+
27+
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.
2528

2629
## Build and test
2730

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

259262
## Open decisions
260263

261-
`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.
264+
`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.

Directory.Build.props

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,30 @@
1313
<Copyright>Copyright (c) gui-cs and contributors</Copyright>
1414

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

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

3442
<ItemGroup>

0 commit comments

Comments
 (0)