diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3a84e80..4895dcf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,69 +14,14 @@ You can also use the command `session-info` for more info on mounts for this pro ## Script Versioning -**CRITICAL RULE**: ALL VERSION NUMBERS MUST BE IDENTICAL ACROSS ALL FILES. No exceptions. +Don't edit version strings by hand. CI sets them. -### Version Format +- Format: `YYYY.MM.DD.N`. `N` is the Nth release of that day. +- `copilot_here.sh`, `copilot_here.ps1`, and `app/Infrastructure/BuildInfo.cs` all carry the literal placeholder `0.0.0-dev` in source. They stay that way. +- The `compute-version` job in `.github/workflows/publish.yml` picks today's UTC date and the daily iteration. `scripts/stamp-version.sh` then writes the real string into the shell scripts before tests and packaging. The .NET binary's version comes from `Directory.Build.props` (local default: `today.0`) or from `-p:CopilotHereVersion=...` in CI. `BuildInfo.BuildDate` reads back from the assembly's `AssemblyInformationalVersion` at runtime. +- There is no `VERSION` file and no `bump-version.sh`. If you're reaching for one of those, stop. Open a PR with the code change and let CI pick the next number on merge. -- **Primary version**: Use current date in format `YYYY.MM.DD` (e.g., `2025.12.02`) -- **Same-day updates**: If the version date already matches today's date, append `.1`, `.2`, `.3`, etc. - - Example: `2025.12.02` → `2025.12.02.1` → `2025.12.02.2` -- **CRITICAL**: Always increment the version when making changes - this triggers re-download for users - -### Where to Update Versions (ALL MUST MATCH) - -**EVERY TIME** you modify shell functions, CLI binary code, or any functionality, update ALL FOUR version locations to the SAME version: - -1. **Bash/Zsh script**: `copilot_here.sh` - - - Line 2: `# Version: YYYY.MM.DD` - - Line 8: `COPILOT_HERE_VERSION="YYYY.MM.DD"` - -2. **PowerShell script**: `copilot_here.ps1` - - - Line 2: `# Version: YYYY.MM.DD` - - Line 8: `$script:CopilotHereVersion = "YYYY.MM.DD"` - -3. **Build properties**: `Directory.Build.props` - - - Reads `CopilotHereVersion` from the `VERSION` file automatically (no manual edit needed) - -4. **Build info**: `app/Infrastructure/BuildInfo.cs` - - - Line 13: `public const string BuildDate = "YYYY.MM.DD";` - -### Verification Checklist - -Before committing, verify all 5 locations have the EXACT SAME version: - -```bash -# Quick check - all should show the same version -cat VERSION -grep "Version: " copilot_here.sh -grep "Version: " copilot_here.ps1 -grep "COPILOT_HERE_VERSION=" copilot_here.sh -grep "CopilotHereVersion =" copilot_here.ps1 -grep "BuildDate = " app/Infrastructure/BuildInfo.cs -``` - -Use `scripts/bump-version.sh YYYY.MM.DD` to update all locations at once. - -### When to Update Version - -- Any modification to shell function code -- Adding new features or options -- Bug fixes in the scripts or CLI binary -- Changes to the CLI binary code -- **Any commit that affects functionality should increment the version** -- **When in doubt, increment the version** - -### Script File Synchronization - -**CRITICAL**: The standalone script files (`copilot_here.sh` and `copilot_here.ps1`) are the source of truth. - -- The README.md uses `curl` commands to download these files directly from the repository. -- Ensure both scripts are kept in sync regarding functionality and version numbers. -- Both scripts MUST have identical version numbers at all times. +See `docs/versioning.md` for the full flow. ## Technology Stack @@ -707,7 +652,6 @@ git commit --no-gpg-sign -m "feat: Add multi-image build pipeline" \ 2. Review documentation in `/docs` for requirements 3. Ensure changes align with project goals 4. Consider impact on both native binary and shell wrappers -5. Check if version numbers need updating ### Making Changes @@ -715,7 +659,6 @@ git commit --no-gpg-sign -m "feat: Add multi-image build pipeline" \ 2. Follow existing code patterns and conventions 3. Update relevant documentation if making structural changes 4. Test changes locally before committing -5. Update version numbers if changing functionality (see Script Versioning section) ### After Making Changes @@ -807,7 +750,6 @@ docker run --rm -it copilot_here:test copilot --version - Test affected functionality - Review file changes with `git diff` - Ensure documentation is updated -- Check version numbers are updated if functionality changed ### Edge Cases to Consider @@ -869,9 +811,8 @@ Each image variant gets multiple tags: 9. **Minor tasks update existing files** - Don't create duplicate task files 10. **Document major changes** - Create task files for significant work 11. **Test before committing** - Build binary and verify functionality -12. **Update versions** - Increment version numbers when changing functionality -13. **Stop containers before updating** - dev-build.sh will prompt to stop running containers -14. **Debug logging** - Use `COPILOT_HERE_DEBUG=1` to enable detailed logging +12. **Stop containers before updating** - dev-build.sh will prompt to stop running containers +13. **Debug logging** - Use `COPILOT_HERE_DEBUG=1` to enable detailed logging ### When to Update These Instructions diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 849e7fd..b52b70c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -83,7 +83,7 @@ jobs: # Test shell function scripts can be sourced and call the native binary test-shell-functions: name: Shell Functions (${{ matrix.os }}) - needs: [test-cli] + needs: [test-cli, compute-version] runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -98,8 +98,17 @@ jobs: with: global-json-file: global.json + # Stamp the in-memory `0.0.0-dev` sentinel to the computed version so the + # update-check inside copilot_here.sh / .ps1 doesn't compare against a + # remote-newer release and prompt during the --version test. + - name: Stamp version into shell scripts + shell: bash + run: | + chmod +x scripts/stamp-version.sh + ./scripts/stamp-version.sh "${{ needs.compute-version.outputs.version }}" + - name: Build CLI binary - run: dotnet publish app/CopilotHere.csproj -c Release -o publish/shell-test --nologo + run: dotnet publish app/CopilotHere.csproj -c Release -p:CopilotHereVersion=${{ needs.compute-version.outputs.version }} -o publish/shell-test --nologo - name: Test Bash script syntax if: runner.os != 'Windows' @@ -565,7 +574,16 @@ jobs: working-directory: tests/CopilotHere.IntegrationTests run: dotnet run --configuration Release - # Compute version from VERSION file + run number as revision + # Compute version: today's UTC date + iteration-of-the-day. + # + # `Release` mode (main + push OR workflow_dispatch) → iteration = + # (count of cli-v$DATE.* releases) + 1, matching the schema in + # docs/versioning.md. + # `Dev-stamp` mode (everything else) → iteration = COUNT (the latest + # already-released number for today). PR / scheduled / branch builds + # need to match-or-beat the cli-latest version so the shell wrappers' + # startup update-check doesn't fire during test-shell-functions; if no + # release exists yet today, COUNT is 0 and we stamp `$DATE.0`. compute-version: name: Compute Version runs-on: ubuntu-24.04 @@ -573,18 +591,35 @@ jobs: version: ${{ steps.compute.outputs.version }} short_sha: ${{ steps.compute.outputs.short_sha }} steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - name: Compute version with revision + - name: Compute version id: compute + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} run: | - BASE_VERSION=$(cat VERSION | tr -d '[:space:]') - VERSION="${BASE_VERSION}.${{ github.run_number }}" - echo "version=$VERSION" >> $GITHUB_OUTPUT + DATE=$(date -u +%Y.%m.%d) SHORT_SHA="${GITHUB_SHA:0:7}" echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT - echo "Computed version: $VERSION (run_number=${{ github.run_number }})" + + COUNT=$(gh release list --limit 200 --json tagName \ + --jq "[.[] | select(.tagName | startswith(\"cli-v${DATE}.\"))] | length") + + IS_RELEASE=false + if [[ "${{ github.ref }}" == "refs/heads/main" ]] && \ + { [[ "${{ github.event_name }}" == "push" ]] || \ + [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; }; then + IS_RELEASE=true + fi + + if [[ "$IS_RELEASE" == "true" ]]; then + N=$((COUNT + 1)) + VERSION="${DATE}.${N}" + echo "Release version: ${VERSION} (release #${N} for ${DATE})" + else + VERSION="${DATE}.${COUNT}" + echo "Dev-stamp version: ${VERSION} (event=${{ github.event_name }}, ref=${{ github.ref }}, existing releases today=${COUNT})" + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT # Publish .NET Tool to NuGet publish-nuget: @@ -741,6 +776,15 @@ jobs: with: path: artifacts + # Stamp the source-of-truth scripts before copying them into release/. + # Earlier this ran *after* the copy and silently left the released + # copilot_here.sh / .ps1 at whatever the sentinel value was, breaking + # users' auto-update check. + - name: Stamp version into shell scripts + run: | + chmod +x scripts/stamp-version.sh + ./scripts/stamp-version.sh "${{ needs.compute-version.outputs.version }}" + - name: Prepare release assets run: | mkdir -p release @@ -750,7 +794,7 @@ jobs: else binary="copilot_here" fi - + # Create archive cd artifacts/copilot_here-$rid if [[ "$rid" == win-* ]]; then @@ -770,11 +814,6 @@ jobs: ls -la release/ - - name: Stamp version into release shell scripts - run: | - chmod +x scripts/stamp-version.sh - ./scripts/stamp-version.sh "${{ needs.compute-version.outputs.version }}" - - name: Create versioned release uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: diff --git a/Directory.Build.props b/Directory.Build.props index 3e32ac2..16bb6e7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,11 @@ - - $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)VERSION').Trim()) + + $([System.DateTime]::UtcNow.ToString('yyyy.MM.dd')).0 diff --git a/VERSION b/VERSION deleted file mode 100644 index b4cbaab..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2026.05.13 diff --git a/app/Infrastructure/BuildInfo.cs b/app/Infrastructure/BuildInfo.cs index 3e842c3..7f1d568 100644 --- a/app/Infrastructure/BuildInfo.cs +++ b/app/Infrastructure/BuildInfo.cs @@ -1,14 +1,46 @@ +using System.Reflection; + namespace CopilotHere.Infrastructure; -/// -/// Provides build-time information for the application. -/// The BuildDate is stamped during CI/CD from the VERSION file via scripts/stamp-version.sh. -/// public static class BuildInfo { - /// - /// The build date in yyyy.MM.dd or yyyy.MM.dd.N format. - /// This is replaced during build via MSBuild property. - /// - public const string BuildDate = "2026.05.13"; + public static string BuildDate { get; } = ComputeBuildDate(); + + private static string ComputeBuildDate() + { + var informational = typeof(BuildInfo).Assembly + .GetCustomAttribute() + ?.InformationalVersion; + + if (string.IsNullOrEmpty(informational) || + informational.StartsWith("0.0.0-dev", StringComparison.Ordinal)) + { + return "0.0.0-dev"; + } + + // Strip the git-sha suffix appended by CopilotHere.csproj + // (`$(Version).$(GitSha)`) + // so consumers see just the YYYY.MM.DD.N portion. Stop at the 5th + // segment, or a '+' (SemVer build metadata). Pre-release tags ('-dev' + // etc.) are handled by the sentinel check above so we don't accidentally + // truncate them mid-string. + var dots = 0; + for (var i = 0; i < informational.Length; i++) + { + var c = informational[i]; + if (c == '+') + { + return informational[..i]; + } + if (c == '.') + { + dots++; + if (dots == 4) + { + return informational[..i]; + } + } + } + return informational; + } } diff --git a/copilot_here.ps1 b/copilot_here.ps1 index 9bf3215..c129a64 100644 --- a/copilot_here.ps1 +++ b/copilot_here.ps1 @@ -1,5 +1,5 @@ # copilot_here PowerShell functions -# Version: 2026.05.13 +# Version: 0.0.0-dev # Repository: https://github.com/GordonBeeming/copilot_here # Set console output encoding to UTF-8 for Unicode character support @@ -23,7 +23,7 @@ $script:DefaultCopilotHereBin = Join-Path $script:DefaultCopilotHereBinDir $scri $script:CopilotHereBin = if ($env:COPILOT_HERE_BIN) { $env:COPILOT_HERE_BIN } else { $script:DefaultCopilotHereBin } $script:CopilotHereReleaseUrl = "https://github.com/GordonBeeming/copilot_here/releases/download/cli-latest" -$script:CopilotHereVersion = "2026.05.13" +$script:CopilotHereVersion = "0.0.0-dev" # Debug logging function function Write-CopilotDebug { diff --git a/copilot_here.sh b/copilot_here.sh index 17a47c7..dfb08cb 100755 --- a/copilot_here.sh +++ b/copilot_here.sh @@ -1,11 +1,11 @@ # copilot_here shell functions -# Version: 2026.05.13 +# Version: 0.0.0-dev # Repository: https://github.com/GordonBeeming/copilot_here # Configuration COPILOT_HERE_BIN="${COPILOT_HERE_BIN:-$HOME/.local/bin/copilot_here}" COPILOT_HERE_RELEASE_URL="https://github.com/GordonBeeming/copilot_here/releases/download/cli-latest" -COPILOT_HERE_VERSION="2026.05.13" +COPILOT_HERE_VERSION="0.0.0-dev" # Ensure user bin directory is on PATH (required for the native binary + shell integration checks) if [ -d "$HOME/.local/bin" ]; then diff --git a/docs/versioning.md b/docs/versioning.md index 611262d..3299187 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -1,52 +1,36 @@ # Versioning -## Single Source of Truth - -The `VERSION` file in the repository root contains the base version. All other version references are derived from it. - ## Format -`YYYY.MM.DD.N` where `.N` is the GitHub Actions run number (e.g., `2026.03.08.42`). - -- The **date portion** is manually maintained by developers. -- The **revision** is the GitHub Actions `run_number`, which auto-increments on every workflow run, ensuring every build has a unique version. - -## How It Works +`YYYY.MM.DD.N`. Date plus iteration. `N` is the Nth release of that day, so the first release on a given day is `.1`, the second is `.2`, and so on. -### VERSION file -Contains a single line with the base version (e.g., `2026.03.08`). +## Releasing a new version -### scripts/stamp-version.sh -Takes a version argument and stamps it into all locations that contain version strings: -- `copilot_here.sh` (comment + variable) -- `copilot_here.ps1` (comment + variable) -- `app/Infrastructure/BuildInfo.cs` (BuildDate constant) +Open a PR. Merge it to `main`. That's the whole process. -Files keep real versions in source (not placeholders) so local dev works without stamping. +There is no version to bump and no file to edit. When the merge lands, the `compute-version` job in `.github/workflows/publish.yml` reads today's UTC date, counts the existing `cli-v$DATE.*` releases, picks the next number, and runs `scripts/stamp-version.sh` to write it into the shell scripts before they get packaged. The .NET binary picks up the same version through `-p:CopilotHereVersion=...` on the `dotnet` command line. -WinGet manifests are not stamped — `wingetcreate update` reads them from `microsoft/winget-pkgs` and writes new ones there using the `--version` flag. Local copies under `packaging/winget/` are gitignored and used only for the first manual submission. +## Where the version lives -### scripts/bump-version.sh -Convenience wrapper: updates the VERSION file and runs stamp-version.sh. +| Path | Source value | Stamped to | +| --- | --- | --- | +| `copilot_here.sh` (2 lines) | `0.0.0-dev` | real version, in CI only | +| `copilot_here.ps1` (2 lines) | `0.0.0-dev` | real version, in CI only | +| `app/Infrastructure/BuildInfo.cs` | derives at runtime from the assembly | n/a | +| `Directory.Build.props` | falls back to `today.0` if `-p:CopilotHereVersion` isn't passed | overridden in CI | -```bash -./scripts/bump-version.sh 2026.03.08 -``` +The `0.0.0-dev` placeholder stays in source forever. Stamping happens on a fresh checkout in CI and is never committed back. -### Directory.Build.props -Reads the version from the VERSION file at build time. CI can override with `-p:CopilotHereVersion=X.Y.Z.N`. +## CI behaviour -### CI (publish.yml) -The `compute-version` job runs on every workflow trigger and: -1. Reads the base version from `VERSION` -2. Appends `github.run_number` as the revision -3. Outputs the full version: `YYYY.MM.DD.N` (e.g., `2026.03.08.42`) +- Push to `main`: `N` = `gh release list` count of `cli-v$DATE.*` + 1. Real release. +- Pull request, schedule, manual dispatch from a branch: `N` = `0`. The dev stamp keeps the version-format tests passing without consuming a release slot. -The `build-cli` job runs `stamp-version.sh` before building, so all artifacts contain the computed version. +## Local development -## Releasing a New Version +`dotnet run --project app -- --version` prints today's date with `.0` because `Directory.Build.props` falls back to that when no version override is passed. To preview what a real stamp would look like, pass it explicitly: -1. Update the date in `VERSION` -2. Run `./scripts/bump-version.sh YYYY.MM.DD` -3. Commit and push to `main` -4. CI auto-computes the revision and publishes +```bash +dotnet publish app/CopilotHere.csproj -c Release -p:CopilotHereVersion=2099.01.02.7 -o /tmp/preview +./scripts/stamp-version.sh 2099.01.02.7 # stamps the shell scripts; revert with `git restore` +``` diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh deleted file mode 100755 index 2900ab1..0000000 --- a/scripts/bump-version.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# bump-version.sh — Update VERSION file and stamp all files. -# Usage: ./scripts/bump-version.sh -# Example: ./scripts/bump-version.sh 2026.03.08 - -set -euo pipefail - -if [ $# -ne 1 ]; then - echo "Usage: $0 " >&2 - echo "Example: $0 2026.03.08" >&2 - exit 1 -fi - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -echo "Bumping version to: $1" -"$SCRIPT_DIR/stamp-version.sh" "$1" diff --git a/scripts/stamp-version.sh b/scripts/stamp-version.sh index 9b1b262..ecbfce9 100755 --- a/scripts/stamp-version.sh +++ b/scripts/stamp-version.sh @@ -1,26 +1,35 @@ #!/usr/bin/env bash -# stamp-version.sh — Stamps a version string into all files that contain it. +# stamp-version.sh — Stamps a version string into the shell scripts so users +# who download the released `copilot_here.sh` / `copilot_here.ps1` get a real +# version their auto-update check can compare against. +# +# Source files carry `0.0.0-dev` placeholders; CI calls this script with the +# version produced by the `compute-version` job before testing and packaging. +# The .NET binary's version is set via -p:CopilotHereVersion=... on the dotnet +# invocation — it doesn't need stamping into source. +# # Usage: ./scripts/stamp-version.sh -# Example: ./scripts/stamp-version.sh 2026.03.08 +# Example: ./scripts/stamp-version.sh 2026.05.21.1 set -euo pipefail if [ $# -ne 1 ]; then echo "Usage: $0 " >&2 - echo "Example: $0 2026.03.08" >&2 + echo "Example: $0 2026.05.21.1" >&2 exit 1 fi NEW_VERSION="$1" -# Validate version format: YYYY.MM.DD or YYYY.MM.DD.N if ! echo "$NEW_VERSION" | grep -qE '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$'; then echo "Error: Version must be in YYYY.MM.DD or YYYY.MM.DD.N format" >&2 exit 1 fi REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -VERSION_REGEX='[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?' +# Matches either the dev sentinel or any previously-stamped real version. +# Flat alternation (no nested groups) keeps BSD sed -E happy on macOS. +VERSION_REGEX='(0\.0\.0-dev|[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+|[0-9]{4}\.[0-9]{2}\.[0-9]{2})' stamp_file() { local file="$1" @@ -32,12 +41,13 @@ stamp_file() { return fi + # Use `~` as the sed delimiter: `|` is the ERE alternation operator and + # `#` appears in the script comments we're matching, so both would terminate + # the s/// expression mid-pattern. `~` shows up nowhere in our regexes. if sed --version >/dev/null 2>&1; then - # GNU sed - sed -i -E "s|${pattern}|${replacement}|g" "$file" + sed -i -E "s~${pattern}~${replacement}~g" "$file" else - # BSD sed (macOS) - sed -i '' -E "s|${pattern}|${replacement}|g" "$file" + sed -i '' -E "s~${pattern}~${replacement}~g" "$file" fi echo " OK $file" } @@ -45,11 +55,6 @@ stamp_file() { echo "Stamping version: $NEW_VERSION" echo "" -# VERSION file -echo "$NEW_VERSION" > "$REPO_ROOT/VERSION" -echo " OK VERSION" - -# copilot_here.sh — line 2 (comment) and line 8 (variable) stamp_file "$REPO_ROOT/copilot_here.sh" \ "^(# Version: )${VERSION_REGEX}" \ "\1${NEW_VERSION}" @@ -57,7 +62,6 @@ stamp_file "$REPO_ROOT/copilot_here.sh" \ "^(COPILOT_HERE_VERSION=\")${VERSION_REGEX}(\")" \ "\1${NEW_VERSION}\3" -# copilot_here.ps1 — line 2 (comment) and line 26 (variable) stamp_file "$REPO_ROOT/copilot_here.ps1" \ "^(# Version: )${VERSION_REGEX}" \ "\1${NEW_VERSION}" @@ -65,10 +69,5 @@ stamp_file "$REPO_ROOT/copilot_here.ps1" \ "^(\\\$script:CopilotHereVersion = \")${VERSION_REGEX}(\")" \ "\1${NEW_VERSION}\3" -# app/Infrastructure/BuildInfo.cs -stamp_file "$REPO_ROOT/app/Infrastructure/BuildInfo.cs" \ - "(BuildDate = \")${VERSION_REGEX}(\")" \ - "\1${NEW_VERSION}\3" - echo "" echo "Done."