Skip to content

Commit 49d6d3b

Browse files
committed
add CI release scripts
1 parent f68f576 commit 49d6d3b

11 files changed

Lines changed: 303 additions & 9 deletions

File tree

.github/scripts/release/ci

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
4+
from common import branch, get_next_dev_version
5+
from status_check import status_check
6+
from update_changelog import update_changelog
7+
from update_rust_version import update_rust_version
8+
from git_commit import git_commit
9+
from git_tag import git_tag
10+
from git_push import git_push
11+
12+
logger = logging.getLogger(__name__)
13+
14+
if __name__ == "__main__":
15+
status_check()
16+
update_changelog()
17+
update_rust_version()
18+
git_commit()
19+
git_tag()
20+
21+
if branch == "main":
22+
update_rust_version(version=get_next_dev_version())
23+
git_commit(message="reopen main for development")
24+
25+
git_push()
26+
logger.info("✅ All done. 🥳")

.github/scripts/release/common.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import re
3+
import subprocess
4+
import logging
5+
import http.client
6+
import json
7+
from typing import Callable
8+
9+
logging.basicConfig(format="[%(asctime)s] %(message)s", level=logging.INFO)
10+
11+
logger = logging.getLogger(__name__)
12+
13+
github_repository: str
14+
project: str
15+
branch: str
16+
get_version: Callable[[], str]
17+
18+
if github_repository := os.environ.get("GITHUB_REPOSITORY", None):
19+
logger.info(f"Got repository from environment: {github_repository}")
20+
else:
21+
_origin_url = subprocess.check_output(
22+
["git", "remote", "get-url", "--push", "origin"], text=True
23+
).strip()
24+
github_repository = re.search(r"^git@github\.com:(.+)\.git$", _origin_url)[1]
25+
logger.info(f"Got repository from Git: {github_repository}")
26+
27+
if project := os.environ.get("PROJECT_NAME", None):
28+
logger.info(f"Got project name from $PROJECT_NAME: {project}")
29+
else:
30+
project = github_repository.partition("/")[2]
31+
logger.info(f"Got project name from repository url: {project}")
32+
33+
branch = subprocess.check_output(["git", "branch", "--show-current"], text=True).strip()
34+
35+
_version: str | None
36+
if _version := os.environ.get("PROJECT_VERSION", None):
37+
logger.info(f"Got project version from $PROJECT_VERSION: {_version}")
38+
elif os.environ.get("GITHUB_REF", "").startswith("refs/tags/"):
39+
_version = os.environ["GITHUB_REF_NAME"]
40+
logger.info(f"Got project version from $GITHUB_REF: {_version}")
41+
else:
42+
_version = None
43+
logger.info("No version information found.")
44+
45+
46+
def get_version() -> str:
47+
if _version is None:
48+
raise RuntimeError("No version information found.")
49+
assert re.match(r"^\d+\.\d+\.\d+$", _version), f"Invalid version: {_version}"
50+
return _version
51+
52+
53+
def get_next_dev_version() -> str:
54+
version = get_version().split(".")
55+
if version[0] == "0":
56+
version[1] = str(int(version[1]) + 1)
57+
else:
58+
version[0] = str(int(version[0]) + 1)
59+
return ".".join(version) + "-dev"
60+
61+
62+
def get_tag_name() -> str:
63+
return f"{os.environ.get('GIT_TAG_PREFIX', '')}{get_version()}"
64+
65+
66+
def http_get(url: str) -> http.client.HTTPResponse:
67+
assert url.startswith("https://")
68+
host, path = re.split(r"(?=/)", url.removeprefix("https://"), maxsplit=1)
69+
logger.info(f"GET {host} {path}")
70+
conn = http.client.HTTPSConnection(host)
71+
conn.request("GET", path, headers={"User-Agent": "mhils/run-tools"})
72+
resp = conn.getresponse()
73+
print(f"HTTP {resp.status} {resp.reason}")
74+
return resp
75+
76+
77+
def http_get_json(url: str) -> dict:
78+
resp = http_get(url)
79+
body = resp.read()
80+
try:
81+
return json.loads(body)
82+
except Exception as e:
83+
raise RuntimeError(f"{resp.status=} {body=}") from e
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
import subprocess
4+
import sys
5+
6+
from common import project, get_version
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def git_commit(message: str = ""):
12+
logger.info("➡️ Git commit...")
13+
subprocess.check_call(
14+
[
15+
"git",
16+
*("-c", f"user.name={project} run bot"),
17+
*("-c", "user.email=git-run-bot@maximilianhils.com"),
18+
"commit",
19+
"--all",
20+
*("-m", message or f"{project} {get_version()}"),
21+
]
22+
)
23+
24+
25+
if __name__ == "__main__":
26+
git_commit(sys.argv[1] if len(sys.argv) > 1 else "")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
import subprocess
4+
5+
from common import branch, get_tag_name
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def git_push(*identifiers: str):
11+
logger.info("➡️ Git push...")
12+
if not identifiers:
13+
identifiers = [
14+
branch,
15+
get_tag_name(),
16+
]
17+
subprocess.check_call(["git", "push", "--atomic", "origin", *identifiers])
18+
19+
20+
if __name__ == "__main__":
21+
git_push()

.github/scripts/release/git_tag.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
import subprocess
4+
5+
from common import get_tag_name
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def git_tag(name: str = ""):
11+
logger.info("➡️ Git tag...")
12+
subprocess.check_call(["git", "tag", name or get_tag_name()])
13+
14+
15+
if __name__ == "__main__":
16+
git_tag()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
import os
4+
import subprocess
5+
6+
from common import branch, github_repository, http_get_json
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def status_check():
12+
if os.environ.get("STATUS_CHECK_SKIP_GIT", None) == "true":
13+
logger.warning("⚠️ Skipping check whether Git repo is clean.")
14+
else:
15+
logger.info("➡️ Working dir clean?")
16+
out = subprocess.check_output(["git", "status", "--porcelain"])
17+
assert not out, "repository is not clean"
18+
19+
if os.environ.get("STATUS_CHECK_SKIP_CI", None) == "true":
20+
logger.warning(f"⚠️ Skipping status check for {branch}.")
21+
else:
22+
logger.info(f"➡️ CI is passing for {branch}?")
23+
assert (
24+
http_get_json(
25+
f"https://api.github.com/repos/{github_repository}/commits/{branch}/status"
26+
)["state"]
27+
== "success"
28+
)
29+
30+
31+
if __name__ == "__main__":
32+
status_check()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env -S python3 -u
2+
import datetime
3+
import logging
4+
import re
5+
from pathlib import Path
6+
7+
from common import project, get_version
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def update_changelog():
13+
logger.info("➡️ Updating CHANGELOG.md...")
14+
path = Path("CHANGELOG.md")
15+
date = datetime.date.today().strftime("%d %B %Y")
16+
title = f"## {date}: {project} {get_version()}"
17+
cl = path.read_text("utf8")
18+
assert title not in cl, f"Version {get_version()} is already present in {path}."
19+
cl, ok = re.subn(rf"(?<=## Unreleased: {project} next)", f"\n\n\n{title}", cl)
20+
assert ok == 1
21+
path.write_text(cl, "utf8")
22+
23+
24+
if __name__ == "__main__":
25+
update_changelog()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env -S python3 -u
2+
import logging
3+
import re
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
from common import get_version
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def update_rust_version(version: str = ""):
14+
logger.info("➡️ Updating Cargo.toml...")
15+
path = Path("Cargo.toml")
16+
cl = path.read_text("utf8")
17+
cl, ok = re.subn(
18+
r"""
19+
(
20+
^\[(?:workspace\.)?package]\n # [package] or [workspace.package] toml block
21+
(?:(?!\[).*\n)* # lines not starting a new section
22+
version[ \t]*=[ \t]*" # beginning of the version line
23+
)
24+
[^"]+
25+
""",
26+
rf"\g<1>{version or get_version()}",
27+
cl,
28+
flags=re.VERBOSE | re.MULTILINE,
29+
)
30+
assert ok == 1, f"{ok=}"
31+
path.write_text(cl, "utf8")
32+
33+
subprocess.check_call(["cargo", "update", "--workspace"])
34+
35+
36+
if __name__ == "__main__":
37+
update_rust_version(sys.argv[1] if len(sys.argv) > 1 else "")

.github/workflows/release.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Version (major.minor.patch)'
8+
required: true
9+
type: string
10+
skip-branch-status-check:
11+
description: 'Skip CI status check.'
12+
default: false
13+
required: false
14+
type: boolean
15+
16+
permissions: {}
17+
18+
jobs:
19+
release:
20+
environment: deploy
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
token: ${{ secrets.GH_PUSH_TOKEN }} # this token works to push to the protected main branch.
26+
- uses: actions/setup-python@v5
27+
with:
28+
python-version-file: .github/python-version.txt
29+
- run: ./.github/scripts/release/ci
30+
env:
31+
PROJECT_VERSION: ${{ inputs.version }}
32+
STATUS_CHECK_SKIP_GIT: ${{ inputs.skip-branch-status-check }}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
## Unreleased: mitmproxy_rs next
12

23
- Move functionality into submodules.
34

0 commit comments

Comments
 (0)