diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 542783c..d3dc0e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 - name: Create Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -218,8 +221,10 @@ jobs: update_release_notes: name: Update Release Notes needs: - # [build_wheels_linux, build_wheels_linux_self_hosted, build_wheels_windows] + # [build_wheels_linux, build_wheels_linux_self_hosted, build_wheels_windows] [build_wheels_linux, build_wheels_windows] + permissions: + contents: write if: always() runs-on: ubuntu-latest steps: @@ -229,6 +234,9 @@ jobs: with: python-version: 3.12 + - name: Install dependencies + run: pip install pandas + - name: Generate Release Notes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -236,3 +244,24 @@ jobs: gh release view "${{ github.ref_name }}" --json assets > /tmp/assets.json python create_release_note.py /tmp/assets.json > /tmp/release_notes.md gh release edit "${{ github.ref_name }}" --notes-file /tmp/release_notes.md + + - name: Update README history and packages + run: | + cat /tmp/release_notes.md | python insert_history.py \ + --notes - \ + --tag "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" + python generate_packages_table.py --update-readme + + - name: Commit and push README updates + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git diff --quiet; then + echo "No README updates to commit." + exit 0 + fi + git commit -am "docs: update README for ${{ github.ref_name }}" + git push origin HEAD:"${DEFAULT_BRANCH}" diff --git a/insert_history.py b/insert_history.py new file mode 100644 index 0000000..eedd75e --- /dev/null +++ b/insert_history.py @@ -0,0 +1,197 @@ +"""Update the History section in README.md from release notes or assets.""" + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Dict, Iterable + + +WHEEL_PATTERN = re.compile( + r"flash_attn-(\d+\.\d+\.\d+)\+cu(\d+)torch(\d+\.\d+)-cp(\d+)-cp\d+-(\w+)\.whl" +) + + +def parse_wheel_filename(filename: str) -> Dict[str, str] | None: + match = WHEEL_PATTERN.match(filename) + if not match: + return None + + flash_version = match.group(1) + cuda_digits = match.group(2) + torch_version = match.group(3) + python_digits = match.group(4) + platform = match.group(5) + + cuda_version = f"{cuda_digits[:2]}.{cuda_digits[2:]}" + python_version = f"{python_digits[:1]}.{python_digits[1:]}" + + return { + "flash_version": flash_version, + "cuda_version": cuda_version, + "torch_version": torch_version, + "python_version": python_version, + "platform": platform, + } + + +def normalize_platform_name(raw: str) -> str: + name = raw[:1].upper() + raw[1:] + name = name.replace("_", " ", 1) + if "Win" in name: + name = name.replace("Win", "Windows") + if "amd64" in name: + name = name.replace("amd64", "x86_64") + return name + + +def collect_versions(assets: Iterable[Dict[str, str]]) -> Dict[str, Dict[str, set[str]]]: + aggregated: Dict[str, Dict[str, set[str]]] = {} + for asset in assets: + info = parse_wheel_filename(asset.get("name", "")) + if not info: + continue + + platform = normalize_platform_name(info["platform"]) + platform_data = aggregated.setdefault( + platform, + { + "flash_versions": set(), + "python_versions": set(), + "torch_versions": set(), + "cuda_versions": set(), + }, + ) + + platform_data["flash_versions"].add(info["flash_version"]) + platform_data["python_versions"].add(info["python_version"]) + platform_data["torch_versions"].add(info["torch_version"]) + platform_data["cuda_versions"].add(info["cuda_version"]) + + return aggregated + + +def format_versions(values: set[str]) -> str: + if not values: + return "-" + return ", ".join(sorted(values)) + + +def render_body_from_aggregated(aggregated: Dict[str, Dict[str, set[str]]]) -> str: + if not aggregated: + raise ValueError("No wheel assets found") + + body_lines: list[str] = [] + + for platform in sorted(aggregated.keys()): + data = aggregated[platform] + body_lines.extend( + [ + f"#### {platform}", + "", + "| Flash-Attention | Python | PyTorch | CUDA |", + "| --- | --- | --- | --- |", + "| " + + " | ".join( + [ + format_versions(data["flash_versions"]), + format_versions(data["python_versions"]), + format_versions(data["torch_versions"]), + format_versions(data["cuda_versions"]), + ] + ) + + " |", + "", + ] + ) + + return "\n".join(body_lines).strip() + + +def convert_release_notes_to_body(notes_text: str) -> str: + converted = re.sub(r"^## ", "#### ", notes_text, flags=re.MULTILINE) + return converted.strip() + + +def build_history_section(tag: str, repo: str, body: str) -> str: + release_url = f"https://github.com/{repo}/releases/tag/{tag}" + lines = [f"### {tag}", "", f"[Release]({release_url})", "", body.strip()] + return "\n".join(lines).rstrip() + "\n\n" + + +def remove_existing_section(content: str, tag: str) -> str: + pattern = re.compile(rf"^### {re.escape(tag)}\n.*?(?=^### |\Z)", re.MULTILINE | re.DOTALL) + return re.sub(pattern, "", content) + + +def insert_history_section(content: str, section: str) -> str: + marker = "## History\n" + idx = content.find(marker) + if idx == -1: + raise ValueError("History section is missing in README.md") + + insert_pos = idx + len(marker) + return content[:insert_pos] + "\n" + section + content[insert_pos:] + + +def main() -> None: + parser = argparse.ArgumentParser(description="Update README.md History section") + parser.add_argument("--assets", type=Path, help="JSON file from gh release view") + parser.add_argument( + "--notes", + help="Release notes markdown file path or '-' to read from stdin", + ) + parser.add_argument("--tag", required=True, help="Release tag name") + parser.add_argument("--repo", required=True, help="Repository in owner/name format") + parser.add_argument( + "--readme", + type=Path, + default=Path("README.md"), + help="Path to README.md", + ) + args = parser.parse_args() + + history_body: str + + if args.notes: + if args.notes == "-": + notes_text = sys.stdin.read() + else: + notes_path = Path(args.notes) + if not notes_path.exists(): + raise FileNotFoundError(f"Notes file not found: {notes_path}") + notes_text = notes_path.read_text(encoding="utf-8") + + history_body = convert_release_notes_to_body(notes_text) + else: + if not args.assets: + raise ValueError("Either --notes or --assets must be provided") + if not args.assets.exists(): + raise FileNotFoundError(f"Assets file not found: {args.assets}") + + data = json.loads(args.assets.read_text(encoding="utf-8")) + assets = data.get("assets", []) + aggregated = collect_versions(assets) + history_body = render_body_from_aggregated(aggregated) + + section = build_history_section(args.tag, args.repo, history_body) + + content = args.readme.read_text(encoding="utf-8") + stripped = remove_existing_section(content, args.tag) + updated = insert_history_section(stripped, section) + + if updated == content: + print("No changes in README.md") + return + + args.readme.write_text(updated, encoding="utf-8") + print(f"Inserted history for {args.tag} into {args.readme}") + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1)