fix: add Postgres-backed rate limiter for multi-worker deployments (#… #29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: propagate | |
| on: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| propagate: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| branch: [modal, hetzner] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate token | |
| id: create_token | |
| uses: tibdex/github-app-token@v2 | |
| with: | |
| app_id: ${{ secrets.APP_ID }} | |
| private_key: ${{ secrets.PRIVATE_KEY }} | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.create_token.outputs.token }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Attempt merge and push, or open PR on conflict | |
| env: | |
| GH_TOKEN: ${{ steps.create_token.outputs.token }} | |
| BRANCH: ${{ matrix.branch }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| open_propagation_pr() { | |
| TEMP_BRANCH="auto-merge/main-to-${BRANCH}-$(date +%Y%m%d%H%M%S)" | |
| git checkout -b "$TEMP_BRANCH" origin/main | |
| git push origin "$TEMP_BRANCH" | |
| gh pr create \ | |
| --base "$BRANCH" \ | |
| --head "$TEMP_BRANCH" \ | |
| --title "Propagate main to ${BRANCH}" \ | |
| --body "$(cat <<'PREOF' | |
| ## Summary | |
| - Auto-generated PR to propagate changes from `main` to the deployment branch | |
| - Opened because an automatic merge had conflicts that need manual resolution | |
| 🤖 Generated by the propagate workflow | |
| PREOF | |
| )" | |
| } | |
| unresolved_conflicts() { | |
| git diff --name-only --diff-filter=U | sort | |
| } | |
| conflicts_are_modal_overlay() { | |
| [ "$BRANCH" = "modal" ] || return 1 | |
| conflicts="$(unresolved_conflicts)" | |
| [ -n "$conflicts" ] || return 1 | |
| while IFS= read -r file; do | |
| case "$file" in | |
| pyproject.toml|uv.lock) ;; | |
| *) return 1 ;; | |
| esac | |
| done <<< "$conflicts" | |
| } | |
| add_modal_dependency() { | |
| python3 <<'PY' | |
| from pathlib import Path | |
| import subprocess | |
| import tomllib | |
| def is_modal_dependency(dependency: str) -> bool: | |
| return dependency.split(";", 1)[0].strip().lower().startswith("modal") | |
| def modal_dependency_from_branch() -> str: | |
| try: | |
| result = subprocess.run( | |
| ["git", "show", ":2:pyproject.toml"], | |
| check=True, | |
| capture_output=True, | |
| text=True, | |
| ) | |
| except subprocess.CalledProcessError: | |
| return "modal>=0.73.162" | |
| dependencies = tomllib.loads(result.stdout).get("project", {}).get("dependencies", []) | |
| return next((dependency for dependency in dependencies if is_modal_dependency(dependency)), "modal>=0.73.162") | |
| path = Path("pyproject.toml") | |
| text = path.read_text() | |
| data = tomllib.loads(text) | |
| dependencies = data.get("project", {}).get("dependencies", []) | |
| if any(is_modal_dependency(dependency) for dependency in dependencies): | |
| raise SystemExit(0) | |
| lines = text.splitlines() | |
| start = next( | |
| (index for index, line in enumerate(lines) if line.strip() == "dependencies = ["), | |
| None, | |
| ) | |
| if start is None: | |
| raise SystemExit("Could not find project dependencies in pyproject.toml") | |
| end = next( | |
| (index for index in range(start + 1, len(lines)) if lines[index].strip() == "]"), | |
| None, | |
| ) | |
| if end is None: | |
| raise SystemExit("Could not find end of project dependencies in pyproject.toml") | |
| insert_at = end | |
| for index in range(start + 1, end): | |
| if lines[index].strip().startswith('"psycopg2-binary'): | |
| insert_at = index | |
| break | |
| lines.insert(insert_at, f' "{modal_dependency_from_branch()}",') | |
| path.write_text("\n".join(lines) + "\n") | |
| PY | |
| } | |
| resolve_modal_overlay() { | |
| if unresolved_conflicts | grep -qx "pyproject.toml"; then | |
| git checkout --theirs pyproject.toml | |
| fi | |
| add_modal_dependency | |
| if unresolved_conflicts | grep -qx "uv.lock"; then | |
| git checkout --ours uv.lock | |
| fi | |
| uv lock | |
| git add pyproject.toml uv.lock | |
| remaining_conflicts="$(unresolved_conflicts)" | |
| if [ -n "$remaining_conflicts" ]; then | |
| echo "Conflicts remain after modal overlay resolution:" | |
| echo "$remaining_conflicts" | |
| return 1 | |
| fi | |
| git commit --no-edit | |
| git push origin "$BRANCH" | |
| } | |
| git checkout "$BRANCH" | |
| if git merge origin/main --no-edit; then | |
| echo "Merge succeeded, pushing to $BRANCH" | |
| git push origin "$BRANCH" | |
| elif conflicts_are_modal_overlay; then | |
| echo "Merge conflict only affects the modal dependency overlay; resolving automatically" | |
| resolve_modal_overlay | |
| else | |
| echo "Merge conflict detected, opening PR" | |
| git merge --abort | |
| open_propagation_pr | |
| fi |