Skip to content

Commit eb5c910

Browse files
authored
Added release tools. (#1)
* Added release tools. * Implement review feedback * Update inline usage comments of the cargo-version.py script
1 parent 92ee85d commit eb5c910

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

release/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Release tools
2+
3+
## Install requirements
4+
5+
Install Python 3 and required packages:
6+
7+
pip install -r release/requirements.txt
8+
9+
Add the `release` folder to your `PATH`.
10+
11+
## Usage
12+
13+
Go to the folder containing a Cargo workspace or crate and run:
14+
15+
release.sh [major|minor|patch]
16+
17+
This pushes two commits in a newly created release branch. When merging, __do not squash them__.
18+
19+

release/cargo-version.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Utility for viewing and managing versions of cargo workspaces and crates.
4+
# For workspaces, it assumes that all crate members use a single shared version.
5+
#
6+
# usage: cargo-version.py [-h] [-p PROJECT] [-r] [-n {major,minor,patch}] [-s SET] [-o]
7+
#
8+
# Change versions of cargo projects.
9+
#
10+
# optional arguments:
11+
# -h, --help show this help message and exit
12+
# -p PROJECT, --project PROJECT
13+
# Project folder
14+
# -r, --release Version
15+
# -n {major,minor,patch}, --next {major,minor,patch}
16+
# Version
17+
# -s SET, --set SET Version
18+
# -o, --show Version
19+
#
20+
21+
import toml
22+
import semver
23+
import argparse
24+
25+
class Crate:
26+
def __init__(self, path, name, version, dependencies):
27+
self.path = path
28+
self.name = name
29+
self.version = version
30+
self.dependencies = dependencies
31+
32+
def with_dependencies(self, names):
33+
deps = {k:v for k,v in self.dependencies.items() if k in names}
34+
return Crate(self.path, self.name, self.version, deps)
35+
36+
@classmethod
37+
def finalize(cls, version):
38+
return str(semver.VersionInfo.parse(version).finalize_version())
39+
40+
@classmethod
41+
def bump_level(cls, version, level):
42+
v = semver.VersionInfo.parse(version)
43+
if level == 'major':
44+
return str(v.bump_major())
45+
elif level == 'minor':
46+
return str(v.bump_minor())
47+
elif level == 'patch':
48+
return str(v.bump_patch())
49+
else:
50+
return str(v.bump_prerelease('nightly'))
51+
52+
def finalize_version(self):
53+
return Crate(self.path, self.name, Crate.finalize(self.version), self.dependencies.copy())
54+
55+
def bump_version(self, level):
56+
return Crate(self.path, self.name, Crate.bump_level(self.version, level), self.dependencies.copy())
57+
58+
def set_version(self, version):
59+
return Crate(self.path, self.name, version, self.dependencies.copy())
60+
61+
def next_version(self):
62+
return Crate(self.path, self.name, str(semver.VersionInfo.parse(self.version).next_version('patch')), self.dependencies.copy())
63+
64+
def show_version(self):
65+
return self.version
66+
67+
def save(self, previous):
68+
contents = []
69+
cargo_file = f"{self.path}/Cargo.toml"
70+
with open(cargo_file, 'r') as r:
71+
for line in r.readlines():
72+
if line.startswith("version"):
73+
line = line.replace(previous.version, self.version)
74+
else:
75+
for dname, dversion in self.dependencies.items():
76+
if line.startswith(dname):
77+
line = line.replace(previous.dependencies[dname], dversion)
78+
contents.append(line)
79+
80+
with open(cargo_file, 'w') as w:
81+
w.write(''.join(contents))
82+
83+
def __str__(self):
84+
return f'Crate({self.path}, {self.name}, {self.version}, {self.dependencies})'
85+
86+
class Workspace:
87+
def __init__(self, crates):
88+
names = set([c.name for c in crates])
89+
self.crates = {c.name: c.with_dependencies(names) for c in crates}
90+
91+
def finalize_version(self):
92+
crates = {c.name: c.finalize_version() for c in self.crates.values()}
93+
return Workspace(Workspace.update_dependencies(crates).values())
94+
95+
def bump_version(self, level):
96+
crates = {c.name: c.bump_version(level) for c in self.crates.values()}
97+
return Workspace(Workspace.update_dependencies(crates).values())
98+
99+
def set_version(self, version):
100+
crates = {c.name: c.set_version(version) for c in self.crates.values()}
101+
return Workspace(Workspace.update_dependencies(crates).values())
102+
103+
def next_version(self):
104+
crates = {c.name: c.next_version() for c in self.crates.values()}
105+
return Workspace(Workspace.update_dependencies(crates).values())
106+
107+
def show_version(self):
108+
for c in self.crates.values():
109+
return c.show_version()
110+
return "0.0.0"
111+
112+
@classmethod
113+
def update_dependencies(cls, crate_dict):
114+
for crate in crate_dict.values():
115+
for dep in crate.dependencies.keys():
116+
crate.dependencies[dep] = crate_dict[dep].version
117+
return crate_dict
118+
119+
def __str__(self):
120+
return f'Workspace({[str(c) for c in self.crates.values()]})'
121+
122+
def save(self, previous):
123+
for cn in self.crates.keys():
124+
self.crates[cn].save(previous.crates[cn])
125+
126+
def load(root):
127+
r = toml.load(f"{root}/Cargo.toml")
128+
if "workspace" in r:
129+
return Workspace([load(f"{root}/{path}") for path in r["workspace"]["members"]])
130+
else:
131+
return Crate(path=root, name=r["package"]["name"], version=r["package"]["version"], dependencies={dn: r["dependencies"][dn]["version"] for dn in r["dependencies"] if "version" in r["dependencies"][dn]})
132+
133+
def parse_args():
134+
parser = argparse.ArgumentParser(description="Change versions of cargo projects.")
135+
parser.add_argument("-p", "--project", help="Project folder", default=".")
136+
parser.add_argument("-r", "--release", help="Version", action="store_true")
137+
parser.add_argument("-n", "--next", help="Version", choices=['major', 'minor', 'patch'])
138+
parser.add_argument("-s", "--set", help="Version" )
139+
parser.add_argument("-o", "--show", help="Version", action="store_true")
140+
return parser.parse_args()
141+
142+
if __name__ == "__main__":
143+
args = parse_args()
144+
145+
old = load(args.project.rstrip('/'))
146+
147+
if args.release:
148+
new = old.finalize_version()
149+
new.save(old)
150+
elif args.next:
151+
new = old.bump_version(args.next).bump_version("prerelease")
152+
new.save(old)
153+
elif args.set:
154+
# sanity check
155+
semver.VersionInfo.parse(args.set)
156+
new = old.set_version(args.set)
157+
new.save(old)
158+
elif args.show:
159+
print(old.show_version())
160+
161+

release/release.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env sh
2+
#
3+
# Usage: release.sh <level>
4+
#
5+
# <level> : "major", "minor" or "patch". Default: "minor".
6+
#
7+
BASE_BRANCH="main"
8+
REPOSITORY="origin"
9+
NOW_UTC=$(date -u '+%Y%m%d%H%M%S')
10+
RELEASE_BRANCH="release-$NOW_UTC"
11+
12+
ensure_release_branch() {
13+
local STATUS=$(git status -s | grep -v '??')
14+
15+
if [ "$STATUS" != "" ]; then
16+
>&2 echo "ERROR Dirty working copy found! Stop."
17+
exit 1
18+
fi
19+
20+
git switch -c ${RELEASE_BRANCH} ${BASE_BRANCH}
21+
git push -u ${REPOSITORY} ${RELEASE_BRANCH}
22+
}
23+
24+
maybe_create_github_pr() {
25+
local TAG=$1
26+
GH_COMMAND=$(which gh)
27+
if [ "$GH_COMMAND" != "" ]; then
28+
gh pr create --base $BASE_BRANCH --head $RELEASE_BRANCH --reviewer "@stackabletech/rust-developers" --title "Release $TAG" --body "Release $TAG"
29+
fi
30+
}
31+
32+
main() {
33+
34+
local NEXT_LEVEL=${1:-minor}
35+
local PUSH=${2:-true}
36+
37+
ensure_release_branch
38+
39+
#
40+
# Release
41+
#
42+
cargo-version.py --release
43+
cargo update --workspace
44+
local TAG=$(cargo-version.py --show)
45+
git commit -am "bump version $TAG"
46+
git tag -a $TAG -m "release $TAG" HEAD
47+
48+
#
49+
# Development
50+
#
51+
cargo-version.py --next ${NEXT_LEVEL}
52+
cargo update --workspace
53+
TAG=$(cargo-version.py --show)
54+
git commit -am "bump version $TAG"
55+
56+
if [ "$PUSH" = "true" ]; then
57+
git push ${REPOSITORY} ${RELEASE_BRANCH}
58+
git push --tags
59+
maybe_create_github_pr $TAG_NAME
60+
fi
61+
}
62+
63+
main $@

release/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
semver==2.13.0
2+
toml==0.10.2

0 commit comments

Comments
 (0)