From a49702f241445b33d19e7c08e126b54071f8745e Mon Sep 17 00:00:00 2001 From: velariumai Date: Sat, 16 May 2026 21:05:42 -0500 Subject: [PATCH 1/2] test(release): add cli smoke readiness checks --- configs/promotion-allowlist.txt | 2 + configs/promotion-manifest.txt | 5 +- docs/release/README.md | 61 +++- pkg/profile/release_readiness_matrix_test.go | 115 +++++++ scripts/release_readiness/readiness.sh | 29 +- scripts/release_readiness/rr002_cli_smoke.sh | 306 ++++++++++++++++++ scripts/release_readiness/test_rr002_smoke.sh | 71 ++++ 7 files changed, 580 insertions(+), 9 deletions(-) create mode 100644 pkg/profile/release_readiness_matrix_test.go create mode 100755 scripts/release_readiness/rr002_cli_smoke.sh create mode 100755 scripts/release_readiness/test_rr002_smoke.sh diff --git a/configs/promotion-allowlist.txt b/configs/promotion-allowlist.txt index 9170df5..6fb89b5 100644 --- a/configs/promotion-allowlist.txt +++ b/configs/promotion-allowlist.txt @@ -12,9 +12,11 @@ scripts/check_release_artifact_matrix.sh scripts/check_release_changelog.sh scripts/release_checklist.sh scripts/release_readiness/readiness.sh +scripts/release_readiness/rr002_cli_smoke.sh scripts/release_readiness/lib/common.sh scripts/release_readiness/lib/report.sh scripts/release_readiness/test_smoke.sh +scripts/release_readiness/test_rr002_smoke.sh Makefile go.mod go.sum diff --git a/configs/promotion-manifest.txt b/configs/promotion-manifest.txt index 5a3673e..7f48dec 100644 --- a/configs/promotion-manifest.txt +++ b/configs/promotion-manifest.txt @@ -1,7 +1,7 @@ # Auto-generated promotion manifest. # Source allowlist: configs/promotion-allowlist.txt # Regenerate with: bash scripts/generate_promotion_manifest.sh -# Generated at (UTC): 2026-05-16T21:41:07Z +# Generated at (UTC): 2026-05-17T01:49:33Z .github/workflows/release.yml .github/workflows/universal-validator.yml @@ -558,6 +558,7 @@ pkg/profile/profile.go pkg/profile/profile_test.go pkg/profile/refs.go pkg/profile/refs_test.go +pkg/profile/release_readiness_matrix_test.go pkg/profile/util.go pkg/providers/auth_resolver.go pkg/providers/auth_resolver_test.go @@ -905,6 +906,8 @@ scripts/release_checklist.sh scripts/release_readiness/lib/common.sh scripts/release_readiness/lib/report.sh scripts/release_readiness/readiness.sh +scripts/release_readiness/rr002_cli_smoke.sh +scripts/release_readiness/test_rr002_smoke.sh scripts/release_readiness/test_smoke.sh scripts/setup_wizard.sh scripts/write_launcher.sh diff --git a/docs/release/README.md b/docs/release/README.md index 33e2848..91e25dc 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -1,7 +1,9 @@ # Release Readiness RR-001 adds local report-only release-readiness tooling. It gathers release facts, -runs validation commands, and writes a human-readable markdown report. +runs validation commands, and writes a human-readable markdown report. RR-002 +adds local CLI smoke and config/profile matrix checks for safe operator-facing +entrypoints. Run from any directory: @@ -15,6 +17,18 @@ Reports are written under: .local/release-readiness/reports/ ``` +Run RR-002 directly from any directory: + +```bash +bash scripts/release_readiness/rr002_cli_smoke.sh +``` + +RR-002 reports are written under: + +```text +.local/release-readiness/rr002/ +``` + ## What RR-001 Checks - Current branch, commit, and working tree status @@ -44,6 +58,39 @@ Reports are written under: RR-001 normally reports `NEEDS_RR_SUITE` unless a concrete blocker is found. Reports also include `status: REPORT_ONLY`. +## What RR-002 Checks + +- Safe `gorkbot` and `gorkweb` help startup modes with bounded timeouts. +- Unsupported version flags as skipped checks instead of failures. +- Unsafe bare `version` arguments as deferred because no safe subcommand exists. +- Conservative profile defaults across beginner, standard, power-user, expert, + lab, enterprise, custom, and unknown profiles. +- Trace, harness, release authority, automatic promotion, policy absence, + vector candidate-only, and vector truth-invariant postures through local tests. +- Report generation and temporary runtime output confined to `.local/`. +- Tracked-file mutation avoidance. + +## What RR-002 Does Not Do + +- It does not call live providers. +- It does not make network calls. +- It does not execute app tools. +- It does not create, move, or delete tags. +- It does not run release workflows. +- It does not publish releases. +- It does not claim final release readiness. +- It does not cover RR-003+ smoke layers. + +The main wrapper runs RR-002 by default: + +```bash +bash scripts/release_readiness/readiness.sh +``` + +If RR-002 passes or is incomplete without blockers, the wrapper still reports +`NEEDS_RR_SUITE` because RR-003 and later release-readiness layers remain +deferred. If RR-002 finds a concrete blocker, the wrapper reports `BLOCKED`. + ## Private Scanner Handling The wrapper only reports whether the private scanner path exists. Scanner @@ -51,9 +98,9 @@ execution remains a separate private preflight for RR-001. ## Future RR Track -- RR-002 CLI smoke + config matrix -- RR-003 VAR spine fixture smoke -- RR-004 policy absence + statelock/paradox smoke -- RR-005 vector/RAG/engram preservation smoke -- RR-006 TUI/operator/session scripted smoke -- RR-007 final release report generator +- RR-002 CLI smoke + config matrix: implemented as local report-only smoke. +- RR-003 VAR spine fixture smoke: deferred. +- RR-004 policy absence + statelock/paradox smoke: deferred. +- RR-005 vector/RAG/engram preservation smoke: deferred. +- RR-006 TUI/operator/session scripted smoke: deferred. +- RR-007 final release report generator: deferred. diff --git a/pkg/profile/release_readiness_matrix_test.go b/pkg/profile/release_readiness_matrix_test.go new file mode 100644 index 0000000..b487497 --- /dev/null +++ b/pkg/profile/release_readiness_matrix_test.go @@ -0,0 +1,115 @@ +package profile + +import ( + "testing" + + "github.com/velariumai/gorkbot/pkg/evidence" + "github.com/velariumai/gorkbot/pkg/harness" + "github.com/velariumai/gorkbot/pkg/trace" +) + +func TestReleaseReadinessProfileMatrixConservativeDefaults(t *testing.T) { + tests := []struct { + name string + profile Profile + traceModes []trace.Mode + harnessMode harness.Mode + }{ + {name: "beginner", profile: ProfileBeginner, traceModes: []trace.Mode{trace.ModeMinimal}, harnessMode: harness.ModeAudit}, + {name: "standard", profile: ProfileStandard, traceModes: []trace.Mode{trace.ModeAudit}, harnessMode: harness.ModeAudit}, + {name: "power_user", profile: ProfilePowerUser, traceModes: []trace.Mode{trace.ModeAudit}, harnessMode: harness.ModeAudit}, + {name: "expert", profile: ProfileExpert, traceModes: []trace.Mode{trace.ModeDebug}, harnessMode: harness.ModeAudit}, + {name: "lab", profile: ProfileLab, traceModes: []trace.Mode{trace.ModeReplay}, harnessMode: harness.ModeAudit}, + {name: "enterprise", profile: ProfileEnterprise, traceModes: []trace.Mode{trace.ModeAudit}, harnessMode: harness.ModeAudit}, + {name: "custom", profile: ProfileCustom, traceModes: []trace.Mode{trace.ModeAudit}, harnessMode: harness.ModeAudit}, + {name: "unknown", profile: ProfileUnknown, traceModes: []trace.Mode{trace.ModeMinimal}, harnessMode: harness.ModeAudit}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := DefaultConfig(tt.profile) + if !containsTraceMode(tt.traceModes, cfg.TraceMode) { + t.Fatalf("unexpected trace mode for %s: got %q want one of %v", tt.name, cfg.TraceMode, tt.traceModes) + } + if cfg.HarnessMode != tt.harnessMode { + t.Fatalf("unexpected harness mode for %s: got %q want %q", tt.name, cfg.HarnessMode, tt.harnessMode) + } + if !cfg.Evidence.VectorCandidateOnly { + t.Fatalf("profile %s must keep vector retrieval candidate-only", tt.name) + } + if tt.profile != ProfileExpert && tt.profile != ProfileLab && tt.profile != ProfileCustom && + cfg.Authority.ReleaseAuthority == AuthorityAllowConfigured { + t.Fatalf("profile %s must not make release authority advanced by default", tt.name) + } + if cfg.Automation.ReleaseMode != AutomationDisabled && cfg.Automation.ReleaseMode != AutomationApprovalRequired && cfg.Automation.ReleaseMode != AutomationAllowConfigured { + t.Fatalf("profile %s has invalid release automation posture: %q", tt.name, cfg.Automation.ReleaseMode) + } + }) + } +} + +func TestReleaseReadinessPolicyAbsenceAndExplicitAuthority(t *testing.T) { + cfg := DefaultConfig(ProfileExpert) + cfg.ConfiguredCapabilities = nil + + release := EvaluateCapability(cfg, CapabilityReleasePublish) + if release.Decision == evidence.DecisionAuditOnly || release.Decision == evidence.DecisionAllowLowRisk { + t.Fatalf("release without explicit configured capability must not be allowed, got %+v", release) + } + if release.ReasonCode != "release_authority_explicit_required" { + t.Fatalf("release must receipt explicit authority requirement, got %+v", release) + } + + network := EvaluateCapability(cfg, CapabilityNetworkEgress) + if network.Decision == evidence.DecisionAuditOnly { + t.Fatalf("policy absence must not authorize network egress, got %+v", network) + } + + vector := EvaluateCapability(cfg, CapabilityVectorRetrieve) + if vector.Metadata["vector_role"] != "candidate_only" { + t.Fatalf("vector retrieval must be candidate-only, got %+v", vector) + } + + assertTruth := EvaluateCapability(cfg, CapabilityVectorAssertTruth) + if assertTruth.Authority != evidence.AuthorityHardInvariant || assertTruth.Decision != evidence.DecisionDenySensitive { + t.Fatalf("vector assert truth must remain hard-denied, got %+v", assertTruth) + } +} + +func TestReleaseReadinessTraceAndHarnessModeMatrix(t *testing.T) { + traceModes := []trace.Mode{ + trace.ModeOff, + trace.ModeMinimal, + trace.ModeAudit, + trace.ModeDebug, + trace.ModeReplay, + } + for _, mode := range traceModes { + t.Run("trace_"+string(mode), func(t *testing.T) { + if got := trace.ParseMode(string(mode)); got != mode { + t.Fatalf("trace mode %q parsed as %q", mode, got) + } + }) + } + + harnessModes := []harness.Mode{ + harness.ModeOff, + harness.ModeAudit, + } + for _, mode := range harnessModes { + t.Run("harness_"+string(mode), func(t *testing.T) { + if got := harness.ParseMode(string(mode)); got != mode { + t.Fatalf("harness mode %q parsed as %q", mode, got) + } + }) + } +} + +func containsTraceMode(modes []trace.Mode, mode trace.Mode) bool { + for _, candidate := range modes { + if candidate == mode { + return true + } + } + return false +} diff --git a/scripts/release_readiness/readiness.sh b/scripts/release_readiness/readiness.sh index f02a492..7eb6c86 100755 --- a/scripts/release_readiness/readiness.sh +++ b/scripts/release_readiness/readiness.sh @@ -60,6 +60,31 @@ run_required_command() { fi } +run_rr002_command() { + local title="RR-002 CLI Smoke + Config Matrix" + local command_text="bash scripts/release_readiness/rr002_cli_smoke.sh" + local output status recommendation + + set +e + rr_run_shell_capture output "${command_text}" + status=$? + set -e + rr_report_command "${REPORT_FILE}" "${title}" "${command_text}" "${status}" "${output}" + + recommendation="$( + printf '%s\n' "${output}" | + awk -F': ' '/^\[rr002\] final recommendation:/ { value=$2 } END { print value }' + )" + + if [[ "${status}" != "0" || "${recommendation}" == "BLOCKED" ]]; then + record_fail "RR-002 CLI smoke + config matrix failed" + elif [[ "${recommendation}" == "RR002_PASS" ]]; then + record_pass + else + record_skip + fi +} + rr_report_begin "${REPORT_FILE}" timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" @@ -257,6 +282,8 @@ release_body="$( rr_report_list "${REPORT_FILE}" "Release workflow safety" "${release_body}" record_pass +run_rr002_command + neutrality_findings="" NEUTRALITY_FINDINGS_TMP="" neutrality_patterns=( @@ -378,7 +405,7 @@ else fi skipped_body="$( - printf 'RR-002 CLI smoke + config matrix: skipped\n' + printf 'RR-002 CLI smoke + config matrix: handled in RR-002 section\n' printf 'RR-003 VAR spine fixture smoke: skipped\n' printf 'RR-004 policy absence + statelock/paradox smoke: skipped\n' printf 'RR-005 vector/RAG/engram preservation smoke: skipped\n' diff --git a/scripts/release_readiness/rr002_cli_smoke.sh b/scripts/release_readiness/rr002_cli_smoke.sh new file mode 100755 index 0000000..714c472 --- /dev/null +++ b/scripts/release_readiness/rr002_cli_smoke.sh @@ -0,0 +1,306 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/lib/common.sh" +# shellcheck source=lib/report.sh +source "${SCRIPT_DIR}/lib/report.sh" + +ROOT="$(rr_repo_root "${SCRIPT_DIR}")" +cd "${ROOT}" + +REPORT_DIR="${RR002_REPORT_DIR:-${ROOT}/.local/release-readiness/rr002}" +RUN_DIR="${REPORT_DIR}/run" +mkdir -p "${REPORT_DIR}" "${RUN_DIR}/home" "${RUN_DIR}/gocache" "${RUN_DIR}/gopath" "${RUN_DIR}/tmp" +REPORT_FILE="${REPORT_DIR}/rr002-cli-smoke.$(rr_timestamp).md" + +TIMEOUT_SECONDS="${RR002_TIMEOUT_SECONDS:-30}" +BUILD_TIMEOUT_SECONDS="${RR002_BUILD_TIMEOUT_SECONDS:-180}" +PASS_COUNT=0 +FAIL_COUNT=0 +SKIP_COUNT=0 +BLOCKERS="" +FINAL_RECOMMENDATION="RR002_PASS" + +rr002_mark_blocker() { + rr_append_unique_line "$1" BLOCKERS +} + +rr002_record_pass() { + PASS_COUNT=$((PASS_COUNT + 1)) +} + +rr002_record_fail() { + FAIL_COUNT=$((FAIL_COUNT + 1)) + rr002_mark_blocker "$1" +} + +rr002_record_skip() { + SKIP_COUNT=$((SKIP_COUNT + 1)) +} + +rr002_truncate_output() { + awk 'NR <= 80 { print; next } NR == 81 { print "... [truncated]"; exit }' +} + +rr002_report_check() { + local title="$1" + local command_text="$2" + local status="$3" + local state="$4" + local output="$5" + + { + printf '## %s\n\n' "${title}" + printf -- '- Command: `%s`\n' "${command_text}" + printf -- '- Exit code: `%s`\n' "${status}" + printf -- '- Check state: `%s`\n\n' "${state}" + printf '```text\n' + if [[ -n "${output}" ]]; then + printf '%s\n' "${output}" | rr002_truncate_output + else + printf '(no output)\n' + fi + printf '```\n\n' + } >> "${REPORT_FILE}" +} + +rr002_run_confined() { + local __out_var="$1" + shift + local captured status + local gomodcache + + gomodcache="$(go env GOMODCACHE 2>/dev/null || true)" + if [[ -z "${gomodcache}" ]]; then + gomodcache="${RUN_DIR}/gopath/pkg/mod" + fi + + set +e + captured="$( + env -i \ + HOME="${RUN_DIR}/home" \ + PATH="${PATH}" \ + GOCACHE="${RUN_DIR}/gocache" \ + GOPATH="${RUN_DIR}/gopath" \ + GOMODCACHE="${gomodcache}" \ + TMPDIR="${RUN_DIR}/tmp" \ + GOTMPDIR="${RUN_DIR}/tmp" \ + GOPROXY=off \ + GOSUMDB=off \ + GOFLAGS=-mod=readonly \ + TERM=dumb \ + "$@" 2>&1 + )" + status=$? + + printf -v "${__out_var}" '%s' "${captured}" + return "${status}" +} + +rr002_run_expected_success_with_timeout() { + local title="$1" + local timeout_seconds="$2" + shift + shift + local output status command_text + command_text="$*" + + set +e + rr002_run_confined output timeout "${timeout_seconds}" "$@" + status=$? + set -e + + if [[ "${status}" == "0" ]]; then + rr002_report_check "${title}" "${command_text}" "${status}" "PASS" "${output}" + rr002_record_pass + elif [[ "${status}" == "124" ]]; then + rr002_report_check "${title}" "${command_text}" "${status}" "BLOCKED" "${output}" + rr002_record_fail "${title} timed out; expected safe command did not exit" + else + rr002_report_check "${title}" "${command_text}" "${status}" "BLOCKED" "${output}" + rr002_record_fail "${title} failed with exit code ${status}" + fi +} + +rr002_run_expected_success() { + local title="$1" + shift + rr002_run_expected_success_with_timeout "${title}" "${TIMEOUT_SECONDS}" "$@" +} + +rr002_run_expected_unsupported() { + local title="$1" + shift + local output status command_text + command_text="$*" + + set +e + rr002_run_confined output timeout "${TIMEOUT_SECONDS}" "$@" + status=$? + set -e + + if [[ "${status}" == "124" ]]; then + rr002_report_check "${title}" "${command_text}" "${status}" "BLOCKED" "${output}" + rr002_record_fail "${title} timed out while probing unsupported safe flag" + elif [[ "${status}" == "0" ]]; then + rr002_report_check "${title}" "${command_text}" "${status}" "PASS" "${output}" + rr002_record_pass + else + rr002_report_check "${title}" "${command_text}" "${status}" "SKIP" "${output}" + rr002_record_skip + fi +} + +rr002_record_static_skip() { + local title="$1" + local command_text="$2" + local reason="$3" + rr002_report_check "${title}" "${command_text}" "not-run" "SKIP" "${reason}" + rr002_record_skip +} + +rr_report_begin "${REPORT_FILE}" +rr_report_section "${REPORT_FILE}" "RR-002 CLI Smoke + Config Matrix" "Local, report-only, deterministic smoke layer. Provider calls, network calls, app tool execution, release workflows, and tag mutation are not performed." + +timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +branch="$(git branch --show-current 2>/dev/null || true)" +commit="$(git log -1 --oneline 2>/dev/null || true)" +status_before="$(git status --short 2>/dev/null || true)" + +preflight_body="$( + printf 'timestamp: %s\n' "${timestamp}" + printf 'branch: %s\n' "${branch:-unknown}" + printf 'commit: %s\n' "${commit:-unknown}" + printf 'repo root: %s\n' "${ROOT}" + printf 'report directory: %s\n' "${REPORT_DIR}" + printf 'timeout seconds: %s\n' "${TIMEOUT_SECONDS}" + printf 'build timeout seconds: %s\n' "${BUILD_TIMEOUT_SECONDS}" + printf 'working tree before:\n' + if [[ -n "${status_before}" ]]; then + printf '%s\n' "${status_before}" + else + printf '(clean)\n' + fi +)" +rr_report_list "${REPORT_FILE}" "Preflight" "${preflight_body}" + +if ! command -v timeout >/dev/null 2>&1; then + rr002_report_check "Timeout availability" "command -v timeout" "1" "BLOCKED" "timeout command is required for bounded CLI smoke checks" + rr002_record_fail "timeout command unavailable" +fi + +inventory_body="$( + printf 'safe CLI entrypoints discovered:\n' + printf 'go run ./cmd/gorkbot --help\n' + printf 'go run ./cmd/gorkbot -h\n' + printf 'go run ./cmd/gorkweb --help\n' + printf 'go run ./cmd/gorkweb -h\n' + printf 'unsupported safe flag probes:\n' + printf 'go run ./cmd/gorkbot --version\n' + printf 'go run ./cmd/gorkweb --version\n' + printf 'unsafe/deferred commands:\n' + printf 'go run ./cmd/gorkbot version: skipped; no version subcommand discovered and bare arg can fall through toward runtime startup\n' + printf 'go run ./cmd/gorkweb version: skipped; no version subcommand discovered and bare arg can fall through toward runtime startup\n' + printf 'config/profile env seams discovered:\n' + printf 'GORKBOT_HARNESS_MODE, GORKBOT_TRACE_MODE, GORKBOT_AUTO_REBUILD, GORKBOT_SAVE_TRAJECTORIES, GORKBOT_PRIMARY, GORKBOT_CONSULTANT, GORKBOT_PRIMARY_MODEL, GORKBOT_CONSULTANT_MODEL, GORKBOT_MEMORY_ENABLE_SEMANTIC, GORKBOT_MEMORY_EMBEDDER\n' + printf 'vector/semantic memory inspected only: yes\n' +)" +rr_report_list "${REPORT_FILE}" "Initial inventory findings" "${inventory_body}" +rr002_record_pass + +if [[ "${RR002_SKIP_CLI:-0}" == "1" ]]; then + rr002_record_static_skip "CLI smoke checks" "go run ./cmd/gorkbot|./cmd/gorkweb help probes" "RR002_SKIP_CLI=1" +else + rr002_run_expected_success_with_timeout "CLI build cache warmup" "${BUILD_TIMEOUT_SECONDS}" "go" "test" "./cmd/gorkbot" "./cmd/gorkweb" "-run" "^$" + rr002_run_expected_success "gorkbot help long" "go" "run" "./cmd/gorkbot" "--help" + rr002_run_expected_success "gorkbot help short" "go" "run" "./cmd/gorkbot" "-h" + rr002_run_expected_unsupported "gorkbot unsupported version flag" "go" "run" "./cmd/gorkbot" "--version" + rr002_record_static_skip "gorkbot unsafe bare version" "go run ./cmd/gorkbot version" "Skipped because no safe version subcommand was discovered." + + rr002_run_expected_success "gorkweb help long" "go" "run" "./cmd/gorkweb" "--help" + rr002_run_expected_success "gorkweb help short" "go" "run" "./cmd/gorkweb" "-h" + rr002_run_expected_unsupported "gorkweb unsupported version flag" "go" "run" "./cmd/gorkweb" "--version" + rr002_record_static_skip "gorkweb unsafe bare version" "go run ./cmd/gorkweb version" "Skipped because no safe version subcommand was discovered." +fi + +if [[ "${RR002_SKIP_CONFIG_MATRIX:-0}" == "1" ]]; then + rr002_record_static_skip "Config/profile matrix" "go test ./pkg/profile" "RR002_SKIP_CONFIG_MATRIX=1" +else + rr002_run_expected_success "Config/profile matrix" "go" "test" "./pkg/profile" +fi + +status_after="$(git status --short 2>/dev/null || true)" +tracked_diff_after="$( + { + git diff --name-only 2>/dev/null || true + git diff --cached --name-only 2>/dev/null || true + } | awk 'NF' | sort -u +)" +mutation_body="$( + printf 'working tree after:\n' + if [[ -n "${status_after}" ]]; then + printf '%s\n' "${status_after}" + else + printf '(clean)\n' + fi + printf 'tracked changed files after:\n' + if [[ -n "${tracked_diff_after}" ]]; then + printf '%s\n' "${tracked_diff_after}" + else + printf '(none)\n' + fi + printf 'generated output confined to: %s\n' "${REPORT_DIR}" +)" +rr_report_list "${REPORT_FILE}" "Mutation confinement" "${mutation_body}" +if [[ "${status_after}" != "${status_before}" ]]; then + rr002_record_fail "working tree status changed during RR-002" +elif [[ -n "${tracked_diff_after}" && -z "${status_before}" ]]; then + rr002_record_fail "tracked files changed during RR-002" +else + rr002_record_pass +fi + +safety_body="$( + printf 'network calls: no\n' + printf 'provider calls: no\n' + printf 'app tool execution: no\n' + printf 'release workflow execution: no\n' + printf 'tag creation/mutation/deletion: no\n' + printf 'release publication: no\n' + printf 'artifact upload: no\n' + printf 'destructive cleanup: no\n' + printf 'Go module network disabled: GOPROXY=off GOSUMDB=off\n' +)" +rr_report_list "${REPORT_FILE}" "Operational safety" "${safety_body}" +rr002_record_pass + +if [[ -n "${BLOCKERS}" ]]; then + FINAL_RECOMMENDATION="BLOCKED" +elif [[ "${SKIP_COUNT}" -gt 0 ]]; then + FINAL_RECOMMENDATION="RR002_INCOMPLETE" +fi + +final_body="$( + printf 'status: REPORT_ONLY\n' + printf 'recommendation: %s\n' "${FINAL_RECOMMENDATION}" + printf 'checks passed: %s\n' "${PASS_COUNT}" + printf 'checks failed: %s\n' "${FAIL_COUNT}" + printf 'checks skipped: %s\n' "${SKIP_COUNT}" + printf 'blockers:\n' + if [[ -n "${BLOCKERS}" ]]; then + printf '%s\n' "${BLOCKERS}" + else + printf '(none)\n' + fi +)" +rr_report_list "${REPORT_FILE}" "Final recommendation" "${final_body}" + +echo "[rr002] report path: ${REPORT_FILE}" +echo "[rr002] final recommendation: ${FINAL_RECOMMENDATION}" +echo "[rr002] checks passed=${PASS_COUNT} failed=${FAIL_COUNT} skipped=${SKIP_COUNT}" + +if [[ "${FINAL_RECOMMENDATION}" == "BLOCKED" ]]; then + exit 1 +fi diff --git a/scripts/release_readiness/test_rr002_smoke.sh b/scripts/release_readiness/test_rr002_smoke.sh new file mode 100755 index 0000000..9904a1a --- /dev/null +++ b/scripts/release_readiness/test_rr002_smoke.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +RR002="${ROOT}/scripts/release_readiness/rr002_cli_smoke.sh" +READINESS="${ROOT}/scripts/release_readiness/readiness.sh" +DOCS="${ROOT}/docs/release/README.md" + +required_files=( + "${RR002}" + "${READINESS}" + "${DOCS}" +) + +for file in "${required_files[@]}"; do + [[ -f "${file}" ]] || { + echo "[rr002-smoke] missing ${file}" >&2 + exit 1 + } +done + +[[ -x "${RR002}" ]] || { + echo "[rr002-smoke] rr002_cli_smoke.sh is not executable" >&2 + exit 1 +} + +bash -n "${RR002}" +bash -n "${READINESS}" + +grep -Fq "RR-002 CLI Smoke + Config Matrix" "${RR002}" +grep -Fq ".local/release-readiness/rr002" "${RR002}" +grep -Fq "RR002_PASS" "${RR002}" +grep -Fq "RR002_INCOMPLETE" "${RR002}" +grep -Fq "BLOCKED" "${RR002}" +grep -Fq "go run ./cmd/gorkbot --help" "${RR002}" +grep -Fq "go run ./cmd/gorkweb --help" "${RR002}" +grep -Fq "go test ./pkg/profile" "${RR002}" +grep -Fq "RR-002 CLI Smoke + Config Matrix" "${READINESS}" +grep -Fq "bash scripts/release_readiness/rr002_cli_smoke.sh" "${DOCS}" + +forbidden_patterns=( + "rm -""rf" + "git pu""sh" + "gh rel""ease" + "gh workflow ""run" + "cu""rl" + "wg""et" + "s""sh" + "s""cp" + " n""c " + "net""cat" + "ev""al" +) + +for pattern in "${forbidden_patterns[@]}"; do + if grep -Fq "${pattern}" "${RR002}"; then + echo "[rr002-smoke] forbidden shell pattern in rr002_cli_smoke.sh: ${pattern}" >&2 + exit 1 + fi +done + +tmp_dir="${ROOT}/.local/release-readiness/rr002-smoke-test" +mkdir -p "${tmp_dir}" +( + cd "${tmp_dir}" + RR002_SKIP_CLI=1 RR002_SKIP_CONFIG_MATRIX=1 bash "${RR002}" >/tmp/rr002-smoke.out +) +grep -Fq "[rr002] report path:" /tmp/rr002-smoke.out +grep -Fq "[rr002] final recommendation:" /tmp/rr002-smoke.out + +echo "[rr002-smoke] OK" From 8a4cd7b1ddd79e9e12e9f14bb50db2816b8ea529 Mon Sep 17 00:00:00 2001 From: velariumai Date: Sat, 16 May 2026 22:14:46 -0500 Subject: [PATCH 2/2] fix(release): make rr002 smoke temp file portable --- scripts/release_readiness/test_rr002_smoke.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/release_readiness/test_rr002_smoke.sh b/scripts/release_readiness/test_rr002_smoke.sh index 9904a1a..e34236d 100755 --- a/scripts/release_readiness/test_rr002_smoke.sh +++ b/scripts/release_readiness/test_rr002_smoke.sh @@ -61,11 +61,14 @@ done tmp_dir="${ROOT}/.local/release-readiness/rr002-smoke-test" mkdir -p "${tmp_dir}" +tmp_root="${TMPDIR:-/tmp}" +out_file="$(mktemp "${tmp_root%/}/rr002-smoke.XXXXXX")" +trap 'rm -f "$out_file"' EXIT ( cd "${tmp_dir}" - RR002_SKIP_CLI=1 RR002_SKIP_CONFIG_MATRIX=1 bash "${RR002}" >/tmp/rr002-smoke.out + RR002_SKIP_CLI=1 RR002_SKIP_CONFIG_MATRIX=1 bash "${RR002}" >"${out_file}" ) -grep -Fq "[rr002] report path:" /tmp/rr002-smoke.out -grep -Fq "[rr002] final recommendation:" /tmp/rr002-smoke.out +grep -Fq "[rr002] report path:" "${out_file}" +grep -Fq "[rr002] final recommendation:" "${out_file}" echo "[rr002-smoke] OK"