forked from wrenn/wrenn
Compare commits
2 Commits
10facf30ed
...
v0.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
| e503afbc0f | |||
| 2f167f406c |
@ -3,24 +3,42 @@ when:
|
||||
branch: main
|
||||
|
||||
steps:
|
||||
sandbox-1:
|
||||
build-go:
|
||||
image: python:3.13
|
||||
environment:
|
||||
WRENN_API_KEY:
|
||||
from_secret: wrenn_api_key
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- pip install wrenn
|
||||
- export GO_VERSION=$$(grep '^go ' go.mod | cut -d' ' -f2)
|
||||
- python .woodpecker/scripts/build_go.py
|
||||
depends_on: []
|
||||
|
||||
build-rust:
|
||||
image: python:3.13
|
||||
environment:
|
||||
WRENN_API_KEY:
|
||||
from_secret: wrenn_api_key
|
||||
commands:
|
||||
- pip install wrenn
|
||||
- export RUST_VERSION=$$(grep '^rust-version ' envd-rs/Cargo.toml | cut -d'"' -f2)
|
||||
- python .woodpecker/scripts/build_rust.py
|
||||
depends_on: []
|
||||
|
||||
tag-release:
|
||||
image: python:3.13
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- VERSION=$$(cat VERSION_CP)
|
||||
- git config user.name "R3dRum92"
|
||||
- git config user.email "tksadik@omukk.dev"
|
||||
- git tag "v$${VERSION}"
|
||||
- git push "https://tksadik92:$${GITEA_TOKEN}@git.omukk.dev/tksadik92/wrenn-releases.git" "v$${VERSION}"
|
||||
depends_on: [build-go, build-rust]
|
||||
|
||||
sandbox-2:
|
||||
release-notes:
|
||||
image: python:3.13
|
||||
environment:
|
||||
WRENN_API_KEY:
|
||||
@ -32,9 +50,9 @@ steps:
|
||||
commands:
|
||||
- pip install wrenn
|
||||
- python .woodpecker/scripts/release_notes.py
|
||||
depends_on: [sandbox-1]
|
||||
depends_on: [tag-release]
|
||||
|
||||
sandbox-3:
|
||||
publish-github:
|
||||
image: python:3.13
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
@ -42,4 +60,4 @@ steps:
|
||||
commands:
|
||||
- pip install httpx
|
||||
- python .woodpecker/scripts/publish_github.py
|
||||
depends_on: [sandbox-2]
|
||||
depends_on: [release-notes]
|
||||
|
||||
@ -10,6 +10,11 @@ REPO_DIR = "/opt/wrenn"
|
||||
BUILDS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "builds")
|
||||
|
||||
|
||||
def read_remote_version(capsule: Capsule, filename: str) -> str:
|
||||
content = capsule.files.read_bytes(f"{REPO_DIR}/{filename}")
|
||||
return content.decode("utf-8").strip()
|
||||
|
||||
|
||||
def run(capsule: Capsule, cmd: str, timeout: int = 30) -> int:
|
||||
result = capsule.commands.run(cmd, timeout=timeout)
|
||||
if result.exit_code != 0:
|
||||
@ -92,18 +97,23 @@ def download_artifacts(capsule: Capsule) -> bool:
|
||||
|
||||
local_dir = os.path.normpath(BUILDS_DIR)
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
versions = {
|
||||
"wrenn-cp": read_remote_version(capsule, "VERSION_CP"),
|
||||
"wrenn-agent": read_remote_version(capsule, "VERSION_AGENT"),
|
||||
}
|
||||
|
||||
for entry in files:
|
||||
name = entry.name or "unknown"
|
||||
remote_path = f"{remote_dir}/{name}"
|
||||
local_path = os.path.join(local_dir, name)
|
||||
print(f"Downloading {name} ({entry.size or '?'} bytes)...")
|
||||
local_name = f"{name}-{versions[name]}" if name in versions else name
|
||||
local_path = os.path.join(local_dir, local_name)
|
||||
print(f"Downloading {name} as {local_name} ({entry.size or '?'} bytes)...")
|
||||
|
||||
with open(local_path, "wb") as f:
|
||||
for chunk in capsule.files.download_stream(remote_path):
|
||||
f.write(chunk)
|
||||
|
||||
print(f"OK [download {name}]")
|
||||
print(f"OK [download {local_name}]")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -13,6 +13,16 @@ RUST_PATH = (
|
||||
)
|
||||
|
||||
|
||||
def read_envd_version(capsule: Capsule) -> str:
|
||||
content = capsule.files.read_bytes(f"{REPO_DIR}/envd-rs/Cargo.toml")
|
||||
for line in content.decode("utf-8").splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("version ="):
|
||||
return stripped.split("=", 1)[1].strip().strip('"')
|
||||
print("FAIL [version]: envd-rs/Cargo.toml has no package version", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run(capsule: Capsule, cmd: str, timeout: int = 30, envs={}) -> int:
|
||||
result = capsule.commands.run(cmd, timeout=timeout, envs=envs)
|
||||
if result.exit_code != 0:
|
||||
@ -63,7 +73,6 @@ def install_rust(capsule: Capsule) -> bool:
|
||||
def clone_repo(capsule: Capsule) -> bool:
|
||||
try:
|
||||
capsule.git.clone(REPO_URL, REPO_DIR)
|
||||
capsule.commands.run(f"cd {REPO_DIR} && git checkout fix/large-operations")
|
||||
print("OK [git clone]")
|
||||
return True
|
||||
except GitCommandError as e:
|
||||
@ -75,19 +84,8 @@ def build_rust(capsule: Capsule) -> bool:
|
||||
if run(capsule, f"mkdir -p {REPO_DIR}/builds") != 0:
|
||||
return False
|
||||
|
||||
# result = capsule.commands.run("file --version")
|
||||
# print(result.stdout)
|
||||
# result = capsule.commands.run(
|
||||
# 'git rev-parse --short HEAD 2>/dev/null || echo "unknown"'
|
||||
# )
|
||||
# commit = result.stdout
|
||||
|
||||
# run(capsule, f"mkdir -p {REPO_DIR}/builds")
|
||||
# result = capsule.commands.run("which musl-gcc")
|
||||
# print(result.stdout)
|
||||
|
||||
handle = capsule.commands.run(
|
||||
"git checkout fix/large-operations && make build-envd",
|
||||
"make build-envd",
|
||||
background=True,
|
||||
cwd=REPO_DIR,
|
||||
envs={"PATH": RUST_PATH},
|
||||
@ -108,38 +106,23 @@ def build_rust(capsule: Capsule) -> bool:
|
||||
return False
|
||||
|
||||
print("OK [rust build]")
|
||||
|
||||
# if (
|
||||
# run(
|
||||
# capsule,
|
||||
# f"cp {REPO_DIR}/envd-rs/target/x86_64-unknown-linux-musl/release/envd {REPO_DIR}/builds/envd",
|
||||
# envs={"BIN_DIR": REPO_DIR},
|
||||
# )
|
||||
# != 0
|
||||
# ):
|
||||
# return False
|
||||
|
||||
# result = capsule.commands.run(f"readelf -d {REPO_DIR}/builds/envd 2>&1")
|
||||
# print(result.stdout, end="")
|
||||
# if result.stderr:
|
||||
# print(result.stderr, end="", file=sys.stderr)
|
||||
# result = capsule.commands.run(f"file {REPO_DIR}/builds/envd 2>&1")
|
||||
# print(result.stdout)
|
||||
return True
|
||||
|
||||
|
||||
def download_artifacts(capsule: Capsule) -> bool:
|
||||
version = read_envd_version(capsule)
|
||||
remote_path = f"{REPO_DIR}/builds/envd"
|
||||
local_dir = os.path.normpath(BUILDS_DIR)
|
||||
local_path = os.path.join(local_dir, "envd")
|
||||
local_name = f"envd-{version}"
|
||||
local_path = os.path.join(local_dir, local_name)
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
|
||||
print("Downloading envd...")
|
||||
print(f"Downloading envd as {local_name}...")
|
||||
with open(local_path, "wb") as f:
|
||||
for chunk in capsule.files.download_stream(remote_path):
|
||||
f.write(chunk)
|
||||
|
||||
print("OK [download envd]")
|
||||
print(f"OK [download {local_name}]")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@ -52,22 +52,6 @@ def run(capsule: Capsule, cmd: str, cwd: str | None = None, timeout: int = 30) -
|
||||
return 0
|
||||
|
||||
|
||||
def install_opencode(capsule: Capsule) -> None:
|
||||
print("Installing OpenCode...")
|
||||
if run(capsule, "apt update", timeout=60) != 0:
|
||||
sys.exit(1)
|
||||
if (
|
||||
run(
|
||||
capsule,
|
||||
"curl -fsSL https://opencode.ai/install | bash -s -- --version 1.14.31",
|
||||
timeout=120,
|
||||
)
|
||||
!= 0
|
||||
):
|
||||
sys.exit(1)
|
||||
print("OK [opencode installed]")
|
||||
|
||||
|
||||
def get_tags(capsule: Capsule) -> tuple[str, str | None]:
|
||||
result = capsule.commands.run(
|
||||
f"cd {REPO_DIR} && git tag --sort=-version:refname",
|
||||
@ -121,32 +105,73 @@ def get_git_context(
|
||||
|
||||
def generate_release_notes(
|
||||
capsule: Capsule,
|
||||
current_tag: str,
|
||||
git_log: str,
|
||||
git_diff: str,
|
||||
output_path: str,
|
||||
model: str,
|
||||
) -> None:
|
||||
prompt = (
|
||||
f"You are writing release notes for version {current_tag} of a software project.\n\n"
|
||||
f"Here is what changed between the previous version and this one:\n\n"
|
||||
f"Commit messages:\n{git_log}\n\n"
|
||||
f"Files and areas that changed:\n{git_diff}\n\n"
|
||||
f"Write the release notes in plain, friendly language that any developer can understand "
|
||||
f"without deep knowledge of the codebase. Avoid jargon like 'goroutine', 'PTY', 'envd', "
|
||||
f"or internal function names — describe what the change means for the user instead. "
|
||||
f"Group related changes under headings that reflect what actually changed. "
|
||||
f"Only include sections that are relevant to these specific changes. "
|
||||
f"Start with a short one-line summary of what this release is about. "
|
||||
f"Keep each bullet point to one clear sentence.\n\n"
|
||||
f"Here is an example of the style to aim for — not a template to copy:\n\n"
|
||||
f"{RELEASE_NOTES_EXAMPLE}\n\n"
|
||||
f"You MUST start the document with `## What's New`\n"
|
||||
f"The very next line MUST be a single short summary sentence.\n"
|
||||
f"Output only the markdown. No intro, no explanation."
|
||||
f"CRITICAL: Do not output any conversational filler, acknowledgments, or thoughts "
|
||||
f"like 'Let me look at the changes'. Output absolutely nothing except the final markdown."
|
||||
)
|
||||
prompt = f"""
|
||||
You are inside a cloned git repository at:
|
||||
|
||||
{REPO_DIR}
|
||||
|
||||
Generate release notes for the latest tagged version of this software project.
|
||||
|
||||
Before writing anything, inspect the repository yourself using git commands.
|
||||
|
||||
You MUST determine:
|
||||
1. The latest version tag.
|
||||
2. The previous version tag, if one exists.
|
||||
3. The commits between the previous tag and the latest tag.
|
||||
4. The files and areas changed between those tags.
|
||||
|
||||
Use commands like:
|
||||
|
||||
git tag --sort=-version:refname
|
||||
|
||||
If there are at least two tags, compare the newest tag against the previous tag:
|
||||
|
||||
git log PREVIOUS_TAG..LATEST_TAG --pretty=format:'%s (%h)'
|
||||
git diff PREVIOUS_TAG..LATEST_TAG --stat
|
||||
git diff PREVIOUS_TAG..LATEST_TAG --name-only
|
||||
|
||||
If there is only one tag, inspect the latest tag with:
|
||||
|
||||
git log LATEST_TAG --pretty=format:'%s (%h)' -n 50
|
||||
git show LATEST_TAG --stat
|
||||
git show LATEST_TAG --name-only
|
||||
|
||||
Do not rely on any pre-injected commit list or diff summary.
|
||||
You must inspect the git history yourself.
|
||||
|
||||
Write the release notes in plain, friendly language that any developer can understand
|
||||
without deep knowledge of the codebase.
|
||||
|
||||
Avoid jargon like "goroutine", "PTY", "envd", or internal function names.
|
||||
Describe what the change means for the user instead.
|
||||
|
||||
Group related changes under headings that reflect what actually changed.
|
||||
Only include sections that are relevant to the actual changes.
|
||||
Do not include CI/CD-only changes.
|
||||
|
||||
Start with:
|
||||
|
||||
## What's New
|
||||
|
||||
The very next line must be a single short summary sentence.
|
||||
|
||||
Keep each bullet point to one clear sentence.
|
||||
|
||||
Here is an example of the style to aim for — not a template to copy:
|
||||
|
||||
{RELEASE_NOTES_EXAMPLE}
|
||||
|
||||
Output only the final markdown.
|
||||
No intro.
|
||||
No explanation.
|
||||
No conversational filler.
|
||||
No acknowledgments.
|
||||
No "I checked the logs" text.
|
||||
No thoughts.
|
||||
""".strip()
|
||||
|
||||
prompt_b64 = base64.b64encode(prompt.encode("utf-8")).decode("utf-8")
|
||||
|
||||
@ -161,23 +186,22 @@ def generate_release_notes(
|
||||
print(f"FAIL [write prompt]: {result.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# FIX: Wrapper function to handle execution and authentication dynamically
|
||||
def run_opencode_with_model(target_model: str) -> int:
|
||||
env = ""
|
||||
if "zhipu" in target_model.lower():
|
||||
env = f"ZHIPU_API_KEY={os.environ.get('ZHIPU_API_KEY', '')}"
|
||||
|
||||
raw_output_path = "/tmp/opencode_raw.txt"
|
||||
cmd = (
|
||||
f"{env} "
|
||||
f"~/.opencode/bin/opencode run "
|
||||
f'"Read the attached file and generate the release notes. Output ONLY markdown." '
|
||||
f"--model {target_model} "
|
||||
f"--file /tmp/oc_prompt.txt "
|
||||
f"> {output_path}"
|
||||
f"> {raw_output_path}"
|
||||
)
|
||||
|
||||
cmd_result = capsule.commands.run(cmd, cwd=REPO_DIR, timeout=120)
|
||||
|
||||
cmd_result = capsule.commands.run(cmd, cwd=REPO_DIR, timeout=300)
|
||||
if cmd_result.exit_code != 0:
|
||||
print(
|
||||
f"FAIL [opencode via {target_model}]: exit={cmd_result.exit_code}",
|
||||
@ -186,6 +210,30 @@ def generate_release_notes(
|
||||
print(f"STDOUT:\n{cmd_result.stdout}", file=sys.stderr)
|
||||
print(f"STDERR:\n{cmd_result.stderr}", file=sys.stderr)
|
||||
|
||||
clean_cmd = (
|
||||
f"awk 'found || /^## What.s [Nn]ew/ {{ found=1; print }}' "
|
||||
f"{raw_output_path} > {output_path}"
|
||||
)
|
||||
|
||||
clean_result = capsule.commands.run(clean_cmd, cwd=REPO_DIR, timeout=10)
|
||||
if clean_result.exit_code != 0:
|
||||
print(f"FAIL [clean output]: {clean_result.stderr}", file=sys.stderr)
|
||||
return clean_result.exit_code
|
||||
|
||||
check_result = capsule.commands.run(
|
||||
f"grep -q '^## What.s New' {output_path}",
|
||||
cwd=REPO_DIR,
|
||||
timeout=10,
|
||||
)
|
||||
if check_result.exit_code != 0:
|
||||
print(
|
||||
"FAIL: Could not find release notes heading in opencode output",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(cmd_result.stdout, file=sys.stderr)
|
||||
print(cmd_result.stderr, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return cmd_result.exit_code
|
||||
|
||||
# First attempt with the target model
|
||||
@ -239,20 +287,10 @@ def main() -> None:
|
||||
)
|
||||
print("OK [git clone]")
|
||||
|
||||
current_tag, previous_tag = get_tags(capsule)
|
||||
git_log, git_diff = get_git_context(capsule, current_tag, previous_tag)
|
||||
|
||||
# Note: This simply creates the directory string safely
|
||||
output_path = os.path.normpath(CAPSULE_OUTPUT)
|
||||
|
||||
generate_release_notes(
|
||||
capsule,
|
||||
current_tag,
|
||||
git_log,
|
||||
git_diff,
|
||||
output_path,
|
||||
model,
|
||||
)
|
||||
generate_release_notes(capsule, output_path, model)
|
||||
|
||||
download_release_notes(capsule)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user