diff --git a/.github/clickup-init.py b/.github/clickup-init.py deleted file mode 100644 index 7a51707a..00000000 --- a/.github/clickup-init.py +++ /dev/null @@ -1,254 +0,0 @@ -import os -import requests -from datetime import datetime - -# ========================= -# Configuration (ENV VARS) -# ========================= - -GITHUB_REPO = os.environ["GITHUB_REPO"] -GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] -CLICKUP_API_TOKEN = os.environ["CLICKUP_API_TOKEN"] -CLICKUP_LIST_ID = os.environ["CLICKUP_LIST_ID"] - -# ========================= -# Headers -# ========================= - -clickup_headers = { - "Authorization": CLICKUP_API_TOKEN, - "Content-Type": "application/json", -} - -github_headers = { - "Authorization": f"Bearer {GITHUB_TOKEN}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", -} - -# ========================= -# ClickUp helpers -# ========================= - -def find_task_by_issue_number(issue_number: int) -> str | None: - url = f"https://api.clickup.com/api/v2/list/{CLICKUP_LIST_ID}/task" - params = {"subtasks": "true", "include_closed": "true"} - - response = requests.get(url, headers=clickup_headers, params=params) - if response.status_code != 200: - print(f"! Error fetching tasks: {response.text}") - return None - - for task in response.json().get("tasks", []): - if f"[Issue #{issue_number}]" in task.get("name", ""): - return task["id"] - - return None - - -def create_task( - issue_number: int, - title: str, - body: str, - url: str, - status: str, - parent_task_id: str | None = None, -) -> str | None: - - clickup_url = f"https://api.clickup.com/api/v2/list/{CLICKUP_LIST_ID}/task" - - task_data = { - "name": f"[Issue #{issue_number}] {title}", - "description": f"GitHub Issue: {url}\n\n{body}", - "status": status, - } - - if parent_task_id: - task_data["parent"] = parent_task_id - - response = requests.post(clickup_url, json=task_data, headers=clickup_headers) - - if response.status_code == 200: - task_id = response.json()["id"] - print( - f" Created {'subtask' if parent_task_id else 'task'} " - f"for issue #{issue_number} ({status})" - ) - return task_id - - print(f"! Failed creating task for #{issue_number}: {response.text}") - return None - - -def add_comment_to_task(task_id: str, comment_text: str) -> bool: - url = f"https://api.clickup.com/api/v2/task/{task_id}/comment" - response = requests.post( - url, json={"comment_text": comment_text}, headers=clickup_headers - ) - return response.status_code == 200 - - -def set_task_dependency(task_id: str, depends_on_task_id: str) -> bool: - url = f"https://api.clickup.com/api/v2/task/{task_id}/dependency" - response = requests.post( - url, params={"depends_on": depends_on_task_id}, headers=clickup_headers - ) - - return response.status_code == 200 or "already exists" in response.text.lower() - -# ========================= -# GitHub helpers -# ========================= - -def get_blocked_by(issue_number: int) -> list[int]: - url = f"https://api.github.com/repos/{GITHUB_REPO}/issues/{issue_number}/dependencies/blocked_by" - response = requests.get(url, headers=github_headers) - - if response.status_code != 200: - print(f"! Error fetching blocked_by for #{issue_number}: {response.text}") - return [] - - blockers = [] - for issue in response.json(): - blockers.append(issue["number"]) - - return blockers - - -def get_all_issues_with_relationships(): - print(f"Fetching issues from {GITHUB_REPO}...") - - all_issues = [] - page = 1 - - while True: - response = requests.get( - f"https://api.github.com/repos/{GITHUB_REPO}/issues", - headers=github_headers, - params={"state": "all", "per_page": 100, "page": page}, - ) - - if response.status_code != 200: - raise RuntimeError(response.text) - - batch = response.json() - if not batch: - break - - all_issues.extend(batch) - page += 1 - - all_issues = [i for i in all_issues if "pull_request" not in i] - print(f"Found {len(all_issues)} issues") - - issue_map = {} - parent_map = {} - - for issue in all_issues: - issue_number = issue["number"] - deps = issue.get("issue_dependencies_summary") or {} - - issue_map[issue_number] = { - "issue": issue, - "sub_issues": [], - "blocked_by": deps.get("blocked_by", 0), - "total_blocked_by": deps.get("total_blocked_by", 0), - } - - parent_url = issue.get("parent_issue_url") - if parent_url: - parent_map[issue_number] = int(parent_url.split("/")[-1]) - - for child, parent in parent_map.items(): - if parent in issue_map: - issue_map[parent]["sub_issues"].append(child) - - return all_issues, issue_map, parent_map - -# ========================= -# Main -# ========================= - -def main(): - print("=" * 60) - print("GitHub → ClickUp Import (manual)") - print("=" * 60) - - all_issues, issue_map, parent_map = get_all_issues_with_relationships() - - task_map = {} - blocked_issues = [] - - print("\nCreating top-level tasks...\n") - - for issue_number, data in sorted(issue_map.items()): - if issue_number in parent_map: - continue - - existing = find_task_by_issue_number(issue_number) - if existing: - task_map[issue_number] = existing - continue - - issue = data["issue"] - - if issue["state"] == "closed": - status = "done" - elif data["total_blocked_by"] > 0: - status = "blocked" - blocked_issues.append(issue_number) - else: - status = "todo" - - task_id = create_task( - issue_number, - issue["title"], - issue.get("body", ""), - issue["html_url"], - status, - ) - - if task_id: - task_map[issue_number] = task_id - - print("\nCreating subtasks...\n") - - for child, parent in parent_map.items(): - if child in task_map: - continue - - parent_task_id = task_map.get(parent) - if not parent_task_id: - continue - - issue = issue_map[child]["issue"] - status = "done" if issue["state"] == "closed" else "todo" - - task_id = create_task( - child, - issue["title"], - issue.get("body", ""), - issue["html_url"], - status, - parent_task_id, - ) - - if task_id: - task_map[child] = task_id - - print("\nSetting dependencies...\n") - - for issue_number in blocked_issues: - task_id = task_map.get(issue_number) - if not task_id: - continue - - for blocker in get_blocked_by(issue_number): - blocker_task_id = task_map.get(blocker) - if blocker_task_id: - set_task_dependency(task_id, blocker_task_id) - - print("\nImport complete! Jolly good!\n") - -if __name__ == "__main__": - main() diff --git a/.github/clickup-sync.py b/.github/clickup-sync.py deleted file mode 100644 index bc5e984e..00000000 --- a/.github/clickup-sync.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -"""Simple ClickUp-GitHub sync script using GitHub issue relationships. Just 2 files needed!""" -import os -import sys -import json -import re -import requests -from github import Github -from time import sleep - -# ============================================================================ -# CONFIGURATION -# ============================================================================ -CLICKUP_TOKEN = os.environ['CLICKUP_API_TOKEN'] -CLICKUP_LIST_ID = os.environ['CLICKUP_LIST_ID'] -GITHUB_TOKEN = os.environ['GITHUB_TOKEN'] -GITHUB_REPO = os.environ['GITHUB_REPOSITORY'] -EVENT_NAME = os.environ['GITHUB_EVENT_NAME'] - -CLICKUP_API = "https://api.clickup.com/api/v2" -GITHUB_API = "https://api.github.com" -gh = Github(GITHUB_TOKEN) -repo = gh.get_repo(GITHUB_REPO) - -# ============================================================================ -# CLICKUP API HELPERS -# ============================================================================ -def clickup_request(method, endpoint, **kwargs): - """Make request to ClickUp API with retry.""" - url = f"{CLICKUP_API}/{endpoint}" - headers = {"Authorization": CLICKUP_TOKEN, "Content-Type": "application/json"} - - for attempt in range(3): - try: - resp = requests.request(method, url, headers=headers, **kwargs) - if resp.status_code == 429: - sleep(60) - continue - resp.raise_for_status() - return resp.json() if resp.content else {} - except Exception as e: - if attempt == 2: - raise - sleep(2 ** attempt) - -def get_task_by_issue(issue_num): - """Find ClickUp task by GitHub issue number.""" - tasks = clickup_request("GET", f"list/{CLICKUP_LIST_ID}/task", params={"subtasks": True, "include_closed": True}) - for task in tasks.get("tasks", []): - if task["name"].startswith(f"#{issue_num}:"): - return task["id"] - return None - -def create_task(name, desc, status="todo", parent=None): - """Create ClickUp task.""" - payload = {"name": name, "description": desc, "status": status} - if parent: - payload["parent"] = parent - return clickup_request("POST", f"list/{CLICKUP_LIST_ID}/task", json=payload) - -def update_task(task_id, **kwargs): - """Update ClickUp task.""" - return clickup_request("PUT", f"task/{task_id}", json=kwargs) - -def add_comment(task_id, text): - """Add comment to task.""" - clickup_request("POST", f"task/{task_id}/comment", json={"comment_text": text}) - -# ============================================================================ -# GITHUB RELATIONSHIP HELPERS -# ============================================================================ -def get_linked_issues(issue_num): - """Get all linked issues from GitHub's relationships section.""" - headers = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"} - - # Get issue details - issue_url = f"{GITHUB_API}/repos/{GITHUB_REPO}/issues/{issue_num}" - resp = requests.get(issue_url, headers=headers) - - blocking = [] - blocked_by = [] - parent_issue = None - - try: - # Get timeline for cross-referenced events (linked issues) - timeline_url = f"{GITHUB_API}/repos/{GITHUB_REPO}/issues/{issue_num}/timeline" - resp = requests.get(timeline_url, headers=headers) - resp.raise_for_status() - - for event in resp.json(): - if event.get("event") == "cross-referenced": - source = event.get("source", {}) - if source and source.get("type") == "issue": - source_issue = source.get("issue", {}) - source_num = source_issue.get("number") - body = source_issue.get("body", "") - - # Check if source blocks this issue - if re.search(rf'[Bb]locks?.*?#{issue_num}\b', body or ""): - blocking.append(source_num) - - # Check if source is blocked by this issue - if re.search(rf'[Bb]locked by.*?#{issue_num}\b', body or ""): - blocked_by.append(source_num) - except: - pass - - # Parse body for parent and remaining relationships - issue = repo.get_issue(issue_num) - body = issue.body or "" - - # Parent relationship - match = re.search(r'[Pp]arent.*?#(\d+)', body) - if match: - parent_issue = int(match.group(1)) - - return blocking, blocked_by, parent_issue - -def parse_linked_issues(body): - """Extract linked issues from PR body.""" - matches = re.findall(r'(?:[Cc]loses?|[Ff]ixes?|[Rr]esolves?).*?#(\d+)', body or "") - return [int(m) for m in matches] - -# ============================================================================ -# SYNC LOGIC -# ============================================================================ -def sync_issue_to_task(issue_num): - """Sync a GitHub issue to ClickUp task.""" - issue = repo.get_issue(issue_num) - task_id = get_task_by_issue(issue_num) - - # Get relationships from GitHub - blocking, blocked_by, parent_num = get_linked_issues(issue_num) - - # Determine status - status = "to do" - if blocked_by and any(repo.get_issue(b).state == "open" for b in blocked_by if b): - status = "blocked" - - # Check for linked PRs - has_pr = False - try: - timeline = issue.get_timeline() - for event in timeline: - if hasattr(event, 'source') and event.source and hasattr(event.source, 'issue'): - if event.source.issue.pull_request: - has_pr = True - break - except: - pass - - if has_pr and status != "blocked": - status = "in progress" - - # Get or create parent task - parent_task_id = None - if parent_num: - parent_task_id = get_task_by_issue(parent_num) - if not parent_task_id: - sync_issue_to_task(parent_num) # Recursive - parent_task_id = get_task_by_issue(parent_num) - - # Create or update task - task_name = f"#{issue_num}: {issue.title}" - task_desc = f"{issue.body or ''}\n\n---\n[GitHub Issue]({issue.html_url})" - - if not task_id: - print(f"Creating task for issue #{issue_num}") - task = create_task(task_name, task_desc, status, parent_task_id) - task_id = task["id"] - else: - print(f"Updating task for issue #{issue_num}") - if issue.state == "closed": - status = "done" - update_task(task_id, name=task_name, description=task_desc, status=status) - - # Handle blocking relationships - for blocked_num in blocking: - blocked_task_id = get_task_by_issue(blocked_num) - if not blocked_task_id: - sync_issue_to_task(blocked_num) - blocked_task_id = get_task_by_issue(blocked_num) - if blocked_task_id: - update_task(blocked_task_id, status="blocked") - - return task_id - -def sync_pr_to_task(pr_num): - """Sync PR info to related issue tasks.""" - pr = repo.get_pull(pr_num) - linked = parse_linked_issues(pr.body) - - # Get last 3 commits - commits = list(pr.get_commits())[-3:] - commit_text = "\n".join([ - f"- `{c.sha[:7]}` {c.commit.message.split(chr(10))[0]} by {c.commit.author.name}" - for c in commits - ]) - - for issue_num in linked: - task_id = get_task_by_issue(issue_num) - if not task_id: - sync_issue_to_task(issue_num) - task_id = get_task_by_issue(issue_num) - - if task_id: - # Update status if not blocked - _, blocked_by, _ = get_linked_issues(issue_num) - is_blocked = any(repo.get_issue(b).state == "open" for b in blocked_by if b) - - if pr.state == "closed" and pr.merged: - update_task(task_id, status="done") - add_comment(task_id, f"✅ PR #{pr_num} merged: {pr.html_url}") - elif not is_blocked: - update_task(task_id, status="in progress") - add_comment(task_id, f"🔗 PR #{pr_num}: {pr.html_url}\n\n**Recent commits:**\n{commit_text}") - -def check_unblock(issue_num): - """Check if issue can be unblocked.""" - _, blocked_by, _ = get_linked_issues(issue_num) - - # Check if all blockers are closed - all_closed = all(repo.get_issue(b).state == "closed" for b in blocked_by if b) - - if all_closed: - task_id = get_task_by_issue(issue_num) - if task_id: - # Check if has PR - has_pr = False - try: - issue = repo.get_issue(issue_num) - timeline = issue.get_timeline() - for event in timeline: - if hasattr(event, 'source') and event.source and hasattr(event.source, 'issue'): - if event.source.issue.pull_request: - has_pr = True - break - except: - pass - - new_status = "in progress" if has_pr else "to do" - update_task(task_id, status=new_status) - add_comment(task_id, "✅ Unblocked - all dependencies resolved") - -# ============================================================================ -# MAIN EVENT HANDLER -# ============================================================================ -def main(): - with open(sys.argv[1]) as f: - event = json.load(f) - - action = event.get("action") - - print(f"Event: {EVENT_NAME}.{action}") - - if EVENT_NAME == "issues": - issue_num = event["issue"]["number"] - - if action in ["opened", "edited", "reopened"]: - sync_issue_to_task(issue_num) - - elif action == "closed": - task_id = get_task_by_issue(issue_num) - if task_id: - update_task(task_id, status="done") - - # Check if this unblocks any other issues - all_issues = repo.get_issues(state="open") - for issue in all_issues: - _, blocked_by, _ = get_linked_issues(issue.number) - if issue_num in blocked_by: - check_unblock(issue.number) - - elif EVENT_NAME == "pull_request": - pr_num = event["pull_request"]["number"] - - if action in ["opened", "edited", "synchronize"]: - sync_pr_to_task(pr_num) - - elif action == "closed": - sync_pr_to_task(pr_num) - - print("✓ Sync complete") - -if __name__ == "__main__": - main() diff --git a/.github/workflows/clickup-init.yml b/.github/workflows/clickup-init.yml deleted file mode 100644 index d2fa95fa..00000000 --- a/.github/workflows/clickup-init.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Initialize ClickUp from GitHub Issues - -on: - workflow_dispatch: - -jobs: - sync: - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install requests - - - name: Run ClickUp task initialization using GitHub Issues - env: - GITHUB_REPO: lhr-solar/Embedded-Sharepoint - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN_PAT }} - CLICKUP_API_TOKEN: ${{ secrets.CLICKUP_API_TOKEN }} - CLICKUP_LIST_ID: ${{ secrets.CLICKUP_LIST_ID }} - run: | - python .github/clickup-init.py diff --git a/.github/workflows/clickup-sync.yml b/.github/workflows/clickup-sync.yml deleted file mode 100644 index 766a8419..00000000 --- a/.github/workflows/clickup-sync.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: ClickUp Sync - -on: - issues: - types: [opened, edited, closed, reopened] - pull_request: - types: [opened, edited, reopened, ready_for_review, closed, synchronize] - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install requests PyGithub - - - name: Run sync - env: - CLICKUP_API_TOKEN: ${{ secrets.CLICKUP_API_TOKEN }} - CLICKUP_LIST_ID: ${{ secrets.CLICKUP_LIST_ID }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_EVENT_NAME: ${{ github.event_name }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - python .github/clickup-sync.py "$GITHUB_EVENT_PATH" diff --git a/.gitignore b/.gitignore index fcf8c1ae..9b4188a5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,7 @@ site/* Zone.Identifier #Vector Lock File -can/dbc/**/*.ldb \ No newline at end of file +can/dbc/**/*.ldb + +# Arduino-cli files +.arduino* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 6bd0aed8..91f04dba 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2022 UT Longhorn Racing Solar +Copyright (c) 2026 UT Longhorn Racing Solar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/flake.nix b/flake.nix index 0b963030..3fde7af8 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "LHRs STM32 Embedded Dev"; + description = "LHRs Embedded Dev"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/23.05"; @@ -38,21 +38,18 @@ pkgs.parallel pkgs.sl pkgs.gcc-arm-embedded - pkgs.picocom python + pkgs.openocd ]; # Extra debug/flash tools, only if available debugPackages = if pkgs.stdenv.isLinux then [ pkgs.gdb - pkgs.openocd pkgs.stlink ] else if pkgs.stdenv.isDarwin then [ - pkgs.openocd pkgs.stlink pkgs.lldb - pkgs.openocd or null ] else []; # Remove nulls @@ -69,32 +66,12 @@ echo "${armGccMessage}" ${if armGcc != null then "export PATH=$PATH:${armGcc}/bin" else ""} - # Provide lsusb-mac alias - if [[ "$OSTYPE" == "darwin"* ]]; then - lsusb_mac() { - system_profiler SPUSBDataType - } - export -f lsusb_mac - echo "Run: lsusb_mac (macOS USB info)" - - ls_stm32_dev_port() { - ls /dev/cu.* - } - export -f ls_stm32_dev_port - echo "On Mac run: ls_stm32_dev_port (to list STM32 serial port)" - else - echo "Run: lsusb (Linux USB info)" + if [ -f "./nix-hook.sh" ]; then + # Make it executable + chmod +x ./nix-hook.sh + source ./nix-hook.sh fi - if [ ! -d .venv ]; then - python3 -m venv .venv - echo "Creating python venv" - fi - source .venv/bin/activate - echo "Installing python requirements" - if [ -f requirements.txt ]; then - pip install -r requirements.txt - fi echo "Dev environment loaded for ${system}!" ''; }; diff --git a/nix-hook.sh b/nix-hook.sh new file mode 100755 index 00000000..d0125150 --- /dev/null +++ b/nix-hook.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Exit on error +set -e + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# --- Python Setup --- +export PIP_DISABLE_PIP_VERSION_CHECK=1 +VENV_PATH="$SCRIPT_DIR/.venv" +REQ_PATH="$SCRIPT_DIR/requirements.txt" +SENTINEL="$VENV_PATH/.pip_installed" + +if [ ! -d "$VENV_PATH" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_PATH" +fi + +source "$VENV_PATH/bin/activate" + +if [ -f "$REQ_PATH" ]; then + # Only install if the sentinel doesn't exist OR requirements.txt is newer than the sentinel + if [ ! -f "$SENTINEL" ] || [ "$REQ_PATH" -nt "$SENTINEL" ]; then + echo "Updating python requirements..." + if pip install -q -r "$REQ_PATH"; then + touch "$SENTINEL" + else + echo "Error: pip install failed." + return 1 + fi + fi +fi + + +# --- Helpers --- +if [[ "$OSTYPE" == "darwin"* ]]; then + lsusb_mac() { system_profiler SPUSBDataType; } + export -f lsusb_mac + + ls_stm32_dev_port() { ls /dev/cu.*; } + export -f ls_stm32_dev_port +fi diff --git a/nix_install.sh b/nix-install.sh similarity index 100% rename from nix_install.sh rename to nix-install.sh