Skip to content

Commit 8d829df

Browse files
committed
CI: Release via github-actions
1 parent 62eac44 commit 8d829df

7 files changed

Lines changed: 766 additions & 78 deletions

File tree

.github/utils/_repo.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env python
2+
from __future__ import annotations
3+
4+
import os
5+
import re
6+
import shlex
7+
from subprocess import PIPE, Popen
8+
from typing import Literal, Sequence, TypeAlias
9+
10+
ReleaseType: TypeAlias = Literal[
11+
"alpha",
12+
"beta",
13+
"candidate",
14+
"development",
15+
"stable",
16+
]
17+
18+
pre_release_lookup: dict[str, ReleaseType] = {
19+
"a": "alpha",
20+
"alpha": "alpha",
21+
"b": "beta",
22+
"beta": "beta",
23+
"rc": "candidate",
24+
"dev": "development",
25+
".dev": "development",
26+
}
27+
28+
# https://docs.github.com/en/actions/learn-github-actions/variables
29+
# #default-environment-variables
30+
GITHUB_VARS = [
31+
"GITHUB_REF_NAME", # main, dev, v0.1.0, v0.1.3a1
32+
"GITHUB_REF_TYPE", # "branch" or "tag"
33+
"GITHUB_REPOSITORY", # has2k1/scikit-misc
34+
"GITHUB_SERVER_URL", # https://github.com
35+
"GITHUB_SHA", # commit shasum
36+
"GITHUB_WORKSPACE", # /home/runner/work/scikit-misc/scikit-misc
37+
"GITHUB_EVENT_NAME", # push, schedule, workflow_dispatch, ...
38+
]
39+
40+
41+
count = r"(?:[0-9]|[1-9][0-9]+)"
42+
DESCRIBE = re.compile(
43+
r"^v"
44+
rf"(?P<version>{count}\.{count}\.{count})"
45+
rf"((?P<pre>a|b|rc|alpha|beta|\.dev){count})?"
46+
r"(-(?P<commits>\d+)-g(?P<hash>[a-z0-9]+))?"
47+
r"(?P<dirty>-dirty)?"
48+
r"$"
49+
)
50+
51+
# Define a stable release version to be valid according to PEP440
52+
# and is a semver
53+
STABLE_TAG = re.compile(r"^v" rf"{count}\.{count}\.{count}" r"$")
54+
55+
# Prerelease version
56+
PRE_RELEASE_TAG = re.compile(
57+
r"^v"
58+
rf"{count}\.{count}\.{count}"
59+
rf"((?P<pre>a|b|rc|alpha|beta|\.dev){count})?"
60+
r"$"
61+
)
62+
63+
REF_NAME = os.environ.get("GITHUB_REF_NAME", "")
64+
REF_TYPE = os.environ.get("GITHUB_REF_TYPE", "")
65+
66+
67+
def run(cmd: str | Sequence[str]) -> str:
68+
if isinstance(cmd, str) and os.name == "posix":
69+
cmd = shlex.split(cmd)
70+
with Popen(
71+
cmd, stdin=PIPE, stderr=PIPE, stdout=PIPE, text=True, encoding="utf-8"
72+
) as p:
73+
stdout, _ = p.communicate()
74+
return stdout.strip()
75+
76+
77+
class Git:
78+
@staticmethod
79+
def checkout(committish):
80+
"""
81+
Return True if inside a git repo
82+
"""
83+
res = run(f"git checkout {committish}")
84+
return res
85+
86+
@staticmethod
87+
def commit_titles(n=1) -> list[str]:
88+
"""
89+
Return a list n of commit titles
90+
"""
91+
output = run(
92+
f"git log --oneline --no-merges --pretty='format:%s' -{n}"
93+
)
94+
return output.split("\n")[:n]
95+
96+
@staticmethod
97+
def commit_messages(n=1) -> list[str]:
98+
"""
99+
Return a list n of commit messages
100+
"""
101+
sep = "______ MESSAGE _____"
102+
output = run(
103+
f"git log --no-merges --pretty='format:%B{sep}' -{n}"
104+
).strip()
105+
if output.endswith(sep):
106+
output = output[: -len(sep)]
107+
return output.split(sep)[:n]
108+
109+
@staticmethod
110+
def commit_title() -> str:
111+
"""
112+
Commit subject
113+
"""
114+
return Git.commit_titles(1)[0]
115+
116+
@staticmethod
117+
def commit_message() -> str:
118+
"""
119+
Commit title
120+
"""
121+
return Git.commit_messages(1)[0]
122+
123+
@staticmethod
124+
def is_repo():
125+
"""
126+
Return True if inside a git repo
127+
"""
128+
res = run("git rev-parse --is-inside-work-tree")
129+
return res == "return"
130+
131+
@staticmethod
132+
def fetch_tags() -> str:
133+
"""
134+
Fetch all tags
135+
"""
136+
return run("git fetch --tags --force")
137+
138+
@staticmethod
139+
def is_shallow() -> bool:
140+
"""
141+
Return True if current repo is shallow
142+
"""
143+
res = run("git rev-parse --is-shallow-repository")
144+
return res == "true"
145+
146+
@staticmethod
147+
def deepen(n: int = 1) -> str:
148+
"""
149+
Fetch n commits beyond the shallow limit
150+
"""
151+
return run(f"git fetch --deepen={n}")
152+
153+
@staticmethod
154+
def describe() -> str:
155+
"""
156+
Git describe to determine version
157+
"""
158+
return run("git describe --dirty --tags --long --match '*[0-9]*'")
159+
160+
@staticmethod
161+
def can_describe() -> bool:
162+
"""
163+
Return True if repo can be "described" from a semver tag
164+
"""
165+
return bool(DESCRIBE.match(Git.describe()))
166+
167+
@staticmethod
168+
def get_tag_at_commit(committish: str) -> str:
169+
"""
170+
Get tag of a given commit
171+
"""
172+
return run(f"git describe --exact-match {committish}")
173+
174+
@staticmethod
175+
def tag_message(tag: str) -> str:
176+
"""
177+
Get the message of a tag
178+
"""
179+
return run(f"git tag -l --format='%(subject)' {tag}")
180+
181+
@staticmethod
182+
def is_annotated(tag: str) -> bool:
183+
"""
184+
Return true if tag is annotated tag
185+
"""
186+
# LHS prints to stderr and returns nothing when
187+
# tag is an empty string
188+
return run(f"git cat-file -t {tag}") == "tag"
189+
190+
@staticmethod
191+
def shallow_checkout(branch: str, url: str, depth: int = 1) -> str:
192+
"""
193+
Shallow clone upto n commits
194+
"""
195+
_branch = f"--branch={branch}"
196+
_depth = f"--depth={depth}"
197+
return run(f"git clone {_depth} {_branch} {url} .")
198+
199+
@staticmethod
200+
def is_stable_release():
201+
"""
202+
Return True if event is a stable release
203+
"""
204+
return REF_TYPE == "tag" and bool(STABLE_TAG.match(REF_NAME))
205+
206+
@staticmethod
207+
def is_pre_release():
208+
"""
209+
Return True if event is any kind of pre-release
210+
"""
211+
return REF_TYPE == "tag" and bool(PRE_RELEASE_TAG.match(REF_NAME))
212+
213+
@staticmethod
214+
def release_type() -> ReleaseType | None:
215+
if Git.is_stable_release():
216+
return "stable"
217+
elif Git.is_pre_release():
218+
match = PRE_RELEASE_TAG.match(REF_NAME)
219+
assert match is not None
220+
pre = match.group("pre")
221+
return pre_release_lookup[pre]
222+
223+
@staticmethod
224+
def branch():
225+
"""
226+
Return event branch
227+
"""
228+
return REF_NAME if REF_TYPE == "branch" else ""

.github/utils/please.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
from typing import Callable, TypeAlias
5+
6+
from _repo import Git
7+
8+
Ask: TypeAlias = Callable[[], bool | str]
9+
Do: TypeAlias = Callable[[], str]
10+
11+
gh_output_file = os.environ.get("GITHUB_OUTPUT")
12+
13+
14+
def set_deploy_to():
15+
"""
16+
Write where to deploy to deploy_on in the GITHUB_OUTPUT env
17+
"""
18+
if not gh_output_file:
19+
return
20+
21+
if Git.is_stable_release():
22+
deploy_to = "website"
23+
elif Git.is_pre_release():
24+
deploy_to = "pre-website"
25+
elif Git.branch() in {"main", "dev"}:
26+
deploy_to = "gh-pages"
27+
else:
28+
deploy_to = ""
29+
30+
with Path(gh_output_file).open("a") as f:
31+
print(f"deploy_to={deploy_to}", file=f)
32+
33+
34+
def set_publish_on():
35+
"""
36+
Write index (pypi or testpypi) to publish_on in the GITHUB_OUTPUT env
37+
38+
i.e. Where to release
39+
"""
40+
# Probably not on GHA
41+
if not gh_output_file:
42+
return
43+
44+
rtype = Git.release_type()
45+
46+
if rtype in {"stable", "alpha", "beta", "development"}:
47+
publish_on = "pypi"
48+
elif rtype == "candidate":
49+
publish_on = "testpypi"
50+
else:
51+
publish_on = ""
52+
53+
with Path(gh_output_file).open("a") as f:
54+
print(f"publish_on={publish_on}", file=f)
55+
56+
57+
def set_commit_title():
58+
"""
59+
Write the commit title to commit_title in the GITHUB_OUTPUT env
60+
"""
61+
if not gh_output_file:
62+
return
63+
64+
with Path(gh_output_file).open("a") as f:
65+
print(f"commit_title={Git.commit_title()}", file=f)
66+
67+
68+
def process_request(task_name: str) -> str | None:
69+
if task_name in TASKS:
70+
return TASKS[task_name]()
71+
72+
73+
TASKS: dict[str, Callable[[], str | None]] = {
74+
"set_deploy_to": set_deploy_to,
75+
"set_publish_on": set_publish_on,
76+
"set_commit_title": set_commit_title,
77+
}
78+
79+
if __name__ == "__main__":
80+
if len(sys.argv) == 2:
81+
arg = sys.argv[1]
82+
output = process_request(arg)
83+
if output:
84+
print(output)

0 commit comments

Comments
 (0)