forked from wrenn/wrenn
Compare commits
2 Commits
10facf30ed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e503afbc0f | |||
| 2f167f406c |
@ -3,24 +3,42 @@ when:
|
|||||||
branch: main
|
branch: main
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
sandbox-1:
|
build-go:
|
||||||
image: python:3.13
|
image: python:3.13
|
||||||
environment:
|
environment:
|
||||||
WRENN_API_KEY:
|
WRENN_API_KEY:
|
||||||
from_secret: wrenn_api_key
|
from_secret: wrenn_api_key
|
||||||
GITEA_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
commands:
|
commands:
|
||||||
- pip install wrenn
|
- pip install wrenn
|
||||||
- export GO_VERSION=$$(grep '^go ' go.mod | cut -d' ' -f2)
|
- export GO_VERSION=$$(grep '^go ' go.mod | cut -d' ' -f2)
|
||||||
- python .woodpecker/scripts/build_go.py
|
- 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)
|
- VERSION=$$(cat VERSION_CP)
|
||||||
- git config user.name "R3dRum92"
|
- git config user.name "R3dRum92"
|
||||||
- git config user.email "tksadik@omukk.dev"
|
- git config user.email "tksadik@omukk.dev"
|
||||||
- git tag "v$${VERSION}"
|
- git tag "v$${VERSION}"
|
||||||
- git push "https://tksadik92:$${GITEA_TOKEN}@git.omukk.dev/tksadik92/wrenn-releases.git" "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
|
image: python:3.13
|
||||||
environment:
|
environment:
|
||||||
WRENN_API_KEY:
|
WRENN_API_KEY:
|
||||||
@ -32,9 +50,9 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- pip install wrenn
|
- pip install wrenn
|
||||||
- python .woodpecker/scripts/release_notes.py
|
- python .woodpecker/scripts/release_notes.py
|
||||||
depends_on: [sandbox-1]
|
depends_on: [tag-release]
|
||||||
|
|
||||||
sandbox-3:
|
publish-github:
|
||||||
image: python:3.13
|
image: python:3.13
|
||||||
environment:
|
environment:
|
||||||
GITHUB_TOKEN:
|
GITHUB_TOKEN:
|
||||||
@ -42,4 +60,4 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- pip install httpx
|
- pip install httpx
|
||||||
- python .woodpecker/scripts/publish_github.py
|
- 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")
|
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:
|
def run(capsule: Capsule, cmd: str, timeout: int = 30) -> int:
|
||||||
result = capsule.commands.run(cmd, timeout=timeout)
|
result = capsule.commands.run(cmd, timeout=timeout)
|
||||||
if result.exit_code != 0:
|
if result.exit_code != 0:
|
||||||
@ -92,18 +97,23 @@ def download_artifacts(capsule: Capsule) -> bool:
|
|||||||
|
|
||||||
local_dir = os.path.normpath(BUILDS_DIR)
|
local_dir = os.path.normpath(BUILDS_DIR)
|
||||||
os.makedirs(local_dir, exist_ok=True)
|
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:
|
for entry in files:
|
||||||
name = entry.name or "unknown"
|
name = entry.name or "unknown"
|
||||||
remote_path = f"{remote_dir}/{name}"
|
remote_path = f"{remote_dir}/{name}"
|
||||||
local_path = os.path.join(local_dir, name)
|
local_name = f"{name}-{versions[name]}" if name in versions else name
|
||||||
print(f"Downloading {name} ({entry.size or '?'} bytes)...")
|
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:
|
with open(local_path, "wb") as f:
|
||||||
for chunk in capsule.files.download_stream(remote_path):
|
for chunk in capsule.files.download_stream(remote_path):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
|
||||||
print(f"OK [download {name}]")
|
print(f"OK [download {local_name}]")
|
||||||
|
|
||||||
return True
|
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:
|
def run(capsule: Capsule, cmd: str, timeout: int = 30, envs={}) -> int:
|
||||||
result = capsule.commands.run(cmd, timeout=timeout, envs=envs)
|
result = capsule.commands.run(cmd, timeout=timeout, envs=envs)
|
||||||
if result.exit_code != 0:
|
if result.exit_code != 0:
|
||||||
@ -63,7 +73,6 @@ def install_rust(capsule: Capsule) -> bool:
|
|||||||
def clone_repo(capsule: Capsule) -> bool:
|
def clone_repo(capsule: Capsule) -> bool:
|
||||||
try:
|
try:
|
||||||
capsule.git.clone(REPO_URL, REPO_DIR)
|
capsule.git.clone(REPO_URL, REPO_DIR)
|
||||||
capsule.commands.run(f"cd {REPO_DIR} && git checkout fix/large-operations")
|
|
||||||
print("OK [git clone]")
|
print("OK [git clone]")
|
||||||
return True
|
return True
|
||||||
except GitCommandError as e:
|
except GitCommandError as e:
|
||||||
@ -75,19 +84,8 @@ def build_rust(capsule: Capsule) -> bool:
|
|||||||
if run(capsule, f"mkdir -p {REPO_DIR}/builds") != 0:
|
if run(capsule, f"mkdir -p {REPO_DIR}/builds") != 0:
|
||||||
return False
|
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(
|
handle = capsule.commands.run(
|
||||||
"git checkout fix/large-operations && make build-envd",
|
"make build-envd",
|
||||||
background=True,
|
background=True,
|
||||||
cwd=REPO_DIR,
|
cwd=REPO_DIR,
|
||||||
envs={"PATH": RUST_PATH},
|
envs={"PATH": RUST_PATH},
|
||||||
@ -108,38 +106,23 @@ def build_rust(capsule: Capsule) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
print("OK [rust build]")
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def download_artifacts(capsule: Capsule) -> bool:
|
def download_artifacts(capsule: Capsule) -> bool:
|
||||||
|
version = read_envd_version(capsule)
|
||||||
remote_path = f"{REPO_DIR}/builds/envd"
|
remote_path = f"{REPO_DIR}/builds/envd"
|
||||||
local_dir = os.path.normpath(BUILDS_DIR)
|
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)
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
print("Downloading envd...")
|
print(f"Downloading envd as {local_name}...")
|
||||||
with open(local_path, "wb") as f:
|
with open(local_path, "wb") as f:
|
||||||
for chunk in capsule.files.download_stream(remote_path):
|
for chunk in capsule.files.download_stream(remote_path):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
|
||||||
print("OK [download envd]")
|
print(f"OK [download {local_name}]")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -52,22 +52,6 @@ def run(capsule: Capsule, cmd: str, cwd: str | None = None, timeout: int = 30) -
|
|||||||
return 0
|
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]:
|
def get_tags(capsule: Capsule) -> tuple[str, str | None]:
|
||||||
result = capsule.commands.run(
|
result = capsule.commands.run(
|
||||||
f"cd {REPO_DIR} && git tag --sort=-version:refname",
|
f"cd {REPO_DIR} && git tag --sort=-version:refname",
|
||||||
@ -121,32 +105,73 @@ def get_git_context(
|
|||||||
|
|
||||||
def generate_release_notes(
|
def generate_release_notes(
|
||||||
capsule: Capsule,
|
capsule: Capsule,
|
||||||
current_tag: str,
|
|
||||||
git_log: str,
|
|
||||||
git_diff: str,
|
|
||||||
output_path: str,
|
output_path: str,
|
||||||
model: str,
|
model: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
prompt = (
|
prompt = f"""
|
||||||
f"You are writing release notes for version {current_tag} of a software project.\n\n"
|
You are inside a cloned git repository at:
|
||||||
f"Here is what changed between the previous version and this one:\n\n"
|
|
||||||
f"Commit messages:\n{git_log}\n\n"
|
{REPO_DIR}
|
||||||
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 "
|
Generate release notes for the latest tagged version of this software project.
|
||||||
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. "
|
Before writing anything, inspect the repository yourself using git commands.
|
||||||
f"Group related changes under headings that reflect what actually changed. "
|
|
||||||
f"Only include sections that are relevant to these specific changes. "
|
You MUST determine:
|
||||||
f"Start with a short one-line summary of what this release is about. "
|
1. The latest version tag.
|
||||||
f"Keep each bullet point to one clear sentence.\n\n"
|
2. The previous version tag, if one exists.
|
||||||
f"Here is an example of the style to aim for — not a template to copy:\n\n"
|
3. The commits between the previous tag and the latest tag.
|
||||||
f"{RELEASE_NOTES_EXAMPLE}\n\n"
|
4. The files and areas changed between those tags.
|
||||||
f"You MUST start the document with `## What's New`\n"
|
|
||||||
f"The very next line MUST be a single short summary sentence.\n"
|
Use commands like:
|
||||||
f"Output only the markdown. No intro, no explanation."
|
|
||||||
f"CRITICAL: Do not output any conversational filler, acknowledgments, or thoughts "
|
git tag --sort=-version:refname
|
||||||
f"like 'Let me look at the changes'. Output absolutely nothing except the final markdown."
|
|
||||||
)
|
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")
|
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)
|
print(f"FAIL [write prompt]: {result.stderr}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# FIX: Wrapper function to handle execution and authentication dynamically
|
|
||||||
def run_opencode_with_model(target_model: str) -> int:
|
def run_opencode_with_model(target_model: str) -> int:
|
||||||
env = ""
|
env = ""
|
||||||
if "zhipu" in target_model.lower():
|
if "zhipu" in target_model.lower():
|
||||||
env = f"ZHIPU_API_KEY={os.environ.get('ZHIPU_API_KEY', '')}"
|
env = f"ZHIPU_API_KEY={os.environ.get('ZHIPU_API_KEY', '')}"
|
||||||
|
|
||||||
|
raw_output_path = "/tmp/opencode_raw.txt"
|
||||||
cmd = (
|
cmd = (
|
||||||
f"{env} "
|
f"{env} "
|
||||||
f"~/.opencode/bin/opencode run "
|
f"~/.opencode/bin/opencode run "
|
||||||
f'"Read the attached file and generate the release notes. Output ONLY markdown." '
|
f'"Read the attached file and generate the release notes. Output ONLY markdown." '
|
||||||
f"--model {target_model} "
|
f"--model {target_model} "
|
||||||
f"--file /tmp/oc_prompt.txt "
|
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:
|
if cmd_result.exit_code != 0:
|
||||||
print(
|
print(
|
||||||
f"FAIL [opencode via {target_model}]: exit={cmd_result.exit_code}",
|
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"STDOUT:\n{cmd_result.stdout}", file=sys.stderr)
|
||||||
print(f"STDERR:\n{cmd_result.stderr}", 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
|
return cmd_result.exit_code
|
||||||
|
|
||||||
# First attempt with the target model
|
# First attempt with the target model
|
||||||
@ -239,20 +287,10 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
print("OK [git clone]")
|
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
|
# Note: This simply creates the directory string safely
|
||||||
output_path = os.path.normpath(CAPSULE_OUTPUT)
|
output_path = os.path.normpath(CAPSULE_OUTPUT)
|
||||||
|
|
||||||
generate_release_notes(
|
generate_release_notes(capsule, output_path, model)
|
||||||
capsule,
|
|
||||||
current_tag,
|
|
||||||
git_log,
|
|
||||||
git_diff,
|
|
||||||
output_path,
|
|
||||||
model,
|
|
||||||
)
|
|
||||||
|
|
||||||
download_release_notes(capsule)
|
download_release_notes(capsule)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user