Skip to content

Commit a530e5a

Browse files
authored
Merge pull request #446 from rvermeulen/rvermeulen/fix-release-checksums-generation
Fix release checksums.txt artifact generation
2 parents 8f8f48f + 43ade57 commit a530e5a

File tree

7 files changed

+113
-27
lines changed

7 files changed

+113
-27
lines changed

.github/workflows/tooling-unit-tests.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,22 @@ jobs:
9696
- name: Run PyTest
9797
run: |
9898
pytest scripts/guideline_recategorization/recategorize_test.py
99+
100+
release-tests:
101+
name: Run release tests
102+
runs-on: ubuntu-22.04
103+
steps:
104+
- name: Checkout
105+
uses: actions/checkout@v2
106+
107+
- name: Install Python
108+
uses: actions/setup-python@v4
109+
with:
110+
python-version: "3.9"
111+
112+
- name: Install Python dependencies
113+
run: pip install -r scripts/release/requirements.txt
114+
115+
- name: Run PyTest
116+
run: |
117+
pytest scripts/release/update_release_assets_test.py

.github/workflows/update-release.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,12 @@ jobs:
5454
GITHUB_TOKEN: ${{ github.token }}
5555
RELEASE_ENGINEERING_TOKEN: ${{ steps.generate-token.outputs.token }}
5656
run: |
57-
python scripts/release/update-release-assets.py \
57+
python scripts/release/update_release_assets.py \
5858
--head-sha $HEAD_SHA \
5959
--layout scripts/release/release-layout.yml \
6060
--repo "$GITHUB_REPOSITORY" \
6161
--github-token "$GITHUB_REPOSITORY:$GITHUB_TOKEN" "github/codeql-coding-standards-release-engineering:$RELEASE_ENGINEERING_TOKEN" \
62-
--skip-checkrun "release-status" \
63-
--skip-checks
62+
--skip-checkrun "release-status"
6463
6564
- name: Update release notes
6665
env:

scripts/release/release-layout.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ layout:
2020
- file: docs/user_manual.md
2121
checksums.txt:
2222
- shell: |
23-
sha256sum ./* > checksums.txt
23+
sha256sum ${{ layout.root }}/* > checksums.txt
24+
# Remove the layout root from the paths in checksums.txt
25+
sed -i -e "s|${{ layout.root }}/||g" checksums.txt

scripts/release/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
semantic-version==2.10.0
22
PyGithub==1.59.1
33
PyYAML==6.0.1
4-
GitPython==3.1.36
4+
GitPython==3.1.36
5+
pytest==7.4.3
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: 0.1.0
2+
3+
layout:
4+
hello-world.txt:
5+
- shell: |
6+
echo "hello world!" > hello-world.txt
7+
hello-world.zip:
8+
- shell: |
9+
echo "hello!" > hello.txt
10+
echo "world!" > world.txt
11+
# reset the creation and modification times to a fixed value
12+
touch -a -m -t 197001010000.00 hello.txt world.txt
13+
checksums.txt:
14+
- shell: |
15+
shasum -a 256 ${{ layout.root }}/* > checksums.txt
16+
# Remove the layout root from the checksums.txt
17+
# We don't use inplace because of BSD vs GNU shenanigans
18+
sed -e "s|${{ layout.root }}/||g" checksums.txt > checksums-rewritten.txt
19+
mv checksums-rewritten.txt checksums.txt

scripts/release/update-release-assets.py renamed to scripts/release/update_release_assets.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations # This enables postponed evaluation of type annotations. Required for typing.TYPE_CHECKING. See https://peps.python.org/pep-0563/
2-
from typing import TYPE_CHECKING, List, Union, cast, Dict, Any
2+
from typing import TYPE_CHECKING, List, Union, cast, Dict, Any, TypeVar, Callable, Sequence, Optional
33
import shutil
44
from tempfile import TemporaryDirectory
55
import subprocess
@@ -12,7 +12,7 @@
1212

1313
if TYPE_CHECKING:
1414
from github import WorkflowRun, Repository
15-
15+
1616

1717
script_path = Path(__file__).resolve()
1818
root_path = script_path.parent.parent.parent
@@ -30,7 +30,7 @@ def get_check_runs(self: Repository.Repository, ref: str, **kwargs: str) -> Pagi
3030
f"{self.url}/commits/{ref}/check-runs",
3131
firstParams=None,
3232
list_item="check_runs")
33-
33+
3434
Repository.Repository = MyRepository
3535

3636
from github import WorkflowRun, Artifact
@@ -51,7 +51,7 @@ def download_logs(self, path: Path) -> None:
5151
if self._requester._Requester__auth is not None: # type: ignore
5252
headers["Authorization"] = f"{self._requester._Requester__auth.token_type} {self._requester._Requester__auth.token}" # type: ignore
5353
headers["User-Agent"] = self._requester._Requester__userAgent # type: ignore
54-
54+
5555
resp = requests.get(url, headers=headers, allow_redirects=True)
5656

5757
if resp.status_code != 200:
@@ -70,7 +70,7 @@ def download_artifacts(self, path: Path) -> None:
7070
if self._requester._Requester__auth is not None: # type: ignore
7171
headers["Authorization"] = f"{self._requester._Requester__auth.token_type} {self._requester._Requester__auth.token}" # type: ignore
7272
headers["User-Agent"] = self._requester._Requester__userAgent # type: ignore
73-
73+
7474
resp = requests.get(artifact.archive_download_url, headers=headers, allow_redirects=True)
7575

7676
if resp.status_code != 200:
@@ -93,15 +93,15 @@ def download_artifact(self, name: str, path: Path) -> None:
9393
if self._requester._Requester__auth is not None: # type: ignore
9494
headers["Authorization"] = f"{self._requester._Requester__auth.token_type} {self._requester._Requester__auth.token}" # type: ignore
9595
headers["User-Agent"] = self._requester._Requester__userAgent # type: ignore
96-
96+
9797
resp = requests.get(artifact.archive_download_url, headers=headers, allow_redirects=True)
9898

9999
if resp.status_code != 200:
100100
raise Exception(f"Unable to download artifact ${artifact.name}. Received status code {resp.status_code} {resp.reason}")
101101

102102
with (path / f"{artifact.name}.zip").open("wb") as f:
103103
f.write(resp.content)
104-
104+
105105

106106
WorkflowRun.WorkflowRun = MyWorkflowRun
107107

@@ -124,12 +124,16 @@ def make(self, directory: Path, workflow_runs: List[WorkflowRun.WorkflowRun]) ->
124124
elif action_type == "workflow-artifact":
125125
actions.append(WorkflowArtifactAction(workflow_runs, **cast(Dict[str, Any], action_args)))
126126
elif action_type == "shell":
127-
actions.append(ShellAction(action_args))
127+
modifiers : List[Callable[[str], str]] = [
128+
lambda cmd: re.sub(pattern=r"\${{\s*coding-standards\.root\s*}}", repl=str(root_path), string=cmd),
129+
lambda cmd: re.sub(pattern=r"\${{\s*layout\.root\s*}}", repl=str(directory), string=cmd)
130+
]
131+
actions.append(ShellAction(action_args, modifiers=modifiers))
128132
elif action_type == "file":
129133
actions.append(FileAction(action_args))
130134
else:
131135
raise Exception(f"Unknown action type {action_type}")
132-
136+
133137
artifacts.append(ReleaseArtifact(artifact, actions, self.skip_checks))
134138

135139
for artifact in artifacts:
@@ -153,7 +157,7 @@ def run(self) -> List[Path]:
153157
print(f"Downloading logs for {workflow_run.name}")
154158
workflow_run.download_logs(Path(self.temp_workdir.name)) # type: ignore
155159
return list(map(Path, Path(self.temp_workdir.name).glob("**/*")))
156-
160+
157161
class WorkflowArtifactAction():
158162

159163
def __init__(self, workflow_runs: List[WorkflowRun.WorkflowRun], **kwargs: str) -> None:
@@ -176,17 +180,29 @@ def run(self) -> List[Path]:
176180
print(f"Downloading artifacts for {workflow_run.name} to {self.temp_workdir.name}")
177181
workflow_run.download_artifacts(Path(self.temp_workdir.name)) # type: ignore
178182
return list(map(Path, Path(self.temp_workdir.name).glob("**/*")))
179-
183+
180184
class ShellAction():
181-
def __init__(self, command: str) -> None:
185+
def __init__(self, command: str, **kwargs: Any) -> None:
182186
self.command = command.strip()
183187
self.temp_workdir = TemporaryDirectory()
188+
self.options = kwargs
189+
190+
def _rewrite_command(self) -> str:
191+
E = TypeVar("E")
192+
R = TypeVar("R")
193+
def lfold(fn: Callable[[R, E], R], lst: Sequence[E], init: R) -> R:
194+
return lfold(fn, lst[1:], fn(init, lst[0])) if lst else init
195+
if 'modifiers' in self.options:
196+
return lfold(lambda acc, x: x(acc), self.options['modifiers'], self.command)
197+
else:
198+
return self.command
184199

185200
def run(self) -> List[Path]:
186-
concrete_command = re.sub(pattern=r"\${{\s*coding-standards\.root\s*}}", repl=str(root_path), string=self.command)
201+
#concrete_command = re.sub(pattern=r"\${{\s*coding-standards\.root\s*}}", repl=str(root_path), string=self.command)
202+
concrete_command = self._rewrite_command()
187203
subprocess.run(concrete_command, cwd=self.temp_workdir.name, check=True, shell=True)
188204
return list(map(Path, Path(self.temp_workdir.name).glob("**/*")))
189-
205+
190206
class FileAction():
191207
def __init__(self, path: Path) -> None:
192208
self.path = path
@@ -200,7 +216,7 @@ def __init__(self, name: str, actions: List[Union[WorkflowLogAction, WorkflowArt
200216
self.actions = actions
201217
self.allow_no_files = allow_no_files
202218

203-
def make(self, directory: Path) -> Path:
219+
def make(self, directory: Path) -> Optional[Path]:
204220
files: list[Path] = [file for action in self.actions for file in action.run()]
205221
if len(files) == 0:
206222
if not self.allow_no_files:
@@ -212,8 +228,8 @@ def make(self, directory: Path) -> Path:
212228
extension = "".join(self.name.suffixes)[1:]
213229
if not extension in ["zip", "tar", "tar.gz", "tar.bz2", "tar.xz"]:
214230
raise Exception(f"Artifact {self.name} is not a support archive file, but has multiple files associated with it!")
215-
216-
ext_format_map = {
231+
232+
ext_format_map = {
217233
"zip": "zip",
218234
"tar": "tar",
219235
"tar.gz": "gztar",
@@ -225,7 +241,7 @@ def make(self, directory: Path) -> Path:
225241
temp_dir_path = Path(temp_dir)
226242
for file in files:
227243
shutil.copy(file, temp_dir_path / file.name)
228-
244+
229245
return Path(shutil.make_archive(str(directory / self.name.with_suffix("")), ext_format_map[extension], root_dir=temp_dir_path))
230246

231247
def main(args: 'argparse.Namespace') -> int:
@@ -248,13 +264,13 @@ def main(args: 'argparse.Namespace') -> int:
248264
if len(pull_candidates) != 1:
249265
print(f"Error: expected exactly one PR for SHA {args.head_sha}, but found {len(pull_candidates)}", file=sys.stderr)
250266
return 1
251-
267+
252268
pull_request = pull_candidates[0]
253269

254270
if pull_request.state != "open":
255271
print(f"Error: PR {pull_request.url} is not open", file=sys.stderr)
256272
return 1
257-
273+
258274
print(f"Found PR {pull_request.url} based on {pull_request.base.ref}")
259275

260276
rc_branch_regex = r"^rc/(?P<version>.*)$"
@@ -286,7 +302,7 @@ def main(args: 'argparse.Namespace') -> int:
286302

287303
action_workflow_run_url_regex = r"^https://(?P<github_url>[^/]+)/(?P<owner>[^/]+)/(?P<repo>[^/]+)/actions/runs/(?P<run_id>\d+)$"
288304
action_workflow_job_run_url_regex = r"^https://(?P<github_url>[^/]+)/(?P<owner>[^/]+)/(?P<repo>[^/]+)/actions/runs/(?P<run_id>\d+)/job/(?P<job_id>\d+)$"
289-
305+
290306
workflow_runs: List[WorkflowRun.WorkflowRun] = []
291307
for check_run in check_runs: # type: ignore
292308
check_run = cast(CheckRun.CheckRun, check_run)
@@ -306,7 +322,7 @@ def main(args: 'argparse.Namespace') -> int:
306322
else:
307323
print(f"Unable to handle checkrun {check_run.name} with id {check_run.id} with {check_run.details_url}")
308324
return 1
309-
325+
310326
print("Filtering workflow runs to only include the latest run for each workflow.")
311327
workflow_runs_per_id: Dict[int, WorkflowRun.WorkflowRun] = {}
312328
for workflow_run in workflow_runs:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pathlib import Path
2+
from tempfile import TemporaryDirectory
3+
import yaml
4+
from update_release_assets import ReleaseLayout
5+
6+
SCRIPT_PATH = Path(__file__)
7+
TEST_DIR = SCRIPT_PATH.parent / 'test-data'
8+
9+
def test_release_layout():
10+
spec = TEST_DIR / 'release-layout.yml'
11+
release_layout = ReleaseLayout(spec)
12+
with TemporaryDirectory() as tmp_dir:
13+
tmp_path = Path(tmp_dir)
14+
release_layout.make(tmp_path, [])
15+
16+
for artifact in yaml.safe_load(spec.read_text())['layout'].keys():
17+
artifact_path = tmp_path / artifact
18+
assert artifact_path.is_file()
19+
20+
if artifact == "hello-world.txt":
21+
content = artifact_path.read_text()
22+
assert content == "hello world!\n"
23+
if artifact == "checksums.txt":
24+
content = artifact_path.read_text()
25+
# The hash of the hello-world.txt is deterministic, so we can assert it here.
26+
assert "ecf701f727d9e2d77c4aa49ac6fbbcc997278aca010bddeeb961c10cf54d435a hello-world.txt" in content
27+
# The has of the hello-world.zip is not deterministic, so we can't assert its hash.
28+
assert "hello-world.zip" in content
29+
30+

0 commit comments

Comments
 (0)