Post-release changes (#543)
- Update version to 0.8.0-dev - Add new release scripts - Update release docs
This commit is contained in:
committed by
GitHub
parent
cd07c64926
commit
35fd7dd536
Executable
+135
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Download all artifacts from a GitHub Actions workflow run."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_URL = "https://api.github.com"
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(cli_token: str | None) -> str:
|
||||||
|
if cli_token:
|
||||||
|
return cli_token
|
||||||
|
env_token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
||||||
|
if env_token:
|
||||||
|
return env_token
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "auth", "token"], capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||||
|
sys.exit(
|
||||||
|
"No token found. Pass --token, set GITHUB_TOKEN/GH_TOKEN, or run `gh auth login`."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo(cli_repo: str | None) -> str:
|
||||||
|
if cli_repo:
|
||||||
|
return cli_repo
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "remote", "get-url", "origin"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
url = result.stdout.strip()
|
||||||
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||||
|
sys.exit("Could not detect repo from git remote. Pass --repo owner/name.")
|
||||||
|
|
||||||
|
# Handle both SSH and HTTPS remote URLs.
|
||||||
|
if url.startswith("git@"):
|
||||||
|
path = url.split(":", 1)[1]
|
||||||
|
else:
|
||||||
|
path = url.split("github.com/", 1)[-1]
|
||||||
|
return path.removesuffix(".git")
|
||||||
|
|
||||||
|
|
||||||
|
def download_artifacts(repo: str, run_id: str, token: str, out_dir: Path):
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
url = f"{API_URL}/repos/{repo}/actions/runs/{run_id}/artifacts"
|
||||||
|
artifacts = []
|
||||||
|
while url:
|
||||||
|
resp = requests.get(url, headers=headers, params={"per_page": 100})
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
artifacts.extend(data["artifacts"])
|
||||||
|
url = resp.links.get("next", {}).get("url")
|
||||||
|
|
||||||
|
if not artifacts:
|
||||||
|
print(f"No artifacts found for run {run_id}.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for artifact in artifacts:
|
||||||
|
name = artifact["name"]
|
||||||
|
if name == "all-artifacts":
|
||||||
|
print(f"Skipping {name}")
|
||||||
|
continue
|
||||||
|
if artifact.get("expired"):
|
||||||
|
print(f"Skipping expired artifact: {name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Downloading {name} ({artifact['size_in_bytes']} bytes)...")
|
||||||
|
download_url = artifact["archive_download_url"]
|
||||||
|
resp = requests.get(download_url, headers=headers, stream=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
zip_path = out_dir / f"{name}.zip"
|
||||||
|
with open(zip_path, "wb") as f:
|
||||||
|
for chunk in resp.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
if name.endswith(".bin"):
|
||||||
|
with zipfile.ZipFile(zip_path) as zf:
|
||||||
|
zf.extractall(out_dir)
|
||||||
|
zip_path.unlink()
|
||||||
|
print(f" Extracted to {out_dir}")
|
||||||
|
else:
|
||||||
|
print(f" Saved to {zip_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("run_id", help="GitHub Actions run ID")
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
help="owner/repo (default: detected from git remote 'origin')",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
help="GitHub token (default: $GITHUB_TOKEN, $GH_TOKEN, or `gh auth token`)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--out",
|
||||||
|
default="artifacts",
|
||||||
|
help="Output directory (default: ./artifacts)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo = get_repo(args.repo)
|
||||||
|
token = get_token(args.token)
|
||||||
|
out_dir = Path(args.out)
|
||||||
|
|
||||||
|
print(f"Repo: {repo}")
|
||||||
|
print(f"Run ID: {args.run_id}")
|
||||||
|
print(f"Output: {out_dir.resolve()}")
|
||||||
|
|
||||||
|
download_artifacts(repo, args.run_id, token, out_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Summarize commits between the last 2 pushed git tags into commits.md and pull-requests.md."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PR_REF_RE = re.compile(r"#(\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo(cli_repo: str | None) -> str:
|
||||||
|
if cli_repo:
|
||||||
|
return cli_repo
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "remote", "get-url", "origin"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
url = result.stdout.strip()
|
||||||
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||||
|
sys.exit("Could not detect repo from git remote. Pass --repo owner/name.")
|
||||||
|
|
||||||
|
if url.startswith("git@"):
|
||||||
|
path = url.split(":", 1)[1]
|
||||||
|
else:
|
||||||
|
path = url.split("github.com/", 1)[-1]
|
||||||
|
return path.removesuffix(".git")
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_two_tags() -> tuple[str, str]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "tag", "--sort=-creatordate"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
tags = [t for t in result.stdout.splitlines() if t]
|
||||||
|
if len(tags) < 2:
|
||||||
|
sys.exit(f"Need at least 2 tags, found {len(tags)}.")
|
||||||
|
return tags[1], tags[0] # previous, latest
|
||||||
|
|
||||||
|
|
||||||
|
def get_commits(prev_tag: str, latest_tag: str) -> list[tuple[str, str]]:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"log",
|
||||||
|
"--pretty=format:%H\x1f%s",
|
||||||
|
f"{prev_tag}..{latest_tag}",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
commits = []
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
sha, subject = line.split("\x1f", 1)
|
||||||
|
commits.append((sha, subject))
|
||||||
|
return commits
|
||||||
|
|
||||||
|
|
||||||
|
def write_commits_md(path: Path, repo: str, prev_tag: str, latest_tag: str, commits: list[tuple[str, str]]):
|
||||||
|
lines = [f"# Commits: {prev_tag}..{latest_tag}", ""]
|
||||||
|
for sha, subject in commits:
|
||||||
|
link = f"https://github.com/{repo}/commit/{sha}"
|
||||||
|
lines.append(f"- [{subject}]({link})")
|
||||||
|
path.write_text("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def write_pull_requests_md(path: Path, repo: str, commits: list[tuple[str, str]]):
|
||||||
|
pr_numbers = []
|
||||||
|
seen = set()
|
||||||
|
for _, subject in commits:
|
||||||
|
for match in PR_REF_RE.finditer(subject):
|
||||||
|
number = match.group(1)
|
||||||
|
if number not in seen:
|
||||||
|
seen.add(number)
|
||||||
|
pr_numbers.append(number)
|
||||||
|
|
||||||
|
lines = ["# Pull Requests", ""]
|
||||||
|
for number in pr_numbers:
|
||||||
|
lines.append(f"- https://github.com/{repo}/pull/{number}")
|
||||||
|
path.write_text("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
help="owner/repo (default: detected from git remote 'origin')",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--out",
|
||||||
|
default=".",
|
||||||
|
help="Output directory for commits.md and pull-requests.md (default: current directory)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo = get_repo(args.repo)
|
||||||
|
prev_tag, latest_tag = get_last_two_tags()
|
||||||
|
commits = get_commits(prev_tag, latest_tag)
|
||||||
|
|
||||||
|
out_dir = Path(args.out)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
commits_path = out_dir / "release-commits.md"
|
||||||
|
prs_path = out_dir / "release-pull-requests.md"
|
||||||
|
|
||||||
|
write_commits_md(commits_path, repo, prev_tag, latest_tag, commits)
|
||||||
|
write_pull_requests_md(prs_path, repo, commits)
|
||||||
|
|
||||||
|
print(f"Repo: {repo}")
|
||||||
|
print(f"Tags: {prev_tag}..{latest_tag}")
|
||||||
|
print(f"Commits: {len(commits)}")
|
||||||
|
print(f"Wrote {commits_path}")
|
||||||
|
print(f"Wrote {prs_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate a release summary markdown by combining gh-release-summary.py output with Claude."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
SECTIONS = [
|
||||||
|
"New Devices",
|
||||||
|
"New Features",
|
||||||
|
"New Apps",
|
||||||
|
"Improvements & Fixes",
|
||||||
|
"Other Changes",
|
||||||
|
"TactilitySDK",
|
||||||
|
"Known Issues",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_gh_release_summary(out_dir: Path, repo: str | None):
|
||||||
|
cmd = [sys.executable, str(SCRIPT_DIR / "gh-release-summary.py"), "--out", str(out_dir)]
|
||||||
|
if repo:
|
||||||
|
cmd += ["--repo", repo]
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt() -> str:
|
||||||
|
section_list = "\n".join(f"- {s}" for s in SECTIONS)
|
||||||
|
return (
|
||||||
|
"You are given a list of commits (with GitHub commit links) and a list of pull "
|
||||||
|
"request links from a software release. Write a release summary in Markdown, "
|
||||||
|
"organizing changes into these sections (omit a section if it has no relevant "
|
||||||
|
"changes):\n\n"
|
||||||
|
f"{section_list}\n\n"
|
||||||
|
"Use concise bullet points. Base every bullet strictly on the given commits and "
|
||||||
|
"pull requests, referencing the pull request link where applicable. Output only "
|
||||||
|
"the Markdown, no commentary before or after it."
|
||||||
|
"If a pull request or commit has multiple changes add them as separate entries."
|
||||||
|
"Ignore irrelevant changes: documentation, minor fixes, non-descript fixes, scripts, github workflows."
|
||||||
|
"For 'New Devices': only mention the device name, don't like pull request."
|
||||||
|
"For 'Improvements & Fixes': Avoid non-descript entries, write specifics."
|
||||||
|
"For 'TactilitySDK': Avoid generic changes, be specific. Mention API changes."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_claude(commits_md: str, prs_md: str) -> str:
|
||||||
|
prompt = build_prompt()
|
||||||
|
stdin_content = f"## Commits\n\n{commits_md}\n\n## Pull Requests\n\n{prs_md}"
|
||||||
|
result = subprocess.run(
|
||||||
|
["claude", "-p", prompt],
|
||||||
|
input=stdin_content,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--repo", help="owner/repo (default: detected from git remote 'origin')")
|
||||||
|
parser.add_argument("--out", default=".", help="Output directory (default: current directory)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
out_dir = Path(args.out)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
run_gh_release_summary(out_dir, args.repo)
|
||||||
|
|
||||||
|
commits_md = (out_dir / "release-commits.md").read_text()
|
||||||
|
prs_md = (out_dir / "release-pull-requests.md").read_text()
|
||||||
|
|
||||||
|
print("Generating release summary via claude...")
|
||||||
|
summary = run_claude(commits_md, prs_md)
|
||||||
|
|
||||||
|
summary_path = out_dir / "release-summary.md"
|
||||||
|
summary_path.write_text(summary)
|
||||||
|
print(f"Wrote {summary_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+13
-15
@@ -4,24 +4,22 @@
|
|||||||
2. Set the new version in the Tactility repository:
|
2. Set the new version in the Tactility repository:
|
||||||
1. Update `version.txt` in the Tactility project and create a branch to start a build for it
|
1. Update `version.txt` in the Tactility project and create a branch to start a build for it
|
||||||
2. Merge the branch and wait for a build.
|
2. Merge the branch and wait for a build.
|
||||||
3. Upload the new Tactility SDK to the CDN
|
3. Push a tag e.g. `v1.2.3` - the `v` prefix is crucial for the pipelines.
|
||||||
1. Upload it to the [CDN](https://dash.cloudflare.com)
|
4. Wait for the SDK and firmwares to be published on the CDN.
|
||||||
2. Update `sdk.json` from [TactilityTool](https://github.com/TactilityProject/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com)
|
5. Test the firmwares on all devices with the web installer
|
||||||
4. Update the [TactilityApps](https://github.com/TactilityProject/TactilityApps) with the new SDK and also release these to the CDN:
|
6. Run `Buildscripts/gh-download-artifacts.py [actionId] --token [token]` and let it run in the background.
|
||||||
1. Download the `cdn-files.zip` from the pipelines
|
Get a token from [here](https://github.com/settings/personal-access-tokens)
|
||||||
2. Upload them to the CDN at `apps/x.y.z/`
|
7. Update the [TactilityApps](https://github.com/TactilityProject/TactilityApps) with the new SDK version and bump the app versions.
|
||||||
3. Also upload them to the CDN for the upcoming version, because the upcoming a.b.c version will also need some basic apps to download
|
8. After merging the apps, manually upload to CDN, then test them.
|
||||||
5. Test the latest unstable version of Tactility on several devices using the [web installer](https://install.tactilityproject.org). Pay special attention to:
|
|
||||||
1. The version of the unstable channel (should be updated!)
|
|
||||||
2. App Hub
|
|
||||||
3. Wi-Fi
|
|
||||||
6. Test the firmwares on all devices with the local web installer
|
|
||||||
7. Push a tag e.g. `v1.2.3` - the `v` prefix is crucial for the pipelines
|
|
||||||
8. The pipelines should now kick off a build that releases the build to the stable channel of the Web Installer. Verify that.
|
|
||||||
9. Make a new version of the docs available at [TactilityDocs](https://github.com/TactilityProject/TactilityDocs)
|
9. Make a new version of the docs available at [TactilityDocs](https://github.com/TactilityProject/TactilityDocs)
|
||||||
|
1. Copy `docs/versions/main/` into new version folder
|
||||||
|
2. Edit `index.html` to include the new version
|
||||||
|
3. Push code directly to `main` branch
|
||||||
10. Make a new [GitHub release](https://github.com/TactilityProject/Tactility/releases/new)
|
10. Make a new [GitHub release](https://github.com/TactilityProject/Tactility/releases/new)
|
||||||
|
Run `Buildscripts/release-summary.py` to generate a release, then review it
|
||||||
11. Double-check that all CDN/TactilityApps/Tactility repository changes are merged.
|
11. Double-check that all CDN/TactilityApps/Tactility repository changes are merged.
|
||||||
12. Ensure that the CDN is not in development mode anymore.
|
12. Add the `-dev` postfix from `version.txt` and bump the version. Merge it to `main`.
|
||||||
|
13. Ensure that the CDN is not in development mode anymore.
|
||||||
|
|
||||||
### Post-release
|
### Post-release
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
0.7.0
|
0.8.0-dev
|
||||||
Reference in New Issue
Block a user