mirror of https://github.com/abpframework/abp.git
2 changed files with 335 additions and 0 deletions
@ -0,0 +1,286 @@ |
|||
import subprocess |
|||
import re |
|||
import os |
|||
import sys |
|||
import xml.etree.ElementTree as ET |
|||
|
|||
|
|||
HEADER = "# Package Version Changes\n" |
|||
DOC_PATH = os.environ.get("DOC_PATH", "docs/en/package-version-changes.md") |
|||
|
|||
|
|||
def get_version(): |
|||
"""Read the current version from common.props.""" |
|||
tree = ET.parse("common.props") |
|||
root = tree.getroot() |
|||
version_elem = root.find(".//Version") |
|||
if version_elem is not None: |
|||
return version_elem.text |
|||
return None |
|||
|
|||
|
|||
def get_diff(base_ref): |
|||
"""Get diff of Directory.Packages.props against the base branch.""" |
|||
result = subprocess.run( |
|||
["git", "diff", f"origin/{base_ref}", "--", "Directory.Packages.props"], |
|||
capture_output=True, |
|||
text=True, |
|||
) |
|||
return result.stdout |
|||
|
|||
|
|||
def get_existing_doc_from_base(base_ref): |
|||
"""Read the existing document from the base branch.""" |
|||
result = subprocess.run( |
|||
["git", "show", f"origin/{base_ref}:{DOC_PATH}"], |
|||
capture_output=True, |
|||
text=True, |
|||
) |
|||
if result.returncode == 0: |
|||
return result.stdout |
|||
return "" |
|||
|
|||
|
|||
def parse_diff_packages(lines, prefix): |
|||
"""Parse package versions from diff lines with the given prefix (+ or -).""" |
|||
packages = {} |
|||
pattern = re.compile(r'Include="([^"]+)".*Version="([^"]+)"') |
|||
for line in lines: |
|||
if line.startswith(prefix) and "PackageVersion" in line and not line.startswith(prefix * 3): |
|||
match = pattern.search(line) |
|||
if match: |
|||
packages[match.group(1)] = match.group(2) |
|||
return packages |
|||
|
|||
|
|||
def classify_changes(old_packages, new_packages, pr_number): |
|||
"""Classify diff into updated, added, and removed with PR attribution.""" |
|||
updated = {} |
|||
added = {} |
|||
removed = {} |
|||
|
|||
all_packages = sorted(set(list(old_packages.keys()) + list(new_packages.keys()))) |
|||
|
|||
for pkg in all_packages: |
|||
if pkg in old_packages and pkg in new_packages: |
|||
if old_packages[pkg] != new_packages[pkg]: |
|||
updated[pkg] = (old_packages[pkg], new_packages[pkg], pr_number) |
|||
elif pkg in new_packages: |
|||
added[pkg] = (new_packages[pkg], pr_number) |
|||
else: |
|||
removed[pkg] = (old_packages[pkg], pr_number) |
|||
|
|||
return updated, added, removed |
|||
|
|||
|
|||
def parse_existing_section(section_text): |
|||
"""Parse an existing markdown section to extract package records with PR info.""" |
|||
updated = {} |
|||
added = {} |
|||
removed = {} |
|||
|
|||
mode = "updated" |
|||
for line in section_text.split("\n"): |
|||
if "**Added:**" in line: |
|||
mode = "added" |
|||
continue |
|||
if "**Removed:**" in line: |
|||
mode = "removed" |
|||
continue |
|||
if not line.startswith("|") or line.startswith("| Package") or line.startswith("|---"): |
|||
continue |
|||
|
|||
parts = [p.strip() for p in line.split("|")[1:-1]] |
|||
if mode == "updated" and len(parts) >= 3: |
|||
pr = parts[3] if len(parts) >= 4 else "" |
|||
updated[parts[0]] = (parts[1], parts[2], pr) |
|||
elif len(parts) >= 2: |
|||
pr = parts[2] if len(parts) >= 3 else "" |
|||
if mode == "added": |
|||
added[parts[0]] = (parts[1], pr) |
|||
else: |
|||
removed[parts[0]] = (parts[1], pr) |
|||
|
|||
return updated, added, removed |
|||
|
|||
|
|||
def merge_prs(existing_pr, new_pr): |
|||
"""Merge PR numbers, avoiding duplicates.""" |
|||
if not existing_pr or not existing_pr.strip(): |
|||
return new_pr |
|||
if not new_pr or not new_pr.strip(): |
|||
return existing_pr |
|||
|
|||
# Parse existing PRs |
|||
existing_prs = [p.strip() for p in existing_pr.split(",") if p.strip()] |
|||
# Add new PR if not already present |
|||
if new_pr not in existing_prs: |
|||
existing_prs.append(new_pr) |
|||
return ", ".join(existing_prs) |
|||
|
|||
|
|||
def merge_changes(existing, new): |
|||
"""Merge new changes into existing records for the same version.""" |
|||
ex_updated, ex_added, ex_removed = existing |
|||
new_updated, new_added, new_removed = new |
|||
|
|||
merged_updated = dict(ex_updated) |
|||
merged_added = dict(ex_added) |
|||
merged_removed = dict(ex_removed) |
|||
|
|||
for pkg, (old_ver, new_ver, pr) in new_updated.items(): |
|||
if pkg in merged_updated: |
|||
existing_old_ver, existing_new_ver, existing_pr = merged_updated[pkg] |
|||
merged_pr = merge_prs(existing_pr, pr) |
|||
merged_updated[pkg] = (existing_old_ver, new_ver, merged_pr) |
|||
elif pkg in merged_added: |
|||
existing_ver, existing_pr = merged_added[pkg] |
|||
merged_pr = merge_prs(existing_pr, pr) |
|||
merged_added[pkg] = (new_ver, merged_pr) |
|||
else: |
|||
merged_updated[pkg] = (old_ver, new_ver, pr) |
|||
|
|||
for pkg, (ver, pr) in new_added.items(): |
|||
if pkg in merged_removed: |
|||
removed_ver, removed_pr = merged_removed.pop(pkg) |
|||
merged_pr = merge_prs(removed_pr, pr) |
|||
merged_updated[pkg] = (removed_ver, ver, merged_pr) |
|||
elif pkg in merged_added: |
|||
existing_ver, existing_pr = merged_added[pkg] |
|||
merged_pr = merge_prs(existing_pr, pr) |
|||
merged_added[pkg] = (ver, merged_pr) |
|||
else: |
|||
merged_added[pkg] = (ver, pr) |
|||
|
|||
for pkg, (ver, pr) in new_removed.items(): |
|||
if pkg in merged_added: |
|||
existing_ver, existing_pr = merged_added[pkg] |
|||
del merged_added[pkg] |
|||
# No need to merge PR here as the package is being removed |
|||
elif pkg in merged_updated: |
|||
old_ver, new_ver, existing_pr = merged_updated.pop(pkg) |
|||
merged_pr = merge_prs(existing_pr, pr) |
|||
merged_removed[pkg] = (old_ver, merged_pr) |
|||
else: |
|||
merged_removed[pkg] = (ver, pr) |
|||
|
|||
merged_updated = {k: v for k, v in merged_updated.items() if v[0] != v[1]} |
|||
|
|||
return merged_updated, merged_added, merged_removed |
|||
|
|||
|
|||
def render_section(version, updated, added, removed): |
|||
"""Render a version section as markdown.""" |
|||
lines = [f"## {version}\n"] |
|||
|
|||
if updated: |
|||
lines.append("| Package | Old Version | New Version | PR |") |
|||
lines.append("|---------|-------------|-------------|-----|") |
|||
for pkg in sorted(updated): |
|||
old_ver, new_ver, pr = updated[pkg] |
|||
lines.append(f"| {pkg} | {old_ver} | {new_ver} | {pr} |") |
|||
lines.append("") |
|||
|
|||
if added: |
|||
lines.append("**Added:**\n") |
|||
lines.append("| Package | Version | PR |") |
|||
lines.append("|---------|---------|-----|") |
|||
for pkg in sorted(added): |
|||
ver, pr = added[pkg] |
|||
lines.append(f"| {pkg} | {ver} | {pr} |") |
|||
lines.append("") |
|||
|
|||
if removed: |
|||
lines.append("**Removed:**\n") |
|||
lines.append("| Package | Version | PR |") |
|||
lines.append("|---------|---------|-----|") |
|||
for pkg in sorted(removed): |
|||
ver, pr = removed[pkg] |
|||
lines.append(f"| {pkg} | {ver} | {pr} |") |
|||
lines.append("") |
|||
|
|||
return "\n".join(lines) |
|||
|
|||
|
|||
def parse_document(content): |
|||
"""Split document into a list of (version, section_text) tuples.""" |
|||
sections = [] |
|||
current_version = None |
|||
current_lines = [] |
|||
|
|||
for line in content.split("\n"): |
|||
match = re.match(r"^## (.+)$", line) |
|||
if match: |
|||
if current_version: |
|||
sections.append((current_version, "\n".join(current_lines))) |
|||
current_version = match.group(1).strip() |
|||
current_lines = [line] |
|||
elif current_version: |
|||
current_lines.append(line) |
|||
|
|||
if current_version: |
|||
sections.append((current_version, "\n".join(current_lines))) |
|||
|
|||
return sections |
|||
|
|||
|
|||
def main(): |
|||
if len(sys.argv) < 3: |
|||
print("Usage: update_dependency_changes.py <base-ref> <pr-number>") |
|||
sys.exit(1) |
|||
|
|||
base_ref = sys.argv[1] |
|||
pr_number = f"#{sys.argv[2]}" |
|||
|
|||
version = get_version() |
|||
if not version: |
|||
print("Could not read version from common.props.") |
|||
sys.exit(1) |
|||
|
|||
diff = get_diff(base_ref) |
|||
if not diff: |
|||
print("No diff found for Directory.Packages.props.") |
|||
sys.exit(0) |
|||
|
|||
diff_lines = diff.split("\n") |
|||
old_packages = parse_diff_packages(diff_lines, "-") |
|||
new_packages = parse_diff_packages(diff_lines, "+") |
|||
|
|||
new_updated, new_added, new_removed = classify_changes(old_packages, new_packages, pr_number) |
|||
|
|||
if not new_updated and not new_added and not new_removed: |
|||
print("No package version changes detected.") |
|||
sys.exit(0) |
|||
|
|||
# Load existing document from the base branch |
|||
existing_content = get_existing_doc_from_base(base_ref) |
|||
sections = parse_document(existing_content) if existing_content else [] |
|||
|
|||
# Find existing section for this version |
|||
version_index = None |
|||
for i, (v, _) in enumerate(sections): |
|||
if v == version: |
|||
version_index = i |
|||
break |
|||
|
|||
if version_index is not None: |
|||
existing = parse_existing_section(sections[version_index][1]) |
|||
merged = merge_changes(existing, (new_updated, new_added, new_removed)) |
|||
section_text = render_section(version, *merged) |
|||
sections[version_index] = (version, section_text) |
|||
else: |
|||
section_text = render_section(version, new_updated, new_added, new_removed) |
|||
sections.insert(0, (version, section_text)) |
|||
|
|||
# Write document |
|||
os.makedirs(os.path.dirname(DOC_PATH), exist_ok=True) |
|||
with open(DOC_PATH, "w") as f: |
|||
f.write(HEADER + "\n") |
|||
for _, text in sections: |
|||
f.write(text.rstrip("\n") + "\n\n") |
|||
|
|||
print(f"Updated {DOC_PATH} for version {version}") |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,49 @@ |
|||
name: Nuget Packages Version Change Detector |
|||
|
|||
on: |
|||
pull_request: |
|||
paths: |
|||
- 'Directory.Packages.props' |
|||
types: |
|||
- opened |
|||
- synchronize |
|||
- reopened |
|||
- ready_for_review |
|||
|
|||
permissions: |
|||
contents: read |
|||
|
|||
jobs: |
|||
label: |
|||
if: ${{ !github.event.pull_request.draft && !startsWith(github.head_ref, 'auto-merge/') }} |
|||
permissions: |
|||
contents: write |
|||
pull-requests: write |
|||
runs-on: ubuntu-latest |
|||
env: |
|||
DOC_PATH: docs/en/package-version-changes.md |
|||
steps: |
|||
- run: gh pr edit "$PR_NUMBER" --add-label "dependency-change" |
|||
env: |
|||
PR_NUMBER: ${{ github.event.pull_request.number }} |
|||
GH_TOKEN: ${{ secrets.BOT_SECRET }} |
|||
GH_REPO: ${{ github.repository }} |
|||
|
|||
- uses: actions/checkout@v4 |
|||
with: |
|||
ref: ${{ github.event.pull_request.head.ref }} |
|||
fetch-depth: 0 |
|||
|
|||
- uses: actions/setup-python@v5 |
|||
with: |
|||
python-version: '3.x' |
|||
|
|||
- run: python .github/scripts/update_dependency_changes.py ${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.number }} |
|||
|
|||
- name: Commit changes |
|||
run: | |
|||
git config user.name "github-actions[bot]" |
|||
git config user.email "github-actions[bot]@users.noreply.github.com" |
|||
git add "$DOC_PATH" |
|||
git diff --staged --quiet || git commit -m "docs: update package version changes" |
|||
git push |
|||
Loading…
Reference in new issue