From 2f49bad19bd831286c9096a0a4fb88e186e47589 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 5 Mar 2026 13:52:36 -0500 Subject: [PATCH] chore: added a recipe to help identify high risk change prs for testing (#7651) --- RELEASE_CHECKLIST.md | 39 +- .../release_risk_check/recipe.yaml | 116 +++++ .../release_risk_check/release_risk_report.py | 435 ++++++++++++++++++ workflow_recipes/release_risk_check/run.sh | 12 + 4 files changed, 588 insertions(+), 14 deletions(-) create mode 100644 workflow_recipes/release_risk_check/recipe.yaml create mode 100755 workflow_recipes/release_risk_check/release_risk_report.py create mode 100755 workflow_recipes/release_risk_check/run.sh diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 66da4374ac..9a10dc6b57 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -2,14 +2,25 @@ ## Version: {{VERSION}} +### Identify the high risk changes in this Release + +``` +./workflow_recipes/release_risk_check/run.sh {{VERSION}} +``` + +It will generate an analysis report in `/tmp/release_report_final.md` and perform testing is necessary for high risk pr changes. + + +## Regression Testing + Make a copy of this document for each version and check off as steps are verified. -## Provider Testing +### Provider Testing - [ ] Run `./scripts/test_providers.sh` locally from the release branch and verify all providers/models work - [ ] Launch goose, click reset providers, choose databricks and a model -## Starting Conversations +### Starting Conversations Test various ways to start a conversation: @@ -26,9 +37,9 @@ Test various ways to start a conversation: - [ ] Open a new window, click chat in left side for new chat - [ ] Click "create a tamagotchi game" in popular chat topics to test developer extension -## Recipes +### Recipes -### Create Recipe from Session +#### Create Recipe from Session - [ ] Start a simple chat conversation like "hi" - [ ] Click "create a recipe from this session" in the bottom chat bar @@ -37,7 +48,7 @@ Test various ways to start a conversation: - [ ] Can launch create and run recipe - launches in a new window showing as a recipe agent chat with parameters filled in and interact with it - [ ] Recipe should be saved in recipe library -### Use Existing Recipe +#### Use Existing Recipe - [ ] Pick trip planner from recipe hub (go/gooserecipes) - [ ] See the warning whether to trust this recipe (only on fresh install) @@ -46,7 +57,7 @@ Test various ways to start a conversation: - [ ] Check results are reasonable - [ ] Ask how many days the trip is for - should say 14 -### Recipe Management +#### Recipe Management - [ ] Go to recipe manager and enter a new recipe to generate a joke - [ ] See that it works if you run it @@ -56,7 +67,7 @@ Test various ways to start a conversation: - [ ] Delete the recipe from the recipe manager - [ ] Verify recipe is actually deleted -### Recipe from File +#### Recipe from File - [ ] Create a file `~/.config/goose/recipes/test-recipe.yaml` with the following content: @@ -79,40 +90,40 @@ recipe: - [ ] Go to hub and enter "what is the value of test_param" - [ ] See a new chat that says it has no idea (recipe is no longer active) -## Extensions +### Extensions -### Manual Extension Addition +#### Manual Extension Addition - [ ] Can manually add an extension using random quotes from project - [ ] Add new custom stdio extension with the following command and save: - [ ] `node /ABSOLUTE/PATH/TO/goose/ui/desktop/tests/e2e/basic-mcp.ts` (use your actual project path) - [ ] Should add and can chat to ask for a random quote -### Playwright Extension +#### Playwright Extension - [ ] Install the playwright extension from the extensions hub - [ ] Tell it to open a browser and search on Google for cats - [ ] Verify that the browser opens and navigates -### Extension with Environment Variables +#### Extension with Environment Variables - [ ] Install an extension from deeplink that needs env variables: - [ ] Use: `goose://extension?cmd=npx&arg=-y&arg=%40upstash%2Fcontext7-mcp&id=context7&name=Context7&description=Use%20up-to-date%20code%20and%20docs&env=TEST_ACCESS_TOKEN` - [ ] Extension page should load with env variables modal showing - [ ] Allow form input and saving extension -## Speech-to-Text (Local Model) +### Speech-to-Text (Local Model) - [ ] Go to Settings > Chat > Voice dictation provider and select the small model - [ ] Run a quick test that speech-to-text is working (click the mic button, speak, verify transcription) - [ ] Also try OpenAI using your OpenAI key -## Settings +### Settings - [ ] Settings page loads and all tabs load - [ ] Can change dark mode setting -## Follow-up Issues +### Follow-up Issues Link any GitHub issues filed during testing: diff --git a/workflow_recipes/release_risk_check/recipe.yaml b/workflow_recipes/release_risk_check/recipe.yaml new file mode 100644 index 0000000000..1ae472e521 --- /dev/null +++ b/workflow_recipes/release_risk_check/recipe.yaml @@ -0,0 +1,116 @@ +version: 1.0.0 +title: "Release Change Risk Check" +description: "Create a report to access the change in an upcoming release" + +instructions: | + ## Step 1: Generate the heuristic report + + Run the script to collect PR data and do initial risk scoring: + + {{recipe_dir}}/release_risk_report.py --version {{version}} -o /tmp/release_report.md + + This produces a report with each PR classified as HIGH/MEDIUM/LOW based on file changes, lines of code, and core path analysis. + + ## Step 2: AI review of MEDIUM and HIGH risk PRs + + Take the MEDIUM and HIGH risk PRs from the Step 1 report and feed them to an LLM with the following prompt: + + --- + + You are a release risk assessor for **Goose**, an open-source AI-powered CLI coding agent built in Rust with a React/Electron desktop UI. + + ### Architecture (most sensitive areas first) + + **CRITICAL — changes here can bypass security or cause data loss:** + - **Permission system** (`crates/goose/src/permission/`) — controls what the agent is allowed to do. Permission bypass = agent has unrestricted access to user's machine. + - **Tool execution pipeline** (`crates/goose/src/agents/tool_execution.rs`, `agent.rs`) — dispatches shell commands, file edits, etc. Bugs here can cause uncontrolled execution. + - **Security inspection** (`crates/goose/src/tool_inspection.rs`) — detects prompt injection and destructive operations. Disabling or weakening = injection attacks succeed. + - **Server action approval** (`crates/goose-server/src/routes/action_required.rs`) — user approval API. If broken, agent executes without user consent. + - **Session database** (`crates/goose/src/session/session_manager.rs`) — stores all conversations in SQLite. Schema changes risk data loss. + - **Authentication** (`crates/goose-server/src/auth.rs`) — access control for the HTTP server. + + **HIGH — changes here affect core functionality:** + - **Agent loop** (`crates/goose/src/agents/`) — message routing, turn limits, conversation compaction. + - **Provider integrations** (`crates/goose/src/providers/`) — LLM API calls, credential handling, cost tracking, response parsing. + - **Extension manager** (`crates/goose/src/agents/extension_manager.rs`) — loads MCP extensions, tool discovery. Malicious extensions could be loaded. + - **Server routes** (`crates/goose-server/src/routes/`) — HTTP API that the desktop UI and CLI talk to. + + **MEDIUM — changes here affect specific features:** + - **CLI commands** (`crates/goose-cli/`) — argument parsing, session management, recipe execution. + - **Desktop UI** (`ui/desktop/src/`) — React components, state management, settings. + - **Platform extensions** (`crates/goose/src/agents/platform_extensions/`) — built-in tools like shell, file edit. + + ### Risk levels — assign ONE per PR: + + - **HIGH**: Change could cause security bypass, data loss, crashes affecting all users, or break core agent functionality. Examples: modifying permission checks, changing tool execution flow, altering session schema, touching auth logic. + - **MEDIUM**: Change could cause issues in specific scenarios but not for all users. Examples: provider-specific bug, UI regression, new feature with limited blast radius, config changes. + - **LOW**: Very unlikely to cause issues. Examples: small isolated fix with tests, additive-only new feature in non-core area, UI cosmetic change, test-only changes. + + ### Signals that INCREASE risk: + - Modifies existing logic in critical/high areas (vs adding new code) + - No testing section in PR description + - No approvers or only bot approvers + - Large diff touching many files across different subsystems + - Reverts or re-applies of previous changes (indicates instability) + - Touches error handling or fallback paths (silent failures) + + ### Signals that DECREASE risk: + - Has thorough testing section with specific test cases mentioned + - Change is purely additive (new files, new feature behind flag) + - Only touches test files or snapshots + - Small, focused diff in one subsystem + + ### Task + + For each PR below: + + 1. **Assess risk** — assign HIGH / MEDIUM / LOW with reasoning + 2. **Testing confidence** — check if the PR has a testing section. If yes, summarise what was tested in one sentence. If no, say "No testing section". + 3. **Suggest testing steps** — for PRs you rate HIGH or MEDIUM, provide 2-4 concrete test steps + + Respond in this format for each PR: + + | PR | Heuristic | AI Risk | Reasoning | Concern | Testing | + |----|-----------|---------|-----------|---------|---------| + + Where: + - **Heuristic** = the score from Step 1 (HIGH or MEDIUM) + - **AI Risk** = your assessment (HIGH / MEDIUM / LOW) + - **Reasoning** = 1-2 sentences explaining why + - **Concern** = specific thing to watch for during release, or "None" + - **Testing** = summary of PR's testing section, or "No testing section" + + If your AI Risk differs from the Heuristic, bold it to highlight the disagreement. + + Then, for each PR you rated HIGH or MEDIUM, list suggested testing steps below the table. Use this guide: + + ### PRs to review: + + + + --- + + ## Step 3: Generate the final report + + Combine the outputs from Step 1 and Step 2 into a final report: + + 1. Start with the Step 1 report header (repo, total PRs, risk summary) + 2. Update the risk summary counts based on AI-revised risk levels + 3. For each MEDIUM/HIGH PR, append the AI assessment: + - `AI assessment: [LEVEL] — reasoning` + - `AI concern: concern text` + - If AI disagreed with heuristic, note: `(downgraded from HIGH)` or `(upgraded from MEDIUM)` + 4. LOW risk PRs and skipped PRs remain unchanged from Step 1 + 5. Add a summary section at the top listing the top concerns across all HIGH risk PRs + +prompt: follow the instructions to generate the final report + +parameters: + - key: "version" + input_type: string + requirement: required + description: "release version" + +extensions: + - type: platform + name: developer \ No newline at end of file diff --git a/workflow_recipes/release_risk_check/release_risk_report.py b/workflow_recipes/release_risk_check/release_risk_report.py new file mode 100755 index 0000000000..16cc1008de --- /dev/null +++ b/workflow_recipes/release_risk_check/release_risk_report.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +""" +Generate a release risk assessment report for a Goose release PR. + +Usage: + .release_risk_report.py --version 1.27.0 + .release_risk_report.py --version 1.27.0 --output report.md + .release_risk_report.py --version 1.27.0 --pr 7611 +""" + +import argparse +import json +import re +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed + +REPO = "block/goose" + +# Paths considered documentation-only +DOC_PATTERNS = [ + r"^documentation/", +] + +# Paths considered high-risk core code +CORE_PATHS = [ + "crates/goose/src/agents/", + "crates/goose/src/providers/", + "crates/goose-server/", + "crates/goose-cli/", + "crates/goose/src/session", + "crates/goose/src/permission", +] + +# Dependency lock files (safe to skip — lock files only, not manifests) +DEP_LOCK_FILES = [ + "Cargo.lock", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", +] + + +def run_gh(args: list[str]) -> str: + """Run a gh CLI command and return stdout.""" + result = subprocess.run( + ["gh"] + args, + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f" [warn] gh command failed: gh {' '.join(args)}", file=sys.stderr) + print(f" {result.stderr.strip()}", file=sys.stderr) + return "" + return result.stdout.strip() + + +def find_release_pr(repo: str, version: str) -> int | None: + """Find the open release PR matching the given version string.""" + output = run_gh([ + "pr", "list", "--repo", repo, "--state", "open", + "--json", "number,title", "--limit", "100", + ]) + if not output: + return None + prs = json.loads(output) + for pr in prs: + if version.lower() in pr["title"].lower(): + return pr["number"] + return None + + +def get_pr_body(repo: str, pr_number: int) -> str: + """Get the body of a PR.""" + return run_gh(["pr", "view", str(pr_number), "--repo", repo, "--json", "body", "--jq", ".body"]) + + +def extract_pr_numbers(body: str) -> list[int]: + """Extract PR numbers from the 'Changes in This Release' section.""" + section_match = re.search(r"## Changes in This Release\s*\n", body) + if not section_match: + print("[error] Could not find 'Changes in This Release' section", file=sys.stderr) + return [] + + section_text = body[section_match.end():] + end_match = re.search(r"\n---|\n##", section_text) + if end_match: + section_text = section_text[:end_match.start()] + + pr_numbers = re.findall(r"\(#(\d+)\)", section_text) + return [int(n) for n in pr_numbers] + + +def is_doc_only(files: list[dict]) -> bool: + """Return True if all changed files are documentation/non-code.""" + if not files: + return False + for f in files: + path = f.get("path", "") + if not any(re.search(pat, path) for pat in DOC_PATTERNS): + return False + return True + + +def is_deps_only(files: list[dict]) -> bool: + """Return True if all changed files are dependency/lock files.""" + if not files: + return False + for f in files: + path = f.get("path", "") + basename = path.rsplit("/", 1)[-1] + if basename not in DEP_LOCK_FILES: + return False + return True + + +def assess_risk(files: list[dict]) -> dict: + """Assess risk level and return risk info dict.""" + total_additions = sum(f.get("additions", 0) for f in files) + total_deletions = sum(f.get("deletions", 0) for f in files) + total_lines = total_additions + total_deletions + num_files = len(files) + + paths = [f.get("path", "") for f in files] + + # Classify files + core_files = [p for p in paths if any(p.startswith(cp) for cp in CORE_PATHS)] + test_files = [p for p in paths if "test" in p.lower() or "snap" in p.lower()] + dep_files = [p for p in paths if p.rsplit("/", 1)[-1] in DEP_LOCK_FILES] + prod_files = [p for p in paths if p not in test_files and p not in dep_files] + + # Collect risk factors + factors = [] + + if total_lines > 500: + factors.append(f"Large change ({total_lines} lines)") + elif total_lines > 200: + factors.append(f"Medium-sized change ({total_lines} lines)") + + if num_files > 10: + factors.append(f"Touches {num_files} files") + + if core_files: + factors.append(f"Modifies core code: {', '.join(summarize_paths(core_files))}") + + if dep_files: + factors.append(f"Dependency changes: {', '.join(f.rsplit('/', 1)[-1] for f in dep_files)}") + + if len(prod_files) > 0 and len(test_files) == 0: + factors.append("No test files changed") + + # Determine risk level + risk_score = 0 + if total_lines > 500: + risk_score += 2 + elif total_lines > 200: + risk_score += 1 + if num_files > 10: + risk_score += 1 + if core_files: + risk_score += 2 + if len(prod_files) > 0 and len(test_files) == 0: + risk_score += 1 + + if risk_score >= 4: + level = "HIGH" + elif risk_score >= 2: + level = "MEDIUM" + else: + level = "LOW" + + # Summarize key paths (deduplicate to directory level) + key_paths = summarize_paths(paths) + + return { + "level": level, + "score": risk_score, + "factors": factors, + "total_additions": total_additions, + "total_deletions": total_deletions, + "total_lines": total_lines, + "num_files": num_files, + "key_paths": key_paths, + } + + +def summarize_paths(paths: list[str], max_display: int = 5) -> list[str]: + """Summarize a list of file paths to their common directories.""" + dirs = set() + for p in paths: + parts = p.split("/") + if len(parts) > 3: + dirs.add("/".join(parts[:3]) + "/") + else: + dirs.add(p) + result = sorted(dirs) + if len(result) > max_display: + return result[:max_display] + [f"... and {len(result) - max_display} more"] + return result + + +def extract_testing_section(body: str) -> str: + """Extract the testing section from a PR description.""" + if not body: + return "No" + + # Try various common testing section headers + patterns = [ + r"##\s*Test(?:ing)?\s*(?:Plan|Strategy)?[ \t]*\r?\n", + r"##\s*How (?:Has This Been |to )Test(?:ed)?[ \t]*\r?\n", + r"##\s*Verification[ \t]*\r?\n", + ] + + for pattern in patterns: + match = re.search(pattern, body, re.IGNORECASE) + if match: + section = body[match.end():] + # Stop at next heading or HR + end = re.search(r"\n##\s|\n---", section) + if end: + section = section[:end.start()] + section = section.strip() + if section: + # Clean up markdown checkboxes and compress whitespace + section = re.sub(r"", "", section, flags=re.DOTALL).strip() + # Remove empty checkbox lines like "- [ ] Feature" + lines = [l for l in section.split("\n") if l.strip()] + cleaned = "\n".join(lines).strip() + if cleaned and not re.match(r"^[\s\-\[\]xX]*$", cleaned): + return cleaned + return "No" + + +def get_pr_details(repo: str, pr_number: int) -> dict | None: + """Fetch title, body, author, files, and approvers for a single PR.""" + output = run_gh([ + "pr", "view", str(pr_number), "--repo", repo, + "--json", "number,title,body,author,files", + ]) + if not output: + return None + + data = json.loads(output) + + # Get reviewers who approved + reviews_output = run_gh([ + "api", f"repos/{repo}/pulls/{pr_number}/reviews", + "--jq", '[.[] | select(.state == "APPROVED") | .user.login] | unique | join(", ")', + ]) + + files = data.get("files", []) or [] + body = (data.get("body") or "").strip() + + return { + "number": data["number"], + "title": data["title"], + "description": body, + "author": data.get("author", {}).get("login", "unknown"), + "approvers": reviews_output if reviews_output else "none", + "files": files, + "testing": extract_testing_section(body), + } + + +def generate_report(repo: str, release_pr: int, pr_details: list[dict], skipped_docs: list[dict]) -> str: + """Generate a risk assessment markdown report.""" + lines = [ + f"# Release Risk Assessment — PR #{release_pr}", + "", + f"**Repository:** {repo}", + f"**Total PRs in release:** {len(pr_details) + len(skipped_docs)}", + f"**Assessed PRs:** {len(pr_details)} (skipped {len(skipped_docs)} doc-only)", + "", + ] + + # Summary counts by risk + risk_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0} + for pr in pr_details: + risk_counts[pr["risk"]["level"]] += 1 + + lines.append(f"**Risk summary:** {risk_counts['HIGH']} High, {risk_counts['MEDIUM']} Medium, {risk_counts['LOW']} Low") + lines.append("") + lines.append("---") + lines.append("") + + # Sort by risk score descending, then by PR number + pr_details_sorted = sorted(pr_details, key=lambda x: (-x["risk"]["score"], x["number"])) + + for i, pr in enumerate(pr_details_sorted, 1): + risk = pr["risk"] + risk_badge = {"HIGH": "HIGH RISK", "MEDIUM": "MEDIUM RISK", "LOW": "LOW RISK"}[risk["level"]] + + lines.append(f"## {i}. #{pr['number']} — {pr['title']} `[{risk_badge}]`") + lines.append("") + lines.append(f"- **Author:** @{pr['author']}") + lines.append(f"- **Approved by:** {pr['approvers']}") + + # Include PR description for MEDIUM and HIGH risk PRs + if risk["level"] in ("HIGH", "MEDIUM") and pr.get("description"): + desc = pr["description"].replace("\n", " ").strip() + if len(desc) > 500: + desc = desc[:500] + "..." + lines.append(f"- **Description:** {desc}") + + lines.append(f"- **Files changed:** {risk['num_files']} (+{risk['total_additions']} / -{risk['total_deletions']})") + + # Include individual file paths for MEDIUM and HIGH risk PRs + if risk["level"] in ("HIGH", "MEDIUM") and pr.get("files"): + file_lines = [f" - `{f['path']}` (+{f.get('additions', 0)}/-{f.get('deletions', 0)})" for f in pr["files"]] + lines.append(f"- **Files:**") + lines.extend(file_lines) + else: + lines.append(f"- **Key paths:** {', '.join(risk['key_paths']) if risk['key_paths'] else 'N/A'}") + + if risk["factors"]: + lines.append(f"- **Risk factors:** {'; '.join(risk['factors'])}") + else: + lines.append("- **Risk factors:** None notable") + + # Testing section + testing = pr["testing"] + if testing == "No": + lines.append("- **Testing:** No") + else: + # Show first 300 chars of testing section + testing_summary = testing.replace("\n", " ").strip() + if len(testing_summary) > 300: + testing_summary = testing_summary[:300] + "..." + lines.append(f"- **Testing:** {testing_summary}") + + lines.append(f"- **Link:** https://github.com/{repo}/pull/{pr['number']}") + lines.append("") + + # Skipped docs section + if skipped_docs: + lines.append("---") + lines.append("") + lines.append(f"## Skipped: {len(skipped_docs)} documentation-only PRs") + lines.append("") + for pr in skipped_docs: + lines.append(f"- #{pr['number']} — {pr['title']} (@{pr['author']})") + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Generate a release risk assessment report") + parser.add_argument("--pr", type=int, help="Release PR number (if omitted, searches for open release PRs)") + parser.add_argument("--version", required=True, help="Version string to search for (e.g. 1.27.0)") + parser.add_argument("--repo", default=REPO, help=f"GitHub repo (default: {REPO})") + parser.add_argument("--output", "-o", help="Output file (default: stdout)") + parser.add_argument("--workers", type=int, default=5, help="Number of parallel workers (default: 5)") + args = parser.parse_args() + + # Step 1: Find the release PR + release_pr = args.pr + if not release_pr: + print(f"Searching for open PR with '{args.version}' in title...", file=sys.stderr) + release_pr = find_release_pr(args.repo, args.version) + if not release_pr: + print(f"[error] No open PR found with '{args.version}' in title", file=sys.stderr) + sys.exit(1) + print(f"Using release PR #{release_pr}", file=sys.stderr) + + # Step 2: Get PR body and extract linked PR numbers + body = get_pr_body(args.repo, release_pr) + if not body: + print("[error] Could not fetch PR body", file=sys.stderr) + sys.exit(1) + + pr_numbers = extract_pr_numbers(body) + if not pr_numbers: + print("[error] No PR numbers found in the release body", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(pr_numbers)} PRs in the release notes", file=sys.stderr) + + # Step 3: Fetch details for each PR in parallel + all_prs = [] + failed = [] + + with ThreadPoolExecutor(max_workers=args.workers) as executor: + futures = { + executor.submit(get_pr_details, args.repo, num): num + for num in pr_numbers + } + for future in as_completed(futures): + num = futures[future] + try: + detail = future.result() + if detail: + all_prs.append(detail) + print(f" Fetched #{num}: {detail['title'][:60]}", file=sys.stderr) + else: + failed.append(num) + except Exception as e: + print(f" [error] Failed to fetch #{num}: {e}", file=sys.stderr) + failed.append(num) + + if failed: + print(f"\n[warn] Failed to fetch {len(failed)} PRs: {failed}", file=sys.stderr) + + # Step 4: Filter and assess + assessed_prs = [] + skipped_docs = [] + + for pr in all_prs: + if is_doc_only(pr["files"]): + skipped_docs.append(pr) + print(f" [skip] #{pr['number']}: doc-only", file=sys.stderr) + elif is_deps_only(pr["files"]): + skipped_docs.append(pr) + print(f" [skip] #{pr['number']}: deps-only", file=sys.stderr) + else: + pr["risk"] = assess_risk(pr["files"]) + assessed_prs.append(pr) + + skipped_docs.sort(key=lambda x: x["number"]) + + print(f"\nAssessed {len(assessed_prs)} PRs, skipped {len(skipped_docs)} (doc/deps-only)", file=sys.stderr) + + # Step 5: Generate report + report = generate_report(args.repo, release_pr, assessed_prs, skipped_docs) + + if args.output: + with open(args.output, "w") as f: + f.write(report) + print(f"Report written to {args.output}", file=sys.stderr) + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/workflow_recipes/release_risk_check/run.sh b/workflow_recipes/release_risk_check/run.sh new file mode 100755 index 0000000000..767d2edc22 --- /dev/null +++ b/workflow_recipes/release_risk_check/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 1.27.0" + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +goose run --recipe "$SCRIPT_DIR/recipe.yaml" --params "version=$1"