Skip to content

Commit

Permalink
Merge pull request #1085 from camptocamp/create-version-script
Browse files Browse the repository at this point in the history
Add tool to automatically create a new release
  • Loading branch information
sbrunner authored Jun 28, 2023
2 parents db31f16 + a873745 commit a58f11d
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ RUN --mount=type=cache,target=/root/.cache \
# Do the conversion
COPY poetry.lock pyproject.toml ./
ENV POETRY_DYNAMIC_VERSIONING_BYPASS=0.0.0
RUN poetry export --extras=checks --extras=publish --extras=audit --output=requirements.txt \
RUN poetry export --extras=checks --extras=publish --extras=audit --extras=version --output=requirements.txt \
&& poetry export --with=dev --output=requirements-dev.txt

# Base, the biggest thing is to install the Python packages
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ It make easier to place the following workflows:
All the provided commands:

- `c2cciutils`: some generic tools.
- `c2cciutils-version`: Create a new version of the project.
- `c2cciutils-checks`: Run the checks on the code (those checks don't need any project dependencies).
- `c2cciutils-audit`: Do the audit, the main difference with checks is that it can change between runs on the same code.
- `c2cciutils-publish`: Publish the project.
Expand All @@ -58,6 +59,18 @@ All the provided commands:

The content of `example-project` can be a good base for a new project.

## New version

Requirements: the right version (>= 1.6) of `c2cciutils` should be installed with the `version` extra.

To create a new minor version you just should run `c2cciutils-version --version=<version>`.

You are welcome to run `c2cciutils-version --help` to see what's it's done.

Note that it didn't create a tag, you should do it manually.

To create a patch version you should just create tag.

## Secrets

In the CI we need to have the following secrets::
Expand Down
217 changes: 217 additions & 0 deletions c2cciutils/scripts/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#!/usr/bin/env python3

import argparse
import json
import os
import re
import subprocess # nosec

import multi_repo_automation as mra
import ruamel.yaml.comments


def main() -> None:
"""Create a new version with its stabilization branch."""

args_parser = argparse.ArgumentParser(
description="Create a new version with its stabilization branch",
usage="""
This will:
- Stash all your changes
- Checkout the master branch
- Pull it from origin
- Push it to a new stabilization branch
- Checkout a new branch named new-version
- Do the changes for the new version
- Update the SECURITY.md config
- Update the Renovate config
- Update the audit workflow
- Create the backport label
- Push it
- Create a pull request
- Go back to your old branch
If you run the tool without any version it will check that everything is OK regarding the SECURITY.md available on GitHub.
""",
)
args_parser.add_argument(
"--version",
help="The version to create",
)
args_parser.add_argument(
"--force",
action="store_true",
help="Force create the branch and push it",
)
args_parser.add_argument(
"--supported-until",
help="The date until the version is supported, can also be To be defined or Best effort",
default="Best effort",
)
args_parser.add_argument(
"--upstream-supported-until",
help="The date until the version is supported upstream",
)
arguments = args_parser.parse_args()

# Get the repo information e.g.:
# {
# "name": "camptocamp/c2cciutils",
# "remote": "origin",
# "dir": "/home/user/src/c2cciutils",
# }
# can be override with a repo.yaml file
repo = mra.get_repo_config()

# Stash all your changes
subprocess.run(["git", "stash", "--all", "--message=Stashed by release creation"], check=True)
old_branch_name = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
stdout=subprocess.PIPE,
check=True,
).stdout.strip()

# Checkout the master branch
subprocess.run(["git", "checkout", repo.get("master_branch", "master")], check=True)

# Pull it from origin
subprocess.run(
["git", "pull", repo.get("remote", "origin"), repo.get("master_branch", "master")], check=True
)

# Push it to a new stabilization branch
if arguments.version:
subprocess.run(
[
"git",
"push",
*(["--force"] if arguments.force else []),
repo.get("remote", "origin"),
f"HEAD:{arguments.version}",
],
check=not arguments.force,
)

version = arguments.version
branch_name = "new-version" if version is None else f"new-version-{version}"

# Checkout a new branch named new-version
if arguments.force:
subprocess.run(["git", "branch", "-D", branch_name]) # pylint: disable=subprocess-run-check
subprocess.run(["git", "checkout", "-b", branch_name], check=True)

# # # Do the changes for the new version # # #

stabilization_branches = mra.get_stabilization_branches(repo)

if version:
stabilization_branches.append(version)

if os.path.exists("SECURITY.md"):
with mra.Edit("SECURITY.md") as security_md:
security_md_lines = security_md.data.split("\n")
index = -1
for i, line in enumerate(security_md_lines):
if line.startswith("| "):
index = i

new_line = f"| {version} | {arguments.supported_until} |"
if arguments.upstream_supported_until:
new_line += f" {arguments.upstream_supported_until} |"

security_md.data = "\n".join(
[*security_md_lines[: index + 1], new_line, *security_md_lines[index + 1 :]]
)

stabilization_branches_with_master = [*stabilization_branches, repo.get("master_branch", "master")]

for labels in mra.gh_json("label", ["name"], "list"):
if labels["name"].startswith("backport "):
if labels["name"].replace("backport ", "") not in stabilization_branches_with_master:
mra.gh("label", "delete", labels["name"], "--yes")

for branch in stabilization_branches_with_master:
mra.gh(
"label",
"create",
"--force",
f"backport {branch}",
"--color=5aed94",
f"--description=Backport the pull request to the '{branch}' branch",
)

if os.path.exists(".github/renovate.json5"):
with mra.EditRenovateConfig(".github/renovate.json5") as renovate_config:
if stabilization_branches:
if "baseBranches: " in renovate_config.data:
renovate_config.data = re.sub(
r"(.*baseBranches: )\[[^\]]*\](.*)",
rf"\1{json.dumps(stabilization_branches_with_master)}\2",
renovate_config.data,
)
else:
renovate_config.add({"baseBranches": stabilization_branches_with_master}, "baseBranches")

if stabilization_branches and os.path.exists(".github/workflows/audit.yaml"):
with mra.EditYAML(".github/workflows/audit.yaml") as yaml:
for job in yaml["jobs"].values():
matrix = job.get("strategy", {}).get("matrix", {})
if "include" in matrix and version:
new_include = dict(matrix["include"][-1])
new_include["branch"] = version
matrix["include"].append(new_include)

if "branch" in matrix and stabilization_branches:
yaml_stabilization_branches = ruamel.yaml.comments.CommentedSeq(stabilization_branches)
yaml_stabilization_branches._yaml_add_comment( # pylint: disable=protected-access
[
ruamel.yaml.CommentToken("\n\n", ruamel.yaml.error.CommentMark(0), None),
None,
None,
None,
],
len(stabilization_branches) - 1,
)
job["strategy"]["matrix"]["branch"] = yaml_stabilization_branches

# Commit the changes
message = f"Create the new version '{version}'" if version else "Update the supported versions"
if os.path.exists(".pre-commit-config.yaml"):
subprocess.run(["pre-commit", "run", "--color=never", "--all-files"], check=False)
subprocess.run(["git", "add", "--all"], check=True)
subprocess.run(["git", "commit", f"--message={message}"], check=True)

# Push it
subprocess.run(
[
"git",
"push",
*(["--force"] if arguments.force else []),
repo.get("remote", "origin"),
branch_name,
],
check=True,
)

# Create a pull request
url_proc = mra.gh(
"pr",
"create",
f"--title={message}",
"--body=",
"--head=new_version",
f"--base={repo.get('master_branch', 'master')}",
)

# Go back to your old branch
subprocess.run(["git", "checkout", old_branch_name, "--"], check=True)

url = url_proc.stdout.strip()
if url_proc.returncode != 0 or not url:
mra.gh("browse")
else:
subprocess.run([mra.get_browser(), url], check=True)


if __name__ == "__main__":
main()
56 changes: 55 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ c2cciutils-checks = "c2cciutils.scripts.env:main"
c2cciutils-pull-request-checks = "c2cciutils.scripts.pr_checks:main"
c2cciutils-audit = "c2cciutils.scripts.audit:main"
c2cciutils-publish = "c2cciutils.scripts.publish:main"
c2cciutils-version = "c2cciutils.scripts.version:main"
c2cciutils-clean = "c2cciutils.scripts.clean:main"
c2cciutils-google-calendar = "c2cciutils.publish:main_calendar"
c2cciutils-k8s-install = "c2cciutils.scripts.k8s.install:main"
Expand Down Expand Up @@ -88,13 +89,15 @@ toml = "0.10.2"
debian-inspector = "31.0.0"
PyYAML = "6.0"
tomlkit = { version = "0.11.8", optional = true }
multi-repo-automation = { version="0.3.1", optional = true }

[tool.poetry.extras]
audit = []
checks = []
publish = ["twine", "google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib", "tomlkit"]
publish_plugins = []
pr_checks = ["codespell"]
version = ["multi-repo-automation"]

[tool.poetry.dev-dependencies]
# pylint = "2.15.6"
Expand Down

0 comments on commit a58f11d

Please sign in to comment.