Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tool to automatically create a new release #1085

Merged
merged 1 commit into from
Jun 28, 2023
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: 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