Skip to content

Commit

Permalink
add CI release scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
mhils committed Sep 4, 2024
1 parent f68f576 commit 30b3574
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 9 deletions.
26 changes: 26 additions & 0 deletions .github/scripts/release/ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env -S python3 -u
import logging

from common import branch, get_next_dev_version
from status_check import status_check
from update_changelog import update_changelog
from update_rust_version import update_rust_version
from git_commit import git_commit
from git_tag import git_tag
from git_push import git_push

logger = logging.getLogger(__name__)

if __name__ == "__main__":
status_check()
update_changelog()
update_rust_version()
git_commit()
git_tag()

if branch == "main":
update_rust_version(version=get_next_dev_version())
git_commit(message="reopen main for development")

git_push()
logger.info("✅ All done. 🥳")
83 changes: 83 additions & 0 deletions .github/scripts/release/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import os
import re
import subprocess
import logging
import http.client
import json
from typing import Callable

logging.basicConfig(format="[%(asctime)s] %(message)s", level=logging.INFO)

logger = logging.getLogger(__name__)

github_repository: str
project: str
branch: str
get_version: Callable[[], str]

if github_repository := os.environ.get("GITHUB_REPOSITORY", None):
logger.info(f"Got repository from environment: {github_repository}")
else:
_origin_url = subprocess.check_output(
["git", "remote", "get-url", "--push", "origin"], text=True
).strip()
github_repository = re.search(r"^git@github\.com:(.+)\.git$", _origin_url)[1]
logger.info(f"Got repository from Git: {github_repository}")

if project := os.environ.get("PROJECT_NAME", None):
logger.info(f"Got project name from $PROJECT_NAME: {project}")
else:
project = github_repository.partition("/")[2]
logger.info(f"Got project name from repository url: {project}")

branch = subprocess.check_output(["git", "branch", "--show-current"], text=True).strip()

_version: str | None
if _version := os.environ.get("PROJECT_VERSION", None):
logger.info(f"Got project version from $PROJECT_VERSION: {_version}")
elif os.environ.get("GITHUB_REF", "").startswith("refs/tags/"):
_version = os.environ["GITHUB_REF_NAME"]
logger.info(f"Got project version from $GITHUB_REF: {_version}")
else:
_version = None
logger.info("No version information found.")


def get_version() -> str:
if _version is None:
raise RuntimeError("No version information found.")
assert re.match(r"^\d+\.\d+\.\d+$", _version), f"Invalid version: {_version}"
return _version


def get_next_dev_version() -> str:
version = get_version().split(".")
if version[0] == "0":
version[1] = str(int(version[1]) + 1)
else:
version[0] = str(int(version[0]) + 1)
return ".".join(version) + "-dev"


def get_tag_name() -> str:
return f"{os.environ.get("GIT_TAG_PREFIX", "")}{get_version()}"


def http_get(url: str) -> http.client.HTTPResponse:
assert url.startswith("https://")
host, path = re.split(r"(?=/)", url.removeprefix("https://"), maxsplit=1)
logger.info(f"GET {host} {path}")
conn = http.client.HTTPSConnection(host)
conn.request("GET", path, headers={"User-Agent": "mhils/run-tools"})
resp = conn.getresponse()
print(f"HTTP {resp.status} {resp.reason}")
return resp


def http_get_json(url: str) -> dict:
resp = http_get(url)
body = resp.read()
try:
return json.loads(body)
except Exception as e:
raise RuntimeError(f"{resp.status=} {body=}") from e
26 changes: 26 additions & 0 deletions .github/scripts/release/git_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env -S python3 -u
import logging
import subprocess
import sys

from common import project, get_version

logger = logging.getLogger(__name__)


def git_commit(message: str = ""):
logger.info("➡️ Git commit...")
subprocess.check_call(
[
"git",
*("-c", f"user.name={project} run bot"),
*("-c", "[email protected]"),
"commit",
"--all",
*("-m", message or f"{project} {get_version()}"),
]
)


if __name__ == "__main__":
git_commit(sys.argv[1] if len(sys.argv) > 1 else "")
21 changes: 21 additions & 0 deletions .github/scripts/release/git_push.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env -S python3 -u
import logging
import subprocess

from common import branch, get_tag_name

logger = logging.getLogger(__name__)


def git_push(*identifiers: str):
logger.info("➡️ Git push...")
if not identifiers:
identifiers = [
branch,
get_tag_name(),
]
subprocess.check_call(["git", "push", "--atomic", "origin", *identifiers])


if __name__ == "__main__":
git_push()
16 changes: 16 additions & 0 deletions .github/scripts/release/git_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env -S python3 -u
import logging
import subprocess

from common import get_tag_name

logger = logging.getLogger(__name__)


def git_tag(name: str = ""):
logger.info("➡️ Git tag...")
subprocess.check_call(["git", "tag", name or get_tag_name()])


if __name__ == "__main__":
git_tag()
32 changes: 32 additions & 0 deletions .github/scripts/release/status_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env -S python3 -u
import logging
import os
import subprocess

from common import branch, github_repository, http_get_json

logger = logging.getLogger(__name__)


def status_check():
if os.environ.get("STATUS_CHECK_SKIP_GIT", None) == "true":
logger.warning("⚠️ Skipping check whether Git repo is clean.")
else:
logger.info("➡️ Working dir clean?")
out = subprocess.check_output(["git", "status", "--porcelain"])
assert not out, "repository is not clean"

if os.environ.get("STATUS_CHECK_SKIP_CI", None) == "true":
logger.warning(f"⚠️ Skipping status check for {branch}.")
else:
logger.info(f"➡️ CI is passing for {branch}?")
assert (
http_get_json(
f"https://api.github.com/repos/{github_repository}/commits/{branch}/status"
)["state"]
== "success"
)


if __name__ == "__main__":
status_check()
25 changes: 25 additions & 0 deletions .github/scripts/release/update_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env -S python3 -u
import datetime
import logging
import re
from pathlib import Path

from common import project, get_version

logger = logging.getLogger(__name__)


def update_changelog():
logger.info("➡️ Updating CHANGELOG.md...")
path = Path("CHANGELOG.md")
date = datetime.date.today().strftime("%d %B %Y")
title = f"## {date}: {project} {get_version()}"
cl = path.read_text("utf8")
assert title not in cl, f"Version {get_version()} is already present in {path}."
cl, ok = re.subn(rf"(?<=## Unreleased: {project} next)", f"\n\n\n{title}", cl)
assert ok == 1
path.write_text(cl, "utf8")


if __name__ == "__main__":
update_changelog()
37 changes: 37 additions & 0 deletions .github/scripts/release/update_rust_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env -S python3 -u
import logging
import re
import subprocess
import sys
from pathlib import Path

from common import get_version

logger = logging.getLogger(__name__)


def update_rust_version(version: str = ""):
logger.info("➡️ Updating Cargo.toml...")
path = Path("Cargo.toml")
cl = path.read_text("utf8")
cl, ok = re.subn(
r"""
(
^\[(?:workspace\.)?package]\n # [package] or [workspace.package] toml block
(?:(?!\[).*\n)* # lines not starting a new section
version[ \t]*=[ \t]*" # beginning of the version line
)
[^"]+
""",
rf"\g<1>{version or get_version()}",
cl,
flags=re.VERBOSE | re.MULTILINE,
)
assert ok == 1, f"{ok=}"
path.write_text(cl, "utf8")

subprocess.check_call(["cargo", "update", "--workspace"])


if __name__ == "__main__":
update_rust_version(sys.argv[1] if len(sys.argv) > 1 else "")
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Release

on:
workflow_dispatch:
inputs:
version:
description: 'Version (major.minor.patch)'
required: true
type: string
skip-branch-status-check:
description: 'Skip CI status check.'
default: false
required: false
type: boolean

permissions: {}

jobs:
release:
environment: deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PUSH_TOKEN }} # this token works to push to the protected main branch.
- uses: actions/setup-python@v5
with:
python-version-file: .github/python-version.txt
- run: ./.github/scripts/release/ci
env:
PROJECT_VERSION: ${{ inputs.version }}
STATUS_CHECK_SKIP_GIT: ${{ inputs.skip-branch-status-check }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## Unreleased: mitmproxy_rs next

- Move functionality into submodules.

Expand Down
13 changes: 4 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,7 @@ be no busy waiting.
If you are the current maintainer of mitmproxy_rs,
you can perform the following steps to ship a release:

1. Make sure that...
- you are on the `main` branch with a clean working tree.
- `cargo test` is passing without errors.
2. Bump the version in [`Cargo.toml`](Cargo.toml).
3. Run `cargo update --workspace` to update the lockfile with the new version.
4. Update [`CHANGELOG.md`](./CHANGELOG.md).
5. Commit the changes and tag them.
- Convention: Tag name is simply the version number, e.g. `1.0.1`.
6. Manually confirm the CI deploy step on GitHub.
1. Make sure that CI is passing without errors.
2. Make sure that CHANGELOG.md is up-to-date with all entries in the "Unreleased" section.
3. Invoke the release workflow from the GitHub UI: https://github.com/mitmproxy/mitmproxy_rs/actions/workflows/release.yml
4. The spawned workflow run will require manual deploy confirmation on GitHub: https://github.com/mitmproxy/mitmproxy/actions

0 comments on commit 30b3574

Please sign in to comment.