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
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ updates:
- "/actions/*"
schedule:
interval: "weekly"
day: "tuesday"
cooldown:
default-days: 7
groups:
Expand All @@ -18,6 +19,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "tuesday"
cooldown:
default-days: 7
# Security updates only; routine lock refreshes run in .github/workflows/lock-update.yml
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# .github/workflows/dependabot-auto-merge.yml
name: Dependabot auto-merge

on: pull_request

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Fetch Dependabot metadata
id: meta
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Approve and enable auto-merge for patch and minor updates
if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor'
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
1 change: 1 addition & 0 deletions .github/workflows/lock-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ jobs:
with:
app-id: ${{ vars.APP_ID }}
app-private-key: ${{ secrets.APP_PRIVATE_KEY }}
auto-merge: "true"
3 changes: 2 additions & 1 deletion .github/workflows/pre-commit-autoupdate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: Pre-commit autoupdate
on:
workflow_dispatch:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am
- cron: '0 9 * * 3' # Every Wednesday at 9am

permissions:
pull-requests: write
Expand All @@ -22,3 +22,4 @@ jobs:
with:
app-id: ${{ vars.APP_ID }}
app-private-key: ${{ secrets.APP_PRIVATE_KEY }}
auto-merge: "true"
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:
run: bash actions/poetry-lock-update/test_diff_lock.sh
- name: Test decide_pr_action.sh
run: bash actions/poetry-lock-update/test_decide_pr_action.sh
- name: Test auto_merge.sh
run: bash actions/common/test_auto_merge.sh
- name: Test stage_update.sh
run: bash actions/poetry-lock-update/test_stage_update.sh
- name: Test resolve_lock.py
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ uses: calysto/maintainer_tools/actions/<name>@v1

### Actions

Each action lives in `actions/<name>/action.yml`. The actions are:
Each action lives in `actions/<name>/action.yml`. Shared shell helpers used by multiple actions live in `actions/common/` (e.g. `auto_merge.sh`). The actions are:

- **`base-setup`** — Sets up Python (auto-detects minimum version from `pyproject.toml` if unspecified), Poetry, and `just` with OS-keyed cache. Poetry defaults to the version pinned by the poetry hook in `.pre-commit-config.yaml`. Must be called before `release`, `test-minimum-versions`, and `test-sdist`.
- **`poetry-lock-update`** — Runs `poetry update` under a minimum-release-age cooldown (`POETRY_SOLVER_MIN_RELEASE_AGE`, default 7 days) and opens a pull request with the `poetry.lock` diff, updating an existing open PR on the branch in place instead of opening a duplicate. Also owns the Poetry version: it bumps the poetry hook in `.pre-commit-config.yaml` to the newest cooldown-eligible release (`prek update --repo`), reinstalls it, and regenerates the lock, so the version and lock always move together. Requires a GitHub App (`APP_ID` / `APP_PRIVATE_KEY`) for authenticated pushes. Must be called after `base-setup`.
- **`pre-commit-autoupdate`** — Runs `prek update` under a cooldown and opens a PR with the changes. Excludes the poetry hook by default (owned by `poetry-lock-update`).
- **`poetry-lock-update`** — Runs `poetry update` under a minimum-release-age cooldown (`POETRY_SOLVER_MIN_RELEASE_AGE`, default 7 days) and opens a pull request with the `poetry.lock` diff, updating an existing open PR on the branch in place instead of opening a duplicate. Also owns the Poetry version: it bumps the poetry hook in `.pre-commit-config.yaml` to the newest cooldown-eligible release (`prek update --repo`), reinstalls it, and regenerates the lock, so the version and lock always move together. Requires a GitHub App (`APP_ID` / `APP_PRIVATE_KEY`) for authenticated pushes. Must be called after `base-setup`. Supports an opt-in `auto-merge` input that approves the PR (with the workflow token, since the App can't approve its own PR) and enables squash auto-merge.
- **`pre-commit-autoupdate`** — Runs `prek update` under a cooldown and opens a PR with the changes. Excludes the poetry hook by default (owned by `poetry-lock-update`). Supports an opt-in `auto-merge` input that approves the PR (with the workflow token, since the App can't approve its own PR) and enables squash auto-merge.
- **`enforce-label`** — Wraps `yogevbd/enforce-label-action`; requires one of: `bug`, `enhancement`, `dependencies`, `maintenance`, `documentation`.
- **`release`** — Full release pipeline: bumps version via Poetry, generates and writes CHANGELOG.md, commits and pushes, creates GitHub release, then bumps to next `.dev` version using `actions/release/bump_dev.py`. Supports dry-run. Requires a GitHub App (`APP_ID` / `APP_PRIVATE_KEY`) for authenticated pushes.
- **`test-minimum-versions`** — Rewrites `pyproject.toml` to pin all deps to their minimum declared versions, then runs the test suite.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ The poetry hook is always excluded — in whatever URL form the config uses (`ht
| `exclude-repos` | No | `""` | Space-separated list of additional hook repositories to leave untouched (passed through as `--exclude-repo`). The configured poetry hook is always excluded, in whatever URL form it uses. |
| `branch` | No | `"pre-commit-autoupdate"` | Branch name for the autoupdate pull request. |
| `labels` | No | `"maintenance"` | Labels to apply to the pull request. |
| `auto-merge` | No | `"false"` | If `"true"`, approve the pull request and enable auto-merge (squash) so it merges once required checks pass. The PR is authored by the GitHub App, which cannot approve its own PR, so approval is submitted with the workflow token — the caller must grant the workflow `pull-requests: write`. Requires "Allow auto-merge" to be enabled in the repository settings. |
| `dry-run` | No | `"false"` | If `"true"`, passes `--dry-run` to `gh pr create` (no PR is actually opened). |

**Usage**
Expand Down Expand Up @@ -177,6 +178,7 @@ The pull request body lists the Poetry version bump (if any) under a `## Poetry
| `min-release-age-days` | No | `"7"` | Minimum release age in days before Poetry's resolver will consider a version. |
| `branch` | No | `"poetry-lock-update"` | Branch name for the update pull request. Reused across runs — an existing open PR on this branch is updated in place rather than duplicated. This branch is exclusively managed by this action and force-pushed on every run, so manual commits pushed to it will not survive. |
| `labels` | No | `"maintenance"` | Labels to apply to the pull request. |
| `auto-merge` | No | `"false"` | If `"true"`, approve the pull request and enable auto-merge (squash) so it merges once required checks pass. The PR is authored by the GitHub App, which cannot approve its own PR, so approval is submitted with the workflow token — the caller must grant the workflow `pull-requests: write`. Requires "Allow auto-merge" to be enabled in the repository settings. |
| `dry-run` | No | `"false"` | If `"true"`, no push or PR mutation happens: `gh pr create` runs with `--dry-run` when no PR is open, or a "would update" message prints when one is. |

**Usage**
Expand Down
37 changes: 37 additions & 0 deletions actions/common/auto_merge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Approve a bot-authored pull request and enable squash auto-merge on it.
#
# The GitHub App that opens these pull requests cannot approve its own PR
# ("Can not approve your own pull request"), so the approval is submitted with
# the workflow token instead, which is a different actor. This requires the
# caller's workflow to grant the default token `pull-requests: write`.
#
# Usage: auto_merge.sh <branch>
#
# Reads the following environment variables:
# AUTO_MERGE - "true" to act; any other value is a no-op
# DRY_RUN - "true" to skip all mutations
# APP_TOKEN - app installation token; when empty, approval is skipped
# (the workflow token would be the PR author)
# WORKFLOW_TOKEN - workflow token used to submit the approval
set -euo pipefail

BRANCH="$1"

if [ "${AUTO_MERGE:-false}" != "true" ] || [ "${DRY_RUN:-false}" = "true" ]; then
exit 0
fi

if [ -n "${APP_TOKEN:-}" ]; then
GH_TOKEN="${WORKFLOW_TOKEN:-}" gh pr review "$BRANCH" --approve
else
echo "No app token configured; skipping approval"
fi

# The branch (and its PR) may be reused across runs, so only enable auto-merge
# when it is not already enabled, since re-enabling it errors. A failed lookup
# must fail the step rather than silently skipping the merge.
AUTO_MERGE_REQUEST=$(gh pr view "$BRANCH" --json autoMergeRequest --jq '.autoMergeRequest') || exit $?
if [ "$AUTO_MERGE_REQUEST" = "null" ]; then
gh pr merge "$BRANCH" --auto --squash --delete-branch
fi
120 changes: 120 additions & 0 deletions actions/common/test_auto_merge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT="$(cd "$(dirname "$0")" && pwd)/auto_merge.sh"
FAIL=0
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

check_contains() {
local desc="$1"
local needle="$2"
local haystack="$3"
if echo "$haystack" | grep -qF -- "$needle"; then
echo "OK: $desc"
else
echo "FAIL: $desc"
echo " expected to find: $needle"
echo " in: $haystack"
FAIL=1
fi
}

check_not_contains() {
local desc="$1"
local needle="$2"
local haystack="$3"
if echo "$haystack" | grep -qF -- "$needle"; then
echo "FAIL: $desc"
echo " expected NOT to find: $needle"
echo " in: $haystack"
FAIL=1
else
echo "OK: $desc"
fi
}

# Fake `gh` that logs the effective GH_TOKEN and arguments for every call. For
# `pr view` it prints a canned autoMergeRequest value, or fails when the
# pr_view_fail sentinel exists. This is not a general gh emulator — it only
# handles the exact call shapes auto_merge.sh makes.
FAKE_GH="$TMPDIR/gh"
cat > "$FAKE_GH" <<FAKE_GH_EOF
#!/usr/bin/env bash
echo "GH_TOKEN=\${GH_TOKEN:-} :: \$*" >> "$TMPDIR/gh_calls.log"
if [ "\$1" = "pr" ] && [ "\$2" = "view" ]; then
if [ -f "$TMPDIR/pr_view_fail" ]; then
exit 1
fi
cat "$TMPDIR/pr_view_response"
fi
FAKE_GH_EOF
chmod +x "$FAKE_GH"
export PATH="$TMPDIR:$PATH"

# run <auto_merge> <dry_run> <app_token> <pr_view_response>
run() {
echo "$4" > "$TMPDIR/pr_view_response"
: > "$TMPDIR/gh_calls.log"
rm -f "$TMPDIR/pr_view_fail"
AUTO_MERGE="$1" DRY_RUN="$2" APP_TOKEN="$3" WORKFLOW_TOKEN="workflow-token" \
bash "$SCRIPT" "poetry-lock-update" > "$TMPDIR/output.log" 2>&1
}

# Case 1: disabled -> no gh calls at all
run "false" "false" "app-token" "null"
CALLS=$(cat "$TMPDIR/gh_calls.log")
if [ -z "$CALLS" ]; then
echo "OK: disabled: no gh calls"
else
echo "FAIL: disabled: expected no gh calls, got: $CALLS"
FAIL=1
fi

# Case 2: dry-run -> no gh calls at all
run "true" "true" "app-token" "null"
CALLS=$(cat "$TMPDIR/gh_calls.log")
if [ -z "$CALLS" ]; then
echo "OK: dry-run: no gh calls"
else
echo "FAIL: dry-run: expected no gh calls, got: $CALLS"
FAIL=1
fi

# Case 3: opt-in, not yet enabled -> approve with the workflow token, then merge
run "true" "false" "app-token" "null"
CALLS=$(cat "$TMPDIR/gh_calls.log")
check_contains "opt-in: approves with the workflow token" "GH_TOKEN=workflow-token :: pr review poetry-lock-update --approve" "$CALLS"
check_contains "opt-in: enables squash auto-merge" "pr merge poetry-lock-update --auto --squash --delete-branch" "$CALLS"

# Case 4: already enabled -> approve, but do not re-enable
run "true" "false" "app-token" '{"enabledBy":{"login":"x"}}'
CALLS=$(cat "$TMPDIR/gh_calls.log")
check_contains "already enabled: still approves" "pr review poetry-lock-update --approve" "$CALLS"
check_not_contains "already enabled: does NOT re-enable auto-merge" "pr merge" "$CALLS"

# Case 5: no app token -> skip approval, still enable auto-merge
run "true" "false" "" "null"
CALLS=$(cat "$TMPDIR/gh_calls.log")
check_not_contains "no app token: does NOT approve" "pr review" "$CALLS"
check_contains "no app token: still enables auto-merge" "pr merge poetry-lock-update --auto --squash --delete-branch" "$CALLS"

# Case 6: gh pr view fails -> step fails, no merge attempted
echo "null" > "$TMPDIR/pr_view_response"
: > "$TMPDIR/gh_calls.log"
touch "$TMPDIR/pr_view_fail"
set +e
AUTO_MERGE="true" DRY_RUN="false" APP_TOKEN="app-token" WORKFLOW_TOKEN="workflow-token" \
bash "$SCRIPT" "poetry-lock-update" > "$TMPDIR/output.log" 2>&1
RC=$?
set -e
CALLS=$(cat "$TMPDIR/gh_calls.log")
if [ "$RC" -ne 0 ]; then
echo "OK: lookup failure: non-zero exit ($RC)"
else
echo "FAIL: lookup failure: expected non-zero exit"
FAIL=1
fi
check_not_contains "lookup failure: does NOT enable auto-merge" "pr merge" "$CALLS"

exit $FAIL
9 changes: 9 additions & 0 deletions actions/poetry-lock-update/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ inputs:
required: false
default: "maintenance"
description: "Labels to apply to the pull request"
auto-merge:
required: false
default: "false"
description: "If 'true', enable auto-merge (squash) so the pull request merges once required checks pass"
dry-run:
required: false
default: "false"
Expand Down Expand Up @@ -88,8 +92,11 @@ runs:
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
WORKFLOW_TOKEN: ${{ github.token }}
BRANCH: ${{ inputs.branch }}
LABELS: ${{ inputs.labels }}
AUTO_MERGE: ${{ inputs.auto-merge }}
DRY_RUN: ${{ inputs.dry-run }}
OLD_LOCK: ${{ runner.temp }}/poetry.lock.before
WAIVED: ${{ steps.resolve.outputs.waived }}
Expand Down Expand Up @@ -144,3 +151,5 @@ runs:

bash "${{ github.action_path }}/decide_pr_action.sh" \
"$BRANCH" "chore: update poetry.lock" "$BODY" "$LABELS" "$DRY_RUN"

bash "${{ github.action_path }}/../common/auto_merge.sh" "$BRANCH"
9 changes: 9 additions & 0 deletions actions/pre-commit-autoupdate/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ inputs:
required: false
default: "maintenance"
description: "Labels to apply to the pull request"
auto-merge:
required: false
default: "false"
description: "If 'true', enable auto-merge (squash) so the pull request merges once required checks pass"
dry-run:
required: false
default: "false"
Expand Down Expand Up @@ -84,8 +88,11 @@ runs:
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
WORKFLOW_TOKEN: ${{ github.token }}
BRANCH: ${{ inputs.branch }}
LABELS: ${{ inputs.labels }}
AUTO_MERGE: ${{ inputs.auto-merge }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
if git diff --quiet; then
Expand Down Expand Up @@ -126,3 +133,5 @@ runs:
--label "$LABELS" \
--head "$FULL_BRANCH" \
$DRY_RUN_FLAG

bash "${{ github.action_path }}/../common/auto_merge.sh" "$FULL_BRANCH"
Loading