1
0
forked from wrenn/wrenn

2 Commits

Author SHA1 Message Date
10facf30ed Add CI for release notes
Some checks failed
ci/woodpecker/push/pipeline Pipeline was canceled
2026-05-13 22:07:44 +06:00
e3a60a990f v0.1.6 (#45)
## What's New?
Performance updates for large capsules, admin panel enhancement and bug fixes

### Envd
- Fixed bug with sandbox metrics calculation
- Page cache drop and balloon inflation to reduce memfile snapshot
- Updated rpc timeout logic for better control
- Added tests

### Admin Panel
- Add/Remove platform admin
- Updated template deletion logic for fine grained permission

### Others
- Minor frontend visual improvement
- Minor bugfixes
- Version bump

Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com>
Reviewed-on: wrenn/wrenn#45
Co-authored-by: pptx704 <rafeed@omukk.dev>
Co-committed-by: pptx704 <rafeed@omukk.dev>
2026-05-13 22:07:10 +06:00
4 changed files with 97 additions and 146 deletions

View File

@ -3,42 +3,24 @@ when:
branch: main
steps:
build-go:
sandbox-1:
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]
release-notes:
sandbox-2:
image: python:3.13
environment:
WRENN_API_KEY:
@ -50,9 +32,9 @@ steps:
commands:
- pip install wrenn
- python .woodpecker/scripts/release_notes.py
depends_on: [tag-release]
depends_on: [sandbox-1]
publish-github:
sandbox-3:
image: python:3.13
environment:
GITHUB_TOKEN:
@ -60,4 +42,4 @@ steps:
commands:
- pip install httpx
- python .woodpecker/scripts/publish_github.py
depends_on: [release-notes]
depends_on: [sandbox-2]

View File

@ -10,11 +10,6 @@ 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:
@ -97,23 +92,18 @@ 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_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)...")
local_path = os.path.join(local_dir, name)
print(f"Downloading {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 {local_name}]")
print(f"OK [download {name}]")
return True

View File

@ -13,16 +13,6 @@ 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:
@ -73,6 +63,7 @@ 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:
@ -84,8 +75,19 @@ 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(
"make build-envd",
"git checkout fix/large-operations && make build-envd",
background=True,
cwd=REPO_DIR,
envs={"PATH": RUST_PATH},
@ -106,23 +108,38 @@ 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_name = f"envd-{version}"
local_path = os.path.join(local_dir, local_name)
local_path = os.path.join(local_dir, "envd")
os.makedirs(local_dir, exist_ok=True)
print(f"Downloading envd as {local_name}...")
print("Downloading envd...")
with open(local_path, "wb") as f:
for chunk in capsule.files.download_stream(remote_path):
f.write(chunk)
print(f"OK [download {local_name}]")
print("OK [download envd]")
return True

View File

@ -52,6 +52,22 @@ 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",
@ -105,73 +121,32 @@ 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 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 = (
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_b64 = base64.b64encode(prompt.encode("utf-8")).decode("utf-8")
@ -186,22 +161,23 @@ 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"> {raw_output_path}"
f"> {output_path}"
)
cmd_result = capsule.commands.run(cmd, cwd=REPO_DIR, timeout=300)
cmd_result = capsule.commands.run(cmd, cwd=REPO_DIR, timeout=120)
if cmd_result.exit_code != 0:
print(
f"FAIL [opencode via {target_model}]: exit={cmd_result.exit_code}",
@ -210,30 +186,6 @@ 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
@ -287,10 +239,20 @@ 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, output_path, model)
generate_release_notes(
capsule,
current_tag,
git_log,
git_diff,
output_path,
model,
)
download_release_notes(capsule)