refactor: split backend into standalone server project
Remove embedded frontend and workflow editor assets, add standalone API deployment configuration, and retain the current backend service updates.
This commit is contained in:
@@ -1,225 +0,0 @@
|
||||
---
|
||||
name: release-version
|
||||
description: Use when preparing or publishing a new semantic version release, updating bilingual changelogs, creating Git tags, or publishing GitHub/Gitee release pages for this repository.
|
||||
---
|
||||
|
||||
# Release Version
|
||||
|
||||
## Overview
|
||||
|
||||
Create releases with a strict `vx.y.z` tag, produce bilingual changelog entries from actual Git history, publish both the `docs` submodule update and repository tag, and create the GitHub and Gitee Release page entries in one controlled workflow.
|
||||
|
||||
Run the workflow from the repository root. Read [references/changelog-style.md](references/changelog-style.md) before drafting the human-facing update notes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Validate the requested version.
|
||||
2. Inspect the repository and determine the comparison range.
|
||||
3. Draft bilingual changelog entries from the actual diff.
|
||||
4. Commit and push the `docs` submodule.
|
||||
5. Commit the parent repository update if the submodule pointer changed.
|
||||
6. Create and push the annotated tag.
|
||||
7. Create GitHub and Gitee Release page entries for the tag.
|
||||
8. Verify both remote tags and both Release pages.
|
||||
|
||||
Do not skip the repository inspection step. Release notes must come from the real diff between tags, not from guesswork.
|
||||
|
||||
## Validate The Version
|
||||
|
||||
- Accept only tags that match `^v\d+\.\d+\.\d+$`.
|
||||
- Reject date-style tags such as `v20260414`.
|
||||
- Confirm the target tag does not already exist locally or on any configured remote.
|
||||
- Prefer the latest reachable semver tag as the previous release tag.
|
||||
- If no earlier semver tag exists, fall back to the latest reachable tag of any format and state that fallback in the changelog drafting notes.
|
||||
|
||||
Use the helper script first:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/release-version/scripts/collect_release_context.py \
|
||||
--repo . \
|
||||
--tag v1.2.3
|
||||
```
|
||||
|
||||
If the caller already specifies the previous tag, pass it explicitly:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/release-version/scripts/collect_release_context.py \
|
||||
--repo . \
|
||||
--tag v1.2.3 \
|
||||
--previous-tag v1.2.2
|
||||
```
|
||||
|
||||
If the repository-local helper is unavailable, fall back to `~/.codex/skills/release-version/scripts/collect_release_context.py`.
|
||||
|
||||
## Inspect The Repository
|
||||
|
||||
- Check `git status --short` in the parent repo.
|
||||
- Check `git -C docs status --short` in the `docs` submodule.
|
||||
- Read the JSON output of `collect_release_context.py`.
|
||||
- Use the commit list, changed files, and insertions/deletions to decide what is user-visible.
|
||||
- Prioritize behavior changes, new features, fixes, migrations, API changes, configuration changes, and documentation changes that matter to adopters.
|
||||
- Ignore pure formatting churn unless it changes usage.
|
||||
|
||||
If the working tree contains unrelated changes that would be risky to include in the release, stop and ask the user before proceeding.
|
||||
|
||||
## Draft The Changelog
|
||||
|
||||
Update these files:
|
||||
|
||||
- `docs/zh/docs/changelog.md`
|
||||
- `docs/en/docs/changelog.md`
|
||||
|
||||
Prepend a new entry using this exact structure:
|
||||
|
||||
```md
|
||||
## ${tag} (${yyyy-MM-dd})
|
||||
|
||||
### 更新内容
|
||||
|
||||
${content}
|
||||
|
||||
### 发布地址
|
||||
|
||||
- Github: <https://github.com/huabeitech/agent-desk/releases/tag/${tag}>
|
||||
- Gitee: <https://gitee.com/huabeitech/agent-desk/releases/tag/${tag}>
|
||||
```
|
||||
|
||||
For the English file, keep the same links and heading level, but translate the section heading and content naturally:
|
||||
|
||||
```md
|
||||
## ${tag} (${yyyy-MM-dd})
|
||||
|
||||
### Updates
|
||||
|
||||
${content}
|
||||
|
||||
### Release Links
|
||||
|
||||
- Github: <https://github.com/huabeitech/agent-desk/releases/tag/${tag}>
|
||||
- Gitee: <https://gitee.com/huabeitech/agent-desk/releases/tag/${tag}>
|
||||
```
|
||||
|
||||
Changelog writing rules:
|
||||
|
||||
- Write concise, user-facing summaries instead of raw commit subjects.
|
||||
- Keep Chinese and English entries semantically aligned.
|
||||
- Prefer 3-6 bullets unless the release is extremely small.
|
||||
- Group related changes into a single bullet when that reads better.
|
||||
- Mention compatibility-sensitive changes explicitly.
|
||||
- If the comparison baseline is a non-semver fallback tag, note that in your private reasoning, not in the public changelog unless the user asks for it.
|
||||
|
||||
## Commit And Push The Docs Submodule
|
||||
|
||||
After editing the changelog files:
|
||||
|
||||
1. Run `git -C docs status --short`.
|
||||
2. Review the diff with `git -C docs diff -- zh/docs/changelog.md en/docs/changelog.md`.
|
||||
3. Commit inside the `docs` submodule with a focused message such as `docs: update changelog for v1.2.3`.
|
||||
4. Push the `docs` submodule commit to its remote branch.
|
||||
|
||||
Branch rule:
|
||||
|
||||
- If `docs` is on a local branch, push that branch.
|
||||
- If `docs` is detached, push `HEAD` to `origin/main` unless the repository clearly uses another default branch.
|
||||
|
||||
## Commit The Parent Repository
|
||||
|
||||
If the `docs` submodule pointer changed in the parent repository, commit it before tagging. Otherwise the release tag will not reference the new changelog revision.
|
||||
|
||||
Recommended flow:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add docs
|
||||
git commit -m "chore: update docs submodule for v1.2.3"
|
||||
```
|
||||
|
||||
Only include unrelated parent-repo changes if the user explicitly wants them in the release commit.
|
||||
|
||||
## Create And Push The Tag
|
||||
|
||||
Create an annotated tag after the repository state is ready:
|
||||
|
||||
```bash
|
||||
git tag -a v1.2.3 -m "Release v1.2.3"
|
||||
```
|
||||
|
||||
Push the commit branch first if needed, then push the tag to every configured remote that should publish releases:
|
||||
|
||||
```bash
|
||||
git push github HEAD
|
||||
git push origin HEAD
|
||||
git push github v1.2.3
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
Adjust the branch name if `HEAD` is not tracking the intended release branch.
|
||||
|
||||
## Create GitHub And Gitee Releases
|
||||
|
||||
Pushing tags is not enough. The release is incomplete until both Release pages exist:
|
||||
|
||||
- GitHub: `https://github.com/huabeitech/agent-desk/releases/tag/${tag}`
|
||||
- Gitee: `https://gitee.com/huabeitech/agent-desk/releases/tag/${tag}`
|
||||
|
||||
Use the same concise release notes derived from the changelog. Prefer a bilingual body with Chinese first and English second.
|
||||
|
||||
Required credentials:
|
||||
|
||||
- GitHub: `GITHUB_TOKEN` or `GH_TOKEN` with access to `huabeitech/agent-desk` and permission to create releases. For a fine-grained PAT, use an organization-allowed lifetime and grant the repository at least `Contents: Read and write` plus `Metadata: Read`.
|
||||
- Gitee: `GITEE_ACCESS_TOKEN` or `GITEE_TOKEN` with release write access to `huabeitech/agent-desk`.
|
||||
|
||||
Never print tokens in command output or final responses. If the user pastes a token into the conversation, use it only for the requested release operation and recommend rotation after use.
|
||||
|
||||
Build the release body from the new changelog entry, for example:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/agent-desk-release
|
||||
awk 'BEGIN{p=0} /^## v1\.2\.3 /{p=1; next} /^## v[0-9]/{if(p) exit} p{print}' \
|
||||
docs/zh/docs/changelog.md | sed '/^### 发布地址/,$d' > /tmp/agent-desk-release/v1.2.3-zh.md
|
||||
awk 'BEGIN{p=0} /^## v1\.2\.3 /{p=1; next} /^## v[0-9]/{if(p) exit} p{print}' \
|
||||
docs/en/docs/changelog.md | sed '/^### Release Links/,$d' > /tmp/agent-desk-release/v1.2.3-en.md
|
||||
{
|
||||
printf '## 更新内容\n\n'
|
||||
sed '1,/^### 更新内容$/d' /tmp/agent-desk-release/v1.2.3-zh.md
|
||||
printf '\n## Updates\n\n'
|
||||
sed '1,/^### Updates$/d' /tmp/agent-desk-release/v1.2.3-en.md
|
||||
} > /tmp/agent-desk-release/v1.2.3-release-body.md
|
||||
```
|
||||
|
||||
Create the GitHub Release:
|
||||
|
||||
```bash
|
||||
token="${GITHUB_TOKEN:-$GH_TOKEN}"
|
||||
curl -sS -o /tmp/github_release_v1.2.3.json -w '%{http_code}' \
|
||||
-X POST https://api.github.com/repos/huabeitech/agent-desk/releases \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @<(jq -n --rawfile body /tmp/agent-desk-release/v1.2.3-release-body.md \
|
||||
'{tag_name:"v1.2.3", target_commitish:"main", name:"v1.2.3", body:$body, draft:false, prerelease:false}')
|
||||
```
|
||||
|
||||
Create the Gitee Release:
|
||||
|
||||
```bash
|
||||
token="${GITEE_ACCESS_TOKEN:-$GITEE_TOKEN}"
|
||||
curl -sS -o /tmp/gitee_release_v1.2.3.json -w '%{http_code}' \
|
||||
-X POST https://gitee.com/api/v5/repos/huabeitech/agent-desk/releases \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @<(jq -n --rawfile body /tmp/agent-desk-release/v1.2.3-release-body.md --arg token "${token}" \
|
||||
'{access_token:$token, tag_name:"v1.2.3", target_commitish:"main", name:"v1.2.3", body:$body, prerelease:false}')
|
||||
```
|
||||
|
||||
If creation returns `422`/already exists, fetch the existing release and verify it references the target tag before treating it as complete. If GitHub returns `Resource not accessible by personal access token`, inspect the API message and ask for a token that satisfies the organization policy and repository permissions.
|
||||
|
||||
## Final Verification
|
||||
|
||||
- Confirm `git rev-parse v1.2.3^{tag}` succeeds.
|
||||
- Confirm `git ls-remote --tags github v1.2.3` and `git ls-remote --tags origin v1.2.3` show the new tag.
|
||||
- Confirm `curl -sS -o /tmp/github_release_verify.json -w '%{http_code}' https://api.github.com/repos/huabeitech/agent-desk/releases/tags/v1.2.3` returns `200`.
|
||||
- Confirm `curl -sS -o /tmp/gitee_release_verify.json -w '%{http_code}' https://gitee.com/api/v5/repos/huabeitech/agent-desk/releases/tags/v1.2.3` returns `200`.
|
||||
- Confirm the `docs` submodule remote contains the changelog commit.
|
||||
- Confirm both parent and `docs` working trees are clean.
|
||||
- Summarize the previous tag used for comparison, the files updated, the commit hashes created, the remotes pushed, and the GitHub/Gitee Release URLs.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "Release Version"
|
||||
short_description: "Create semver tags and changelog drafts"
|
||||
default_prompt: "Use $release-version to create a new vx.y.z release, update changelogs, and push docs plus tags."
|
||||
@@ -1,61 +0,0 @@
|
||||
# Changelog Style
|
||||
|
||||
Use this guide when drafting `docs/zh/changelog.md` and `docs/en/changelog.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Turn Git history into short release notes that explain what changed for adopters.
|
||||
|
||||
## Keep
|
||||
|
||||
- New user-facing features.
|
||||
- Bug fixes with clear impact.
|
||||
- Breaking or compatibility-sensitive changes.
|
||||
- API, configuration, deployment, model, workflow, or schema changes that affect usage.
|
||||
- Important documentation updates when they unlock new workflows.
|
||||
|
||||
## Drop Or Compress
|
||||
|
||||
- Pure refactors with no visible effect.
|
||||
- Formatting-only changes.
|
||||
- Internal rename churn.
|
||||
- Mechanical dependency updates unless they fix a real issue.
|
||||
|
||||
## Chinese Style
|
||||
|
||||
- Use concise bullets.
|
||||
- Prefer product or workflow language over commit jargon.
|
||||
- Start with the effect, then mention the area if needed.
|
||||
- Keep terms consistent across bullets.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
- 优化会话列表查询与筛选逻辑,减少后台定位问题时的人工排查成本。
|
||||
- 修复消息发送链路中的异常处理,避免部分失败场景下页面状态不同步。
|
||||
```
|
||||
|
||||
## English Style
|
||||
|
||||
- Mirror the Chinese meaning instead of translating word by word.
|
||||
- Use direct release-note phrasing.
|
||||
- Prefer active wording and concrete impact.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
- Improved conversation list querying and filtering so operators can locate problem cases faster.
|
||||
- Fixed error handling in the message send flow to prevent UI state from drifting after partial failures.
|
||||
```
|
||||
|
||||
## Grouping Heuristics
|
||||
|
||||
- Merge multiple commits into one bullet when they deliver one outcome.
|
||||
- Separate bullets when the audience or impact differs.
|
||||
- Keep both language versions aligned in bullet count when possible.
|
||||
|
||||
## Before Finalizing
|
||||
|
||||
- Re-check that every bullet is supported by the diff.
|
||||
- Remove statements that depend on assumptions you cannot verify from code, tests, or commits.
|
||||
- Keep the notes short enough to scan in under a minute.
|
||||
@@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect release context between a target tag and its comparison baseline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SEMVER_TAG_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
|
||||
|
||||
|
||||
def run_git(
|
||||
repo: Path,
|
||||
args: list[str],
|
||||
allow_failure: bool = False,
|
||||
timeout_seconds: int = 15,
|
||||
) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
if allow_failure:
|
||||
return ""
|
||||
raise RuntimeError(f"git command timed out: git {' '.join(args)}")
|
||||
if result.returncode != 0 and not allow_failure:
|
||||
raise RuntimeError(result.stderr.strip() or "git command failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def parse_semver(tag: str) -> tuple[int, int, int] | None:
|
||||
match = SEMVER_TAG_RE.match(tag)
|
||||
if not match:
|
||||
return None
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def list_reachable_tags(repo: Path) -> list[str]:
|
||||
output = run_git(repo, ["tag", "--merged", "HEAD"])
|
||||
return [line.strip() for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def choose_previous_tag(repo: Path, tags: list[str], target_tag: str | None) -> tuple[str | None, str | None]:
|
||||
semver_tags: list[tuple[tuple[int, int, int], str]] = []
|
||||
other_tags: list[str] = []
|
||||
target_semver = parse_semver(target_tag) if target_tag else None
|
||||
|
||||
for tag in tags:
|
||||
if target_tag and tag == target_tag:
|
||||
continue
|
||||
parsed = parse_semver(tag)
|
||||
if parsed is None:
|
||||
other_tags.append(tag)
|
||||
continue
|
||||
if target_semver is not None and parsed >= target_semver:
|
||||
continue
|
||||
semver_tags.append((parsed, tag))
|
||||
|
||||
if semver_tags:
|
||||
semver_tags.sort(key=lambda item: item[0], reverse=True)
|
||||
return semver_tags[0][1], "semver"
|
||||
|
||||
if not other_tags:
|
||||
return None, None
|
||||
|
||||
candidates: list[tuple[int, str]] = []
|
||||
for tag in other_tags:
|
||||
ts = run_git(repo, ["log", "-1", "--format=%ct", tag], allow_failure=True)
|
||||
try:
|
||||
candidates.append((int(ts), tag))
|
||||
except ValueError:
|
||||
continue
|
||||
if not candidates:
|
||||
return other_tags[-1], "fallback"
|
||||
candidates.sort(reverse=True)
|
||||
return candidates[0][1], "fallback"
|
||||
|
||||
|
||||
def ensure_tag_absent(repo: Path, tag: str) -> None:
|
||||
local = run_git(repo, ["tag", "--list", tag])
|
||||
if local:
|
||||
raise ValueError(f"target tag already exists locally: {tag}")
|
||||
|
||||
remotes_output = run_git(repo, ["remote"])
|
||||
for remote in [line.strip() for line in remotes_output.splitlines() if line.strip()]:
|
||||
remote_hit = run_git(
|
||||
repo,
|
||||
["ls-remote", "--tags", remote, tag],
|
||||
allow_failure=True,
|
||||
timeout_seconds=8,
|
||||
)
|
||||
if remote_hit:
|
||||
raise ValueError(f"target tag already exists on remote {remote}: {tag}")
|
||||
|
||||
|
||||
def ensure_tag_exists(repo: Path, tag: str) -> None:
|
||||
hit = run_git(repo, ["rev-parse", "--verify", "--quiet", tag], allow_failure=True)
|
||||
if not hit:
|
||||
raise ValueError(f"previous tag does not exist locally: {tag}")
|
||||
|
||||
|
||||
def get_commit_list(repo: Path, rev_range: str) -> list[dict[str, str]]:
|
||||
output = run_git(repo, ["log", "--reverse", "--date=short", "--pretty=format:%H%x09%ad%x09%s", rev_range])
|
||||
commits: list[dict[str, str]] = []
|
||||
for line in output.splitlines():
|
||||
commit_hash, date_str, subject = line.split("\t", 2)
|
||||
commits.append(
|
||||
{
|
||||
"hash": commit_hash,
|
||||
"short_hash": commit_hash[:7],
|
||||
"date": date_str,
|
||||
"subject": subject,
|
||||
}
|
||||
)
|
||||
return commits
|
||||
|
||||
|
||||
def get_changed_files(repo: Path, rev_range: str) -> list[str]:
|
||||
output = run_git(repo, ["diff", "--name-only", rev_range])
|
||||
return [line.strip() for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def get_numstat(repo: Path, rev_range: str) -> dict[str, int]:
|
||||
output = run_git(repo, ["diff", "--numstat", rev_range])
|
||||
changed_files = 0
|
||||
insertions = 0
|
||||
deletions = 0
|
||||
for line in output.splitlines():
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
added, removed = parts[0], parts[1]
|
||||
changed_files += 1
|
||||
if added.isdigit():
|
||||
insertions += int(added)
|
||||
if removed.isdigit():
|
||||
deletions += int(removed)
|
||||
return {
|
||||
"changed_files": changed_files,
|
||||
"insertions": insertions,
|
||||
"deletions": deletions,
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repo", default=".", help="Repository root. Defaults to current directory.")
|
||||
parser.add_argument("--tag", help="Target release tag to validate, for example v1.2.3.")
|
||||
parser.add_argument("--previous-tag", help="Explicit comparison baseline.")
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
repo_path = Path(args.repo).expanduser().resolve()
|
||||
|
||||
if not (repo_path / ".git").exists():
|
||||
print(json.dumps({"error": f"not a git repository: {repo_path}"}))
|
||||
return 1
|
||||
|
||||
if args.tag and parse_semver(args.tag) is None:
|
||||
print(json.dumps({"error": f"invalid tag format: {args.tag}", "expected": "vx.y.z"}))
|
||||
return 1
|
||||
|
||||
try:
|
||||
if args.tag:
|
||||
ensure_tag_absent(repo_path, args.tag)
|
||||
if args.previous_tag:
|
||||
ensure_tag_exists(repo_path, args.previous_tag)
|
||||
except ValueError as exc:
|
||||
print(json.dumps({"error": str(exc)}))
|
||||
return 1
|
||||
|
||||
tags = list_reachable_tags(repo_path)
|
||||
previous_tag = args.previous_tag
|
||||
previous_tag_source = "explicit" if previous_tag else None
|
||||
|
||||
if previous_tag is None:
|
||||
previous_tag, previous_tag_source = choose_previous_tag(repo_path, tags, args.tag)
|
||||
|
||||
rev_range = "HEAD"
|
||||
if previous_tag:
|
||||
rev_range = f"{previous_tag}..HEAD"
|
||||
|
||||
try:
|
||||
commits = get_commit_list(repo_path, rev_range)
|
||||
changed_files = get_changed_files(repo_path, rev_range)
|
||||
stats = get_numstat(repo_path, rev_range)
|
||||
head_commit = run_git(repo_path, ["rev-parse", "HEAD"])
|
||||
except RuntimeError as exc:
|
||||
print(json.dumps({"error": str(exc)}))
|
||||
return 1
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"repo": str(repo_path),
|
||||
"target_tag": args.tag,
|
||||
"previous_tag": previous_tag,
|
||||
"previous_tag_source": previous_tag_source,
|
||||
"rev_range": rev_range,
|
||||
"head_commit": head_commit,
|
||||
"commit_count": len(commits),
|
||||
"commits": commits,
|
||||
"changed_files": changed_files,
|
||||
}
|
||||
payload.update(stats)
|
||||
|
||||
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user