Skip to content

Commit 93be127

Browse files
committed
Add auto merge tooling
PL-133248
1 parent 86830e9 commit 93be127

File tree

13 files changed

+814
-4
lines changed

13 files changed

+814
-4
lines changed

.github/workflows/auto-merge.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: auto-merge
2+
3+
on:
4+
workflow_dispatch: {}
5+
schedule:
6+
- cron: "5 12-23 * * MON-FRI"
7+
8+
jobs:
9+
run-auto-merge:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: cachix/install-nix-action@v21
14+
- uses: actions/create-github-app-token@v1
15+
id: app-token
16+
with:
17+
app-id: ${{ vars.AUTO_MERGE_APP_ID }}
18+
private-key: ${{ secrets.AUTO_MERGE_APP_PRIVATE_KEY }}
19+
owner: ${{ github.repository_owner }}
20+
- run: |
21+
echo "::add-mask::${{steps.app-token.outputs.token}}"
22+
- name: Get GitHub App User ID
23+
id: get-user-id
24+
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
25+
env:
26+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
27+
- run: |
28+
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
29+
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
30+
- name: build release tooling
31+
run: |
32+
nix build .#
33+
- name: run merge tool
34+
run: |
35+
./result/bin/auto-merge merge --action-run-repo-name ${{ github.repository }} --fc-nixos-dir fc-nixos
36+
env:
37+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
38+
MATRIX_HOOKSHOT_URL: ${{ secrets.MATRIX_HOOKSHOT_URL }}
39+
MONITORING_REVIEW_URL: ""
40+
- uses: actions/upload-artifact@v4
41+
with:
42+
name: status-json
43+
path: auto-merge-status.json
44+
if-no-files-found: error

auto-merge-config.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[general]
2+
# 0 = Monday, 6 = Sunday
3+
production_merge_day = 3
4+
# Times are in Europe/Berlin
5+
production_merge_cutoff_hour = 12
6+
platform_versions = [ "24.05" ]
7+
fc_nixos_repo_name = "flyingcircusio/fc-nixos-testing"
8+
9+
[monitoring_review]
10+
# Names of monitoring review columns
11+
name = "platform-dev"
12+
notification_cutoff_hour = 15
13+
14+
# Weekdays relative to the production_merge_day
15+
# 0 = production_merge_day, 1 = production_merge_day + 1
16+
# e.g. production_merge_day = Thursday. pr_merge_days.0 = THU, pr_merge_days.1=FRI, pr_merge_days.2=MON
17+
[pr_merge_days]
18+
[pr_merge_days.0]
19+
max_risk = 5
20+
min_urgency = 1
21+
22+
[pr_merge_days.1]
23+
max_risk = 4
24+
min_urgency = 1
25+
26+
[pr_merge_days.2]
27+
max_risk = 3
28+
min_urgency = 2
29+
30+
[pr_merge_days.3]
31+
max_risk = 2
32+
min_urgency = 3
33+
34+
[pr_merge_days.4]
35+
max_risk = 1
36+
min_urgency = 5

flake.nix

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616
src = ./.;
1717
pyproject = true;
1818
build-system = [ pypkgs.setuptools-scm ];
19+
nativeCheckInputs = [
20+
pypkgs.pytestCheckHook
21+
];
1922
dependencies = [
23+
pypkgs.python-dateutil
24+
pypkgs.gql
25+
pypkgs.gql.optional-dependencies.requests
26+
pypkgs.pydantic
2027
pypkgs.setuptools
2128
pypkgs.gitpython
29+
pypkgs.requests
2230
pypkgs.pygithub
2331
pypkgs.rich
2432
pkgs.gh

pyproject.toml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@ name = "fc-nixos-release-tools"
1414
dynamic = ["version"]
1515
requires-python = ">= 3.11"
1616
dependencies = [
17-
"pygithub~=2.5",
18-
"gitpython~=3.1",
19-
"requests~=2.32.2",
20-
"rich~=13.8.1"
17+
"pygithub>=2.5",
18+
"gitpython>=3.1",
19+
"requests>=2.32",
20+
"rich>=13.8",
21+
"gql[requests]~=3.5",
22+
"pydantic>=2.9",
23+
"pygithub>=2.5",
24+
"python-dateutil>=2.9"
25+
]
26+
27+
[project.optional-dependencies]
28+
dev = [
29+
"pytest>=7.4"
2130
]
2231

2332
[project.scripts]
2433
release = "release:main"
2534
update-nixpkgs = "update_nixpkgs:main"
35+
auto-merge = "auto_merge:main"

src/__init__.py

Whitespace-only changes.

src/auto_merge/__init__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import argparse
2+
import logging
3+
import os
4+
import sys
5+
6+
from auto_merge import check_pr, merge
7+
from auto_merge.config import load_config
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser()
12+
parser.set_defaults(func="print_usage")
13+
subparsers = parser.add_subparsers(help="subcommand help")
14+
15+
parser_check_pr = subparsers.add_parser("check-pr", help="check-pr help")
16+
parser_check_pr.add_argument(
17+
"pr_id", type=int, help="ID of the pull request, we want to consider"
18+
)
19+
parser_check_pr.set_defaults(func=check_pr.check_pr)
20+
21+
parser_merge = subparsers.add_parser("merge", help="merge help")
22+
parser_merge.add_argument(
23+
"--action-run-repo-name",
24+
type=str,
25+
help="Repository name including owner, e.g. flyingcircusio/fc-nixos-release-tooling containing this workflow",
26+
required=True,
27+
default="",
28+
)
29+
parser_merge.add_argument(
30+
"--fc-nixos-dir",
31+
help="Directory where the fc-nixos git checkout is in",
32+
required=True,
33+
)
34+
parser_merge.set_defaults(func=merge.run)
35+
36+
args = parser.parse_args()
37+
func = args.func
38+
if func == "print_usage":
39+
parser.print_usage()
40+
sys.exit(1)
41+
42+
kwargs = dict(args._get_kwargs())
43+
del kwargs["func"]
44+
kwargs["config"] = load_config()
45+
try:
46+
kwargs["github_access_token"] = os.environ["GH_TOKEN"]
47+
except KeyError:
48+
raise Exception("Missing `GH_TOKEN` environment variable.")
49+
if func == merge.run:
50+
try:
51+
kwargs["monitoring_review_url"] = os.environ[
52+
"MONITORING_REVIEW_URL"
53+
]
54+
except KeyError:
55+
raise Exception(
56+
"Missing `MONITORING_REVIEW_URL` environment variable."
57+
)
58+
try:
59+
kwargs["matrix_hookshot_url"] = os.environ["MATRIX_HOOKSHOT_URL"]
60+
except KeyError:
61+
raise Exception(
62+
"Missing `MATRIX_HOOKSHOT_URL` environment variable."
63+
)
64+
65+
logging.basicConfig(level=logging.INFO)
66+
func(**kwargs)
67+
68+
69+
if __name__ == "__main__":
70+
main()

src/auto_merge/check_pr.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from github import Auth, Github
2+
3+
from auto_merge import utils
4+
from auto_merge.config import Config
5+
6+
7+
def check_pr(pr_id: int, github_access_token: str, config: Config):
8+
gh = Github(auth=Auth.Token(github_access_token))
9+
repository = gh.get_repo(config.general.fc_nixos_repo_name)
10+
11+
pr = repository.get_pull(pr_id)
12+
risk, urgency = utils.get_label_values_for_pr(pr.labels)
13+
if risk is None or urgency is None:
14+
# This raises a runtime error, so it shows as a red check indicator in GitHub
15+
raise RuntimeError(
16+
"PR doesn't have risk and urgency labels. Not mergeable."
17+
)
18+
# check if PR is approved
19+
mergeable = utils.check_pr_mergeable(
20+
repository, pr, github_access_token, config
21+
)
22+
if mergeable:
23+
merge_date = utils.calculate_merge_date(risk, urgency, config)
24+
msg = f"This PR is ready to merge. Merge scheduled for {merge_date.isoformat()}"
25+
for comment in pr.get_issue_comments():
26+
if comment.body == msg:
27+
return
28+
pr.create_issue_comment(msg)

src/auto_merge/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import tomllib
2+
3+
from pydantic import BaseModel
4+
5+
6+
class PRMergeDayConfig(BaseModel):
7+
max_risk: int
8+
min_urgency: int
9+
10+
11+
class GeneralConfig(BaseModel):
12+
# Our days are virtual to the production merge day and cutoff hour
13+
production_merge_day: int
14+
production_merge_cutoff_hour: int
15+
fc_nixos_repo_name: str
16+
platform_versions: list[str]
17+
18+
19+
class MonitoringReviewConfig(BaseModel):
20+
name: str
21+
notification_cutoff_hour: int
22+
23+
24+
class Config(BaseModel):
25+
pr_merge_days: dict[int, PRMergeDayConfig]
26+
general: GeneralConfig
27+
monitoring_review: MonitoringReviewConfig
28+
29+
30+
def load_config() -> Config:
31+
with open("auto-merge-config.toml", "rb") as f:
32+
data = tomllib.load(f)
33+
return Config.model_validate(data)

0 commit comments

Comments
 (0)