Build tagged releases on Gitea (#79)
Some checks failed
Tagged release / prepare-release (push) Successful in 4s
Tagged release / build-unix (aarch64-apple-darwin, darwin-arm64, aarch64-apple-darwin) (push) Failing after 54s
Tagged release / build-unix (aarch64-unknown-linux-gnu.2.28, linux-arm64, aarch64-unknown-linux-gnu) (push) Failing after 0s
Tagged release / build-unix (x86_64-unknown-linux-gnu.2.28, linux-x64, x86_64-unknown-linux-gnu) (push) Failing after 1s
Tagged release / build-windows (push) Failing after 35s
Tagged release / publish-release (push) Has been skipped
Some checks failed
Tagged release / prepare-release (push) Successful in 4s
Tagged release / build-unix (aarch64-apple-darwin, darwin-arm64, aarch64-apple-darwin) (push) Failing after 54s
Tagged release / build-unix (aarch64-unknown-linux-gnu.2.28, linux-arm64, aarch64-unknown-linux-gnu) (push) Failing after 0s
Tagged release / build-unix (x86_64-unknown-linux-gnu.2.28, linux-x64, x86_64-unknown-linux-gnu) (push) Failing after 1s
Tagged release / build-windows (push) Failing after 35s
Tagged release / publish-release (push) Has been skipped
This commit is contained in:
154
.gitea/scripts/release.py
Normal file
154
.gitea/scripts/release.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create and publish Gitea releases for tagged IronStorage builds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
def issue_numbers(message: str) -> list[int]:
|
||||
return sorted({int(number) for number in re.findall(r"#(\d+)", message)})
|
||||
|
||||
|
||||
def markdown(text: str) -> str:
|
||||
return text.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
|
||||
|
||||
|
||||
def release_body(issues: list[dict[str, object]], server: str, repository: str, sha: str) -> str:
|
||||
groups = {"Fixes": [], "Improvements": [], "Other changes": []}
|
||||
for issue in issues:
|
||||
labels = {label["name"] for label in issue.get("labels", [])} # type: ignore[index]
|
||||
group = "Fixes" if "bug" in labels else "Improvements" if "enhancement" in labels else "Other changes"
|
||||
title = " ".join(str(issue["title"]).split())
|
||||
groups[group].append(f"- [#{issue['number']} {markdown(title)}]({issue['html_url']})")
|
||||
|
||||
sections = [f"## {name}\n" + "\n".join(items) for name, items in groups.items() if items]
|
||||
if not sections:
|
||||
sections.append("No closed issues were linked from commits in this release.")
|
||||
sections.append(f"Built from [{sha[:12]}]({server}/{repository}/commit/{sha}).")
|
||||
return "\n\n".join(sections)
|
||||
|
||||
|
||||
class Gitea:
|
||||
def __init__(self) -> None:
|
||||
self.api_url = os.environ["GITEA_API_URL"].rstrip("/")
|
||||
self.repository = os.environ["GITEA_REPOSITORY"]
|
||||
self.token = os.environ["GITEA_TOKEN"]
|
||||
|
||||
def request(self, method: str, path: str, payload: object | None = None) -> object:
|
||||
data = None if payload is None else json.dumps(payload).encode()
|
||||
request = Request(
|
||||
f"{self.api_url}{path}",
|
||||
data=data,
|
||||
method=method,
|
||||
headers={"Authorization": f"token {self.token}", "Content-Type": "application/json"},
|
||||
)
|
||||
with urlopen(request) as response:
|
||||
content = response.read()
|
||||
return None if not content else json.loads(content)
|
||||
|
||||
def issue(self, number: int) -> dict[str, object]:
|
||||
return self.request("GET", f"/repos/{self.repository}/issues/{number}") # type: ignore[return-value]
|
||||
|
||||
|
||||
def commit_messages(sha: str) -> str:
|
||||
try:
|
||||
previous = subprocess.check_output(
|
||||
["git", "describe", "--tags", "--abbrev=0", f"{sha}^"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
except subprocess.CalledProcessError:
|
||||
previous = ""
|
||||
revision = f"{previous}..{sha}" if previous else sha
|
||||
return subprocess.check_output(["git", "log", "--format=%s%n%b", revision], text=True)
|
||||
|
||||
|
||||
def prepare() -> None:
|
||||
gitea = Gitea()
|
||||
tag = os.environ["GITEA_REF_NAME"]
|
||||
sha = os.environ["GITEA_SHA"]
|
||||
server = os.environ["GITEA_SERVER_URL"].rstrip("/")
|
||||
|
||||
issues = [gitea.issue(number) for number in issue_numbers(commit_messages(sha))]
|
||||
closed = [issue for issue in issues if issue["state"] == "closed"]
|
||||
payload = {
|
||||
"tag_name": tag,
|
||||
"target_commitish": sha,
|
||||
"name": tag,
|
||||
"body": release_body(closed, server, gitea.repository, sha),
|
||||
"draft": True,
|
||||
"prerelease": False,
|
||||
}
|
||||
|
||||
try:
|
||||
release = gitea.request("GET", f"/repos/{gitea.repository}/releases/tags/{quote(tag, safe='')}")
|
||||
except HTTPError as error:
|
||||
if error.code != 404:
|
||||
raise
|
||||
release = gitea.request("POST", f"/repos/{gitea.repository}/releases", payload)
|
||||
else:
|
||||
if not release["draft"]: # type: ignore[index]
|
||||
raise RuntimeError(f"release {tag} is already published")
|
||||
release = gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release['id']}", payload) # type: ignore[index]
|
||||
for asset in release["assets"] or []: # type: ignore[index]
|
||||
gitea.request(
|
||||
"DELETE",
|
||||
f"/repos/{gitea.repository}/releases/{release['id']}/assets/{asset['id']}", # type: ignore[index]
|
||||
)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
|
||||
output.write(f"release_id={release['id']}\n") # type: ignore[index]
|
||||
|
||||
|
||||
def publish() -> None:
|
||||
gitea = Gitea()
|
||||
release_id = int(os.environ["RELEASE_ID"])
|
||||
gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release_id}", {"draft": False})
|
||||
|
||||
|
||||
class ReleaseTests(unittest.TestCase):
|
||||
def test_notes_are_unique_sorted_grouped_and_linked(self) -> None:
|
||||
self.assertEqual(issue_numbers("Fix #12, refs #3 and #12"), [3, 12])
|
||||
body = release_body(
|
||||
[
|
||||
{
|
||||
"number": 12,
|
||||
"title": "Fix [unlock]",
|
||||
"html_url": "https://example.test/issues/12",
|
||||
"labels": [{"name": "bug"}],
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"title": "Add search",
|
||||
"html_url": "https://example.test/issues/3",
|
||||
"labels": [{"name": "enhancement"}],
|
||||
},
|
||||
],
|
||||
"https://example.test",
|
||||
"owner/repo",
|
||||
"1234567890abcdef",
|
||||
)
|
||||
self.assertIn("## Fixes\n- [#12 Fix \\[unlock\\]](https://example.test/issues/12)", body)
|
||||
self.assertIn("## Improvements\n- [#3 Add search](https://example.test/issues/3)", body)
|
||||
self.assertIn("[1234567890ab](https://example.test/owner/repo/commit/1234567890abcdef)", body)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command = sys.argv[1] if len(sys.argv) == 2 else ""
|
||||
if command == "prepare":
|
||||
prepare()
|
||||
elif command == "publish":
|
||||
publish()
|
||||
elif command == "test":
|
||||
unittest.main(argv=[sys.argv[0]])
|
||||
else:
|
||||
raise SystemExit("usage: release.py prepare|publish|test")
|
||||
171
.gitea/workflows/release.yml
Normal file
171
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,171 @@
|
||||
name: Tagged release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Test release-note generation
|
||||
run: python3 .gitea/scripts/release.py test
|
||||
|
||||
- name: Create draft release
|
||||
id: release
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: python3 .gitea/scripts/release.py prepare
|
||||
|
||||
build-unix:
|
||||
needs: prepare-release
|
||||
runs-on: linux-arm64
|
||||
container: ghcr.io/rust-cross/cargo-zigbuild@sha256:8797479160b221b4ead24b242279af86684771a3c20944b0d7004dfe946275bc
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
build-target: aarch64-unknown-linux-gnu.2.28
|
||||
platform: linux-arm64
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
build-target: x86_64-unknown-linux-gnu.2.28
|
||||
platform: linux-x64
|
||||
- target: aarch64-apple-darwin
|
||||
build-target: aarch64-apple-darwin
|
||||
platform: darwin-arm64
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cross-build release binaries
|
||||
run: >-
|
||||
cargo zigbuild --release --locked
|
||||
--target ${{ matrix.build-target }}
|
||||
--package ironstorage-cli
|
||||
--package ironstorage-tui
|
||||
--package ironstorage-desktop
|
||||
|
||||
- name: Package CLI, TUI, and desktop application
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TAG: ${{ gitea.ref_name }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(printf '%s' "$TAG" | sed 's/[^A-Za-z0-9._-]/-/g')
|
||||
cli="ironstorage-cli-${version}-${PLATFORM}"
|
||||
desktop="ironstorage-desktop-${version}-${PLATFORM}"
|
||||
mkdir -p "dist/$cli" "dist/$desktop"
|
||||
cp "target/$TARGET/release/ironstorage" "dist/$cli/"
|
||||
cp "target/$TARGET/release/ironstorage-tui" "dist/$cli/"
|
||||
cp "target/$TARGET/release/ironstorage" "dist/$desktop/"
|
||||
cp "target/$TARGET/release/ironstorage-tui" "dist/$desktop/"
|
||||
cp "target/$TARGET/release/ironstorage-desktop" "dist/$desktop/"
|
||||
tar -C dist -czf "dist/${cli}.tar.gz" "$cli"
|
||||
tar -C dist -czf "dist/${desktop}.tar.gz" "$desktop"
|
||||
rm -r "dist/$cli" "dist/$desktop"
|
||||
|
||||
- name: Upload release packages
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for asset in dist/*.tar.gz; do
|
||||
name=$(basename "$asset")
|
||||
curl --fail --silent --show-error --retry 3 \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$asset" \
|
||||
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/releases/$RELEASE_ID/assets?name=$name"
|
||||
done
|
||||
|
||||
build-windows:
|
||||
needs: prepare-release
|
||||
runs-on: linux-arm64
|
||||
container: messense/cargo-xwin@sha256:4696dd4e79edf8569fa99c4b06bd99273e0501c7adc983aa61d57945f795bef0
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install the repository Rust toolchain
|
||||
run: rustup toolchain install 1.92.0 --profile minimal --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Cross-build release binaries
|
||||
run: >-
|
||||
cargo +1.92.0 xwin build --release --locked
|
||||
--target x86_64-pc-windows-msvc
|
||||
--package ironstorage-cli
|
||||
--package ironstorage-tui
|
||||
--package ironstorage-desktop
|
||||
|
||||
- name: Package CLI, TUI, and desktop application
|
||||
env:
|
||||
TAG: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(printf '%s' "$TAG" | sed 's/[^A-Za-z0-9._-]/-/g')
|
||||
cli="ironstorage-cli-${version}-windows-x64"
|
||||
desktop="ironstorage-desktop-${version}-windows-x64"
|
||||
release=target/x86_64-pc-windows-msvc/release
|
||||
mkdir -p "dist/$cli" "dist/$desktop"
|
||||
cp "$release/ironstorage.exe" "$release/ironstorage-tui.exe" "dist/$cli/"
|
||||
cp "$release/ironstorage.exe" "$release/ironstorage-tui.exe" \
|
||||
"$release/ironstorage-desktop.exe" "dist/$desktop/"
|
||||
tar -C dist -czf "dist/${cli}.tar.gz" "$cli"
|
||||
tar -C dist -czf "dist/${desktop}.tar.gz" "$desktop"
|
||||
rm -r "dist/$cli" "dist/$desktop"
|
||||
|
||||
- name: Upload release packages
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for asset in dist/*.tar.gz; do
|
||||
name=$(basename "$asset")
|
||||
curl --fail --silent --show-error --retry 3 \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$asset" \
|
||||
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/releases/$RELEASE_ID/assets?name=$name"
|
||||
done
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
- prepare-release
|
||||
- build-unix
|
||||
- build-windows
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish complete release
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
|
||||
run: python3 .gitea/scripts/release.py publish
|
||||
Reference in New Issue
Block a user