Skip to content

Commit baa219b

Browse files
committed
feat(pypub): add option to create a manual release on GitHub
1 parent 763f8f7 commit baa219b

1 file changed

Lines changed: 112 additions & 51 deletions

File tree

clit/dev/packaging.py

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Publisher:
2121
TOOL_CONVENTIONAL_CHANGELOG = "conventional-changelog"
2222
TOOL_POETRY = "poetry"
2323
TOOL_GIT = "git"
24+
TOOL_HUB = "hub"
2425
TOOL_TWINE = "twine"
2526
TOOL_CONVENTIONAL_GITHUB_RELEASER = "conventional-github-releaser"
2627

@@ -32,6 +33,7 @@ class Publisher:
3233
),
3334
TOOL_POETRY: "Install from https://github.com/sdispater/poetry#installation",
3435
TOOL_GIT: "Install using your OS package tools",
36+
TOOL_HUB: "Install from https://github.com/github/hub#installation",
3537
TOOL_TWINE: "Install from https://github.com/pypa/twine#installation",
3638
TOOL_CONVENTIONAL_GITHUB_RELEASER: (
3739
"Install from https://github.com/conventional-changelog/releaser-tools/tree"
@@ -47,36 +49,35 @@ class Publisher:
4749
}
4850

4951
# https://github.com/peritus/bumpversion
50-
BUMP_VERSION = TOOL_BUMPVERSION + " {allow_dirty} {part}"
51-
BUMP_VERSION_SIMPLE_CHECK = f"{BUMP_VERSION} --dry-run"
52-
BUMP_VERSION_VERBOSE = f"{BUMP_VERSION_SIMPLE_CHECK} --verbose 2>&1"
53-
BUMP_VERSION_VERBOSE_FILES = f"{BUMP_VERSION_VERBOSE} | grep -i -E -e '^would'"
54-
BUMP_VERSION_GREP = (
55-
f'{BUMP_VERSION_VERBOSE} | grep -i -E -e "would commit to git.+bump" -e "^new version" | grep -E -o "\'(.+)\'"'
56-
)
52+
CMD_BUMP_VERSION = TOOL_BUMPVERSION + " {allow_dirty} {part}"
53+
CMD_BUMP_VERSION_SIMPLE_CHECK = f"{CMD_BUMP_VERSION} --dry-run"
54+
CMD_BUMP_VERSION_VERBOSE = f"{CMD_BUMP_VERSION_SIMPLE_CHECK} --verbose 2>&1"
55+
CMD_BUMP_VERSION_VERBOSE_FILES = f"{CMD_BUMP_VERSION_VERBOSE} | grep -i -E -e '^would'"
56+
CMD_BUMP_VERSION_GREP = f'{CMD_BUMP_VERSION_VERBOSE} | grep -i -E -e "would commit to git.+bump" -e "^new version" | grep -E -o "\'(.+)\'"'
5757

5858
# https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-cli
59-
CHANGELOG = f"{TOOL_CONVENTIONAL_CHANGELOG} -i CHANGELOG.md -p angular"
59+
CMD_CHANGELOG = f"{TOOL_CONVENTIONAL_CHANGELOG} -i CHANGELOG.md -p angular"
6060

61-
BUILD_SETUP_PY = "python setup.py sdist bdist_wheel --universal"
61+
CMD_BUILD_SETUP_PY = "python setup.py sdist bdist_wheel --universal"
6262

6363
# https://poetry.eustace.io/
64-
POETRY_BUILD = f"{TOOL_POETRY} build"
64+
CMD_POETRY_BUILD = f"{TOOL_POETRY} build"
6565

66-
GIT_ADD_AND_COMMIT = TOOL_GIT + " add . && git commit -m'{}' --no-verify"
67-
GIT_PUSH = f"{TOOL_GIT} push"
68-
GIT_TAG = TOOL_GIT + " tag v{}"
66+
CMD_GIT_ADD_AND_COMMIT = TOOL_GIT + " add . && git commit -m'{}' --no-verify"
67+
CMD_GIT_PUSH = f"{TOOL_GIT} push"
68+
CMD_GIT_CHECKOUT_MASTER = f"echo {TOOL_GIT} checkout master && echo {TOOL_GIT} pull"
6969

7070
# https://github.com/pypa/twine
7171
# I tried using "poetry publish -u $TWINE_USERNAME -p $TWINE_PASSWORD"; the command didn't fail,
7272
# but nothing was uploaded
7373
# I also tried setting $TWINE_USERNAME and $TWINE_PASSWORD on the environment,
7474
# but then "twine upload" didn't work for some reason.
75-
TWINE_UPLOAD = TOOL_TWINE + " upload {repo} dist/*"
75+
CMD_TWINE_UPLOAD = TOOL_TWINE + " upload {repo} dist/*"
7676

7777
# https://www.npmjs.com/package/conventional-github-releaser
78-
GITHUB_RELEASE = TOOL_CONVENTIONAL_GITHUB_RELEASER + " -p angular -v --token {}"
79-
GITHUB_RELEASE_ENVVAR = "CONVENTIONAL_GITHUB_RELEASER_TOKEN"
78+
CMD_GITHUB_RELEASE = TOOL_CONVENTIONAL_GITHUB_RELEASER + " -p angular -v --token {}"
79+
CMD_MANUAL_GITHUB_RELEASE = f"echo {TOOL_HUB} browse"
80+
CMD_GITHUB_RELEASE_ENVVAR = "CONVENTIONAL_GITHUB_RELEASER_TOKEN"
8081

8182
def __init__(self, dry_run: bool):
8283
self.dry_run = dry_run
@@ -113,7 +114,7 @@ def github_access_token_option(cls):
113114
"-t",
114115
help=(
115116
f"GitHub access token used by {cls.TOOL_CONVENTIONAL_GITHUB_RELEASER}. If not defined, will use the value"
116-
+ f" from the ${cls.GITHUB_RELEASE_ENVVAR} environment variable"
117+
+ f" from the ${cls.CMD_GITHUB_RELEASE_ENVVAR} environment variable"
117118
),
118119
)
119120

@@ -136,8 +137,8 @@ def check_tools(self, github_access_token: str = None) -> None:
136137
self.github_access_token = github_access_token
137138
else:
138139
error_message = "Missing access token"
139-
if self.GITHUB_RELEASE_ENVVAR in os.environ:
140-
variable = self.GITHUB_RELEASE_ENVVAR
140+
if self.CMD_GITHUB_RELEASE_ENVVAR in os.environ:
141+
variable = self.CMD_GITHUB_RELEASE_ENVVAR
141142
else:
142143
token_keys = {k for k in os.environ.keys() if "github_access_token".casefold() in k.casefold()}
143144
if len(token_keys) == 1:
@@ -152,7 +153,7 @@ def check_tools(self, github_access_token: str = None) -> None:
152153
else:
153154
click.secho(f"{error_message}. ", fg="bright_red", nl=False)
154155
click.echo(
155-
f"Set the variable ${self.GITHUB_RELEASE_ENVVAR} or use"
156+
f"Set the variable ${self.CMD_GITHUB_RELEASE_ENVVAR} or use"
156157
+ " --github-access-token to define a GitHub access token"
157158
)
158159
all_ok = False
@@ -174,27 +175,27 @@ def _bump(cls, base_command: str, part: str, allow_dirty: bool):
174175
def check_bumped_version(self, part: str, allow_dirty: bool) -> Tuple[str, str]:
175176
"""Check the version that will be bumped."""
176177
shell(
177-
self._bump(self.BUMP_VERSION_SIMPLE_CHECK, part, allow_dirty),
178+
self._bump(self.CMD_BUMP_VERSION_SIMPLE_CHECK, part, allow_dirty),
178179
exit_on_failure=True,
179180
header="Check the version that will be bumped",
180181
)
181182

182-
bump_cmd = self._bump(self.BUMP_VERSION_VERBOSE_FILES, part, allow_dirty)
183+
bump_cmd = self._bump(self.CMD_BUMP_VERSION_VERBOSE_FILES, part, allow_dirty)
183184
shell(bump_cmd, dry_run=self.dry_run, header=f"Display what files would be changed", exit_on_failure=True)
184185
if not self.dry_run:
185-
chosen_lines = shell(self._bump(self.BUMP_VERSION_GREP, part, allow_dirty), return_lines=True)
186+
chosen_lines = shell(self._bump(self.CMD_BUMP_VERSION_GREP, part, allow_dirty), return_lines=True)
186187
new_version = chosen_lines[0].strip("'")
187-
commit_message = chosen_lines[1].strip("'")
188+
commit_message = chosen_lines[1].strip("'").lower()
188189
click.echo(f"New version: {new_version}\nCommit message: {commit_message}")
189190
prompt("Were all versions correctly displayed?")
190191
else:
191-
commit_message = "Bump version from X to Y"
192+
commit_message = "bump version from X to Y"
192193
new_version = "<new version here>"
193-
return commit_message, new_version
194+
return f"build: {commit_message}", new_version
194195

195196
def actually_bump_version(self, part: str, allow_dirty: bool) -> None:
196197
"""Actually bump the version."""
197-
shell(self._bump(self.BUMP_VERSION, part, allow_dirty), dry_run=self.dry_run, header=f"Bump versions")
198+
shell(self._bump(self.CMD_BUMP_VERSION, part, allow_dirty), dry_run=self.dry_run, header=f"Bump versions")
198199

199200
def recreate_setup_py(self, ctx) -> None:
200201
"""Recreate the setup.py if it exists."""
@@ -206,14 +207,16 @@ def recreate_setup_py(self, ctx) -> None:
206207

207208
def generate_changelog(self) -> None:
208209
"""Generate the changelog."""
209-
shell(f"{Publisher.CHANGELOG} -s", dry_run=self.dry_run, header="Generate the changelog")
210+
shell(f"{Publisher.CMD_CHANGELOG} -s", dry_run=self.dry_run, header="Generate the changelog")
210211

211212
def build_with_poetry(self) -> None:
212213
"""Build the project with poetry."""
213214
if not self.dry_run:
214215
remove_previous_builds()
215216

216-
shell(Publisher.POETRY_BUILD, dry_run=self.dry_run, header=f"Build the project with {Publisher.TOOL_POETRY}")
217+
shell(
218+
Publisher.CMD_POETRY_BUILD, dry_run=self.dry_run, header=f"Build the project with {Publisher.TOOL_POETRY}"
219+
)
217220

218221
if not self.dry_run:
219222
shell("ls -l dist")
@@ -235,28 +238,60 @@ def show_diff(self) -> None:
235238
)
236239

237240
@classmethod
238-
def commit_push_tag(cls, commit_message: str, new_version: str) -> List[HeaderCommand]:
241+
def commit_push_tag(cls, commit_message: str, new_version: str, manual_release: bool) -> List[HeaderCommand]:
239242
"""Prepare the commands to commit, push and tag."""
240-
return [
241-
("Add all files and commit (skipping hooks)", Publisher.GIT_ADD_AND_COMMIT.format(commit_message)),
242-
("Push", Publisher.GIT_PUSH),
243-
(
244-
f"Create the tag but don't push it yet ({Publisher.TOOL_CONVENTIONAL_GITHUB_RELEASER} will do that)",
245-
Publisher.GIT_TAG.format(new_version),
246-
),
243+
commands = [
244+
("Add all files and commit (skipping hooks)", Publisher.CMD_GIT_ADD_AND_COMMIT.format(commit_message)),
245+
("Push", Publisher.CMD_GIT_PUSH),
247246
]
247+
if manual_release:
248+
commands.extend(
249+
[
250+
(
251+
"Approve the pull request on GitHub, then return here and run the following commands",
252+
Publisher.CMD_GIT_CHECKOUT_MASTER,
253+
),
254+
("Create the tag manually", cls.cmd_tag(new_version, echo=True)),
255+
("Push the tags manually", cls.cmd_push_tags()),
256+
]
257+
)
258+
else:
259+
commands.append(
260+
(
261+
f"Create the tag but don't push it yet ({Publisher.TOOL_CONVENTIONAL_GITHUB_RELEASER} will do that)",
262+
cls.cmd_tag(new_version),
263+
)
264+
)
265+
return commands
266+
267+
@classmethod
268+
def cmd_tag(cls, version: str, echo=False) -> str:
269+
"""Command to create a Git tag."""
270+
return f"{'echo ' if echo else ''}{cls.TOOL_GIT} tag v{version}"
271+
272+
@classmethod
273+
def cmd_push_tags(cls) -> str:
274+
"""Command to push tags."""
275+
return f"echo {cls.TOOL_GIT} push --tags"
248276

249277
@classmethod
250278
def upload_pypi(cls) -> List[HeaderCommand]:
251279
"""Prepare commands to upload to PyPI."""
252280
return [
253-
("Test upload the files to TestPyPI via Twine", Publisher.TWINE_UPLOAD.format(repo="-r testpypi")),
254-
("Upload the files to PyPI via Twine", Publisher.TWINE_UPLOAD.format(repo="")),
281+
("Test upload the files to TestPyPI via Twine", Publisher.CMD_TWINE_UPLOAD.format(repo="-r testpypi")),
282+
("Upload the files to PyPI via Twine", Publisher.CMD_TWINE_UPLOAD.format(repo="")),
255283
]
256284

257-
def release(self) -> List[HeaderCommand]:
285+
def release(self, manual_release) -> List[HeaderCommand]:
258286
"""Prepare release commands."""
259-
return [("Create a GitHub release", Publisher.GITHUB_RELEASE.format(self.github_access_token))]
287+
if manual_release:
288+
return [
289+
(
290+
"Open GitHub and create a GitHub release manually, copying the content from CHANGELOG.md",
291+
Publisher.CMD_MANUAL_GITHUB_RELEASE,
292+
)
293+
]
294+
return [("Create a GitHub release", Publisher.CMD_GITHUB_RELEASE.format(self.github_access_token))]
260295

261296
def run_commands(self, commands: List[HeaderCommand]):
262297
"""Run a list of commands."""
@@ -273,7 +308,15 @@ def success(self, new_version: str, upload_destination: str):
273308
return
274309
click.secho(f"The new version {new_version} was uploaded to {upload_destination}! ✨ 🍰 ✨", fg="bright_white")
275310

276-
def publish(self, pypi: bool, ctx, part: str, allow_dirty: bool, github_access_token: str = None):
311+
def publish(
312+
self,
313+
pypi: bool,
314+
ctx,
315+
part: str,
316+
allow_dirty: bool,
317+
github_access_token: str = None,
318+
manual_release: bool = False,
319+
):
277320
"""Publish a package."""
278321
self.check_tools(github_access_token)
279322
commit_message, new_version = self.check_bumped_version(part, allow_dirty)
@@ -283,10 +326,10 @@ def publish(self, pypi: bool, ctx, part: str, allow_dirty: bool, github_access_t
283326
self.build_with_poetry()
284327
self.show_diff()
285328

286-
commands = self.commit_push_tag(commit_message, new_version)
329+
commands = self.commit_push_tag(commit_message, new_version, manual_release)
287330
if pypi:
288331
commands.extend(self.upload_pypi())
289-
commands.extend(self.release())
332+
commands.extend(self.release(manual_release))
290333

291334
self.run_commands(commands)
292335
self.success(new_version, "PyPI" if pypi else "GitHub")
@@ -313,9 +356,10 @@ def pypub():
313356

314357

315358
@pypub.command()
316-
def check():
359+
@Publisher.github_access_token_option()
360+
def check(github_access_token: str = None):
317361
"""Check if all needed tools and files are present."""
318-
Publisher(False).check_tools()
362+
Publisher(False).check_tools(github_access_token)
319363

320364

321365
@pypub.command()
@@ -349,16 +393,26 @@ def pypi(ctx, dry_run: bool, part: str, allow_dirty: bool, github_access_token:
349393
@Publisher.part_option()
350394
@Publisher.allow_dirty_option()
351395
@Publisher.github_access_token_option()
396+
@click.option(
397+
"--manual-release",
398+
"-r",
399+
default=False,
400+
is_flag=True,
401+
type=bool,
402+
help=f"Run commands up until tagging. Tag, merge, create the release: all have to be done manually",
403+
)
352404
@click.pass_context
353-
def github(ctx, dry_run: bool, part: str, allow_dirty: bool, github_access_token: str = None):
405+
def github(
406+
ctx, dry_run: bool, part: str, allow_dirty: bool, github_access_token: str = None, manual_release: bool = False
407+
):
354408
"""Release to GitHub only (bump version, changelog, package, upload)."""
355-
Publisher(dry_run).publish(False, ctx, part, allow_dirty, github_access_token)
409+
Publisher(dry_run).publish(False, ctx, part, allow_dirty, github_access_token, manual_release)
356410

357411

358412
@pypub.command()
359413
def changelog():
360414
"""Preview the changelog."""
361-
shell(f"{Publisher.CHANGELOG} -u | less")
415+
shell(f"{Publisher.CMD_CHANGELOG} -u | less")
362416

363417

364418
@click.group()
@@ -381,8 +435,15 @@ def setup_py():
381435
1,
382436
dedent(
383437
'''
384-
"""NOTICE: This file was generated automatically by the command: xpoetry setup-py."""
385-
'''
438+
"""
439+
Setup for this package.
440+
441+
.. note::
442+
443+
This file was generated automatically by ``xpoetry setup-py``.
444+
A ``setup.py`` file is needed to install this project in editable mode (``pip install -e /path/to/project``).
445+
"""
446+
'''
386447
).strip(),
387448
)
388449

0 commit comments

Comments
 (0)