From adcf8875ba9d04ddbe807611e068af2586c4a8ed Mon Sep 17 00:00:00 2001 From: Shui Song Luar Date: Thu, 11 Jun 2026 21:29:06 +0800 Subject: [PATCH 1/4] ci(release): add workflow_dispatch prepare-release workflow --- .github/workflows/prepare-release.yml | 231 ++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .github/workflows/prepare-release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..9118de0 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,231 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + name: Prepare ${{ inputs.bump_type }} release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + token: ${{ secrets.OPENROAD_CI_PAT }} + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + + - name: Install dependencies + run: make sync + + - name: Run tests + run: make test + + - name: Run checks + run: make check + + - name: Compute new version + id: version + run: | + CURRENT=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + IFS='.' read -r MAJ MIN PAT <<< "$CURRENT" + case "${{ inputs.bump_type }}" in + major) MAJ=$((MAJ+1)); MIN=0; PAT=0 ;; + minor) MIN=$((MIN+1)); PAT=0 ;; + patch) PAT=$((PAT+1)) ;; + esac + NEW="${MAJ}.${MIN}.${PAT}" + echo "current=${CURRENT}" >> "$GITHUB_OUTPUT" + echo "new=${NEW}" >> "$GITHUB_OUTPUT" + echo "tag=v${NEW}" >> "$GITHUB_OUTPUT" + echo "branch=release/v${NEW}" >> "$GITHUB_OUTPUT" + echo "Bumping ${CURRENT} → ${NEW} (${{ inputs.bump_type }})" + + - name: Generate changelog entries + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + NEW_TAG: ${{ steps.version.outputs.tag }} + run: | + python3 - <<'PYEOF' + import subprocess, re, os + + tags = subprocess.check_output(['git', 'tag', '--sort=-v:refname']).decode().strip() + last_tag = tags.splitlines()[0] if tags else '' + + if last_tag: + log = subprocess.check_output(['git', 'log', f'{last_tag}..HEAD', '--oneline']).decode().strip() + else: + log = subprocess.check_output(['git', 'log', '--oneline']).decode().strip() + + added, changed, fixed = [], [], [] + + for line in log.splitlines(): + parts = line.split(' ', 1) + if len(parts) < 2: + continue + msg = parts[1] + + pr_match = re.search(r'\(#(\d+)\)', msg) + pr_num = pr_match.group(1) if pr_match else None + + clean = re.sub(r'^[a-z]+\([^)]+\):\s*', '', msg) + clean = re.sub(r'^[a-z]+:\s*', '', clean) + clean = re.sub(r'\s*\(#\d+\)\s*$', '', clean).strip() + + if pr_num: + entry = f'- {clean} ([#{pr_num}](https://github.com/The-OpenROAD-Project/openroad-mcp/pull/{pr_num}))' + else: + entry = f'- {clean}' + + if re.match(r'feat(\(|:)', msg): + added.append(entry) + elif re.match(r'fix(\(|:)', msg): + fixed.append(entry) + elif re.match(r'(chore|build|ci|perf|refactor|docs|style|test)(\(|:)', msg): + changed.append(entry) + + sections = [] + if added: + sections.append('### Added\n' + '\n'.join(added)) + if changed: + sections.append('### Changed\n' + '\n'.join(changed)) + if fixed: + sections.append('### Fixed\n' + '\n'.join(fixed)) + + body = '\n\n'.join(sections) if sections else '### Changed\n- Minor updates and maintenance' + + with open('/tmp/changelog_section.txt', 'w') as f: + f.write(body) + + print('Generated changelog section:') + print(body) + PYEOF + + - name: Update pyproject.toml + run: | + OLD="${{ steps.version.outputs.current }}" + NEW="${{ steps.version.outputs.new }}" + sed -i "s/^version = \"${OLD}\"/version = \"${NEW}\"/" pyproject.toml + + - name: Update server.json + run: | + if [ -f server.json ]; then + OLD="${{ steps.version.outputs.current }}" + NEW="${{ steps.version.outputs.new }}" + sed -i "s/\"version\": \"${OLD}\"/\"version\": \"${NEW}\"/g" server.json + sed -i "s|ghcr.io/[Tt]he-[Oo]pen[Rr][Oo][Aa][Dd]-[Pp]roject/openroad-mcp:${OLD}|ghcr.io/the-openroad-project/openroad-mcp:${NEW}|g" server.json + fi + + - name: Update README.md URLs + run: | + TAG="${{ steps.version.outputs.tag }}" + perl -i -pe \ + 's!git\+https://github\.com/The-OpenROAD-Project/openroad-mcp(?:\@v[\d.]+)?(?="|$)!git+https://github.com/The-OpenROAD-Project/openroad-mcp\@'"${TAG}"'!g' \ + README.md + + - name: Update CHANGELOG.md + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + NEW_TAG: ${{ steps.version.outputs.tag }} + run: | + python3 - <<'PYEOF' + import os, subprocess + from datetime import date + + new_version = os.environ['NEW_VERSION'] + new_tag = os.environ['NEW_TAG'] + today = date.today().isoformat() + + with open('/tmp/changelog_section.txt') as f: + section = f.read().strip() + + with open('CHANGELOG.md') as f: + content = f.read() + + # Insert new version section before the first existing ## [...] version heading + new_entry = f'## [{new_version}] - {today}\n\n{section}' + insert_marker = '\n## [' + idx = content.find(insert_marker) + if idx == -1: + content += f'\n{new_entry}\n' + else: + content = content[:idx] + f'\n\n{new_entry}' + content[idx:] + + # Append reference link at the bottom + tags = subprocess.check_output(['git', 'tag', '--sort=-v:refname']).decode().strip() + last_tag = tags.splitlines()[0] if tags else '' + if last_tag: + link = f'[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/compare/{last_tag}...{new_tag}' + else: + link = f'[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/releases/tag/{new_tag}' + + content = content.rstrip('\n') + f'\n{link}\n' + + with open('CHANGELOG.md', 'w') as f: + f.write(content) + + print(f'Inserted {new_version} section into CHANGELOG.md') + PYEOF + + - name: Regenerate uv.lock + run: uv lock + + - name: Verify no stale version references + run: | + OLD="${{ steps.version.outputs.current }}" + FOUND=$(grep -r "${OLD}" --include="*.toml" --include="*.json" . 2>/dev/null || true) + if [ -n "$FOUND" ]; then + echo "WARNING: old version ${OLD} still found in:" + echo "$FOUND" + else + echo "All version references updated cleanly." + fi + + - name: Create release branch and commit + run: | + TAG="${{ steps.version.outputs.tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + git config user.name "openroad-ci" + git config user.email "54529053+openroad-ci@users.noreply.github.com" + git checkout -b "${BRANCH}" + git add pyproject.toml server.json uv.lock README.md CHANGELOG.md + if [ -f ROADMAP.md ] && ! git diff --cached --quiet ROADMAP.md 2>/dev/null; then + git add ROADMAP.md + fi + git commit -m "chore: release ${TAG}" + git push -u origin "${BRANCH}" + + - name: Open release PR + env: + GH_TOKEN: ${{ secrets.OPENROAD_CI_PAT }} + run: | + TAG="${{ steps.version.outputs.tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + gh pr create \ + --title "chore: release ${TAG}" \ + --body "$(cat < Date: Thu, 11 Jun 2026 21:32:04 +0800 Subject: [PATCH 2/4] refactor(release): extract inline scripts to .github/scripts/ --- .github/scripts/generate_changelog_section.py | 65 +++++++++++ .github/scripts/update_changelog.py | 51 +++++++++ .github/workflows/prepare-release.yml | 103 +----------------- 3 files changed, 120 insertions(+), 99 deletions(-) create mode 100644 .github/scripts/generate_changelog_section.py create mode 100644 .github/scripts/update_changelog.py diff --git a/.github/scripts/generate_changelog_section.py b/.github/scripts/generate_changelog_section.py new file mode 100644 index 0000000..ee8f929 --- /dev/null +++ b/.github/scripts/generate_changelog_section.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Generate a Keep-a-Changelog section from conventional commits since the last tag. + +Usage: generate_changelog_section.py [output_file] + +Writes the section text to output_file (default: /tmp/changelog_section.txt). +""" +import subprocess +import re +import sys + +REPO = "The-OpenROAD-Project/openroad-mcp" +OUTPUT_FILE = sys.argv[1] if len(sys.argv) > 1 else "/tmp/changelog_section.txt" + +tags = subprocess.check_output(["git", "tag", "--sort=-v:refname"]).decode().strip() +last_tag = tags.splitlines()[0] if tags else "" + +if last_tag: + log = subprocess.check_output(["git", "log", f"{last_tag}..HEAD", "--oneline"]).decode().strip() +else: + log = subprocess.check_output(["git", "log", "--oneline"]).decode().strip() + +added, changed, fixed = [], [], [] + +for line in log.splitlines(): + parts = line.split(" ", 1) + if len(parts) < 2: + continue + msg = parts[1] + + pr_match = re.search(r"\(#(\d+)\)", msg) + pr_num = pr_match.group(1) if pr_match else None + + clean = re.sub(r"^[a-z]+\([^)]+\):\s*", "", msg) + clean = re.sub(r"^[a-z]+:\s*", "", clean) + clean = re.sub(r"\s*\(#\d+\)\s*$", "", clean).strip() + + if pr_num: + entry = f"- {clean} ([#{pr_num}](https://github.com/{REPO}/pull/{pr_num}))" + else: + entry = f"- {clean}" + + if re.match(r"feat(\(|:)", msg): + added.append(entry) + elif re.match(r"fix(\(|:)", msg): + fixed.append(entry) + elif re.match(r"(chore|build|ci|perf|refactor|docs|style|test)(\(|:)", msg): + changed.append(entry) + +sections = [] +if added: + sections.append("### Added\n" + "\n".join(added)) +if changed: + sections.append("### Changed\n" + "\n".join(changed)) +if fixed: + sections.append("### Fixed\n" + "\n".join(fixed)) + +body = "\n\n".join(sections) if sections else "### Changed\n- Minor updates and maintenance" + +with open(OUTPUT_FILE, "w") as f: + f.write(body) + +print(f"Generated changelog section ({OUTPUT_FILE}):") +print(body) diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py new file mode 100644 index 0000000..277c031 --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Insert a new version section into CHANGELOG.md (Keep-a-Changelog format). + +Usage: update_changelog.py [section_file] + + new_version Version string without prefix, e.g. 0.6.0 + new_tag Git tag with prefix, e.g. v0.6.0 + section_file Path to the section body (default: /tmp/changelog_section.txt) +""" +import subprocess +import sys +from datetime import date + +if len(sys.argv) < 3: + print(__doc__) + sys.exit(1) + +new_version = sys.argv[1] +new_tag = sys.argv[2] +section_file = sys.argv[3] if len(sys.argv) > 3 else "/tmp/changelog_section.txt" + +today = date.today().isoformat() + +with open(section_file) as f: + section = f.read().strip() + +with open("CHANGELOG.md") as f: + content = f.read() + +new_entry = f"## [{new_version}] - {today}\n\n{section}" +insert_marker = "\n## [" +idx = content.find(insert_marker) +if idx == -1: + content += f"\n{new_entry}\n" +else: + content = content[:idx] + f"\n\n{new_entry}" + content[idx:] + +tags = subprocess.check_output(["git", "tag", "--sort=-v:refname"]).decode().strip() +last_tag = tags.splitlines()[0] if tags else "" +if last_tag: + link = f"[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/compare/{last_tag}...{new_tag}" +else: + link = f"[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/releases/tag/{new_tag}" + +content = content.rstrip("\n") + f"\n{link}\n" + +with open("CHANGELOG.md", "w") as f: + f.write(content) + +print(f"Inserted {new_version} section into CHANGELOG.md") diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9118de0..27162ef 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -56,64 +56,7 @@ jobs: echo "Bumping ${CURRENT} → ${NEW} (${{ inputs.bump_type }})" - name: Generate changelog entries - env: - NEW_VERSION: ${{ steps.version.outputs.new }} - NEW_TAG: ${{ steps.version.outputs.tag }} - run: | - python3 - <<'PYEOF' - import subprocess, re, os - - tags = subprocess.check_output(['git', 'tag', '--sort=-v:refname']).decode().strip() - last_tag = tags.splitlines()[0] if tags else '' - - if last_tag: - log = subprocess.check_output(['git', 'log', f'{last_tag}..HEAD', '--oneline']).decode().strip() - else: - log = subprocess.check_output(['git', 'log', '--oneline']).decode().strip() - - added, changed, fixed = [], [], [] - - for line in log.splitlines(): - parts = line.split(' ', 1) - if len(parts) < 2: - continue - msg = parts[1] - - pr_match = re.search(r'\(#(\d+)\)', msg) - pr_num = pr_match.group(1) if pr_match else None - - clean = re.sub(r'^[a-z]+\([^)]+\):\s*', '', msg) - clean = re.sub(r'^[a-z]+:\s*', '', clean) - clean = re.sub(r'\s*\(#\d+\)\s*$', '', clean).strip() - - if pr_num: - entry = f'- {clean} ([#{pr_num}](https://github.com/The-OpenROAD-Project/openroad-mcp/pull/{pr_num}))' - else: - entry = f'- {clean}' - - if re.match(r'feat(\(|:)', msg): - added.append(entry) - elif re.match(r'fix(\(|:)', msg): - fixed.append(entry) - elif re.match(r'(chore|build|ci|perf|refactor|docs|style|test)(\(|:)', msg): - changed.append(entry) - - sections = [] - if added: - sections.append('### Added\n' + '\n'.join(added)) - if changed: - sections.append('### Changed\n' + '\n'.join(changed)) - if fixed: - sections.append('### Fixed\n' + '\n'.join(fixed)) - - body = '\n\n'.join(sections) if sections else '### Changed\n- Minor updates and maintenance' - - with open('/tmp/changelog_section.txt', 'w') as f: - f.write(body) - - print('Generated changelog section:') - print(body) - PYEOF + run: python3 .github/scripts/generate_changelog_section.py - name: Update pyproject.toml run: | @@ -138,48 +81,10 @@ jobs: README.md - name: Update CHANGELOG.md - env: - NEW_VERSION: ${{ steps.version.outputs.new }} - NEW_TAG: ${{ steps.version.outputs.tag }} run: | - python3 - <<'PYEOF' - import os, subprocess - from datetime import date - - new_version = os.environ['NEW_VERSION'] - new_tag = os.environ['NEW_TAG'] - today = date.today().isoformat() - - with open('/tmp/changelog_section.txt') as f: - section = f.read().strip() - - with open('CHANGELOG.md') as f: - content = f.read() - - # Insert new version section before the first existing ## [...] version heading - new_entry = f'## [{new_version}] - {today}\n\n{section}' - insert_marker = '\n## [' - idx = content.find(insert_marker) - if idx == -1: - content += f'\n{new_entry}\n' - else: - content = content[:idx] + f'\n\n{new_entry}' + content[idx:] - - # Append reference link at the bottom - tags = subprocess.check_output(['git', 'tag', '--sort=-v:refname']).decode().strip() - last_tag = tags.splitlines()[0] if tags else '' - if last_tag: - link = f'[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/compare/{last_tag}...{new_tag}' - else: - link = f'[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/releases/tag/{new_tag}' - - content = content.rstrip('\n') + f'\n{link}\n' - - with open('CHANGELOG.md', 'w') as f: - f.write(content) - - print(f'Inserted {new_version} section into CHANGELOG.md') - PYEOF + python3 .github/scripts/update_changelog.py \ + "${{ steps.version.outputs.new }}" \ + "${{ steps.version.outputs.tag }}" - name: Regenerate uv.lock run: uv lock From 6b77e240e7a3f4aa78138f09fb735d481928a317 Mon Sep 17 00:00:00 2001 From: Jack Luar <39641663+luarss@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:00:43 +0000 Subject: [PATCH 3/4] fix(ci): consistent changelog link format and guard missing server.json --- .github/scripts/update_changelog.py | 9 ++------- .github/workflows/prepare-release.yml | 6 ++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py index 277c031..e0c5174 100644 --- a/.github/scripts/update_changelog.py +++ b/.github/scripts/update_changelog.py @@ -8,7 +8,7 @@ new_tag Git tag with prefix, e.g. v0.6.0 section_file Path to the section body (default: /tmp/changelog_section.txt) """ -import subprocess + import sys from datetime import date @@ -36,12 +36,7 @@ else: content = content[:idx] + f"\n\n{new_entry}" + content[idx:] -tags = subprocess.check_output(["git", "tag", "--sort=-v:refname"]).decode().strip() -last_tag = tags.splitlines()[0] if tags else "" -if last_tag: - link = f"[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/compare/{last_tag}...{new_tag}" -else: - link = f"[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/releases/tag/{new_tag}" +link = f"[{new_version}]: https://github.com/The-OpenROAD-Project/openroad-mcp/releases/tag/{new_tag}" content = content.rstrip("\n") + f"\n{link}\n" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 27162ef..2c9c477 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -108,10 +108,8 @@ jobs: git config user.name "openroad-ci" git config user.email "54529053+openroad-ci@users.noreply.github.com" git checkout -b "${BRANCH}" - git add pyproject.toml server.json uv.lock README.md CHANGELOG.md - if [ -f ROADMAP.md ] && ! git diff --cached --quiet ROADMAP.md 2>/dev/null; then - git add ROADMAP.md - fi + git add pyproject.toml uv.lock README.md CHANGELOG.md + if [ -f server.json ]; then git add server.json; fi git commit -m "chore: release ${TAG}" git push -u origin "${BRANCH}" From 1e1e9a8876f471c1f9cebf33dfdf7367a6012153 Mon Sep 17 00:00:00 2001 From: Jack Luar <39641663+luarss@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:08:29 +0000 Subject: [PATCH 4/4] fix(ci): sort imports in generate_changelog_section.py to pass lint --- .github/scripts/generate_changelog_section.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/generate_changelog_section.py b/.github/scripts/generate_changelog_section.py index ee8f929..e164a8d 100644 --- a/.github/scripts/generate_changelog_section.py +++ b/.github/scripts/generate_changelog_section.py @@ -6,8 +6,9 @@ Writes the section text to output_file (default: /tmp/changelog_section.txt). """ -import subprocess + import re +import subprocess import sys REPO = "The-OpenROAD-Project/openroad-mcp"