From 30c9c23e2f8e61ea2df9c2cfb2e5dc2c4220b900 Mon Sep 17 00:00:00 2001 From: Steffen Maechtel Date: Fri, 25 Sep 2026 22:27:36 +0200 Subject: [PATCH 1/3] feat: opk upgrade-opencode stays on the current major; --major/--version switches (issue #99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opk upgrade-opencode used GitHub releases/latest — always 1.x — so a 2.x install was silently downgraded (v2.0.11 -> 1.18.32). Version resolution now never crosses majors: - default: the latest release of the CURRENT major — 1.x through GitHub releases/latest, 2.x through the npm dist-tag latest of @opencode/cli- (the channel the official v2 installer resolves through; legacy @opencode-ai scope as fallback). A resolution off the requested major fails loudly. - --major 1|2 switches the major explicitly; --version pins an exact release (channel by prefix). Both work through opk upgrade-opencode pass-through. - a major flip re-anchors the TUI registration immediately (sync_tui_registration: 2.x plugin dir registered / removed on 1.x) even in --only-binary runs; full updates keep it anchored to the stamp. - detect_target split out of detect_asset (npm package suffix == GitHub target string). Unit tests: fake-curl resolution matrix (majors, off-major dist-tag refusal, npm vs GitHub tarball layouts, current-major default). e2e: new 2.x-gated section 11d runs a live 'opk upgrade-opencode' and asserts the binary stays 2.x with the plugin registered. Docs: cli.md, how-to/update.md, opencode-2x.md §5. --- docs/design/opencode-2x.md | 11 + docs/how-to/update.md | 4 +- docs/reference/cli.md | 20 +- files/opencode-permissions-kit-lib/bin/opk | 10 +- .../management/update.sh | 239 ++++++++++++++---- tests/e2e/run.sh | 24 ++ tests/unit/test-tui-mode.sh | 6 +- tests/unit/test-update-flags.sh | 121 +++++++-- 8 files changed, 361 insertions(+), 74 deletions(-) diff --git a/docs/design/opencode-2x.md b/docs/design/opencode-2x.md index d81e052..469d8d4 100644 --- a/docs/design/opencode-2x.md +++ b/docs/design/opencode-2x.md @@ -88,6 +88,17 @@ edits reload only after seconds). Both were observed in the e2e as then a pkill fallback that catches a wedged daemon; both best-effort, 1.x ignores the unknown subcommand) and re-stamps `OPENCODE_MAJOR`. +*Issue #99:* version upgrades never cross majors silently. Without flags +the latest release **of the installed major** resolves — 1.x through +GitHub `releases/latest`, 2.x through the npm dist-tag `latest` of +`@opencode/cli-` (§8's channel split, now in `update.sh` itself; +a resolution off the requested major fails loudly instead). `--major 1|2` +switches majors explicitly, `--version ` pins an exact release. A +major flip re-anchors the TUI registration immediately +(`sync_tui_registration`: plugin dir on 2.x, removed on 1.x — the 1.x +tui.json/danger theme are major-agnostic and stay), even in +`--only-binary` runs. + ## 6. Headless classification: subcommand union The wrapper's HEADLESS list keeps every 1.x name (unknown to 2.x, which diff --git a/docs/how-to/update.md b/docs/how-to/update.md index 6a2ee1f..0f5f4b0 100644 --- a/docs/how-to/update.md +++ b/docs/how-to/update.md @@ -114,7 +114,9 @@ the bundled config sets `autoupdate: false`, and `update.sh` is the upgrade entry point: ```bash -opk upgrade-opencode # latest release +opk upgrade-opencode # latest release of the current major +opk upgrade-opencode --major 2 # switch to the latest opencode 2.x +opk upgrade-opencode --version 2.0.11 # pin an exact version opk upgrade-opencode --binary-path ./opencode # specific file # equivalent long form: opk update --only-binary diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 940d26b..93a42d8 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -184,14 +184,26 @@ sudo opk update --channel stable # or master, a feature branch, or a tag |---|---| | `--yes` | Skip the confirmation prompt | | `--refresh` | Also re-apply the group baseline | -| `--binary` | Also upgrade the opencode binary to the latest release | +| `--binary` | Also upgrade the opencode binary to the latest release **of the current major** | | `--only-binary` | Skip every kit step, only upgrade the opencode binary | | `--binary-path ` | Install a specific binary file instead | | `--channel ` | Switch the tracking ref for this and every future update (re-stamps `KIT_CHANNEL`) | +| `--major 1\|2` | Switch the opencode major (TUI registration flips with it) | +| `--version ` | Upgrade to exactly this opencode version (channel by prefix: `2.*` from npm, `1.x` from GitHub) | -`opk upgrade-opencode` is the shorthand for -`update --yes --only-binary` — extra flags (e.g. `--binary-path`) pass -through. +Upgrades never cross majors silently (issue #99): without `--major` / +`--version` the latest release **of the installed major** is used — 1.x +resolves through GitHub releases, 2.x through the npm dist-tag `latest` +(the channel the official v2 installer uses). `opk upgrade-opencode` is +the shorthand for `update --yes --only-binary` — extra flags (e.g. +`--binary-path`, `--major 2`, `--version 2.0.11`) pass through: + +```bash +sudo opk upgrade-opencode # latest of the current major +sudo opk upgrade-opencode --major 2 # switch 1.x -> 2.x (latest 2.x) +sudo opk upgrade-opencode --major 1 # switch back to latest 1.x +sudo opk upgrade-opencode --version 2.0.11 +``` Never touches `projects.conf` or the agent's `opencode.jsonc`. See [update](../how-to/update.md). diff --git a/files/opencode-permissions-kit-lib/bin/opk b/files/opencode-permissions-kit-lib/bin/opk index a3fe855..613eca2 100755 --- a/files/opencode-permissions-kit-lib/bin/opk +++ b/files/opencode-permissions-kit-lib/bin/opk @@ -67,10 +67,14 @@ Commands: skips every kit step and only upgrades it; --channel switches the tracking ref (stable, master, a branch, a tag) for this - and every future update + and every future update; --major 1|2 + switches the opencode major, --version + pins an exact version (default: latest of + the current major — never crosses majors) upgrade-opencode [flags...] Just upgrade the opencode binary — shorthand - for 'update --yes --only-binary' (extra - flags pass through, e.g. --binary-path) + for 'update --yes --only-binary' (extra + flags pass through, e.g. --binary-path, + --major 1|2, --version ) ddev-hosts-add [dir|hostname] Add ddev hostnames to the Windows hosts file (ddev's own UAC flow; run as your user, Windows asks permission). A dir adds diff --git a/files/opencode-permissions-kit-lib/management/update.sh b/files/opencode-permissions-kit-lib/management/update.sh index d8149eb..55f7961 100755 --- a/files/opencode-permissions-kit-lib/management/update.sh +++ b/files/opencode-permissions-kit-lib/management/update.sh @@ -243,6 +243,8 @@ REFRESH=false BINARY_UPDATE=false ONLY_BINARY=false BINARY_PATH="" +UPGRADE_MAJOR="" +UPGRADE_VERSION="" while [ "$#" -gt 0 ]; do case "$1" in --yes|-y) YES=true ;; @@ -262,21 +264,43 @@ while [ "$#" -gt 0 ]; do --channel) # consumed by the pre-scan above (before the self-fetch); # accepted here so it never reaches the unknown-option trap - [ "$#" -ge 2 ] || { echo "error: --channel requires a ref (stable, master, a branch, or a tag)" >&2; exit 1; } + [ "$#" -ge 2 ] || { echo "error: --channel requires a ref (stable, master, a feature branch, or a tag)" >&2; exit 1; } + shift + ;; + --major) + # issue #99: switch the opencode major (1 <-> 2); without it + # upgrades stay within the current major + [ "$#" -ge 2 ] || { echo "error: --major requires a number (1 or 2)" >&2; exit 1; } + case "$2" in + 1|2) UPGRADE_MAJOR="$2" ;; + *) echo "error: --major must be 1 or 2" >&2; exit 1 ;; + esac + shift + ;; + --version) + # issue #99: pin an exact opencode version; the download + # channel follows the version prefix (2.* npm, 1.x GitHub) + [ "$#" -ge 2 ] || { echo "error: --version requires an opencode version (e.g. 2.0.11)" >&2; exit 1; } + UPGRADE_VERSION="$2" shift ;; -h|--help) cat <] [--channel ] +Usage: ./update.sh [--yes] [--refresh] [--binary] [--only-binary] [--binary-path ] [--channel ] [--major 1|2] [--version ] --yes skip the confirmation prompt --refresh also re-apply the group baseline (chgrp/setgid/default ACLs) --binary also upgrade the opencode binary to the latest release + of the CURRENT major (issue #99: never crosses majors) --only-binary skip every kit step, ONLY upgrade the opencode binary --binary-path install the given binary file instead of downloading --channel switch the tracking ref for this and every future update (stable, master, a feature branch, or a pinned tag) + --major switch the opencode major: 1 or 2 (default: stay on the + current major; the TUI registration flips with it) + --version upgrade to exactly this opencode version (channel by + prefix: 2.* from npm, 1.x from GitHub) EOF exit 0 ;; @@ -568,8 +592,10 @@ if [ -x "$SYSTEM_BIN" ]; then sudo chmod 750 "$SYSTEM_BIN" 2>/dev/null || true fi -# Detect the release asset name for this host (mirrors the official installer). -detect_asset() { +# Detect the opencode release target for this host (mirrors the official +# installer; the same string is the npm package suffix, see +# tests/e2e/lib.sh). +detect_target() { local os arch target os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in @@ -587,7 +613,12 @@ detect_asset() { if [ "$os" = "linux" ] && { [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; }; then target="$target-musl" fi - echo "opencode-$target.tar.gz" + echo "$target" +} + +# Release asset name for this host (1.x GitHub assets). +detect_asset() { + echo "opencode-$(detect_target).tar.gz" } # Verify a candidate binary actually runs, then install it over $SYSTEM_BIN. @@ -624,28 +655,121 @@ install_binary() { log "opencode binary upgraded: ${current} -> ${new}" } -# Download + extract the latest opencode release into . Prints the -# candidate binary path on success, nothing on failure. The CALLER owns -# — cleanup happens only after the install attempt (the old flow -# deleted the extracted candidate before verification could run: every -# downloaded upgrade failed with "candidate failed verification", -# issue #24). -fetch_latest_opencode() { - _flo_dst="${1:-}" - [ -n "$_flo_dst" ] && [ -d "$_flo_dst" ] || return 1 - _flo_ver=$(curl -fsSL --max-time 10 https://api.github.com/repos/anomalyco/opencode/releases/latest 2>/dev/null \ - | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) - [ -n "$_flo_ver" ] || return 1 - _flo_asset=$(detect_asset || true) - [ -n "$_flo_asset" ] || return 1 - curl -fsSL --max-time 120 "https://github.com/anomalyco/opencode/releases/download/v$_flo_ver/$_flo_asset" \ - -o "$_flo_dst/opencode.tar.gz" || return 1 - tar -xzf "$_flo_dst/opencode.tar.gz" -C "$_flo_dst" || return 1 - [ -x "$_flo_dst/opencode" ] || return 1 - echo "$_flo_dst/opencode" +# Major of an opencode --version line (issue #99): 2.x prints +# "opencode v2.0.11" -> 2; 1.x prints the bare version ("1.18.31") -> 1. +version_major() { + printf '%s' "$1" | sed -n 's/^opencode v\([0-9][0-9]*\).*/\1/p' +} + +# Major of the currently installed binary — the source of truth; the +# install.conf stamp is the fallback (older kits), 1 the last resort. +current_opencode_major() { + _com_ver=$("$SYSTEM_BIN" --version 2>/dev/null | head -1 || true) + if [ -n "$_com_ver" ]; then + _com_maj=$(version_major "$_com_ver") + [ -n "$_com_maj" ] || _com_maj=1 + echo "$_com_maj" + return 0 + fi + _com_maj=$(sed -n 's/^OPENCODE_MAJOR=//p' "$CONFDIR/install.conf" 2>/dev/null | tail -1) + echo "${_com_maj:-1}" +} + +# Latest version for a major (issue #99): upgrades never hop majors +# silently. 1.x resolves through GitHub releases/latest (still the 1.x +# channel), 2.x through the npm dist-tag `latest` of @opencode/cli- +# — the channel the official v2 installer resolves through; 2.x ships no +# GitHub release assets (docs/design/opencode-2x.md §8). A resolution +# outside the requested major fails loudly instead of crossing majors. +resolve_latest_opencode_version() { + _rlov_major="$1" + _rlov_target=$(detect_target) || return 1 + if [ "$_rlov_major" = 2 ]; then + _rlov_ver=$(curl -fsSL --max-time 10 "https://registry.npmjs.org/@opencode/cli-$_rlov_target" 2>/dev/null \ + | tr ',' '\n' | sed -n 's/.*"latest": *"\([^"]*\)".*/\1/p' | head -1 || true) + case "$_rlov_ver" in + 2.*) echo "$_rlov_ver"; return 0 ;; + *) return 1 ;; + esac + else + _rlov_ver=$(curl -fsSL --max-time 10 https://api.github.com/repos/anomalyco/opencode/releases/latest 2>/dev/null \ + | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + case "$_rlov_ver" in + 1.*) echo "$_rlov_ver"; return 0 ;; + *) return 1 ;; + esac + fi +} + +# Download + extract one exact opencode version into (channel by +# version prefix, same split as tests/e2e/lib.sh): 2.* from the npm +# registry (legacy @opencode-ai scope as fallback for pre-migration +# versions, tarball layout package/bin/opencode), 1.x from GitHub release +# assets. Prints the candidate binary path on success, nothing on failure. +# The CALLER owns — cleanup happens only after the install attempt +# (issue #24). +fetch_opencode_version() { + _fov_dst="$1" _fov_ver="$2" + [ -n "$_fov_dst" ] && [ -d "$_fov_dst" ] || return 1 + _fov_target=$(detect_target) || return 1 + case "$_fov_ver" in + 2.*) + if ! curl -fsSL --max-time 240 "https://registry.npmjs.org/@opencode/cli-$_fov_target/-/cli-$_fov_target-$_fov_ver.tgz" \ + -o "$_fov_dst/opencode.tar.gz" 2>/dev/null; then + curl -fsSL --max-time 240 "https://registry.npmjs.org/@opencode-ai/cli-$_fov_target/-/cli-$_fov_target-$_fov_ver.tgz" \ + -o "$_fov_dst/opencode.tar.gz" || return 1 + fi + mkdir -p "$_fov_dst/npmx" || return 1 + tar -xzf "$_fov_dst/opencode.tar.gz" -C "$_fov_dst/npmx" || return 1 + [ -x "$_fov_dst/npmx/package/bin/opencode" ] || return 1 + mv "$_fov_dst/npmx/package/bin/opencode" "$_fov_dst/opencode" || return 1 + rm -rf "$_fov_dst/npmx" "$_fov_dst/opencode.tar.gz" + ;; + *) + curl -fsSL --max-time 240 "https://github.com/anomalyco/opencode/releases/download/v$_fov_ver/opencode-$_fov_target.tar.gz" \ + -o "$_fov_dst/opencode.tar.gz" || return 1 + tar -xzf "$_fov_dst/opencode.tar.gz" -C "$_fov_dst" || return 1 + rm -f "$_fov_dst/opencode.tar.gz" + ;; + esac + [ -x "$_fov_dst/opencode" ] || return 1 + echo "$_fov_dst/opencode" return 0 } +# Latest release for a major, downloaded into (issue #99 wrapper). +fetch_latest_opencode() { + _fll_dst="$1" _fll_major="$2" + [ -n "$_fll_major" ] || _fll_major=$(current_opencode_major) + _fll_ver=$(resolve_latest_opencode_version "$_fll_major") || return 1 + fetch_opencode_version "$_fll_dst" "$_fll_ver" || return 1 +} + +# TUI mode display per major (issue #80): the 1.x artifacts (tui.json + +# danger theme) are major-agnostic — 2.x ignores them and they make a 1.x +# swap work instantly. Only the 2.x CLI-plugin dir flips: present on 2.x +# (symlink to kit-mode-2x.tsx, LIBDIR stays the source of truth), removed +# on 1.x. Inert file-path entries from pre-0.0.35 kits are unregistered +# best-effort on both paths. +sync_tui_registration() { + _str_major="$1" + for _str_dir_user in "/home/$OPENCODE_USER/.config/opencode:$OPENCODE_USER" "/home/$DEFAULT_USER/.config/opencode:$DEFAULT_USER"; do + _str_user_dir="${_str_dir_user%%:*}" + _str_dir_owner="${_str_dir_user#*:}" + if [ "$_str_major" = 2 ]; then + sudo mkdir -p "$_str_user_dir/plugins/opencode-permissions-kit" + sudo ln -sfn "$LIBDIR/tui/kit-mode-2x.tsx" "$_str_user_dir/plugins/opencode-permissions-kit/tui.tsx" + sudo chown "$_str_dir_owner:$NEW_OPENCODE_GROUP" "$_str_user_dir/plugins" "$_str_user_dir/plugins/opencode-permissions-kit" 2>/dev/null || true + sudo chown -h "$_str_dir_owner:$NEW_OPENCODE_GROUP" "$_str_user_dir/plugins/opencode-permissions-kit/tui.tsx" 2>/dev/null || true + log "tui mode registered for 2.x: $_str_user_dir/plugins/opencode-permissions-kit/tui.tsx" + else + sudo rm -rf "$_str_user_dir/plugins/opencode-permissions-kit" + log "tui mode 2.x registration removed: $_str_user_dir/plugins/opencode-permissions-kit" + fi + sudo python3 "$LIBDIR/py/tui-register.py" "$_str_user_dir/cli.json" unregister "$LIBDIR/tui/kit-mode-2x.tsx" --drop "$LIBDIR/tui/kit-mode.tsx" >/dev/null 2>&1 || true + done +} + if [ "$BINARY_UPDATE" = true ]; then ui_section "Upgrading opencode binary" SRC="" @@ -658,14 +782,32 @@ if [ "$BINARY_UPDATE" = true ]; then log "opencode binary upgrade skipped: --binary-path not executable" fi else - TMP="$(mktemp -d)" - if SRC=$(fetch_latest_opencode "$TMP"); then - : # candidate extracted; TMP stays alive until after the install + # Version resolution (issue #99): --version pin > --major switch > + # stay on the current major. Never crosses majors silently. + _up_ver="" + if [ -n "$UPGRADE_VERSION" ]; then + _up_ver="$UPGRADE_VERSION" + ui_detail "target version pinned: $_up_ver" else - ui_warn "download of the latest opencode release failed — binary left untouched" - log "opencode binary upgrade skipped: download failed" - rm -rf "$TMP" - TMP="" + _up_major="${UPGRADE_MAJOR:-$(current_opencode_major)}" + ui_detail "resolving the latest opencode $_up_major.x release" + if ! _up_ver=$(resolve_latest_opencode_version "$_up_major"); then + ui_warn "could not resolve the latest opencode $_up_major.x release — binary left untouched" + if [ -z "$UPGRADE_MAJOR" ]; then + ui_detail "to switch the opencode major run: opk upgrade-opencode --major 1|2" + fi + log "opencode binary upgrade skipped: no $_up_major.x release resolved" + fi + fi + if [ -n "$_up_ver" ]; then + TMP="$(mktemp -d)" + if ! SRC=$(fetch_opencode_version "$TMP" "$_up_ver"); then + ui_warn "download of opencode $_up_ver failed — binary left untouched" + log "opencode binary upgrade skipped: download of $_up_ver failed" + rm -rf "$TMP" + TMP="" + SRC="" + fi fi fi if [ -n "$SRC" ]; then @@ -673,8 +815,18 @@ if [ "$BINARY_UPDATE" = true ]; then if [ -x "$SYSTEM_BIN" ]; then sudo cp "$SYSTEM_BIN" "$BACKUP_DIR/opencode.current" fi + _maj_before=$(current_opencode_major) if install_binary "$SRC"; then ui_detail "backup kept in $BACKUP_DIR (remove once you are satisfied)" + # A major flip (v1 <-> v2, issue #99) must re-anchor the TUI + # registration even in --only-binary runs — the plugin dir is + # binary-coupled, not kit-coupled. + _maj_after=$(current_opencode_major) + if [ "$_maj_before" != "$_maj_after" ]; then + sync_tui_registration "$_maj_after" + ui_detail "opencode major changed ($_maj_before -> $_maj_after): TUI registration flipped" + log "opencode major flipped: $_maj_before -> $_maj_after (tui registration synced)" + fi else ui_warn "candidate binary failed verification/install — binary left untouched" log "opencode binary upgrade skipped: candidate failed verification/install" @@ -789,25 +941,14 @@ if [ ! -f "$DEFAULT_TUI_CONF" ] || grep -q '"_opencode_permissions_kit"' "$DEFAU log "tui danger theme refreshed: $DEFAULT_TUI_CONF" fi -# opencode 2.x (issue #80): re-register the kit-mode-2x.tsx port for both -# users — the TUI discovers local plugins as DIRECTORIES under -# ~/.config/opencode/plugins// with a tui entrypoint; file paths in -# cli.json are skipped (entries from pre-0.0.35 kits are unregistered, -# best-effort). Symlink into LIBDIR keeps one source of truth. The major -# comes from the install.conf stamp; binary upgrades re-stamp it. +# opencode 2.x (issue #80, #99): keep the TUI mode registration anchored +# to the installed major — register the kit-mode-2x.tsx plugin dir on 2.x, +# remove it on 1.x (the 1.x tui.json/danger theme are major-agnostic and +# stay). The major comes from the install.conf stamp, freshly re-stamped +# by any binary upgrade above. _oc_major=$(sed -n 's/^OPENCODE_MAJOR=//p' "$CONFDIR/install.conf" 2>/dev/null | tail -1) -if [ "$_oc_major" = "2" ]; then - for _oc_dir_user in "/home/$OPENCODE_USER/.config/opencode:$OPENCODE_USER" "/home/$DEFAULT_USER/.config/opencode:$DEFAULT_USER"; do - _oc_user_dir="${_oc_dir_user%%:*}" - _oc_dir_owner="${_oc_dir_user#*:}" - sudo mkdir -p "$_oc_user_dir/plugins/opencode-permissions-kit" - sudo ln -sfn "$LIBDIR/tui/kit-mode-2x.tsx" "$_oc_user_dir/plugins/opencode-permissions-kit/tui.tsx" - sudo chown "$_oc_dir_owner:$NEW_OPENCODE_GROUP" "$_oc_user_dir/plugins" "$_oc_user_dir/plugins/opencode-permissions-kit" 2>/dev/null || true - sudo chown -h "$_oc_dir_owner:$NEW_OPENCODE_GROUP" "$_oc_user_dir/plugins/opencode-permissions-kit/tui.tsx" 2>/dev/null || true - sudo python3 "$LIBDIR/py/tui-register.py" "$_oc_user_dir/cli.json" unregister "$LIBDIR/tui/kit-mode-2x.tsx" --drop "$LIBDIR/tui/kit-mode.tsx" >/dev/null 2>&1 || true - log "tui mode registered for 2.x: $_oc_user_dir/plugins/opencode-permissions-kit/tui.tsx" - done -fi +[ -n "$_oc_major" ] || _oc_major=1 +sync_tui_registration "$_oc_major" # --- optional group-baseline refresh ------------------------------------------ diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index 0818f10..a8efd17 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -702,6 +702,30 @@ check ".env still readable after binary upgrade (soft-only)" \ check_fail "new binary writable by opencode user" \ E 'sudo -u opencode sh -c "test -w /usr/local/lib/opencode-permissions-kit/bin/opencode"' +# opencode 2.x runs only (issue #99): a plain upgrade must stay on the +# 2.x major — the npm dist-tag resolution replaces the old GitHub +# releases/latest lookup, which downgraded 2.x installs to 1.x latest. +case "$OC_VERSION" in + 2.*) + echo "" + echo "--- 11d. upgrade-opencode stays on the 2.x major (issue #99) ---" + # --only-binary skips the kit self-fetch; the only download is the + # binary itself (npm @opencode/cli-, dist-tag latest). + E 'opk upgrade-opencode' && \ + echo " ${GREEN}OK${NC} opk upgrade-opencode (live npm resolution) completed" + check "11d: binary is still opencode 2.x after the upgrade" \ + E 'test "$(/usr/local/lib/opencode-permissions-kit/bin/opencode --version 2>/dev/null | head -1 | sed "s/^opencode v//; s/\..*//")" = 2' + check "11d: OPENCODE_MAJOR stamp still 2" \ + E 'grep -q "^OPENCODE_MAJOR=2$" /etc/opencode-permissions-kit/install.conf' + check "11d: 2.x TUI plugin still registered for the agent user" \ + E 'test -L /home/opencode/.config/opencode/plugins/opencode-permissions-kit/tui.tsx' + # restore the pinned binary so later sections stay deterministic + E 'sudo -u opencode /usr/local/lib/opencode-permissions-kit/bin/opencode service stop >/dev/null 2>&1 || true; sudo pkill -u opencode -f "serve --servic[e]" >/dev/null 2>&1 || true' + E 'sudo cp /opencode-cache/opencode-'"$OC_VERSION"'/opencode /usr/local/lib/opencode-permissions-kit/bin/opencode && sudo chown root:opencode /usr/local/lib/opencode-permissions-kit/bin/opencode && sudo chmod 750 /usr/local/lib/opencode-permissions-kit/bin/opencode' && \ + echo " ${GREEN}OK${NC} pinned binary $OC_VERSION restored" + ;; +esac + echo "" echo "--- 12. config.sh adds a project non-interactively ---" # extra-project carries a dev-owned .ddev to prove the projects-add handover diff --git a/tests/unit/test-tui-mode.sh b/tests/unit/test-tui-mode.sh index a8b671f..ba5d8ff 100755 --- a/tests/unit/test-tui-mode.sh +++ b/tests/unit/test-tui-mode.sh @@ -180,8 +180,10 @@ check "install.sh unregisters inert cli.json path entries (2x cleanup)" \ grep -q 'tui-register.py" "$_oc_user_dir/cli.json" unregister' "$INSTALL" check "update.sh fetch list includes the 2x plugin" \ grep -q 'opencode-permissions-kit-lib/tui/kit-mode-2x.tsx' "$UPDATE" -check "update.sh re-registers the 2x plugin dir (major-gated)" \ - grep -q 'ln -sfn "$LIBDIR/tui/kit-mode-2x.tsx" "$_oc_user_dir/plugins/opencode-permissions-kit/tui.tsx"' "$UPDATE" +check "update.sh re-registers the 2x plugin dir (major-gated, sync function)" \ + grep -q 'ln -sfn "$LIBDIR/tui/kit-mode-2x.tsx" "$_str_user_dir/plugins/opencode-permissions-kit/tui.tsx"' "$UPDATE" \ + && grep -q 'sync_tui_registration "$_oc_major"' "$UPDATE" \ + && grep -q 'sync_tui_registration "$_maj_after"' "$UPDATE" check "uninstall.sh removes the 2x plugin dir and cli.json entries" \ grep -q 'plugins/opencode-permissions-kit' "$UNINSTALL" && grep -q 'kit-mode-2x.tsx' "$UNINSTALL" diff --git a/tests/unit/test-update-flags.sh b/tests/unit/test-update-flags.sh index 38e81c7..5522ca0 100755 --- a/tests/unit/test-update-flags.sh +++ b/tests/unit/test-update-flags.sh @@ -44,43 +44,114 @@ else exit 1 fi -# --- 2. functional: candidate survives until the caller installs it ------------- -# Fake curl on PATH: without -o it answers the GitHub API call, with -o it -# "downloads" a fixture tarball containing an executable opencode stub. -mkdir -p "$WORK/bin" "$WORK/fixture" +# --- 2. functional: major-aware resolution + candidate survives (issue #99) ------ +# Fake curl on PATH: registry.npmjs.org answers the dist-tags doc (no -o) +# or "downloads" an npm-layout tarball (-o); api.github.com answers the +# releases/latest tag (no -o) or a GitHub-layout tarball (-o). +mkdir -p "$WORK/bin" "$WORK/fixture" "$WORK/fixture-npm/package/bin" printf '#!/bin/sh\necho "opencode version 9.9.9"\n' > "$WORK/fixture/opencode" chmod +x "$WORK/fixture/opencode" tar -czf "$WORK/fixture/release.tar.gz" -C "$WORK/fixture" opencode +printf '#!/bin/sh\necho "opencode v2.1.99"\n' > "$WORK/fixture-npm/package/bin/opencode" +chmod +x "$WORK/fixture-npm/package/bin/opencode" +tar -czf "$WORK/fixture/release-npm.tgz" -C "$WORK/fixture-npm" package cat > "$WORK/bin/curl" < "$FUNCS" + +# Stubs for current_opencode_major (2.x and 1.x --version shapes). +printf '#!/bin/sh\necho "opencode v2.0.11"\n' > "$WORK/stub-v2"; chmod +x "$WORK/stub-v2" +printf '#!/bin/sh\necho "1.18.31"\n' > "$WORK/stub-v1"; chmod +x "$WORK/stub-v1" +mkdir -p "$WORK/conf" + +# 2a. major detection from the binary's --version shape +run_resolver() { + # run_resolver + env PATH="$1:$PATH" SYSTEM_BIN="$2" CONFDIR="$WORK/conf" \ + sh -c ". '$FUNCS' && $3" 2>/dev/null +} +_re=$(run_resolver "$WORK/bin" "$WORK/stub-v2" 'current_opencode_major') +[ "$_re" = "2" ] && pass "resolve: 2.x --version line maps to major 2" || fail "resolve: 2.x --version line maps to major 2 (got: '$_re')" +_re=$(run_resolver "$WORK/bin" "$WORK/stub-v1" 'current_opencode_major') +[ "$_re" = "1" ] && pass "resolve: bare 1.x --version line maps to major 1" || fail "resolve: bare 1.x --version line maps to major 1 (got: '$_re')" + +# 2b. latest resolution per major (npm dist-tag vs GitHub releases/latest) +_re=$(run_resolver "$WORK/bin" "$WORK/stub-v2" 'resolve_latest_opencode_version 2') +[ "$_re" = "2.1.99" ] && pass "resolve: major 2 resolves through the npm dist-tag" || fail "resolve: major 2 resolves through the npm dist-tag (got: '$_re')" +_re=$(run_resolver "$WORK/bin" "$WORK/stub-v1" 'resolve_latest_opencode_version 1') +[ "$_re" = "1.18.99" ] && pass "resolve: major 1 resolves through GitHub releases/latest" || fail "resolve: major 1 resolves through GitHub releases/latest (got: '$_re')" + +# 2c. a dist-tag outside the wanted major fails loudly (no silent crossing) +mkdir -p "$WORK/bin3" +sed 's/"latest":"2.1.99"/"latest":"3.0.0"/' "$WORK/bin/curl" > "$WORK/bin3/curl" +chmod +x "$WORK/bin3/curl" +_re=$(run_resolver "$WORK/bin3" "$WORK/stub-v2" 'resolve_latest_opencode_version 2' >/dev/null && echo resolved || echo refused) +[ "$_re" = "refused" ] && pass "resolve: dist-tag off the wanted major is refused (issue #99)" || fail "resolve: dist-tag off the wanted major is refused (got: '$_re')" + +# 2d. exact-version download: npm layout (2.*) and GitHub layout (1.x) DL="$(mktemp -d)" -OUT=$(PATH="$WORK/bin:$PATH" sh -c " - eval \"\$(sed -n '/^detect_asset() {/,/^}/p' \"\$1\")\" - eval \"\$(sed -n '/^fetch_latest_opencode() {/,/^}/p' \"\$1\")\" - fetch_latest_opencode \"\$2\" -" _ "$UPDATE" "$DL" 2>/dev/null || true) -check "fetch: prints the candidate path" [ "$OUT" = "$DL/opencode" ] -check "fetch: candidate binary exists and is executable" test -x "$DL/opencode" -check "fetch: candidate runs (--version works for install_binary)" \ +OUT=$(PATH="$WORK/bin:$PATH" sh -c ". '$FUNCS' && fetch_opencode_version '$DL' '2.1.99'" 2>/dev/null || true) +check "fetch: 2.* downloads through npm (package/bin/opencode)" [ "$OUT" = "$DL/opencode" ] +check "fetch: 2.* candidate runs and reports the 2.x version" \ + sh -c "\"\$1\" --version 2>/dev/null | grep -q '^opencode v2\.1\.99\$'" _ "$DL/opencode" +rm -rf "$DL"; DL="$(mktemp -d)" +OUT=$(PATH="$WORK/bin:$PATH" sh -c ". '$FUNCS' && fetch_opencode_version '$DL' '1.18.99'" 2>/dev/null || true) +check "fetch: 1.x downloads through GitHub (tarball-root opencode)" [ "$OUT" = "$DL/opencode" ] +check "fetch: 1.x candidate runs (--version works for install_binary)" \ sh -c "\"\$1\" --version >/dev/null 2>&1" _ "$DL/opencode" rm -rf "$DL" +# 2e. fetch_latest_opencode: default stays on the CURRENT major (no 2.x -> +# 1.x downgrade); explicit major picks the channel +DL="$(mktemp -d)" +OUT=$(PATH="$WORK/bin:$PATH" SYSTEM_BIN="$WORK/stub-v2" CONFDIR="$WORK/conf" \ + sh -c ". '$FUNCS' && fetch_latest_opencode '$DL'" 2>/dev/null || true) +check "fetch: current major 2 -> npm candidate (issue #99: no downgrade)" \ + sh -c "[ \"\$1\" = \"\$2/opencode\" ] && \"\$1\" --version 2>/dev/null | grep -q '^opencode v2'" _ "$OUT" "$DL" +rm -rf "$DL"; DL="$(mktemp -d)" +OUT=$(PATH="$WORK/bin:$PATH" SYSTEM_BIN="$WORK/stub-v1" CONFDIR="$WORK/conf" \ + sh -c ". '$FUNCS' && fetch_latest_opencode '$DL'" 2>/dev/null || true) +check "fetch: current major 1 -> GitHub candidate" \ + sh -c "[ \"\$1\" = \"\$2/opencode\" ] && \"\$1\" --version >/dev/null 2>&1" _ "$OUT" "$DL" +rm -rf "$DL" + # --- 3. TMP cleanup order: never before the install attempt ---------------------- IB_LINE=$(grep -n 'install_binary "\$SRC"' "$UPDATE" | head -1 | cut -d: -f1) LAST_TMP_RM=$(grep -n 'rm -rf "\$TMP"' "$UPDATE" | tail -1 | cut -d: -f1) @@ -188,6 +259,26 @@ check "channel: missing ref is rejected (arg loop, from a checkout)" \ check "channel: --help with --channel still works (pre-scan is silent)" \ sh -c "sh \"\$1\" --channel testref --help >/dev/null 2>&1" _ "$UPDATE" +# --- 8. --major / --version (issue #99: upgrades never cross majors) ------------ +check "flag: --major parsed, only 1 or 2 accepted" \ + sh -c "grep -q -- '--major)' \"\$1\" && grep -q -- '--major must be 1 or 2' \"\$1\"" _ "$UPDATE" +check "flag: --version parsed with a value" \ + sh -c "grep -q -- '--version)' \"\$1\" && grep -q -- '--version requires an opencode version' \"\$1\"" _ "$UPDATE" +check "flag: --major 3 is rejected" \ + sh -c "! sh \"\$1\" --binary --major 3 >/dev/null 2>&1" _ "$UPDATE" +check "flag: --version without a value is rejected" \ + sh -c "! sh \"\$1\" --binary --version >/dev/null 2>&1" _ "$UPDATE" +check "flag: help text documents --major and --version" \ + sh -c "grep -q -- '--major 1|2' \"\$1\" && grep -q -- '--version ' \"\$1\"" _ "$UPDATE" +check "resolve: latest-version resolution guards the requested major" \ + sh -c "grep -q '2\.\*) echo \"\$_rlov_ver\"' \"\$1\" && grep -q '1\.\*) echo \"\$_rlov_ver\"' \"\$1\"" _ "$UPDATE" +check "resolve: 2.x channel is the npm registry (with scope fallback)" \ + sh -c "grep -q 'registry.npmjs.org/@opencode/cli-' \"\$1\" && grep -q 'registry.npmjs.org/@opencode-ai/cli-' \"\$1\"" _ "$UPDATE" +check "tui: registration flips with the major (sync function, both directions)" \ + sh -c "grep -q '^sync_tui_registration() {' \"\$1\" && grep -q 'if \[ \"\$_str_major\" = 2 \]' \"\$1\" && grep -q 'rm -rf \"\$_str_user_dir/plugins/opencode-permissions-kit\"' \"\$1\"" _ "$UPDATE" +check "tui: a major flip re-anchors the registration even in --only-binary runs" \ + sh -c "grep -n 'sync_tui_registration \"\$_maj_after\"' \"\$1\" | head -1 | cut -d: -f1 | grep -q ." _ "$UPDATE" + # --- Summary ---------------------------------------------------------------------- echo "" echo "====================================" From 227ccaaed32495224cfe2b2134dbca6e5f17b4b8 Mon Sep 17 00:00:00 2001 From: Steffen Maechtel Date: Fri, 25 Sep 2026 22:39:07 +0200 Subject: [PATCH 2/3] fix: rate-limit-resilient version resolution in the e2e suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e-ddev-2x CI job died at 'cannot resolve a ddev version' (2026-09-25): the ddev version was resolved through unauthenticated api.github.com (60 req/h per IP), and shared GitHub-runner IPs routinely exhaust that — five retries burned ~52 s, the golden-image label fallback does not exist on a fresh runner, and only the ddev jobs need the API (the 2x opencode jobs resolve through npm), so everything else stayed green. lib.sh gains gh_latest_tag : REST API first — authorized with OPK_GH_TOKEN/GITHUB_TOKEN when exported (1000+ req/h) — then the releases/latest HTML redirect (no API, no per-IP rate limit) as fallback. e2e_resolve_cache (opencode 1.x) and run-ddev.sh (ddev) now resolve through it. The workflows map the ephemeral workflow token onto OPK_GH_TOKEN at every e2e step — nothing to configure, no secret. Verified live: gh_latest_tag resolves ddev 1.25.4 and opencode 1.18.32 (both paths); e2e-ddev with the failing CI config (opencode 2.0.16) 70 passed, e2e 269 passed, e2e-rootless 47 passed. New structural checks in test-e2e-sources.sh pin the token header, the redirect fallback, and the workflow exports. --- .github/workflows/test-e2e-ddev.yml | 6 ++++ .github/workflows/test-e2e.yml | 10 ++++++ tests/e2e/lib.sh | 48 ++++++++++++++++++++++++----- tests/e2e/run-ddev.sh | 14 +++++---- tests/unit/test-e2e-sources.sh | 17 ++++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test-e2e-ddev.yml b/.github/workflows/test-e2e-ddev.yml index dfc16c1..29f3dfc 100644 --- a/.github/workflows/test-e2e-ddev.yml +++ b/.github/workflows/test-e2e-ddev.yml @@ -121,6 +121,10 @@ jobs: chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode ./tests/e2e/run-ddev.sh + env: + # Authorize api.github.com calls (rate-limit resilience, lib.sh + # gh_latest_tag) — the ephemeral workflow token, no secret needed. + OPK_GH_TOKEN: ${{ github.token }} - name: Summary if: always() env: @@ -179,6 +183,8 @@ jobs: chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode E2E_OC_VERSION=${{ steps.version2x.outputs.OC2_VERSION }} ./tests/e2e/run-ddev.sh + env: + OPK_GH_TOKEN: ${{ github.token }} - name: Summary if: always() env: diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 679b9cd..82616e0 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -120,6 +120,10 @@ jobs: chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./scripts/release.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode ./tests/e2e/run.sh + env: + # Authorize api.github.com calls (rate-limit resilience, lib.sh + # gh_latest_tag) — the ephemeral workflow token, no secret needed. + OPK_GH_TOKEN: ${{ github.token }} # docker-rootless daemon e2e (Phase 3). Same triggers + shared binary cache; # needs systemd-in-container + nested user namespaces, and SKIPs wholesale @@ -152,6 +156,8 @@ jobs: ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode E2E_STRICT="${{ inputs.strict == true && '1' || '0' }}" ./tests/e2e/run-docker-rootless.sh + env: + OPK_GH_TOKEN: ${{ github.token }} # opencode 2.x pin (issue #80): the same suite against the CURRENT 2.x # release, resolved from the npm registry at run time — 2.x ships no @@ -200,6 +206,8 @@ jobs: chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./scripts/release.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode E2E_OC_VERSION=${{ steps.version2x.outputs.OC2_VERSION }} ./tests/e2e/run.sh + env: + OPK_GH_TOKEN: ${{ github.token }} # docker-rootless daemon e2e, opencode 2.x pin (issue #80) — the same # mirrored trigger/cache layout as its v1 job above; see e2e-2x for the @@ -240,3 +248,5 @@ jobs: ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode E2E_STRICT="${{ inputs.strict == true && '1' || '0' }}" E2E_OC_VERSION=${{ steps.version2x.outputs.OC2_VERSION }} ./tests/e2e/run-docker-rootless.sh + env: + OPK_GH_TOKEN: ${{ github.token }} diff --git a/tests/e2e/lib.sh b/tests/e2e/lib.sh index 583e403..23fbc36 100755 --- a/tests/e2e/lib.sh +++ b/tests/e2e/lib.sh @@ -164,20 +164,54 @@ e2e_fetch_opencode() { chmod +x "$dest" } +# Resolve the latest release tag of a GitHub repo, rate-limit resilient +# (CI crash 2026-09-25: unauthenticated api.github.com is 60 req/h per IP +# and shared GitHub-runner IPs routinely exhaust it — the ddev-2x job died +# in "cannot resolve a ddev version" while every npm-based job stayed +# green). Chain: REST API (authorized when OPK_GH_TOKEN/GITHUB_TOKEN is +# exported by the workflows — 1000+ req/h), then the releases/latest HTML +# redirect (no API, no rate limit). Prints the bare tag (no leading v), +# or nothing when both endpoints refuse. +# $1 = owner/repo. +gh_latest_tag() { + local repo="$1" tag _auth + repo="${repo#https://github.com/}" + _auth="" + if [ -n "${OPK_GH_TOKEN:-}" ]; then _auth="Authorization: Bearer $OPK_GH_TOKEN" + elif [ -n "${GITHUB_TOKEN:-}" ]; then _auth="Authorization: Bearer $GITHUB_TOKEN" + fi + if [ -n "$_auth" ]; then + tag=$(curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors --max-time 30 \ + -H "$_auth" "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null \ + | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + else + tag=$(curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors --max-time 30 \ + "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null \ + | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + fi + if [ -z "$tag" ]; then + # No API fallback: releases/latest 302s to releases/tag/v — + # plain HTML endpoints carry no per-IP API rate limit. + tag=$(curl -fsSL --retry 3 --retry-delay 5 --retry-all-errors --max-time 30 \ + -o /dev/null -w '%{url_effective}' "https://github.com/$repo/releases/latest" 2>/dev/null \ + | sed -n 's|.*/tag/v\([^/?#]*\).*|\1|p' || true) + fi + printf '%s\n' "$tag" +} + e2e_resolve_cache() { OC_CACHE_DIR="$SCRIPT_DIR/cache" mkdir -p "$OC_CACHE_DIR" # Resolve the opencode version under test. E2E_OC_VERSION pins it; the - # default resolves the current latest from GitHub releases (tiny - # request). If the endpoint is unreachable, fall back to the newest - # cached version so repeat runs work offline. A pinned version that is - # not cached yet is downloaded on demand (see e2e_fetch_opencode). + # default resolves the current latest from GitHub releases (rate-limit + # resilient, see gh_latest_tag). If the endpoint is unreachable, fall + # back to the newest cached version so repeat runs work offline. A + # pinned version that is not cached yet is downloaded on demand (see + # e2e_fetch_opencode). OC_VERSION="$E2E_OC_VERSION" if [ -z "$OC_VERSION" ]; then - OC_VERSION=$(curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors --max-time 30 \ - https://api.github.com/repos/anomalyco/opencode/releases/latest 2>/dev/null \ - | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + OC_VERSION=$(gh_latest_tag anomalyco/opencode) fi if [ -z "$OC_VERSION" ]; then OC_VERSION=$(ls -1d "$OC_CACHE_DIR"/opencode-* 2>/dev/null | sed 's|.*/opencode-||' | sort -V | tail -1) diff --git a/tests/e2e/run-ddev.sh b/tests/e2e/run-ddev.sh index 670060b..4d7250b 100755 --- a/tests/e2e/run-ddev.sh +++ b/tests/e2e/run-ddev.sh @@ -64,14 +64,16 @@ echo "${CYAN}========================================================${NC}" echo "" # --- resolve the ddev version + fetch the binary (host-side cache) ----------- -# Mirrors e2e_resolve_cache: pin via DDEV_VERSION, else latest release, else -# the golden image's label (offline fallback). The tarball lands in -# tests/e2e/cache/ddev-/ (gitignored, mounted read-only into the container). +# Mirrors e2e_resolve_cache: pin via DDEV_VERSION, else latest release +# (rate-limit resilient, see lib.sh gh_latest_tag — the CI crash of +# 2026-09-25 died here on api.github.com rate limits), else the golden +# image's label (offline fallback). The tarball lands in +# tests/e2e/cache/ddev-/ (gitignored, mounted read-only into the +# container). DD_WANT="${DDEV_VERSION:-}" if [ -z "$DD_WANT" ]; then - DD_WANT=$(curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors --max-time 30 \ - https://api.github.com/repos/ddev/ddev/releases/latest 2>/dev/null \ - | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + _dd_tag=$(gh_latest_tag ddev/ddev) + [ -n "$_dd_tag" ] && DD_WANT="v$_dd_tag" fi if [ -z "$DD_WANT" ]; then DD_WANT=$(docker image inspect -f '{{ index .Config.Labels "kit.e2e.ddev.version" }}' "$GOLDEN_IMAGE" 2>/dev/null || true) diff --git a/tests/unit/test-e2e-sources.sh b/tests/unit/test-e2e-sources.sh index 1b135ed..3672399 100755 --- a/tests/unit/test-e2e-sources.sh +++ b/tests/unit/test-e2e-sources.sh @@ -56,6 +56,23 @@ check "e2e_resolve_cache uses e2e_fetch_opencode" \ check "e2e_fetch_old uses e2e_fetch_opencode" \ grep_q 'e2e_fetch_opencode "\$OLD_VERSION"' +# --- rate-limit resilience (CI crash 2026-09-25: api.github.com 60 req/h +# per IP on shared runners killed the ddev-2x job at version resolution) --- +RUN_DDEV="$(cd "$(dirname "$0")/../e2e" && pwd)/run-ddev.sh" +WF_E2E="$(cd "$(dirname "$0")/../../.github/workflows" && pwd)/test-e2e.yml" +WF_DDEV="$(cd "$(dirname "$0")/../../.github/workflows" && pwd)/test-e2e-ddev.yml" + +check "gh_latest_tag helper exists in lib.sh" \ + grep -q '^gh_latest_tag() {' "$LIB" +check "version resolution authorizes the API when a token is present" \ + sh -c "grep -q 'OPK_GH_TOKEN' \"\$1\" && grep -q 'Authorization: Bearer' \"\$1\"" _ "$LIB" +check "version resolution falls back to the releases/latest redirect" \ + sh -c "grep -q 'url_effective' \"\$1\" && grep -q '/tag/v' \"\$1\"" _ "$LIB" +check "run-ddev.sh resolves ddev through gh_latest_tag (no bare API call)" \ + sh -c "grep -q 'gh_latest_tag ddev/ddev' \"\$1\" && ! grep -q 'https://api.github.com/repos/ddev' \"\$1\"" _ "$RUN_DDEV" +check "workflows export the workflow token to the e2e steps" \ + sh -c "grep -q 'OPK_GH_TOKEN: \${{ github.token }}' \"\$1\" && grep -q 'OPK_GH_TOKEN: \${{ github.token }}' \"\$2\"" _ "$WF_E2E" "$WF_DDEV" + echo "" if [ "$failures" -gt 0 ]; then echo " ${RED}$failures test(s) failed.${NC}" From 192965dacef5142572b63251cba7f3c2d1c18149 Mon Sep 17 00:00:00 2001 From: Steffen Maechtel Date: Fri, 25 Sep 2026 22:42:56 +0200 Subject: [PATCH 3/3] fix: merge OPK_GH_TOKEN into the existing step env blocks (invalid workflow) The ddev workflow's run steps already carried env blocks (E2E_DDEV_SITE, DDEV_VERSION); appending a second env: made GitHub reject the whole file ('env' is already defined, lines 124/186). Merge the token into the existing blocks. New structural guard in test-workflows.sh fails make test on any step with more than one env block, and both workflows parse clean (PyYAML). --- .github/workflows/test-e2e-ddev.yml | 11 +++++------ tests/unit/test-workflows.sh | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-e2e-ddev.yml b/.github/workflows/test-e2e-ddev.yml index 29f3dfc..ff1196f 100644 --- a/.github/workflows/test-e2e-ddev.yml +++ b/.github/workflows/test-e2e-ddev.yml @@ -117,14 +117,14 @@ jobs: env: E2E_DDEV_SITE: ${{ (github.event_name != 'workflow_dispatch' || inputs.site_tier == true) && 'camino' || 'none' }} DDEV_VERSION: ${{ inputs.ddev_version }} + # Authorize api.github.com calls (rate-limit resilience, lib.sh + # gh_latest_tag) — the ephemeral workflow token, no secret needed. + OPK_GH_TOKEN: ${{ github.token }} run: | chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode ./tests/e2e/run-ddev.sh - env: - # Authorize api.github.com calls (rate-limit resilience, lib.sh - # gh_latest_tag) — the ephemeral workflow token, no secret needed. - OPK_GH_TOKEN: ${{ github.token }} + - name: Summary if: always() env: @@ -179,12 +179,11 @@ jobs: env: E2E_DDEV_SITE: ${{ (github.event_name != 'workflow_dispatch' || inputs.site_tier == true) && 'camino' || 'none' }} DDEV_VERSION: ${{ inputs.ddev_version }} + OPK_GH_TOKEN: ${{ github.token }} run: | chmod +x ./tests/e2e/run.sh ./tests/e2e/run-docker-rootless.sh ./tests/e2e/run-ddev.sh ./tests/e2e/lib.sh ./tests/check-host.sh ./tests/unit/test-opencode-as-opencode.sh ./tests/unit/test-jsonc-parser.sh ./tests/unit/test-git-config.sh ./tests/unit/test-container-backend.sh ./tests/unit/test-bypass-guard.sh ./tests/unit/test-ddev-as-opencode.sh ./tests/unit/test-ddev-migrate.sh ./tests/unit/test-ddev-hosts.sh ./tests/unit/test-mkcert-reuse.sh ./tests/unit/test-wsl-exposure.sh ./tests/unit/test-ui.sh ./tests/unit/test-docs.sh ./tests/unit/test-kit-cli.sh ./tests/unit/test-project-paths.sh ./tests/unit/test-workflows.sh ./tests/unit/test-install-args.sh ./tests/unit/test-kit-files.sh ./tests/unit/test-tui-mode.sh ./tests/unit/test-uninstall.sh ./tests/unit/test-status.sh ./tests/unit/test-update-flags.sh ./tests/unit/test-release.sh ./tests/unit/test-fs-baseline.sh ./tests/unit/test-browser-bridge.sh ./tests/unit/test-e2e-sources.sh \ ./files/install.sh ./files/etc/umask.sh ./files/opencode-permissions-kit-lib/management/config.sh ./files/opencode-permissions-kit-lib/management/update.sh ./files/opencode-permissions-kit-lib/management/uninstall.sh ./files/opencode-permissions-kit-lib/management/status.sh ./files/opencode-permissions-kit-lib/bin/opencode-as-opencode ./files/opencode-permissions-kit-lib/bin/opk ./files/opencode-permissions-kit-lib/sh/log.sh ./files/opencode-permissions-kit-lib/sh/ui.sh ./files/opencode-permissions-kit-lib/sh/shell-warn.sh ./files/opencode-permissions-kit-lib/bin/setup-container-backend ./files/opencode-permissions-kit-lib/bin/socket-check ./files/opencode-permissions-kit-lib/bin/cwd-check ./files/opencode-permissions-kit-lib/sh/ddev-terminal.sh ./files/opencode-permissions-kit-lib/sh/ddev-handover.sh ./files/opencode-permissions-kit-lib/sh/ddev-migrate.sh ./files/opencode-permissions-kit-lib/bin/ddev-migrate ./files/opencode-permissions-kit-lib/sh/ddev-hosts.sh ./files/opencode-permissions-kit-lib/sh/fs-baseline.sh ./files/opencode-permissions-kit-lib/sh/wsl-browser-bridge.sh ./files/opencode-permissions-kit-lib/bin/browser-bridge ./files/opencode-permissions-kit-lib/tui/kit-mode.tsx ./files/opencode-permissions-kit-lib/tui/kit-mode-2x.tsx ./files/opencode-permissions-kit-lib/tui/opencode-danger.theme.json ./files/opencode-permissions-kit-lib/tui/tui.json ./files/opencode-permissions-kit-lib/tui/tui-danger.json ./files/opencode-permissions-kit-lib/bin/ddev-as-opencode E2E_OC_VERSION=${{ steps.version2x.outputs.OC2_VERSION }} ./tests/e2e/run-ddev.sh - env: - OPK_GH_TOKEN: ${{ github.token }} - name: Summary if: always() env: diff --git a/tests/unit/test-workflows.sh b/tests/unit/test-workflows.sh index 3a9dc33..7fc6a9d 100755 --- a/tests/unit/test-workflows.sh +++ b/tests/unit/test-workflows.sh @@ -138,6 +138,22 @@ check_2x "$WF_DDEV_E2E" \ '2\.\*) echo "OC2_VERSION=' \ '::error::npm dist-tag latest' +# Structural YAML guard (workflow-upload breakage 2026-09-25: a second +# env: block on a step that already had one made GitHub reject the whole +# file): every step may carry at most ONE env: block. +_dups=0 +for _wf in "$WF_TEST" "$WF_E2E" "$WF_DDEV_E2E"; do + _out=$(awk '/^ - /{ if (c>1) { print FILENAME ": " prev " (" c " env blocks)" }; c=0; prev=$0 } + /^ env:/{c++} + END{ if (c>1) { print FILENAME ": " prev " (" c " env blocks)" } }' "$_wf") + [ -n "$_out" ] && { echo "$_out"; _dups=1; } +done +if [ "$_dups" = 0 ]; then + pass "no workflow step carries more than one env block" +else + fail "no workflow step carries more than one env block" +fi + echo "" if [ "$failures" -gt 0 ]; then echo " ${RED}$failures test(s) failed.${NC}"