From bbf9c3c2414d6a9e489c7f229a1829fafb3b42b4 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:27:33 -0500 Subject: [PATCH 1/3] chore: align CI Python matrix with devguide release lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run the pytest matrix only on the bugfix (maintenance) releases — 3.13 and 3.14 — instead of 3.11/3.12/3.13, and point the ruff lint job at the latest interpreter (3.14). The supported floor stays at requires-python >= 3.11 (oldest non-EOL security release): older security versions are supported by claim and fixed reactively rather than gated on a wide per-commit matrix. Also add macos-latest to the OS matrix so macOS regressions are caught. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8dde19633..be3caa784c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: - python-version: "3.13" + python-version: "3.14" - name: Run ruff check run: uvx ruff check src/ @@ -30,8 +30,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] - python-version: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.13", "3.14"] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 From fdaaa98608ddf59d7565380b2d0cd03469d968a2 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:27:33 -0500 Subject: [PATCH 2/3] fix: make bash scripts portable to bash 3.2 (macOS system /bin/bash) Adding macos-latest to the CI matrix surfaced two pre-existing bash 3.2 incompatibilities (macOS ships bash 3.2 as /bin/bash): 1. update-agent-context.sh embedded Python heredocs inside $(...) command substitution. bash 3.2 mis-parses an apostrophe in a heredoc body nested in $(...), failing with "unexpected EOF while looking for matching `''". Removed the apostrophes from the affected $()-nested heredoc body and documented the constraint to prevent regressions. 2. create-new-feature-branch.sh and create-new-feature.sh used the bash 4+ ${word^^} uppercase parameter expansion, which errors as a "bad substitution" on bash 3.2 and caused short uppercase acronyms (e.g. "GO") to be dropped from derived branch names. Replaced with a portable `tr '[:lower:]' '[:upper:]'` pipeline. Verified the full test suite passes under bash 3.2.57 and shellcheck (--severity=error) is clean. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/bash/update-agent-context.sh | 14 +++++++++++--- .../git/scripts/bash/create-new-feature-branch.sh | 2 +- scripts/bash/create-new-feature.sh | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/extensions/agent-context/scripts/bash/update-agent-context.sh b/extensions/agent-context/scripts/bash/update-agent-context.sh index c3e5c2020e..67c152d78d 100755 --- a/extensions/agent-context/scripts/bash/update-agent-context.sh +++ b/extensions/agent-context/scripts/bash/update-agent-context.sh @@ -59,6 +59,13 @@ case "$(uname -s 2>/dev/null || true)" in esac # Parse extension config once; emit context files as JSON, followed by marker strings. +# +# NOTE (bash 3.2 / macOS portability): the embedded Python heredocs below run +# inside $(...) command substitution. bash 3.2 (the system /bin/bash on macOS) +# mis-parses a single-quote/apostrophe in a heredoc body nested in $(...), +# failing with "unexpected EOF while looking for matching `''". Keep these +# $(...)-nested heredoc bodies free of apostrophes (use double quotes in Python +# string literals and avoid contractions in comments). if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" "$PROJECT_ROOT" <<'PY' import json import sys @@ -115,7 +122,8 @@ if not context_files: if not context_files: # Self-seed: the agent-context extension owns its lifecycle, so when its # own config declares no target it derives one from the active integration - # recorded in init-options.json, using the extension's OWN bundled mapping + # recorded in init-options.json, using the OWN bundled mapping of the + # agent-context extension # (agent-context-defaults.json). This is independent of the Specify CLI by # design — nothing here imports specify_cli. project_root = sys.argv[3] if len(sys.argv) > 3 else "." @@ -144,7 +152,7 @@ if not context_files: except Exception: print( "agent-context: unable to read %s; cannot self-seed the context " - "file. Set 'context_file' in the extension config." % defaults_path, + "file. Set context_file in the extension config." % defaults_path, file=sys.stderr, ) mapping = {} @@ -152,7 +160,7 @@ if not context_files: if not context_files: print( "agent-context: no default context file is known for integration " - "'%s'. Set 'context_file' in the extension config to choose one." + "%s. Set context_file in the extension config to choose one." % integration_key, file=sys.stderr, ) diff --git a/extensions/git/scripts/bash/create-new-feature-branch.sh b/extensions/git/scripts/bash/create-new-feature-branch.sh index d638b048c9..f025252cc0 100755 --- a/extensions/git/scripts/bash/create-new-feature-branch.sh +++ b/extensions/git/scripts/bash/create-new-feature-branch.sh @@ -288,7 +288,7 @@ generate_branch_name() { if ! echo "$word" | grep -qiE "$stop_words"; then if [ ${#word} -ge 3 ]; then meaningful_words+=("$word") - elif echo "$description" | grep -qw -- "${word^^}"; then + elif echo "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then meaningful_words+=("$word") fi fi diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index c9609764f7..71877a12a9 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -152,7 +152,7 @@ generate_branch_name() { if ! echo "$word" | grep -qiE "$stop_words"; then if [ ${#word} -ge 3 ]; then meaningful_words+=("$word") - elif echo "$description" | grep -q "\b${word^^}\b"; then + elif echo "$description" | grep -q "\b$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')\b"; then # Keep short words if they appear as uppercase in original (likely acronyms) meaningful_words+=("$word") fi From b2472988445048ef188a478cd7f1e91841ba0633 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 30 Jun 2026 06:06:58 -0500 Subject: [PATCH 3/3] fix: address review feedback on bash 3.2 portability changes - create-new-feature.sh: replace the non-portable `\b...\b` grep word-boundary (BSD grep treats `\b` as a backspace, so the acronym branch could silently fail) with `grep -qw`, matching its twin create-new-feature-branch.sh, and pipe the description via `printf '%s'` instead of `echo`. - create-new-feature-branch.sh: switch the acronym check to `printf '%s'` as well so both twins are identical and avoid `echo` on user-provided text. - update-agent-context.sh: reword the apostrophe-free self-seeding comment to be clearer and less easy to misread. Verified under bash 3.2.57 (full bash-script suite green) and shellcheck --severity=error. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/bash/update-agent-context.sh | 11 +++++------ .../git/scripts/bash/create-new-feature-branch.sh | 2 +- scripts/bash/create-new-feature.sh | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/extensions/agent-context/scripts/bash/update-agent-context.sh b/extensions/agent-context/scripts/bash/update-agent-context.sh index 67c152d78d..b7121a2f64 100755 --- a/extensions/agent-context/scripts/bash/update-agent-context.sh +++ b/extensions/agent-context/scripts/bash/update-agent-context.sh @@ -120,12 +120,11 @@ if isinstance(raw_files, list): if not context_files: add_context_file(get_str(data, "context_file")) if not context_files: - # Self-seed: the agent-context extension owns its lifecycle, so when its - # own config declares no target it derives one from the active integration - # recorded in init-options.json, using the OWN bundled mapping of the - # agent-context extension - # (agent-context-defaults.json). This is independent of the Specify CLI by - # design — nothing here imports specify_cli. + # Self-seed: the agent-context extension manages its own lifecycle, so when + # its config declares no target, it derives one from the active integration + # recorded in init-options.json, mapped through the bundled + # agent-context-defaults.json file. This is independent of the Specify CLI + # by design; nothing here imports specify_cli. project_root = sys.argv[3] if len(sys.argv) > 3 else "." integration_key = "" try: diff --git a/extensions/git/scripts/bash/create-new-feature-branch.sh b/extensions/git/scripts/bash/create-new-feature-branch.sh index f025252cc0..a13e49812c 100755 --- a/extensions/git/scripts/bash/create-new-feature-branch.sh +++ b/extensions/git/scripts/bash/create-new-feature-branch.sh @@ -288,7 +288,7 @@ generate_branch_name() { if ! echo "$word" | grep -qiE "$stop_words"; then if [ ${#word} -ge 3 ]; then meaningful_words+=("$word") - elif echo "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then + elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then meaningful_words+=("$word") fi fi diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 71877a12a9..746e88a2dc 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -152,7 +152,7 @@ generate_branch_name() { if ! echo "$word" | grep -qiE "$stop_words"; then if [ ${#word} -ge 3 ]; then meaningful_words+=("$word") - elif echo "$description" | grep -q "\b$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')\b"; then + elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then # Keep short words if they appear as uppercase in original (likely acronyms) meaningful_words+=("$word") fi