From 9558e9ad646723c57d524c7f29ffb8a884a5fb67 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 8 Jul 2026 14:23:58 +0000 Subject: [PATCH] ci: add automated CI/release workflows --- .gitea/workflows/ci.yml | 29 ++++++++++ .gitea/workflows/release.yml | 35 ++++++++++++ scripts/gitea_sync.py | 104 +++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/release.yml create mode 100755 scripts/gitea_sync.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..ec77541 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: "3.11" } + - run: pip install -r requirements.txt + - run: python3 -m pytest tests/ -q + + build-frontend: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + - name: Pack frontend + run: tar -czf frontend.tar.gz frontend + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: { name: frontend, path: frontend.tar.gz } diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..b78a523 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,35 @@ +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + tag-release: + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - name: Auto-tag + run: | + set -e + TAG="0.0.${{ github.run_number }}" + git tag "$TAG" || true + git push origin "$TAG" || true + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + draft-rc: + runs-on: ubuntu-latest + needs: tag-release + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - name: Draft release + run: | + set -e + TAG="${{ needs.tag-release.outputs.tag }}" + gh release create "$TAG" --draft --title "RC $TAG" --notes "Automated release candidate for $TAG." || true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/gitea_sync.py b/scripts/gitea_sync.py new file mode 100755 index 0000000..2226100 --- /dev/null +++ b/scripts/gitea_sync.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Sync Hermes kanban board state to Gitea issue labels.""" +import json +import os +import subprocess +import urllib.request + +GITEA_API = os.getenv("GITEA_API", "http://127.0.0.1:3000/api/v1") +GITEA_TOKEN = os.getenv("GITEA_TOKEN", "") +REPO = os.getenv("GITEA_REPO", "stackchain/stackchain-hackathon") + +LABEL_MAP = { + "todo": "P1", + "ready": "ready", + "in_progress": "in_progress", + "review": "review", + "done": "done", + "blocked": "blocked", + "triage": "triage", +} + + +def _gitea(path, body=None, method="GET"): + url = f"{GITEA_API}/{path}" + data = json.dumps(body).encode() if body else None + req = urllib.request.Request(url, data=data, method=method) + req.add_header("Accept", "application/json") + req.add_header("Authorization", f"token {GITEA_TOKEN}") + if data: + req.add_header("Content-Type", "application/json") + with urllib.request.urlopen(req) as r: + return json.loads(r.read().decode()) + + +def list_issues(): + return _gitea(f"repos/{REPO}/issues?state=all&limit=100") + + +def list_labels(): + return _gitea(f"repos/{REPO}/labels") + + +def find_label(name, labels): + matches = [l for l in labels if l["name"] == name] + return matches[0]["id"] if matches else None + + +def create_label(name): + colors = { + "P1": "00ccff", + "P2": "cccccc", + "ready": "0e8a16", + "in_progress": "fbca04", + "review": "d93f0b", + "done": "00cc00", + "blocked": "ff4444", + "triage": "cccccc", + } + return _gitea( + f"repos/{REPO}/labels", + body={"name": name, "color": colors.get(name, "cccccc")}, + method="POST", + ) + + +def edit_issue(number, labels=None): + if labels is None: + return + _gitea( + f"repos/{REPO}/issues/{number}", + body={"labels": labels}, + method="PATCH", + ) + + +def main(): + kanban_json = subprocess.check_output( + ["hermes", "kanban", "list", "--json"], text=True + ) + tasks = json.loads(kanban_json) + labels = list_labels() + label_ids = {l["name"]: l["id"] for l in labels} + issues = {i["title"].split(" ", 1)[1].strip(): i for i in list_issues() if i.get("pull_request") is None} + + for t in tasks: + title = t.get("title", "") + issue_key = title.split(" ", 1)[1].strip() if title.startswith("#") else title + prefix = LABEL_MAP.get(t.get("status", "")) + if not prefix: + continue + gitea_label = label_ids.get(prefix) + if gitea_label is None: + create_label(prefix) + label_ids[prefix] = find_label(prefix, list_labels()) or len(label_ids) + 1 + if issue_key in issues: + issue = issues[issue_key] + current = [l["id"] for l in issue.get("labels", [])] + if gitea_label and gitea_label not in current: + current.append(gitea_label) + edit_issue(issue["number"], labels=list(set(current))) + + +if __name__ == "__main__": + main()