Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/scripts/bump_beta_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Resolve the next FA4 beta tag and optionally create + push it.

Usage:
python bump_beta_tag.py # dry-run by default
python bump_beta_tag.py --push # create and push the tag
"""

from __future__ import annotations

import argparse
import os
import re
import subprocess
import sys

TAG_PATTERN = re.compile(r"^(fa4-v.+\.beta)(\d+)$")


def git(*args: str) -> str:
result = subprocess.run(
["git", *args], capture_output=True, text=True, check=True
)
return result.stdout.strip()


def get_beta_tags() -> list[tuple[str, int]]:
raw = git("tag", "-l", "fa4-v*.beta*")
if not raw:
return []
tags = []
for line in raw.splitlines():
m = TAG_PATTERN.match(line.strip())
if m:
tags.append((line.strip(), int(m.group(2))))
return sorted(tags, key=lambda t: t[1])


def tag_exists(tag: str) -> bool:
result = subprocess.run(
["git", "rev-parse", tag], capture_output=True, text=True
)
return result.returncode == 0


def set_github_output(key: str, value: str) -> None:
path = os.environ.get("GITHUB_OUTPUT")
if path:
with open(path, "a") as f:
f.write(f"{key}={value}\n")


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--push", action="store_true", help="Create and push the tag (default: dry-run)")
args = parser.parse_args()

tags = get_beta_tags()
if not tags:
print("::error::No existing fa4-v*.beta* tags found", file=sys.stderr)
sys.exit(1)

latest_tag, latest_num = tags[-1]
next_num = latest_num + 1
prefix = TAG_PATTERN.match(latest_tag)
if prefix is None:
print(f"::error::Latest tag {latest_tag!r} no longer matches pattern", file=sys.stderr)
sys.exit(1)
next_tag = f"{prefix.group(1)}{next_num}"

already_exists = tag_exists(next_tag)

if already_exists:
print(f"Tag {next_tag} already exists, reusing it")
else:
print(f"Bumping: {latest_tag} -> {next_tag}")

set_github_output("next_tag", next_tag)

if args.push and not already_exists:
try:
git("tag", next_tag)
git("push", "origin", next_tag)
except subprocess.CalledProcessError:
if tag_exists(next_tag):
print(f"Tag {next_tag} was created by a concurrent run, reusing it")
else:
raise
else:
print(f"Pushed {next_tag}")
elif not args.push:
print(f"Dry-run: would create and push {next_tag}")


if __name__ == "__main__":
main()
13 changes: 10 additions & 3 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ This repository uses separate tag lanes so FA2 and FA4 publishing do not collide

### FA4 / CUTE package lane

1. Create a tag matching `fa4-v*` (example: `fa4-v0.1.0`).
2. Push that tag.
3. `publish-fa4.yml` builds from `flash_attn/cute`, creates a GitHub release, and uploads `flash-attn-4` to PyPI.
**Manual release**: create and push a tag matching `fa4-v*` (example: `fa4-v4.0.0`).

**Weekly beta**: `publish-fa4.yml` also runs every Wednesday at 08:00 UTC via cron. The scheduled or manual run creates and pushes the next `fa4-v*.beta*` tag, then continues in the same workflow run to build and publish that beta. Manual dispatch is restricted to the repository default branch so it cannot tag a feature branch commit. The pushed tag matches the `fa4-v*` trigger, but GitHub suppresses workflow runs for events created by `GITHUB_TOKEN`, so no recursive run is triggered.

| Week | Tag created | PyPI version |
| --- | --- | --- |
| 1 | `fa4-v4.0.0.beta5` | `4.0.0b5` |
| 2 | `fa4-v4.0.0.beta6` | `4.0.0b6` |

To stop weekly betas: GitHub repo → Actions → "Publish flash-attn-4 to PyPI" → `···` menu → **Disable workflow**. Re-enable when ready to resume, or switch to manual tag pushes only by removing the `schedule` trigger. Users can still push a `fa4-v*.beta*` tag directly when they need to cut a beta outside the schedule.

## Guardrails

Expand Down
43 changes: 42 additions & 1 deletion .github/workflows/publish-fa4.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,55 @@ on:
push:
tags:
- 'fa4-v*'
schedule:
- cron: '0 8 * * 3' # Wednesday 08:00 UTC
workflow_dispatch:

permissions:
contents: write

jobs:
prepare-release:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.resolve-tag.outputs.release_tag }}
steps:
- name: Require default branch for manual runs
if: github.event_name == 'workflow_dispatch'
run: |
if [ "${{ github.ref_name }}" != "${{ github.event.repository.default_branch }}" ]; then
echo "::error::Run this workflow from ${{ github.event.repository.default_branch }} only"
exit 1
fi
- uses: actions/checkout@v4
if: github.event_name != 'push'
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
- uses: actions/setup-python@v5
if: github.event_name != 'push'
with:
python-version: '3.12'
- name: Bump beta tag
if: github.event_name != 'push'
id: bump
run: python .github/scripts/bump_beta_tag.py --push
- name: Resolve release tag
id: resolve-tag
run: |
if [ "${{ github.event_name }}" = "push" ]; then
echo "release_tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
else
echo "release_tag=${{ steps.bump.outputs.next_tag }}" >> "$GITHUB_OUTPUT"
fi

build:
needs: prepare-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-release.outputs.release_tag }}
fetch-depth: 0
- uses: actions/setup-python@v5
with:
Expand All @@ -33,7 +72,7 @@ jobs:
path: flash_attn/cute/dist/

github-release:
needs: build
needs: [prepare-release, build]
runs-on: ubuntu-latest
steps:
- name: Download distribution packages
Expand All @@ -44,8 +83,10 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare-release.outputs.release_tag }}
files: dist/*
generate_release_notes: true
prerelease: ${{ contains(needs.prepare-release.outputs.release_tag, '.beta') }}

publish-to-pypi:
needs: build
Expand Down
Loading