Skip to content

Commit 9a7eee6

Browse files
authored
KAFKA-16934: Clean up and refactor release.py (apache#16287)
The current release script has a couple of issues: * It's a single long file with duplicated logic, which makes it difficult to understand and make changes * When a command fails, the user is forced to start from the beginning, expanding feedback loops. e.g. publishing step fails because the credentials were set incorrectly in ~/.gradle/gradle.properties Reviewers: Mickael Maison <[email protected]>
1 parent baa0fc9 commit 9a7eee6

13 files changed

+1520
-962
lines changed

release.py

-845
This file was deleted.

release/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Releasing Apache Kafka
2+
======================
3+
4+
This directory contains the tools used to publish a release.
5+
6+
# Requirements
7+
8+
* python 3.12
9+
* git
10+
* gpg 2.4
11+
* sftp
12+
13+
The full instructions for producing a release are available in
14+
https://cwiki.apache.org/confluence/display/KAFKA/Release+Process.
15+
16+
17+
# Setup
18+
19+
Create a virtualenv for python, activate it and install dependencies:
20+
21+
```
22+
python3 -m venv .venv
23+
source .venv/bin/activate
24+
pip install -r requirements.txt
25+
```
26+
27+
# Usage
28+
29+
To start a release, first activate the virutalenv, and then run
30+
the release script.
31+
32+
```
33+
source .venv/bin/activate
34+
```
35+
36+
You'll need to setup `PUSH_REMOTE_NAME` to refer to
37+
the git remote for `apache/kafka`.
38+
39+
```
40+
export PUSH_REMOTE_NAME=<value>
41+
```
42+
43+
It should be the value shown with this command:
44+
45+
```
46+
git remote -v | grep -w 'github.com' | grep -w 'apache/kafka' | grep -w '(push)' | awk '{print $1}'
47+
```
48+
49+
Then start the release script:
50+
51+
```
52+
python release.py
53+
```
54+

release/git.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
"""
19+
Auxiliary function to interact with git.
20+
"""
21+
22+
import os
23+
24+
from runtime import repo_dir, execute, cmd
25+
26+
push_remote_name = os.environ.get("PUSH_REMOTE_NAME", "apache-github")
27+
28+
29+
def __defaults(kwargs):
30+
if "cwd" not in kwargs:
31+
kwargs["cwd"] = repo_dir
32+
33+
34+
def has_staged_changes(**kwargs):
35+
__defaults(kwargs)
36+
execute("git diff --cached --exit-code --quiet", **kwargs)
37+
38+
39+
def has_unstaged_changes(**kwargs):
40+
__defaults(kwargs)
41+
execute("git diff --exit-code --quiet", **kwargs)
42+
43+
44+
def fetch_tags(remote=push_remote_name, **kwargs):
45+
__defaults(kwargs)
46+
cmd(f"Fetching tags from {remote}", f"git fetch --tags {remote}", **kwargs)
47+
48+
49+
def tags(**kwargs):
50+
__defaults(kwargs)
51+
return execute("git tag", **kwargs).split()
52+
53+
54+
def tag_exists(tag, **kwargs):
55+
__defaults(kwargs)
56+
return tag in tags(**kwargs)
57+
58+
59+
def delete_tag(tag, **kwargs):
60+
__defaults(kwargs)
61+
if tag_exists(tag, **kwargs):
62+
execute(f"git tag -d {tag}", **kwargs)
63+
64+
65+
def current_branch(**kwargs):
66+
__defaults(kwargs)
67+
return execute("git rev-parse --abbrev-ref HEAD", **kwargs)
68+
69+
70+
def reset_hard_head(**kwargs):
71+
__defaults(kwargs)
72+
cmd("Resetting branch", "git reset --hard HEAD", **kwargs)
73+
74+
75+
def contributors(from_rev, to_rev, **kwargs):
76+
__defaults(kwargs)
77+
kwargs["shell"] = True
78+
line = "git shortlog -sn --group=author --group=trailer:co-authored-by"
79+
line += f" --group=trailer:Reviewers --no-merges {from_rev}..{to_rev}"
80+
line += " | cut -f2 | sort --ignore-case | uniq"
81+
return [str(x) for x in filter(None, execute(line, **kwargs).split('\n'))]
82+
83+
def branches(**kwargs):
84+
output = execute('git branch')
85+
return [line.replace('*', ' ').strip() for line in output.splitlines()]
86+
87+
88+
def branch_exists(branch, **kwargs):
89+
__defaults(kwargs)
90+
return branch in branches(**kwargs)
91+
92+
93+
def delete_branch(branch, **kwargs):
94+
__defaults(kwargs)
95+
if branch_exists(branch, **kwargs):
96+
cmd(f"Deleting git branch {branch}", f"git branch -D {branch}", **kwargs)
97+
98+
99+
def switch_branch(branch, **kwargs):
100+
__defaults(kwargs)
101+
execute(f"git checkout {branch}", **kwargs)
102+
103+
104+
def create_branch(branch, ref, **kwargs):
105+
__defaults(kwargs)
106+
cmd(f"Creating git branch {branch} to track {ref}", f"git checkout -b {branch} {ref}", **kwargs)
107+
108+
109+
def clone(url, target, **kwargs):
110+
__defaults(kwargs)
111+
execute(f"git clone {url} {target}", **kwargs)
112+
113+
114+
def targz(rev, prefix, target, **kwargs):
115+
__defaults(kwargs)
116+
line = "git archive --format tar.gz"
117+
line += f" --prefix {prefix} --output {target} {rev}"
118+
cmd(f"Creating targz {target} from git rev {rev}", line, **kwargs)
119+
120+
121+
def commit(message, **kwargs):
122+
__defaults(kwargs)
123+
cmd("Committing git changes", ["git", "commit", "-a", "-m", message], **kwargs)
124+
125+
126+
def create_tag(tag, **kwargs):
127+
__defaults(kwargs)
128+
cmd(f"Creating git tag {tag}", ["git", "tag", "-a", tag, "-m", tag], **kwargs)
129+
130+
131+
def push_tag(tag, remote=push_remote_name, **kwargs):
132+
__defaults(kwargs)
133+
cmd("Pushing tag {tag} to {remote}", f"git push {remote} {tag}")
134+
135+

release/gpg.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
"""
19+
Auxiliary functions to interact with GNU Privacy Guard (GPG).
20+
"""
21+
22+
import hashlib
23+
import subprocess
24+
import tempfile
25+
26+
from runtime import execute
27+
28+
29+
def key_exists(key_id):
30+
"""
31+
Checks whether the specified GPG key exists locally.
32+
"""
33+
try:
34+
execute(f"gpg --list-keys {key_id}")
35+
except Exception as e:
36+
return False
37+
return True
38+
39+
40+
def agent_kill():
41+
"""
42+
Tries to kill the GPG agent process.
43+
"""
44+
try:
45+
execute("gpgconf --kill gpg-agent")
46+
except FileNotFoundError as e:
47+
if e.filename != 'gpgconf':
48+
raise e
49+
50+
51+
def sign(key_id, passphrase, content, target):
52+
"""
53+
Generates a GPG signature, using the given key and passphrase,
54+
of the specified content into the target path.
55+
"""
56+
execute(f"gpg --passphrase-fd 0 -u {key_id} --armor --output {target} --detach-sig {content}", input=passphrase.encode())
57+
58+
59+
def verify(content, signature):
60+
"""
61+
Verify the given GPG signature for the specified content.
62+
"""
63+
execute(f"gpg --verify {signature} {content}")
64+
65+
66+
def valid_passphrase(key_id, passphrase):
67+
"""
68+
Checks whether the given passphrase is workable for the given key.
69+
"""
70+
with tempfile.TemporaryDirectory() as tmpdir:
71+
content = __file__
72+
signature = tmpdir + '/sig.asc'
73+
# if the agent is running, the suplied passphrase may be ignored
74+
agent_kill()
75+
try:
76+
sign(key_id, passphrase, content, signature)
77+
verify(content, signature)
78+
except subprocess.CalledProcessError as e:
79+
False
80+
return True
81+
82+
83+
def key_pass_id(key_id, passphrase):
84+
"""
85+
Generates a deterministic identifier for the key and passphrase combination.
86+
"""
87+
h = hashlib.sha512()
88+
h.update(key_id.encode())
89+
h.update(passphrase.encode())
90+
return h.hexdigest()
91+
92+

0 commit comments

Comments
 (0)