SuperAPI AI Directory

publish

Deployment & operations

View repository

Updated 10-16 08:46

About this skill

Publish

Automate the complete release process for the Octop Python package.

At the start, announce: "Using the publish skill to release version {VERSION}."

Configuration items

The following configurations have default values and can be overridden in the project's .cursor/skills/publish/SKILL.md .

Configuration items Default values Description
CHANGELOG_FILE CHANGELOG.md Path relative to the repository root; skipped if the file does not exist
VERSION_FILE pyproject.toml File containing the version number
VERSION_PATTERN ^\s*version\s*=\s*"[^"]+" Regex matching the version line
README_GLOB README*.md README with a shields.io version badge (including multilingual variants, such as README.md、README_CN.md); all are upgraded in sync
INIT_VERSION_FILE src/octop/__init__.py File containing the runtime constant __version__ (skipped with a warning if missing)
TAG_PREFIX v Git tag prefix; the workflow listens for v*, generating v0.1.14 -style tags
REMOTE origin Git remote repository name
INTEGRATION_BRANCH develop Daily integration branch; releases must be cut from the latest tip
TARGET_BRANCH main Target branch for merge requests (the production source of truth)
RELEASE_BRANCH_PREFIX release/ Release branch name prefix

Usage

/publish 0.1.14

The target version number is the only required parameter; all others are obtained from configuration or auto-detected.

Release process

Strict sequential constraints: First merge the PR into main, then on main tip create and push the v* tag。
Prohibitedto push a production tag before the release branch is merged into main .

Step 1 — Read configuration and confirm the version

  1. Get the repository root:git rev-parse --show-toplevel
  2. Record the current branch as {original_branch}。
  3. git fetch {REMOTE} {INTEGRATION_BRANCH} {TARGET_BRANCH}
  4. From VERSION_FILE read the current version (on the integration tip about to be based on):
    git show {REMOTE}/{INTEGRATION_BRANCH}:pyproject.toml | grep -E '^\s*version\s*=\s*"[^"]+"'
    
  5. Check for uncommitted changes:
    git status --short
    
    If the working tree is not clean:Abortand ask the user to commit or stash first. Releases must not carry unrelated dirty files.
  6. Find the most recent git tag:
    git tag --sort=-creatordate | head -1
    
    If there is no tag, treat it as the first release (in step 3, use all commits from the repository's initial state to HEAD).
  7. Display confirmation information:
当前版本 (pyproject.toml @ develop): X.Y.Z
目标版本:                           A.B.C
上次发布 tag:                       vX.Y.Z (YYYY-MM-DD)
Release 分支:                       release/A.B.C
集成起点:                           develop
合入目标:                           main
Tag 时机:                           main 合并之后(不会在 release 上先打 tag)

确认发布 X.Y.Z → A.B.C?[y/N]

If the user does not enter y to confirm, abort immediately.

Step 2 — Create the release branch from develop

git checkout -B {RELEASE_BRANCH_PREFIX}{version} {REMOTE}/{INTEGRATION_BRANCH}

If a release branch with the same name already exists locally or remotely, abort:

✗ 分支 {RELEASE_BRANCH_PREFIX}{version} 已存在。
请手动删除后再运行 /publish。

Step 3 — Analyze changes and generate a CHANGELOG draft

  1. Get the commits since the last tag (relative to the current release HEAD, i.e. the develop tip):

    git log {last_tag}..HEAD --oneline
    # 首次发布时:
    git log --oneline
    
  2. If no commits are found:

    ⚠ 自上次发布 tag ({last_tag}) 以来没有新提交。
    是否继续?[y/N]
    

    Abort if the user does not confirm.

  3. Classify by commit prefix and generate entries in Keep a Changelog format.

    The CHANGELOG content must be written in Chinese. Summarize each commit as a concise Chinese bullet point — do not translate commit messages verbatim. Merge related commits as appropriate.

    Classification rules:

    • Starts with feat: or feat( → Added
    • Starts with fix: or fix( → Fixed
    • Starts with refactor: or perf: → Changed
    • Contains !: or commit body contains BREAKING CHANGE: → Change, add **Breaking:** prefix
    • starts with docs: → Change
    • starts with chore:、test:、ci: → ignore (infrastructure noise)
    • all other commits → Change
    • removed features → Removed
    • security fixes → Security

    Output format:

    ## [A.B.C] - YYYY-MM-DD
    
    ### 新增
    - 中文描述新增功能
    
    ### 修复
    - 中文描述修复内容
    
    ### 变更
    - 中文描述行为变更
    
    ### 移除
    - 中文描述移除内容
    
    ### 安全
    - 中文描述安全修复
    

    Omit empty categories. Use ISO 8601 format for the date (today's date). Keep a blank line between the date line and the first category, and between categories.

  4. Show the draft to the user and request confirmation:

    CHANGELOG 草稿:
    
    {draft}
    
    添加到 CHANGELOG.md?[y/N/edit]
    
    • y → continue
    • n → abort
    • edit or other feedback → ask the user: "What changes are needed?" — wait for a reply, regenerate, and show the confirmation again. Loop until y or n。
  5. If CHANGELOG_FILE does not exist, skip this step (no warning needed).

Step 4 — Update files, commit, and push the release branch

Execute in order:

4a. Update CHANGELOG:

In CHANGELOG_FILE find the ## [Unreleased] heading, and insert the new version entry after it (keep [Unreleased] empty):

## [Unreleased]

## [A.B.C] - YYYY-MM-DD
### 新增
- ...

If the ## [Unreleased] heading does not exist, insert the new entry after the # Changelog heading line (or at the top of the file if there is no heading).

4b. Bump the version number (replace all old X.Y.Z numbers with the current A.B.C):

Pinned numbers representing the "current release version" must be consistent,do not change only pyproject.toml. Replace the old version X.Y.Z with the target version A.B.C. If a file is missing, warn and continue (do not abort).

Required changes list (in order, use the Edit tool for precise replacements; do not use a global replace that would damage historical CHANGELOG entries):

  1. VERSION_FILE(pyproject.toml) — the sole version source for wheel / PyPI:
    grep -n '^\s*version\s*=\s*"[^"]+"' pyproject.toml
    # "X.Y.Z" → "A.B.C"
    
  2. All READMEs matching README_GLOB and containing the shields.io version badge (including multilingual ones):
    grep -l 'shields.io/badge/version-' README.md README_*.md 2>/dev/null
    grep -n 'shields.io/badge/version-' {file}
    # `version-X.Y.Z-orange` → `version-A.B.C-orange`
    
    The current repository includes at least README.md and README_CN.md. If none match, warn and continue.
  3. INIT_VERSION_FILE(src/octop/__init__.py) — runtime constant __version__:
    grep -n '__version__' src/octop/__init__.py
    # `__version__ = "X.Y.Z"` → `"A.B.C"`
    
  4. The version=(scripts/build-fpk.sh of the two FnOS manifests will be injected again during packaging, but the repository source files must be changed first):
    grep -n '^version=' fnos/docker/manifest fnos/native/manifest
    # `version=X.Y.Z` → `version=A.B.C`(两个文件都要改)
    
  5. FnOS Docker compose image tag (same as this package version, for FPK to pull from GHCR):
    grep -n 'ghcr.io/tencentcloud/octop:' fnos/docker/app/docker/docker-compose.yaml
    # `ghcr.io/tencentcloud/octop:X.Y.Z` → `:A.B.C`
    # 不得改成 `:latest`(测试禁止 latest)
    
  6. uv.lock editable package in octop version (missing this will cause the lock and pyproject to be inconsistent):
    grep -n -A2 'name = "octop"' uv.lock | head -5
    # 将该包的 `version = "X.Y.Z"` 改为 `"A.B.C"`
    # 或在改完 pyproject.toml 后执行 `uv lock`,只接受 octop 版本行变化
    

Cleanup (required): Search the entire repository for the old number, and change any missed "current version pins" as well:

git grep -n --fixed-strings "X.Y.Z"

For each match:

Type Action
Current version pin (compose / badge / manifest / lock / __version__ / "current version" in docs) Replace with A.B.C
Hardcoded assertions in tests that "equal the current release number" Change to read _pyproject_version()(or sync to A.B.C)
CHANGELOG.md released section (## [X.Y.Z] and body) KeepDo not change history
Use old numbers for comparison in test fixtures (e.g. 1.0.2b4 vs 1.0.2b5, parsing examples) Keep
Example old numbers in workflow / comments (e.g. 0.9.28) Keep(not currently pinned)

Before committing, scan the must-change checklist again; there must be no old numbers remaining:

git grep -n --fixed-strings "X.Y.Z" -- \
  pyproject.toml src/octop/__init__.py \
  README.md README_CN.md \
  fnos/docker/manifest fnos/native/manifest \
  fnos/docker/app/docker/docker-compose.yaml \
  uv.lock

If any file still matches an old number:Fix and commit again before submitting, do not push a release with a half-updated version number.

4c. Commit:

git status --short
  • If there are changes: stage and commit:
    git add -A
    git commit -m "chore: release {version}"
    
  • If the working tree is already clean: no commit needed, skip.

4d. Push the release branch:

git push -u {REMOTE} {RELEASE_BRANCH_PREFIX}{version}

If the push fails, abort.

Step 5 — Create the Pull Request to merge into main (merge first, then auto-tag)

Use gh CLI:

gh pr create \
  --base {TARGET_BRANCH} \
  --head {RELEASE_BRANCH_PREFIX}{version} \
  --title "chore: release {version}" \
  --body "$(cat <<'EOF'
{步骤 3 生成的 CHANGELOG 条目}

## Release checklist
- [ ] CI green
- [ ] Merge this PR into main
- [ ] After merge, GitHub Action auto-pushes v{version} tag on main tip
EOF
)"
  • On success, display the PR URL and clearly state:
    • Do not manually create the tag before merging;
    • after merging, the release will be published automatically (the tag will be created automatically).
  • If gh fails: abort (no release has been made at this point), and prompt to create the PR manually: {RELEASE_BRANCH_PREFIX}{version} → {TARGET_BRANCH}

Step 6 — After the merge, the Action creates the tag on the main tip

  1. Ask the user whether the PR has been merged, or poll:

    gh pr view {pr_url} --json state,mergedAt
    

    If not merged, wait; there is no need to create the tag locally.

  2. After merging:

    • auto-tag-on-release.yml will read the merged main 's pyproject.toml version and push {TAG_PREFIX}{version}。
    • If the tag already exists, the Action will skip and output a log.
  3. Prompt the user to confirm in Actions:

    • Auto Tag On Release Merge succeeded;
    • Release and Docker Publish are triggered by the v* tag and pass;
    • Sync Main Into Develop after the GitHub Release is published, syncs main back to develop。

Step 7 — Delete the release branch; develop is synced by the Action

  1. Delete the remote and local release branches:

    git push {REMOTE} --delete {RELEASE_BRANCH_PREFIX}{version}
    git branch -D {RELEASE_BRANCH_PREFIX}{version}
    

    If deletion fails, warn (non-fatal) and prompt for manual deletion.

  2. develop sync: after the GitHub Release is published successfully,sync-main-to-develop.yml will automatically open a main → develop PR and, if there are no conflicts, merge it using merge commit .

    • If it is already fast-forwarded with no diff, the Action will skip.
    • If there are conflicts or branch protection blocks it, the Action leaves the PR and raises an alert, requiring manual handling.
    • The skill side does not need to manually create the sync PR again (unless the Action fails).

Step 8 — Switch back to the original branch

git checkout {original_branch}

Ensure that after the process ends, the user is not left on the release / temporary checkout.

Error Handling Reference

Scenario Behavior
VERSION_FILE not found Abort: "VERSION_FILE not found: {path}"
No version number matched in the file Abort: "Could not find a version line matching {VERSION_PATTERN} in {VERSION_FILE}"
Working tree not clean Abort: clean up first, then release
No git tag (first release) Use the full history; prompt "first release"
No commits since the last tag Warn and ask whether to continue
Release branch already exists Abort and provide deletion instructions
Step 4 push failed Abort: files already updated locally but not pushed
Step 5 PR creation failed Abort (tag not yet created / not yet released)
Step 6 manually create tag when not merged Prohibited — hard red line
Step 6 tag already exists Abort and provide deletion instructions
Step 6 tag pushed successfully but Action failed Non-fatal: prompt to Actions Re-run
Step 7 delete branch or sync PR failed Warn and provide manual commands

Red line rules

Never:

  • On release / feature branches, before merging main beforepush production v* tag
  • Upload directly to PyPI before pushing the tag (release is handled by GitHub Action)
  • Push develop directly push / merge into main(must go through PR)
  • Skip step 1 user confirmation
  • Skip step 3 CHANGELOG confirmation
  • Continue execution after any step fails (except step 7 cleanup/sync warnings)
  • Leave the user on the release branch after the process ends
  • Keep the already-released release/* as a long-term branch

Always:

  • Cut release from the latest {REMOTE}/{INTEGRATION_BRANCH} cut release
  • Merge {TARGET_BRANCH}first, then the Action creates the tag on main tip
  • Delete after release release/*;main → develop is automatically synced by sync-main-to-develop.yml (manually supplement if it fails)
  • Show full error output before aborting
  • After inserting the new version entry, keep [Unreleased] empty
  • Replace the old version number X.Y.Z with A.B.C:pyproject.toml、__version__, multilingual README badges,fnos/*/manifest、fnos/docker/app/docker/docker-compose.yaml image tags,uv.lock 's octop package version; before committing git grep must-change checklist confirms no old version numbers
  • Do not modify CHANGELOG historical sections, old version comparisons in test fixtures, or workflow example numbers
  • After pushing the tag, prompt the user to watch the GitHub Actions release result