diff --git a/.github/scripts/generate_changelog_section.py b/.github/scripts/generate_changelog_section.py new file mode 100644 index 0000000..e164a8d --- /dev/null +++ b/.github/scripts/generate_changelog_section.py @@ -0,0 +1,66 @@ +#!/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 re +import subprocess +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..e0c5174 --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,46 @@ +#!/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 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:] + +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 new file mode 100644 index 0000000..2c9c477 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,134 @@ +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 + run: python3 .github/scripts/generate_changelog_section.py + + - 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 + run: | + python3 .github/scripts/update_changelog.py \ + "${{ steps.version.outputs.new }}" \ + "${{ steps.version.outputs.tag }}" + + - 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 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}" + + - 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 <