mirror of
https://github.com/Heretek-AI/heretek-actions.git
synced 2026-10-11 21:09:53 +00:00
Initial commit: Heretek Actions — agent-facing GitHub Actions
Complete implementation of all 7 phases: Phase 1: Foundation - Repository directory structure - .agent/schema.json — Standard JSON envelope schema - .github/CODEOWNERS, LICENSE (MIT), .gitignore - README.md shell Phase 2: Agent-Facing Actions (5 composite actions) - agent-context — Scans repo, detects stack, queries GitHub API - pr-summary — Analyzes PR diff, categorizes files, links issues - issue-triage — Classifies, labels, detects duplicates - check-status — Watches CI checks, produces merged status - review — Lightweight structural code analysis - Shared agent-envelope.sh helper for standardized .agent/output.json Phase 3: Pre-commit Hooks - .pre-commit-config.yaml — Universal hook config - pre-commit-ci.yml — CI workflow running pre-commit Phase 4: CI Composite Actions - rust-ci — fmt, clippy, test, coverage (opt), audit (opt) - js-ci — lint, typecheck, test (npm/pnpm/yarn/bun) - python-ci — lint (ruff), typecheck (mypy), test (pytest) Phase 5: Universal CI Orchestrator - check.yml — Auto-detects languages, routes to CI actions Phase 6: Universal Release - release action — Docker, Flatpak, npm, GitHub releases - release.yml — workflow_call reusable workflow Phase 7: Documentation - Full README with quick start, per-action docs, agent integration guide Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/Heretek-AI/heretek-actions/.agent/schema.json",
|
||||
"title": "Heretek Actions Agent Envelope",
|
||||
"description": "Standard JSON envelope produced by all Heretek Actions for agent consumption",
|
||||
"type": "object",
|
||||
"required": ["agent_action", "version", "status", "summary", "created_at"],
|
||||
"properties": {
|
||||
"agent_action": {
|
||||
"description": "The action that produced this envelope",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"agent-context",
|
||||
"pr-summary",
|
||||
"issue-triage",
|
||||
"check-status",
|
||||
"review",
|
||||
"ci-rust",
|
||||
"ci-js",
|
||||
"ci-python",
|
||||
"release",
|
||||
"pre-commit"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"description": "Schema version",
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+$"
|
||||
},
|
||||
"status": {
|
||||
"description": "Overall status of the action run",
|
||||
"type": "string",
|
||||
"enum": ["success", "failure", "partial", "skipped", "cancelled"]
|
||||
},
|
||||
"summary": {
|
||||
"description": "Human-readable one-line summary of results",
|
||||
"type": "string",
|
||||
"maxLength": 200
|
||||
},
|
||||
"outputs": {
|
||||
"description": "Action-specific structured data",
|
||||
"type": "object",
|
||||
"default": {}
|
||||
},
|
||||
"suggestions": {
|
||||
"description": "Suggested actions for an agent to take based on results",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["type", "reason"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"issue:create",
|
||||
"issue:close",
|
||||
"issue:label",
|
||||
"pr:create",
|
||||
"pr:merge",
|
||||
"pr:review_request",
|
||||
"pr:comment",
|
||||
"comment:post",
|
||||
"label:add",
|
||||
"label:remove",
|
||||
"review:request",
|
||||
"release:create",
|
||||
"none"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Why the agent should take this action",
|
||||
"maxLength": 500
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Parameters for the suggested action",
|
||||
"default": {}
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["high", "medium", "low"],
|
||||
"default": "medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"description": "Individual check results (for CI-type actions)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "status"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "fail", "warn", "skip", "error"]
|
||||
},
|
||||
"summary": { "type": "string", "maxLength": 200 },
|
||||
"details_url": { "type": "string", "format": "uri" },
|
||||
"duration_ms": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"findings": {
|
||||
"description": "Individual issues found (lint errors, bugs, etc.)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["error", "warning", "info", "note"]
|
||||
},
|
||||
"rule": { "type": "string" },
|
||||
"file": { "type": "string" },
|
||||
"line": { "type": "integer", "minimum": 0 },
|
||||
"column": { "type": "integer", "minimum": 0 },
|
||||
"message": { "type": "string" },
|
||||
"suggested_fix": { "type": "string" }
|
||||
},
|
||||
"required": ["severity", "message"]
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"description": "Release information (for release-type actions)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"published": { "type": "boolean" },
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"size_bytes": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"release_url": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"created_at": {
|
||||
"description": "ISO 8601 timestamp of when this envelope was produced",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"duration_ms": {
|
||||
"description": "Total execution time in milliseconds",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"repository": {
|
||||
"description": "Repository context",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": { "type": "string" },
|
||||
"repo": { "type": "string" },
|
||||
"sha": { "type": "string" },
|
||||
"ref": { "type": "string" },
|
||||
"workflow": { "type": "string" },
|
||||
"run_id": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Default owners
|
||||
* @Heretek-AI
|
||||
|
||||
# Agent-facing actions
|
||||
/.github/actions/agent-context/ @Heretek-AI
|
||||
/.github/actions/pr-summary/ @Heretek-AI
|
||||
/.github/actions/issue-triage/ @Heretek-AI
|
||||
/.github/actions/check-status/ @Heretek-AI
|
||||
/.github/actions/review/ @Heretek-AI
|
||||
|
||||
# CI composite actions
|
||||
/.github/actions/rust-ci/ @Heretek-AI
|
||||
/.github/actions/js-ci/ @Heretek-AI
|
||||
/.github/actions/python-ci/ @Heretek-AI
|
||||
/.github/actions/release/ @Heretek-AI
|
||||
|
||||
# Workflows
|
||||
/.github/workflows/ @Heretek-AI
|
||||
|
||||
# Configuration
|
||||
/.agent/ @Heretek-AI
|
||||
/.pre-commit-config.yaml @Heretek-AI
|
||||
@@ -0,0 +1,58 @@
|
||||
name: "Agent Context"
|
||||
description: |
|
||||
Scans the repository structure and produces a machine-readable context file
|
||||
(.agent/output.json) containing stack information, workspace layout, configured
|
||||
hooks, open issues, and recent PRs for agent consumption.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
max-depth:
|
||||
description: "Maximum directory depth for structure scanning"
|
||||
default: "3"
|
||||
required: false
|
||||
include-test-files:
|
||||
description: "Whether to include test directory contents in the output"
|
||||
default: "false"
|
||||
required: false
|
||||
github-token:
|
||||
description: "GitHub token for API queries (issues, PRs)"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'agent-context'"
|
||||
value: "agent-context"
|
||||
status:
|
||||
description: "success | failure"
|
||||
value: ${{ steps.build.outputs.status }}
|
||||
summary:
|
||||
description: "Human-readable summary of repo context"
|
||||
value: ${{ steps.build.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "1"
|
||||
|
||||
- name: "Build Repo Context"
|
||||
id: build
|
||||
shell: bash
|
||||
env:
|
||||
MAX_DEPTH: ${{ inputs.max-depth }}
|
||||
INCLUDE_TESTS: ${{ inputs.include-test-files }}
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
run: |
|
||||
${{ github.action_path }}/build-context.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source the envelope helper
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
# Determine repo structure
|
||||
MAX_DEPTH="${MAX_DEPTH:-3}"
|
||||
INCLUDE_TESTS="${INCLUDE_TESTS:-false}"
|
||||
|
||||
# Detect stack
|
||||
detect_stack() {
|
||||
local stack=()
|
||||
local manager=""
|
||||
local workspaces=()
|
||||
|
||||
if [ -f "Cargo.toml" ]; then
|
||||
stack+=("rust")
|
||||
# Detect workspace members
|
||||
if grep -q "\[workspace\]" Cargo.toml 2>/dev/null; then
|
||||
while IFS= read -r member; do
|
||||
workspaces+=("$member")
|
||||
done < <(grep -A100 "\[workspace\]" Cargo.toml | grep "members" | sed 's/.*=\[//;s/\].*//;s/,/\n/g' | tr -d '" ' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "package.json" ]; then
|
||||
local pm="npm"
|
||||
if [ -f "pnpm-lock.yaml" ]; then pm="pnpm"; fi
|
||||
if [ -f "yarn.lock" ]; then pm="yarn"; fi
|
||||
if [ -f "bun.lock" ] || [ -f "bun.lockb" ]; then pm="bun"; fi
|
||||
manager="${pm}"
|
||||
stack+=("javascript")
|
||||
|
||||
# Detect framework
|
||||
if [ -f "next.config.js" ] || [ -f "next.config.mjs" ] || [ -f "next.config.ts" ]; then
|
||||
stack+=("nextjs")
|
||||
elif [ -f "nuxt.config.ts" ] || [ -f "nuxt.config.js" ]; then
|
||||
stack+=("nuxt")
|
||||
elif [ -f "astro.config.mjs" ] || [ -f "astro.config.ts" ]; then
|
||||
stack+=("astro")
|
||||
fi
|
||||
|
||||
# Detect monorepo workspaces
|
||||
if grep -q '"workspaces"' package.json 2>/dev/null; then
|
||||
while IFS= read -r ws; do
|
||||
workspaces+=("$ws")
|
||||
done < <(jq -r '.workspaces[] // empty' package.json 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "pyproject.toml" ]; then
|
||||
stack+=("python")
|
||||
if command -v poetry &>/dev/null || grep -q "\[tool.poetry\]" pyproject.toml 2>/dev/null; then
|
||||
manager="poetry"
|
||||
elif [ -f "Pipfile" ]; then
|
||||
manager="pipenv"
|
||||
elif [ -f "requirements.txt" ]; then
|
||||
manager="pip"
|
||||
elif [ -f "uv.lock" ]; then
|
||||
manager="uv"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] || [ -f "compose.yml" ]; then
|
||||
stack+=("docker")
|
||||
fi
|
||||
|
||||
if [ -f ".github/workflows/release.yml" ] || ls .github/workflows/*release* &>/dev/null 2>&1; then
|
||||
stack+=("release")
|
||||
fi
|
||||
|
||||
# Output
|
||||
echo "STACK=$(echo "${stack[@]}" | jq -Rc 'split(" ") | map(select(length > 0))')" >> "$GITHUB_ENV"
|
||||
echo "PACKAGE_MANAGER=${manager}" >> "$GITHUB_ENV"
|
||||
echo "WORKSPACES=$(echo "${workspaces[@]}" | jq -Rc 'split(" ") | map(select(length > 0))')" >> "$GITHUB_ENV"
|
||||
}
|
||||
|
||||
# Scan directory structure
|
||||
scan_structure() {
|
||||
local depth="$1"
|
||||
local outfile="$2"
|
||||
|
||||
find . -maxdepth "$depth" -type d \
|
||||
-not -path './.git/*' \
|
||||
-not -path './node_modules/*' \
|
||||
-not -path './target/*' \
|
||||
-not -path './.venv/*' \
|
||||
-not -path './__pycache__/*' \
|
||||
-not -path './.next/*' \
|
||||
-not -path './dist/*' \
|
||||
-not -path './build/*' \
|
||||
-not -path './.claude/*' \
|
||||
2>/dev/null | sort > "${outfile}.dirs"
|
||||
|
||||
find . -maxdepth "$depth" -type f \
|
||||
-not -path './.git/*' \
|
||||
-not -path './node_modules/*' \
|
||||
-not -path './target/*' \
|
||||
-not -path './.venv/*' \
|
||||
-not -path './__pycache__/*' \
|
||||
-not -path './.next/*' \
|
||||
-not -path './dist/*' \
|
||||
-not -path './build/*' \
|
||||
-not -path './.claude/*' \
|
||||
-not -name '*.lock' \
|
||||
-not -name '*.sum' \
|
||||
2>/dev/null | sort > "${outfile}.files"
|
||||
}
|
||||
|
||||
# Detect hooks config
|
||||
detect_hooks() {
|
||||
local hooks_framework=""
|
||||
local hooks_config=""
|
||||
|
||||
if [ -f ".pre-commit-config.yaml" ]; then
|
||||
hooks_framework="pre-commit"
|
||||
hooks_config=".pre-commit-config.yaml"
|
||||
elif [ -f ".husky/pre-commit" ]; then
|
||||
hooks_framework="husky"
|
||||
hooks_config=".husky/"
|
||||
elif [ -f "lefthook.yml" ] || [ -f "lefthook.yaml" ]; then
|
||||
hooks_framework="lefthook"
|
||||
hooks_config="lefthook.yml"
|
||||
fi
|
||||
|
||||
echo "HOOKS_FRAMEWORK=${hooks_framework}" >> "$GITHUB_ENV"
|
||||
echo "HOOKS_CONFIG=${hooks_config}" >> "$GITHUB_ENV"
|
||||
}
|
||||
|
||||
# Query GitHub for repo metadata (issues, PRs, labels)
|
||||
query_github() {
|
||||
local owner repo
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
if [ -z "$owner" ] || [ -z "$repo" ] || [ -z "$GH_TOKEN" ]; then
|
||||
echo "NO_GITHUB_DATA=true" >> "$GITHUB_ENV"
|
||||
return
|
||||
fi
|
||||
|
||||
# Recent merged PRs (last 5)
|
||||
RECENT_PRS=$(gh api "repos/${owner}/${repo}/pulls?state=closed&per_page=5&sort=updated&direction=desc" \
|
||||
--jq '[.[] | select(.merged_at != null) | {title, number, merged_at, labels: [.labels[].name]}] | .[0:5]' 2>/dev/null || echo "[]")
|
||||
|
||||
# Open issues count
|
||||
OPEN_ISSUES=$(gh api "repos/${owner}/${repo}/issues?state=open&per_page=1&filter=all" \
|
||||
--jq '[.[]] | length' 2>/dev/null || echo "0")
|
||||
|
||||
# Open PRs count
|
||||
OPEN_PRS=$(gh api "repos/${owner}/${repo}/pulls?state=open&per_page=1" \
|
||||
--jq '[.[]] | length' 2>/dev/null || echo "0")
|
||||
|
||||
# Labels
|
||||
LABELS=$(gh api "repos/${owner}/${repo}/labels?per_page=50" \
|
||||
--jq '[.[].name]' 2>/dev/null || echo "[]")
|
||||
|
||||
# Code owners
|
||||
CODEOWNERS=$(cat .github/CODEOWNERS 2>/dev/null || echo "")
|
||||
|
||||
echo "RECENT_PRS=${RECENT_PRS}" >> "$GITHUB_ENV"
|
||||
echo "OPEN_ISSUES=${OPEN_ISSUES}" >> "$GITHUB_ENV"
|
||||
echo "OPEN_PRS=${OPEN_PRS}" >> "$GITHUB_ENV"
|
||||
echo "LABELS=${LABELS}" >> "$GITHUB_ENV"
|
||||
echo "CODEOWNERS<<EOF" >> "$GITHUB_ENV"
|
||||
echo "${CODEOWNERS}" >> "$GITHUB_ENV"
|
||||
echo "EOF" >> "$GITHUB_ENV"
|
||||
echo "NO_GITHUB_DATA=false" >> "$GITHUB_ENV"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
echo "🔍 Building repo context..."
|
||||
|
||||
detect_stack
|
||||
detect_hooks
|
||||
query_github
|
||||
|
||||
# Build structure
|
||||
structure_tmp=$(mktemp -d)
|
||||
scan_structure "$MAX_DEPTH" "${structure_tmp}/structure"
|
||||
|
||||
# Package context into outputs
|
||||
TOP_FILES=$(head -30 "${structure_tmp}/structure.files" 2>/dev/null | jq -Rc '[inputs]' 2>/dev/null || echo "[]")
|
||||
TOP_DIRS=$(head -30 "${structure_tmp}/structure.dirs" 2>/dev/null | jq -Rc '[inputs]' 2>/dev/null || echo "[]")
|
||||
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"name": "${GITHUB_REPOSITORY#*/}",
|
||||
"owner": "${GITHUB_REPOSITORY_OWNER:-}",
|
||||
"default_branch": "${GITHUB_BASE_REF:-main}",
|
||||
"stack": ${STACK:-"[]"},
|
||||
"package_manager": "${PACKAGE_MANAGER:-}",
|
||||
"workspaces": ${WORKSPACES:-"[]"},
|
||||
"hooks": {
|
||||
"framework": "${HOOKS_FRAMEWORK:-}",
|
||||
"config": "${HOOKS_CONFIG:-}"
|
||||
},
|
||||
"structure": {
|
||||
"files": ${TOP_FILES},
|
||||
"directories": ${TOP_DIRS}
|
||||
},
|
||||
"issues": {
|
||||
"open": ${OPEN_ISSUES:-0},
|
||||
"open_prs": ${OPEN_PRS:-0}
|
||||
},
|
||||
"recent_prs": ${RECENT_PRS:-"[]"},
|
||||
"labels": ${LABELS:-"[]"}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
# Determine overall summary
|
||||
SUMMARY="Repo: ${GITHUB_REPOSITORY} | Stack: ${STACK} | Open Issues: ${OPEN_ISSUES:-0} | Open PRs: ${OPEN_PRS:-0}"
|
||||
|
||||
# Save context to .claude/context.json for Claude users
|
||||
mkdir -p .claude
|
||||
echo "$AGENT_OUTPUTS" > .claude/context.json
|
||||
|
||||
write_envelope "agent-context" "success" "$SUMMARY"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$structure_tmp"
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
# agent-envelope.sh — Write a standardized .agent/output.json from any action
|
||||
#
|
||||
# Usage: source ./agent-envelope.sh && write_envelope "action-name" "status" "summary"
|
||||
#
|
||||
# All actions should source this file and call write_envelope at the end.
|
||||
# Override defaults by setting env vars before calling:
|
||||
# AGENT_OUTPUTS=... — JSON object for the "outputs" field
|
||||
# AGENT_SUGGESTIONS=... — JSON array for the "suggestions" field
|
||||
# AGENT_CHECKS=... — JSON array for the "checks" field
|
||||
# AGENT_FINDINGS=... — JSON array for the "findings" field
|
||||
# AGENT_RELEASE=... — JSON object for the "release" field
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${AGENT_OUTPUT_DIR:=${GITHUB_WORKSPACE:-.}/.agent}"
|
||||
: "${AGENT_ENVELOPE_FILE:=${AGENT_OUTPUT_DIR}/output.json}"
|
||||
|
||||
write_envelope() {
|
||||
local action="$1"
|
||||
local status="$2"
|
||||
local summary="$3"
|
||||
|
||||
mkdir -p "$AGENT_OUTPUT_DIR"
|
||||
|
||||
local created_at
|
||||
created_at="$(date -u +%Y-%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
# Build envelope via jq if available, otherwise cat+json
|
||||
if command -v jq &>/dev/null; then
|
||||
jq -n \
|
||||
--arg action "$action" \
|
||||
--arg version "1.0" \
|
||||
--arg status "$status" \
|
||||
--arg summary "$summary" \
|
||||
--arg created_at "$created_at" \
|
||||
--arg duration_ms "${AGENT_DURATION_MS:-0}" \
|
||||
--argjson outputs "${AGENT_OUTPUTS:-{}}" \
|
||||
--argjson suggestions "${AGENT_SUGGESTIONS:-[]}" \
|
||||
--argjson checks "${AGENT_CHECKS:-[]}" \
|
||||
--argjson findings "${AGENT_FINDINGS:-[]}" \
|
||||
--argjson release "${AGENT_RELEASE:-null}" \
|
||||
--arg repo_owner "${GITHUB_REPOSITORY_OWNER:-}" \
|
||||
--arg repo_name "${GITHUB_REPOSITORY#*/}" \
|
||||
--arg sha "${GITHUB_SHA:-}" \
|
||||
--arg ref "${GITHUB_REF:-}" \
|
||||
--arg workflow "${GITHUB_WORKFLOW:-}" \
|
||||
--arg run_id "${GITHUB_RUN_ID:-}" \
|
||||
'{
|
||||
agent_action: $action,
|
||||
version: $version,
|
||||
status: $status,
|
||||
summary: $summary,
|
||||
created_at: $created_at,
|
||||
duration_ms: ($duration_ms | tonumber),
|
||||
outputs: $outputs,
|
||||
suggestions: $suggestions,
|
||||
checks: $checks,
|
||||
findings: $findings,
|
||||
release: $release,
|
||||
repository: {
|
||||
owner: $repo_owner,
|
||||
repo: $repo_name,
|
||||
sha: $sha,
|
||||
ref: $ref,
|
||||
workflow: $workflow,
|
||||
run_id: ($run_id | tonumber?)
|
||||
}
|
||||
}' > "$AGENT_ENVELOPE_FILE"
|
||||
else
|
||||
# Fallback — simple JSON without jq
|
||||
cat > "$AGENT_ENVELOPE_FILE" <<ENVELOPE
|
||||
{
|
||||
"agent_action": "${action}",
|
||||
"version": "1.0",
|
||||
"status": "${status}",
|
||||
"summary": "${summary}",
|
||||
"created_at": "${created_at}",
|
||||
"duration_ms": ${AGENT_DURATION_MS:-0},
|
||||
"outputs": ${AGENT_OUTPUTS:-{}},
|
||||
"suggestions": ${AGENT_SUGGESTIONS:-[]},
|
||||
"checks": ${AGENT_CHECKS:-[]},
|
||||
"findings": ${AGENT_FINDINGS:-[]},
|
||||
"release": ${AGENT_RELEASE:-null},
|
||||
"repository": {
|
||||
"owner": "${GITHUB_REPOSITORY_OWNER:-}",
|
||||
"repo": "${GITHUB_REPOSITORY#*/}",
|
||||
"sha": "${GITHUB_SHA:-}",
|
||||
"ref": "${GITHUB_REF:-}",
|
||||
"workflow": "${GITHUB_WORKFLOW:-}",
|
||||
"run_id": ${GITHUB_RUN_ID:-null}
|
||||
}
|
||||
}
|
||||
ENVELOPE
|
||||
fi
|
||||
|
||||
# Also emit as GitHub Action output
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
{
|
||||
echo "agent_action=${action}"
|
||||
echo "status=${status}"
|
||||
echo "summary=${summary}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
echo "✓ Wrote agent envelope: ${AGENT_ENVELOPE_FILE}"
|
||||
echo " action=${action} status=${status} summary=${summary}"
|
||||
}
|
||||
|
||||
# Helper to add a suggestion
|
||||
add_suggestion() {
|
||||
local type="$1"
|
||||
local reason="$2"
|
||||
local data="${3:-{}}"
|
||||
local priority="${4:-medium}"
|
||||
|
||||
if [ -z "${AGENT_SUGGESTIONS:-}" ]; then
|
||||
AGENT_SUGGESTIONS='[]'
|
||||
fi
|
||||
|
||||
local suggestion
|
||||
suggestion=$(cat <<SUGGESTION
|
||||
{"type":"${type}","reason":"${reason}","data":${data},"priority":"${priority}"}
|
||||
SUGGESTION
|
||||
)
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
AGENT_SUGGESTIONS=$(echo "$AGENT_SUGGESTIONS" | jq --argjson s "$suggestion" '. + [$s]')
|
||||
else
|
||||
AGENT_SUGGESTIONS=$(echo "$AGENT_SUGGESTIONS" | sed 's/\]$/,/' | (cat - && echo "${suggestion}]")
|
||||
fi
|
||||
export AGENT_SUGGESTIONS
|
||||
}
|
||||
|
||||
# Helper to add a check result
|
||||
add_check() {
|
||||
local name="$1"
|
||||
local status="$2" # pass/fail/warn/skip/error
|
||||
local summary="${3:-}"
|
||||
|
||||
local check
|
||||
check=$(cat <<CHECK
|
||||
{"name":"${name}","status":"${status}","summary":"${summary}"}
|
||||
CHECK
|
||||
)
|
||||
|
||||
if [ -z "${AGENT_CHECKS:-}" ]; then
|
||||
AGENT_CHECKS='[]'
|
||||
fi
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
AGENT_CHECKS=$(echo "$AGENT_CHECKS" | jq --argjson c "$check" '. + [$c]')
|
||||
else
|
||||
AGENT_CHECKS=$(echo "$AGENT_CHECKS" | sed 's/\]$/,/' | (cat - && echo "${check}]")
|
||||
fi
|
||||
export AGENT_CHECKS
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
name: "Check Status"
|
||||
description: |
|
||||
Watches all CI check runs on a commit and produces a clean merged status
|
||||
summary. Agent can use this to determine if a PR is ready to merge or
|
||||
what failed and where.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
github-token:
|
||||
description: "GitHub token for API access"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
wait-for-completion:
|
||||
description: "Poll until all checks complete (max seconds)"
|
||||
default: "600"
|
||||
required: false
|
||||
poll-interval:
|
||||
description: "Seconds between polls"
|
||||
default: "15"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'check-status'"
|
||||
value: "check-status"
|
||||
status:
|
||||
description: "success | failure | running | timeout"
|
||||
value: ${{ steps.watch.outputs.status }}
|
||||
summary:
|
||||
description: "Check status summary"
|
||||
value: ${{ steps.watch.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "1"
|
||||
|
||||
- name: "Watch Checks"
|
||||
id: watch
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
WAIT_MAX: ${{ inputs.wait-for-completion }}
|
||||
POLL_INTERVAL: ${{ inputs.poll-interval }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
${{ github.action_path }}/watch-checks.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source the envelope helper
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "👀 Watching check runs for ${HEAD_SHA}..."
|
||||
|
||||
WAIT_MAX="${WAIT_MAX:-600}"
|
||||
POLL_INTERVAL="${POLL_INTERVAL:-15}"
|
||||
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
start_time=$(date +%s)
|
||||
elapsed=0
|
||||
complete=false
|
||||
|
||||
while [ "$elapsed" -lt "$WAIT_MAX" ]; do
|
||||
# Fetch check runs
|
||||
CHECKS_DATA=$(gh api "repos/${owner}/${repo}/commits/${HEAD_SHA}/check-runs" \
|
||||
--jq '{
|
||||
total_count,
|
||||
check_runs: [.check_runs[] | {
|
||||
name,
|
||||
status,
|
||||
conclusion,
|
||||
html_url,
|
||||
started_at,
|
||||
completed_at,
|
||||
app: .app.name // "unknown"
|
||||
}]
|
||||
}' 2>/dev/null || echo '{"total_count": 0, "check_runs": []}')
|
||||
|
||||
TOTAL_COUNT=$(echo "$CHECKS_DATA" | jq -r '.total_count // 0')
|
||||
INCOMPLETE=$(echo "$CHECKS_DATA" | jq '[.check_runs[] | select(.status != "completed")] | length')
|
||||
FAILURES=$(echo "$CHECKS_DATA" | jq '[.check_runs[] | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required")] | length')
|
||||
SUCCESSES=$(echo "$CHECKS_DATA" | jq '[.check_runs[] | select(.conclusion == "success" or .conclusion == "neutral")] | length')
|
||||
PENDING=$(echo "$CHECKS_DATA" | jq '[.check_runs[] | select(.status == "queued" or .status == "in_progress")] | length')
|
||||
|
||||
echo " checks: ${TOTAL_COUNT} total, ${SUCCESSES} passed, ${FAILURES} failed, ${INCOMPLETE} incomplete"
|
||||
|
||||
if [ "$INCOMPLETE" -eq 0 ] && [ "$TOTAL_COUNT" -gt 0 ]; then
|
||||
complete=true
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$TOTAL_COUNT" -eq 0 ] && [ "$elapsed" -gt 60 ]; then
|
||||
# No checks after 60 seconds — likely no CI configured
|
||||
break
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
elapsed=$(( $(date +%s) - start_time ))
|
||||
done
|
||||
|
||||
# Build check list
|
||||
CHECKS=$(echo "$CHECKS_DATA" | jq '[.check_runs[] | {name, status: (.conclusion // .status), details_url: .html_url}]')
|
||||
|
||||
# Determine overall status
|
||||
if [ "$complete" = true ]; then
|
||||
if [ "$FAILURES" -gt 0 ]; then
|
||||
OVERALL_STATUS="failure"
|
||||
SUMMARY="${FAILURES} check(s) failed, ${SUCCESSES} passed"
|
||||
else
|
||||
OVERALL_STATUS="success"
|
||||
SUMMARY="${SUCCESSES} check(s) all passed"
|
||||
fi
|
||||
elif [ "$elapsed" -ge "$WAIT_MAX" ]; then
|
||||
OVERALL_STATUS="timeout"
|
||||
SUMMARY="${PENDING} check(s) still running after ${WAIT_MAX}s timeout"
|
||||
else
|
||||
OVERALL_STATUS="running"
|
||||
SUMMARY="${TOTAL_COUNT} check(s) found, ${INCOMPLETE} still running"
|
||||
fi
|
||||
|
||||
# Build outputs
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"head_sha": "${HEAD_SHA}",
|
||||
"total_checks": ${TOTAL_COUNT},
|
||||
"passed": ${SUCCESSES},
|
||||
"failed": ${FAILURES},
|
||||
"pending": ${PENDING},
|
||||
"elapsed_seconds": ${elapsed},
|
||||
"all_complete": ${complete},
|
||||
"wait_timed_out": $( [ "$elapsed" -ge "$WAIT_MAX" ] && echo true || echo false )
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
# Build suggestions
|
||||
SUGGESTIONS='[]'
|
||||
if [ "$OVERALL_STATUS" = "success" ]; then
|
||||
if [ -n "${PR_NUMBER:-}" ]; then
|
||||
add_suggestion "pr:merge" "All checks passed" \
|
||||
"{\"pr_number\": ${PR_NUMBER}, \"merge_method\": \"squash\"}" "high"
|
||||
fi
|
||||
elif [ "$OVERALL_STATUS" = "failure" ]; then
|
||||
add_suggestion "comment:post" "${FAILURES} check(s) failed — review details before proceeding" "{}" "high"
|
||||
fi
|
||||
|
||||
write_envelope "check-status" "$OVERALL_STATUS" "$SUMMARY"
|
||||
|
||||
echo "status=${OVERALL_STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,67 @@
|
||||
name: "Issue Triage"
|
||||
description: |
|
||||
Classifies new GitHub issues, suggests labels, detects duplicates,
|
||||
auto-assigns priorities, and posts a structured comment.
|
||||
Outputs .agent/output.json for agent consumption.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
github-token:
|
||||
description: "GitHub token for API access"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
auto-label:
|
||||
description: "Auto-apply labels to issues"
|
||||
default: "true"
|
||||
required: false
|
||||
post-comment:
|
||||
description: "Post triage results as an issue comment"
|
||||
default: "true"
|
||||
required: false
|
||||
duplicate-threshold:
|
||||
description: "Minimum similarity score (0-1) to flag as potential duplicate"
|
||||
default: "0.7"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'issue-triage'"
|
||||
value: "issue-triage"
|
||||
status:
|
||||
description: "success | failure"
|
||||
value: ${{ steps.triage.outputs.status }}
|
||||
summary:
|
||||
description: "Triage summary text"
|
||||
value: ${{ steps.triage.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "1"
|
||||
|
||||
- name: "Triage Issue"
|
||||
id: triage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
AUTO_LABEL: ${{ inputs.auto-label }}
|
||||
POST_COMMENT: ${{ inputs.post-comment }}
|
||||
DUPLICATE_THRESHOLD: ${{ inputs.duplicate-threshold }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
run: |
|
||||
${{ github.action_path }}/triage-issue.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source the envelope helper
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "🏷️ Triaging Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}"
|
||||
|
||||
AUTO_LABEL="${AUTO_LABEL:-true}"
|
||||
POST_COMMENT="${POST_COMMENT:-true}"
|
||||
DUPLICATE_THRESHOLD="${DUPLICATE_THRESHOLD:-0.7}"
|
||||
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
# Classify issue type from title and body
|
||||
classify_issue() {
|
||||
local title="$1"
|
||||
local body="$2"
|
||||
local combined
|
||||
combined=$(echo "${title} ${body}" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
local labels_to_add=()
|
||||
|
||||
# Bug detection
|
||||
if echo "$combined" | grep -qiE '\b(bug|crash|error|fail|wrong|incorrect|broken|issue|fix|regression|panic|exception)\b'; then
|
||||
labels_to_add+=("bug")
|
||||
fi
|
||||
|
||||
# Feature request
|
||||
if echo "$combined" | grep -qiE '\b(feature|request|suggest|idea|would be nice|enhancement|want|wishlist|new:\|add:)\b'; then
|
||||
labels_to_add+=("enhancement")
|
||||
fi
|
||||
|
||||
# Question
|
||||
if echo "$combined" | grep -qiE '\b(how|question|help|does this|can you|example|tutorial)\b.*\?'; then
|
||||
labels_to_add+=("question")
|
||||
fi
|
||||
|
||||
# Documentation
|
||||
if echo "$combined" | grep -qiE '\b(doc|documentation|readme|typo|spelling|comment)\b'; then
|
||||
labels_to_add+=("documentation")
|
||||
fi
|
||||
|
||||
# Security
|
||||
if echo "$combined" | grep -qiE '\b(security|cve|vulnerability|xss|injection|exploit|auth|permission)\b'; then
|
||||
labels_to_add+=("security")
|
||||
fi
|
||||
|
||||
# Performance
|
||||
if echo "$combined" | grep -qiE '\b(performance|slow|latency|memory|leak|optimize|bottleneck)\b'; then
|
||||
labels_to_add+=("performance")
|
||||
fi
|
||||
|
||||
# Needs more info
|
||||
if [ -z "${body:-}" ] || [ "${#body}" -lt 30 ]; then
|
||||
labels_to_add+=("needs-more-info")
|
||||
fi
|
||||
|
||||
echo "${labels_to_add[@]}"
|
||||
}
|
||||
|
||||
# Check for potential duplicates
|
||||
check_duplicates() {
|
||||
local title="$1"
|
||||
local threshold="$2"
|
||||
|
||||
# Get recent open issues
|
||||
local open_issues
|
||||
open_issues=$(gh api "repos/${owner}/${repo}/issues?state=open&per_page=50" \
|
||||
--jq '[.[] | {number, title, body: (.body // ""), labels: [.labels[].name]}]' 2>/dev/null || echo '[]')
|
||||
|
||||
# Simple word-overlap similarity
|
||||
local title_words
|
||||
title_words=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | sort -u | grep -v '^$')
|
||||
|
||||
local duplicates=()
|
||||
|
||||
while IFS= read -r item; do
|
||||
local existing_title
|
||||
existing_title=$(echo "$item" | jq -r '.title // ""' | tr '[:upper:]' '[:lower:]')
|
||||
local existing_num
|
||||
existing_num=$(echo "$item" | jq -r '.number')
|
||||
|
||||
if [ "$existing_num" = "${ISSUE_NUMBER}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Count word overlap
|
||||
local overlap=0
|
||||
local total=0
|
||||
for word in $title_words; do
|
||||
total=$((total + 1))
|
||||
if echo "$existing_title" | grep -qi "\b${word}\b"; then
|
||||
overlap=$((overlap + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$total" -gt 0 ]; then
|
||||
local similarity
|
||||
similarity=$(echo "scale=2; ${overlap} / ${total}" | bc 2>/dev/null || echo "0")
|
||||
if (( $(echo "$similarity >= $threshold" | bc -l 2>/dev/null || echo 0) )); then
|
||||
duplicates+=("$existing_num")
|
||||
fi
|
||||
fi
|
||||
done < <(echo "$open_issues" | jq -c '.[]' 2>/dev/null || echo '')
|
||||
|
||||
echo "${duplicates[@]}"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
# Classify
|
||||
LABELS=($(classify_issue "${ISSUE_TITLE:-}" "${ISSUE_BODY:-}"))
|
||||
DUPLICATES=($(check_duplicates "${ISSUE_TITLE:-}" "$DUPLICATE_THRESHOLD"))
|
||||
|
||||
# Apply labels
|
||||
LABELS_JSON='[]'
|
||||
if [ "$AUTO_LABEL" = "true" ] && [ ${#LABELS[@]} -gt 0 ]; then
|
||||
# Create labels if they don't exist (no-op if they already do)
|
||||
for label in "${LABELS[@]}"; do
|
||||
gh api "repos/${owner}/${repo}/labels" --field name="$label" --field color="ededed" > /dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# Apply labels to issue
|
||||
gh api "repos/${owner}/${repo}/issues/${ISSUE_NUMBER}/labels" \
|
||||
--field labels="[$(printf '"%s",' "${LABELS[@]}" | sed 's/,$//')]" > /dev/null 2>&1 || true
|
||||
|
||||
LABELS_JSON=$(printf '%s\n' "${LABELS[@]}" | jq -Rc '[inputs | select(length > 0)]')
|
||||
fi
|
||||
|
||||
# Determine priority
|
||||
PRIORITY="medium"
|
||||
for label in "${LABELS[@]}"; do
|
||||
case "$label" in
|
||||
security) PRIORITY="high" ;;
|
||||
bug) PRIORITY="high" ;;
|
||||
performance) PRIORITY="medium" ;;
|
||||
enhancement) PRIORITY="low" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build outputs
|
||||
DUPLICATE_JSON='[]'
|
||||
if [ ${#DUPLICATES[@]} -gt 0 ]; then
|
||||
DUPLICATE_JSON=$(printf '%s\n' "${DUPLICATES[@]}" | jq -Rc '[inputs | {issue: ., reason: "similar title"}]')
|
||||
fi
|
||||
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"issue_number": ${ISSUE_NUMBER},
|
||||
"title": "${ISSUE_TITLE:-}",
|
||||
"author": "${ISSUE_AUTHOR:-}",
|
||||
"classification": {
|
||||
"labels": ${LABELS_JSON},
|
||||
"priority": "${PRIORITY}",
|
||||
"auto_assigned": ${AUTO_LABEL}
|
||||
},
|
||||
"potential_duplicates": ${DUPLICATE_JSON},
|
||||
"needs_more_info": $(echo "${LABELS_JSON}" | jq 'contains(["needs-more-info"])')
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
SUMMARY="Issue #${ISSUE_NUMBER}: classified as ${LABELS[*]:-(uncategorized)} | priority: ${PRIORITY}"
|
||||
if [ ${#DUPLICATES[@]} -gt 0 ]; then
|
||||
SUMMARY="${SUMMARY} | potential duplicates: #${DUPLICATES[*]}"
|
||||
fi
|
||||
|
||||
# Build suggestions
|
||||
SUGGESTIONS='[]'
|
||||
if [ ${#DUPLICATES[@]} -gt 0 ]; then
|
||||
add_suggestion "issue:close" "Potential duplicate of issues: #${DUPLICATES[*]}" \
|
||||
"{\"duplicates\": [${DUPLICATES[*]}]}" "medium"
|
||||
fi
|
||||
if echo "${LABELS_JSON}" | jq -e 'contains(["needs-more-info"])' > /dev/null 2>&1; then
|
||||
add_suggestion "comment:post" "Request more information from the author" "{}" "medium"
|
||||
fi
|
||||
if [ "$PRIORITY" = "high" ]; then
|
||||
add_suggestion "review:request" "High priority issue — needs maintainer attention" "{}" "high"
|
||||
fi
|
||||
|
||||
write_envelope "issue-triage" "success" "$SUMMARY"
|
||||
|
||||
# Post triage comment
|
||||
if [ "$POST_COMMENT" = "true" ]; then
|
||||
DUPLICATE_TEXT=""
|
||||
if [ ${#DUPLICATES[@]} -gt 0 ]; then
|
||||
DUPLICATE_TEXT="⚠️ **Potential duplicates detected:** "
|
||||
for dup in "${DUPLICATES[@]}"; do
|
||||
DUPLICATE_TEXT="${DUPLICATE_TEXT}[#${dup}](https://github.com/${owner}/${repo}/issues/${dup}) "
|
||||
done
|
||||
DUPLICATE_TEXT="${DUPLICATE_TEXT}
|
||||
|
||||
_If this is a duplicate, please close this issue._"
|
||||
fi
|
||||
|
||||
COMMENT="## 🤖 Triage Summary
|
||||
|
||||
**Classification:** ${LABELS[*]:-(uncategorized)}
|
||||
**Priority:** ${PRIORITY}
|
||||
|
||||
**Labels applied:**
|
||||
$(echo "$LABELS_JSON" | jq -r '.[] | "- \`\(.)\`"')
|
||||
|
||||
${DUPLICATE_TEXT}
|
||||
|
||||
---
|
||||
_Automated triage by [heretek-actions/issue-triage](https://github.com/Heretek-AI/heretek-actions)_"
|
||||
|
||||
gh api "repos/${owner}/${repo}/issues/${ISSUE_NUMBER}/comments" \
|
||||
--field body="${COMMENT}" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,177 @@
|
||||
name: "JavaScript/TypeScript CI"
|
||||
description: |
|
||||
Composite action for JS/TS projects: setup Node, cache, install deps,
|
||||
lint, typecheck, and test. Supports npm, pnpm, yarn, and bun.
|
||||
Outputs .agent/output.json.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: "Working directory"
|
||||
default: "."
|
||||
required: false
|
||||
package-manager:
|
||||
description: "Package manager: npm, pnpm, yarn, bun, or auto (default)"
|
||||
default: "auto"
|
||||
required: false
|
||||
node-version:
|
||||
description: "Node.js version"
|
||||
default: "20"
|
||||
required: false
|
||||
lint-command:
|
||||
description: "Override lint command"
|
||||
default: ""
|
||||
required: false
|
||||
typecheck-command:
|
||||
description: "Override typecheck command"
|
||||
default: ""
|
||||
required: false
|
||||
test-command:
|
||||
description: "Override test command"
|
||||
default: ""
|
||||
required: false
|
||||
lint-continue-on-error:
|
||||
description: "Continue if lint fails"
|
||||
default: "false"
|
||||
required: false
|
||||
test-continue-on-error:
|
||||
description: "Continue if tests fail"
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'ci-js'"
|
||||
value: "ci-js"
|
||||
status:
|
||||
description: "success | failure | partial"
|
||||
value: ${{ steps.envelope.outputs.status }}
|
||||
summary:
|
||||
description: "CI summary"
|
||||
value: ${{ steps.envelope.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Detect Package Manager"
|
||||
id: detect
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
PM="${{ inputs.package-manager }}"
|
||||
if [ "$PM" = "auto" ]; then
|
||||
if [ -f "pnpm-lock.yaml" ]; then PM="pnpm"
|
||||
elif [ -f "yarn.lock" ]; then PM="yarn"
|
||||
elif [ -f "bun.lock" ] || [ -f "bun.lockb" ]; then PM="bun"
|
||||
else PM="npm"
|
||||
fi
|
||||
fi
|
||||
echo "pm=${PM}" >> "$GITHUB_OUTPUT"
|
||||
echo "manager: ${PM}"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: "Setup pnpm"
|
||||
if: steps.detect.outputs.pm == 'pnpm'
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: "Setup Bun"
|
||||
if: steps.detect.outputs.pm == 'bun'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: "Cache Dependencies"
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ inputs.working-directory }}/node_modules
|
||||
${{ inputs.working-directory }}/.pnpm-store
|
||||
key: ${{ runner.os }}-${{ steps.detect.outputs.pm }}-${{ hashFiles(format('{0}/{1}', inputs.working-directory, steps.detect.outputs.pm == 'pnpm' && 'pnpm-lock.yaml' || steps.detect.outputs.pm == 'yarn' && 'yarn.lock' || steps.detect.outputs.pm == 'bun' && 'bun.lock' || 'package-lock.json')) }}
|
||||
|
||||
- name: "Install Dependencies"
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
case "${{ steps.detect.outputs.pm }}" in
|
||||
pnpm) pnpm install --frozen-lockfile ;;
|
||||
yarn) yarn install --frozen-lockfile ;;
|
||||
bun) bun install --frozen-lockfile ;;
|
||||
*) npm ci ;;
|
||||
esac
|
||||
|
||||
- name: "Lint"
|
||||
id: lint
|
||||
continue-on-error: ${{ inputs.lint-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.lint-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
case "${{ steps.detect.outputs.pm }}" in
|
||||
pnpm) CMD="pnpm lint" ;;
|
||||
yarn) CMD="yarn lint" ;;
|
||||
bun) CMD="bun run lint" ;;
|
||||
*) CMD="npx eslint ." ;;
|
||||
esac
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .lint-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "TypeCheck"
|
||||
id: typecheck
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.typecheck-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
if [ -f "tsconfig.json" ]; then
|
||||
case "${{ steps.detect.outputs.pm }}" in
|
||||
pnpm) CMD="pnpm typecheck || pnpm exec tsc --noEmit" ;;
|
||||
yarn) CMD="yarn typecheck || yarn tsc --noEmit" ;;
|
||||
bun) CMD="bun run typecheck || bun x tsc --noEmit" ;;
|
||||
*) CMD="npx tsc --noEmit" ;;
|
||||
esac
|
||||
else
|
||||
echo "No tsconfig.json — skipping typecheck"
|
||||
echo "exit_code=0" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .typecheck-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Test"
|
||||
id: test
|
||||
continue-on-error: ${{ inputs.test-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.test-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
case "${{ steps.detect.outputs.pm }}" in
|
||||
pnpm) CMD="pnpm test" ;;
|
||||
yarn) CMD="yarn test" ;;
|
||||
bun) CMD="bun test" ;;
|
||||
*) CMD="npm test" ;;
|
||||
esac
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .test-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Generate Agent Envelope"
|
||||
id: envelope
|
||||
shell: bash
|
||||
env:
|
||||
LINT_EXIT: ${{ steps.lint.outputs.exit_code }}
|
||||
TC_EXIT: ${{ steps.typecheck.outputs.exit_code }}
|
||||
TEST_EXIT: ${{ steps.test.outputs.exit_code }}
|
||||
run: |
|
||||
${{ github.action_path }}/write-envelope.sh
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "📝 Generating JS/TS CI envelope..."
|
||||
|
||||
LINT_OK=1; [ "${LINT_EXIT:-0}" = "0" ] 2>/dev/null && LINT_OK=0 || true
|
||||
TC_OK=1; [ "${TC_EXIT:-0}" = "0" ] 2>/dev/null && TC_OK=0 || true
|
||||
TEST_OK=1; [ "${TEST_EXIT:-0}" = "0" ] 2>/dev/null && TEST_OK=0 || true
|
||||
|
||||
ERRORS=0
|
||||
[ "$LINT_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$TC_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$TEST_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
STATUS="failure"
|
||||
else
|
||||
STATUS="success"
|
||||
fi
|
||||
|
||||
SUMMARY="js-ci: lint=$([ "$LINT_OK" -eq 0 ] && echo '✓' || echo '✗') typecheck=$([ "$TC_OK" -eq 0 ] && echo '✓' || echo '✗') test=$([ "$TEST_OK" -eq 0 ] && echo '✓' || echo '✗')"
|
||||
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"package_manager": "${{ steps.detect.outputs.pm }}",
|
||||
"checks": {
|
||||
"lint": {"passed": $([ "$LINT_OK" -eq 0 ] && echo 'true' || echo 'false')},
|
||||
"typecheck": {"passed": $([ "$TC_OK" -eq 0 ] && echo 'true' || echo 'false')},
|
||||
"test": {"passed": $([ "$TEST_OK" -eq 0 ] && echo 'true' || echo 'false')}
|
||||
}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
write_envelope "ci-js" "$STATUS" "$SUMMARY"
|
||||
|
||||
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,74 @@
|
||||
name: "PR Summary"
|
||||
description: |
|
||||
Analyzes a pull request diff and generates a structured summary with
|
||||
file breakdown, change statistics, and linked issues.
|
||||
Outputs both a PR comment and .agent/output.json.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
github-token:
|
||||
description: "GitHub token for API access"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
max-files:
|
||||
description: "Maximum files to include in detailed breakdown"
|
||||
default: "50"
|
||||
required: false
|
||||
include-diff-stats:
|
||||
description: "Include per-file diff stats in output"
|
||||
default: "true"
|
||||
required: false
|
||||
include-suggested-reviewers:
|
||||
description: "Suggest reviewers from CODEOWNERS"
|
||||
default: "true"
|
||||
required: false
|
||||
post-comment:
|
||||
description: "Post summary as a PR comment"
|
||||
default: "true"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'pr-summary'"
|
||||
value: "pr-summary"
|
||||
status:
|
||||
description: "success | failure"
|
||||
value: ${{ steps.analyze.outputs.status }}
|
||||
summary:
|
||||
description: "PR summary text"
|
||||
value: ${{ steps.analyze.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: "Analyze PR"
|
||||
id: analyze
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
MAX_FILES: ${{ inputs.max-files }}
|
||||
INCLUDE_DIFF_STATS: ${{ inputs.include-diff-stats }}
|
||||
INCLUDE_REVIEWERS: ${{ inputs.include-suggested-reviewers }}
|
||||
POST_COMMENT: ${{ inputs.post-comment }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_BASE: ${{ github.event.pull_request.base.ref }}
|
||||
PR_HEAD: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
${{ github.action_path }}/analyze-pr.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source the envelope helper
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "📝 Analyzing PR #${PR_NUMBER}: ${PR_TITLE}"
|
||||
|
||||
MAX_FILES="${MAX_FILES:-50}"
|
||||
INCLUDE_DIFF_STATS="${INCLUDE_DIFF_STATS:-true}"
|
||||
INCLUDE_REVIEWERS="${INCLUDE_REVIEWERS:-true}"
|
||||
POST_COMMENT="${POST_COMMENT:-true}"
|
||||
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
# Get PR diff stats
|
||||
PR_DATA=$(gh api "repos/${owner}/${repo}/pulls/${PR_NUMBER}" --jq '{
|
||||
additions, deletions, changed_files,
|
||||
labels: [.labels[].name],
|
||||
milestone: .milestone.title // null,
|
||||
draft
|
||||
}' 2>/dev/null || echo '{}')
|
||||
|
||||
ADDITIONS=$(echo "$PR_DATA" | jq -r '.additions // 0')
|
||||
DELETIONS=$(echo "$PR_DATA" | jq -r '.deletions // 0')
|
||||
CHANGED_FILES=$(echo "$PR_DATA" | jq -r '.changed_files // 0')
|
||||
DRAFT=$(echo "$PR_DATA" | jq -r '.draft // false')
|
||||
PR_LABELS=$(echo "$PR_DATA" | jq -r '.labels // []')
|
||||
|
||||
# Get file list with diff stats
|
||||
FILES_DATA=$(gh api "repos/${owner}/${repo}/pulls/${PR_NUMBER}/files?per_page=${MAX_FILES}" \
|
||||
--jq '[.[] | {path, status, additions, deletions, changes, blob_url}]' 2>/dev/null || echo '[]')
|
||||
|
||||
# Categorize files by type
|
||||
FILE_TYPES=$(echo "$FILES_DATA" | jq -r '
|
||||
group_by(.path | split(".") | last // "unknown") |
|
||||
map({type: .[0].path | split(".") | last // "unknown", count: length, changes: (map(.changes) | add)}) |
|
||||
sort_by(-.changes)
|
||||
')
|
||||
|
||||
CATEGORIES=$(echo "$FILES_DATA" | jq -r '
|
||||
map(
|
||||
if .path | test("^src/|^lib/|^app/|^packages/|^crates/|^components/") then "source"
|
||||
elif .path | test("^test|^spec|^__tests__|_test\\.") then "test"
|
||||
elif .path | test("\\.(md|txt|rst|adoc)$") then "docs"
|
||||
elif .path | test("\\.(ya?ml|json|toml|ini|cfg)$") then "config"
|
||||
elif .path | test("\\.(css|scss|less|sass|styled\\.)" ) then "styles"
|
||||
elif .path | test("\\.(svg|png|jpg|gif|ico|webp)$") then "assets"
|
||||
elif .path | test("^ci/|^\\.github/|Dockerfile") then "ci"
|
||||
else "other"
|
||||
end
|
||||
) | group_by(.) | map({category: .[0], count: length}) | sort_by(-.count)
|
||||
')
|
||||
|
||||
# Extract linked issues from PR body and branch name
|
||||
EXTRACTED_ISSUES=$(echo "${PR_BODY:-}" | grep -oE '#[0-9]+' | tr -d '#' | sort -u | jq -Rc '[inputs | {issue: ., source: "pr_body"}]' 2>/dev/null || echo '[]')
|
||||
BRANCH_ISSUES=$(echo "${PR_HEAD:-}" | grep -oE '[0-9]+' | head -3 | jq -Rc '[{issue: ., source: "branch_name"}]' 2>/dev/null || echo '[]')
|
||||
|
||||
# Suggest reviewers from CODEOWNERS if available
|
||||
SUGGESTED_REVIEWERS="[]"
|
||||
if [ "$INCLUDE_REVIEWERS" = "true" ] && [ -f ".github/CODEOWNERS" ]; then
|
||||
CHANGED_PATHS=$(echo "$FILES_DATA" | jq -r '.[].path')
|
||||
# Simple heuristic: match CODEOWNERS patterns against changed files
|
||||
while IFS= read -r pattern_line; do
|
||||
pattern=$(echo "$pattern_line" | awk '{print $1}' || true)
|
||||
owners=$(echo "$pattern_line" | awk '{$1=""; print $0}' || true)
|
||||
if [ -n "$pattern" ] && [ -n "$owners" ]; then
|
||||
while IFS= read -r changed_file; do
|
||||
if [[ "$changed_file" == $pattern ]] 2>/dev/null; then
|
||||
for owner in $owners; do
|
||||
SUGGESTED_REVIEWERS=$(echo "$SUGGESTED_REVIEWERS" | jq --arg o "$owner" --arg f "$changed_file" \
|
||||
'. + [{"owner": $o, "file": $f}]' 2>/dev/null || echo "$SUGGESTED_REVIEWERS")
|
||||
done
|
||||
fi
|
||||
done <<< "$CHANGED_PATHS"
|
||||
fi
|
||||
done < <(grep -v '^\s*#' .github/CODEOWNERS | grep -v '^\s*$' || true)
|
||||
fi
|
||||
|
||||
# Build outputs
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"pr_number": ${PR_NUMBER},
|
||||
"title": "${PR_TITLE:-}",
|
||||
"author": "${PR_AUTHOR:-}",
|
||||
"base": "${PR_BASE:-}",
|
||||
"head": "${PR_HEAD:-}",
|
||||
"draft": ${DRAFT},
|
||||
"stats": {
|
||||
"additions": ${ADDITIONS},
|
||||
"deletions": ${DELETIONS},
|
||||
"changed_files": ${CHANGED_FILES},
|
||||
"labels": ${PR_LABELS}
|
||||
},
|
||||
"file_types": ${FILE_TYPES},
|
||||
"categories": ${CATEGORIES},
|
||||
"files": ${FILES_DATA},
|
||||
"linked_issues": {
|
||||
"from_pr_body": ${EXTRACTED_ISSUES},
|
||||
"from_branch": ${BRANCH_ISSUES}
|
||||
},
|
||||
"suggested_reviewers": ${SUGGESTED_REVIEWERS}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
# Build summary
|
||||
SUMMARY="${CHANGED_FILES} files changed, +${ADDITIONS}/-${DELETIONS} lines"
|
||||
|
||||
# Build suggestions
|
||||
SUGGESTIONS='[]'
|
||||
add_suggestion "none" "PR data collected for agent processing" "{}" "low"
|
||||
|
||||
# Suggest review if large PR
|
||||
if [ "$CHANGED_FILES" -gt 10 ] || [ "$ADDITIONS" -gt 500 ]; then
|
||||
if [ -n "$SUGGESTED_REVIEWERS" ] && [ "$SUGGESTED_REVIEWERS" != "[]" ]; then
|
||||
REVIEWER_DATA=$(echo "$SUGGESTED_REVIEWERS" | jq '[.[].owner] | unique')
|
||||
add_suggestion "review:request" "Large PR ($CHANGED_FILES files, +$ADDITIONS) — request review from code owners" "${REVIEWER_DATA}" "high"
|
||||
else
|
||||
add_suggestion "review:request" "Large PR ($CHANGED_FILES files, +$ADDITIONS) — request review" "{}" "medium"
|
||||
fi
|
||||
fi
|
||||
|
||||
write_envelope "pr-summary" "success" "$SUMMARY"
|
||||
|
||||
# Post PR comment if enabled
|
||||
if [ "$POST_COMMENT" = "true" ]; then
|
||||
COMMENT=$(cat <<EOCOMMENT
|
||||
## 📋 PR Summary
|
||||
|
||||
**${PR_TITLE}** by @${PR_AUTHOR}
|
||||
|
||||
### Stats
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Files Changed | ${CHANGED_FILES} |
|
||||
| Additions | +${ADDITIONS} |
|
||||
| Deletions | -${DELETIONS} |
|
||||
| Draft | ${DRAFT} |
|
||||
|
||||
### File Categories
|
||||
$(echo "$CATEGORIES" | jq -r '.[] | "- **\(.category)**: \(.count) files"')
|
||||
|
||||
### File Types
|
||||
$(echo "$FILE_TYPES" | jq -r '.[] | " - \`\(.type)\`: \(.count) files, \(.changes) changes"')
|
||||
|
||||
### Linked Issues
|
||||
$(echo "$EXTRACTED_ISSUES" | jq -r '.[] | " - #\(.issue) (from PR body)"' 2>/dev/null || echo "_None detected_")
|
||||
|
||||
### Suggested Reviewers
|
||||
$(echo "$SUGGESTED_REVIEWERS" | jq -r '[.[].owner] | unique | .[] | "- @\(.)"' 2>/dev/null || echo "_Auto-detected from CODEOWNERS_")
|
||||
|
||||
---
|
||||
_Generated by [heretek-actions/pr-summary](https://github.com/Heretek-AI/heretek-actions)_
|
||||
EOCOMMENT
|
||||
)
|
||||
|
||||
gh api "repos/${owner}/${repo}/issues/${PR_NUMBER}/comments" \
|
||||
--field body="${COMMENT}" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=PR #${PR_NUMBER}: ${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,154 @@
|
||||
name: "Python CI"
|
||||
description: |
|
||||
Composite action for Python projects: setup Python, cache, install deps,
|
||||
lint (via ruff), typecheck (via mypy), and test (via pytest).
|
||||
Outputs .agent/output.json.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: "Working directory"
|
||||
default: "."
|
||||
required: false
|
||||
python-version:
|
||||
description: "Python version"
|
||||
default: "3.12"
|
||||
required: false
|
||||
package-manager:
|
||||
description: "Package manager: pip, pipenv, poetry, uv, or auto (default)"
|
||||
default: "auto"
|
||||
required: false
|
||||
lint-command:
|
||||
description: "Override lint command"
|
||||
default: ""
|
||||
required: false
|
||||
test-command:
|
||||
description: "Override test command"
|
||||
default: ""
|
||||
required: false
|
||||
lint-continue-on-error:
|
||||
description: "Continue if lint fails"
|
||||
default: "false"
|
||||
required: false
|
||||
test-continue-on-error:
|
||||
description: "Continue if tests fail"
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'ci-python'"
|
||||
value: "ci-python"
|
||||
status:
|
||||
description: "success | failure | partial"
|
||||
value: ${{ steps.envelope.outputs.status }}
|
||||
summary:
|
||||
description: "CI summary"
|
||||
value: ${{ steps.envelope.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Detect Package Manager"
|
||||
id: detect
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
PM="${{ inputs.package-manager }}"
|
||||
if [ "$PM" = "auto" ]; then
|
||||
if grep -q "\[tool.poetry\]" pyproject.toml 2>/dev/null; then PM="poetry"
|
||||
elif [ -f "Pipfile" ]; then PM="pipenv"
|
||||
elif [ -f "uv.lock" ]; then PM="uv"
|
||||
else PM="pip"
|
||||
fi
|
||||
fi
|
||||
echo "pm=${PM}" >> "$GITHUB_OUTPUT"
|
||||
echo "manager: ${PM}"
|
||||
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: "Cache Dependencies"
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ inputs.working-directory }}/.venv
|
||||
${{ inputs.working-directory }}/__pypackages__
|
||||
~/.cache/pip
|
||||
~/.cache/pypoetry
|
||||
key: ${{ runner.os }}-py${{ inputs.python-version }}-${{ hashFiles(format('{0}/{1}', inputs.working-directory, inputs.package-manager == 'poetry' && 'poetry.lock' || inputs.package-manager == 'pipenv' && 'Pipfile.lock' || 'pyproject.toml')) }}
|
||||
|
||||
- name: "Install Dependencies"
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
case "${{ steps.detect.outputs.pm }}" in
|
||||
poetry)
|
||||
pip install poetry
|
||||
poetry install
|
||||
;;
|
||||
pipenv)
|
||||
pip install pipenv
|
||||
pipenv install --dev
|
||||
;;
|
||||
uv)
|
||||
pip install uv
|
||||
uv sync
|
||||
;;
|
||||
*)
|
||||
pip install -e ".[dev]" 2>/dev/null || pip install -r requirements.txt 2>/dev/null || pip install -r requirements-dev.txt 2>/dev/null || true
|
||||
pip install ruff pytest mypy
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: "Lint (ruff)"
|
||||
id: lint
|
||||
continue-on-error: ${{ inputs.lint-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.lint-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
CMD="ruff check ."
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .lint-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "TypeCheck (mypy)"
|
||||
id: typecheck
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
if [ -f "mypy.ini" ] || [ -f ".mypy.ini" ] || grep -q "\[tool.mypy" pyproject.toml 2>/dev/null; then
|
||||
mypy . 2>&1 | tee .typecheck-output.txt
|
||||
else
|
||||
echo "No mypy config — skipping typecheck"
|
||||
fi
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Test (pytest)"
|
||||
id: test
|
||||
continue-on-error: ${{ inputs.test-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.test-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
CMD="python -m pytest . --junitxml=test-results.xml -v"
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .test-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Generate Agent Envelope"
|
||||
id: envelope
|
||||
shell: bash
|
||||
env:
|
||||
LINT_EXIT: ${{ steps.lint.outputs.exit_code }}
|
||||
TC_EXIT: ${{ steps.typecheck.outputs.exit_code }}
|
||||
TEST_EXIT: ${{ steps.test.outputs.exit_code }}
|
||||
run: |
|
||||
${{ github.action_path }}/write-envelope.sh
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "📝 Generating Python CI envelope..."
|
||||
|
||||
LINT_OK=1; [ "${LINT_EXIT:-0}" = "0" ] 2>/dev/null && LINT_OK=0 || true
|
||||
TC_OK=1; [ "${TC_EXIT:-0}" = "0" ] 2>/dev/null && TC_OK=0 || true
|
||||
TEST_OK=1; [ "${TEST_EXIT:-0}" = "0" ] 2>/dev/null && TEST_OK=0 || true
|
||||
|
||||
# Parse test output for pass/fail counts
|
||||
TEST_PASS=0
|
||||
TEST_FAIL=0
|
||||
if [ -f .test-output.txt ]; then
|
||||
grep -E 'passed|failed' .test-output.txt | tail -1 | while IFS= read -r line; do
|
||||
if echo "$line" | grep -qP '\d+ passed'; then
|
||||
TEST_PASS=$(echo "$line" | grep -oP '\d+(?= passed)' || echo 0)
|
||||
fi
|
||||
if echo "$line" | grep -qP '\d+ failed'; then
|
||||
TEST_FAIL=$(echo "$line" | grep -oP '\d+(?= failed)' || echo 0)
|
||||
fi
|
||||
echo "TEST_PASS=${TEST_PASS}" >> "$GITHUB_ENV"
|
||||
echo "TEST_FAIL=${TEST_FAIL}" >> "$GITHUB_ENV"
|
||||
done
|
||||
fi
|
||||
|
||||
ERRORS=0
|
||||
[ "$LINT_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$TC_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$TEST_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
STATUS="failure"
|
||||
else
|
||||
STATUS="success"
|
||||
fi
|
||||
|
||||
SUMMARY="python-ci: lint=$([ "$LINT_OK" -eq 0 ] && echo '✓' || echo '✗') typecheck=$([ "$TC_OK" -eq 0 ] && echo '✓' || echo '✗') test=${TEST_PASS:-?}✓/${TEST_FAIL:-0}✗"
|
||||
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"package_manager": "${{ steps.detect.outputs.pm }}",
|
||||
"python_version": "${{ inputs.python-version }}",
|
||||
"checks": {
|
||||
"lint": {"passed": $([ "$LINT_OK" -eq 0 ] && echo 'true' || echo 'false')},
|
||||
"typecheck": {"passed": $([ "$TC_OK" -eq 0 ] && echo 'true' || echo 'false')},
|
||||
"test": {"passed": $([ "$TEST_OK" -eq 0 ] && echo 'true' || echo 'false'), "passed_count": ${TEST_PASS:-0}, "failed_count": ${TEST_FAIL:-0}}
|
||||
}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
write_envelope "ci-python" "$STATUS" "$SUMMARY"
|
||||
|
||||
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,104 @@
|
||||
name: "Universal Release"
|
||||
description: |
|
||||
Composite action for creating releases: Docker images (ghcr.io), Flatpak
|
||||
packages, npm packages, or plain GitHub releases. Outputs .agent/output.json.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
release-type:
|
||||
description: "Release target: docker, flatpak, npm, github-release"
|
||||
required: true
|
||||
version:
|
||||
description: "Semver version (e.g., v1.2.3)"
|
||||
required: true
|
||||
github-token:
|
||||
description: "GitHub token"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
dockerfile:
|
||||
description: "Path to Dockerfile (docker type)"
|
||||
default: "./Dockerfile"
|
||||
required: false
|
||||
docker-image-name:
|
||||
description: "Docker image name (docker type)"
|
||||
default: "${{ github.repository }}"
|
||||
required: false
|
||||
docker-platforms:
|
||||
description: "Comma-separated platforms (docker type)"
|
||||
default: "linux/amd64,linux/arm64"
|
||||
required: false
|
||||
flatpak-manifest:
|
||||
description: "Path to Flatpak manifest (flatpak type)"
|
||||
default: "./build-aux/com.example.App.yml"
|
||||
required: false
|
||||
flatpak-branch:
|
||||
description: "Flatpak branch (flatpak type)"
|
||||
default: "stable"
|
||||
required: false
|
||||
npm-token:
|
||||
description: "npm authentication token (npm type)"
|
||||
required: false
|
||||
npm-dry-run:
|
||||
description: "Dry run for npm publish"
|
||||
default: "false"
|
||||
required: false
|
||||
release-notes:
|
||||
description: "Custom release notes"
|
||||
default: ""
|
||||
required: false
|
||||
generate-notes:
|
||||
description: "Auto-generate release notes from commits"
|
||||
default: "true"
|
||||
required: false
|
||||
prerelease:
|
||||
description: "Mark as prerelease"
|
||||
default: "false"
|
||||
required: false
|
||||
make-latest:
|
||||
description: "Mark as latest release"
|
||||
default: "true"
|
||||
required: false
|
||||
assets:
|
||||
description: "JSON array of asset paths to upload ({name, path} format)"
|
||||
default: "[]"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'release'"
|
||||
value: "release"
|
||||
status:
|
||||
description: "success | failure"
|
||||
value: ${{ steps.release.outputs.status }}
|
||||
summary:
|
||||
description: "Release summary"
|
||||
value: ${{ steps.release.outputs.summary }}
|
||||
release_url:
|
||||
description: "URL of the created release"
|
||||
value: ${{ steps.release.outputs.release_url }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: "Run Release"
|
||||
id: release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
NPM_TOKEN: ${{ inputs.npm-token }}
|
||||
run: |
|
||||
${{ github.action_path }}/run-release.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "🚀 Running release: ${{ inputs.release-type }} version=${{ inputs.version }}"
|
||||
|
||||
RELEASE_TYPE="${{ inputs.release-type }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
NOTES="${{ inputs.release-notes }}"
|
||||
GENERATE_NOTES="${{ inputs.generate-notes }}"
|
||||
PRERELEASE="${{ inputs.prerelease }}"
|
||||
MAKE_LATEST="${{ inputs.make-latest }}"
|
||||
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
# Strip leading v if present
|
||||
TAG="${VERSION#v}"
|
||||
TAG="v${TAG}"
|
||||
|
||||
ASSETS="${{ inputs.assets }}"
|
||||
|
||||
release_url=""
|
||||
published=false
|
||||
assets_list='[]'
|
||||
|
||||
case "$RELEASE_TYPE" in
|
||||
docker)
|
||||
echo "🐳 Building Docker image..."
|
||||
|
||||
IMAGE_NAME="${{ inputs.docker-image-name }}"
|
||||
PLATFORMS="${{ inputs.docker-platforms }}"
|
||||
|
||||
# Convert owner to lowercase for ghcr.io
|
||||
IMAGE_NAME_LC=$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')
|
||||
GHCR_IMAGE="ghcr.io/${IMAGE_NAME_LC}:${TAG}"
|
||||
|
||||
# Set up Docker Buildx
|
||||
docker buildx create --use --name multiarch 2>/dev/null || true
|
||||
docker buildx build \
|
||||
--file "${{ inputs.dockerfile }}" \
|
||||
--platform "${PLATFORMS}" \
|
||||
--tag "${GHCR_IMAGE}" \
|
||||
--push \
|
||||
. 2>&1
|
||||
|
||||
echo "✅ Docker image pushed: ${GHCR_IMAGE}"
|
||||
published=true
|
||||
release_url="https://github.com/${owner}/${repo}/pkgs/container/${IMAGE_NAME_LC}"
|
||||
assets_list=$(cat <<ASSETS
|
||||
[{"name": "${IMAGE_NAME_LC}:${TAG}", "url": "${release_url}/versions"}]
|
||||
ASSETS
|
||||
)
|
||||
;;
|
||||
|
||||
flatpak)
|
||||
echo "📦 Building Flatpak..."
|
||||
|
||||
MANIFEST="${{ inputs.flatpak-manifest }}"
|
||||
BRANCH="${{ inputs.flatpak-branch }}"
|
||||
|
||||
# Install flatpak-builder if needed
|
||||
command -v flatpak-builder >/dev/null 2>&1 || {
|
||||
apt-get update && apt-get install -y flatpak flatpak-builder 2>/dev/null ||
|
||||
dnf install -y flatpak flatpak-builder 2>/dev/null || true
|
||||
}
|
||||
|
||||
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 2>/dev/null || true
|
||||
|
||||
flatpak-builder \
|
||||
--repo=repo \
|
||||
--force-clean \
|
||||
--default-branch="${BRANCH}" \
|
||||
build-dir \
|
||||
"${MANIFEST}" 2>&1
|
||||
|
||||
flatpak build-bundle repo "${repo}-${TAG}.flatpak" \
|
||||
"$(grep 'app-id' "${MANIFEST}" | awk '{print $2}' | tr -d '"')" \
|
||||
"${BRANCH}" 2>&1
|
||||
|
||||
published=true
|
||||
release_url="https://github.com/${owner}/${repo}/releases/tag/${TAG}"
|
||||
assets_list=$(cat <<ASSETS
|
||||
[{"name": "${repo}-${TAG}.flatpak", "url": "${release_url}"}]
|
||||
ASSETS
|
||||
)
|
||||
;;
|
||||
|
||||
npm)
|
||||
echo "📦 Publishing npm package..."
|
||||
|
||||
# Set version
|
||||
npm version "${VERSION#v}" --no-git-tag-version 2>&1 || true
|
||||
|
||||
if [ "${{ inputs.npm-dry-run }}" = "true" ]; then
|
||||
npm publish --dry-run 2>&1
|
||||
published=false
|
||||
else
|
||||
if [ -n "${NPM_TOKEN:-}" ]; then
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
fi
|
||||
npm publish 2>&1
|
||||
published=true
|
||||
fi
|
||||
|
||||
release_url="https://www.npmjs.com/package/${repo}"
|
||||
assets_list='[]'
|
||||
;;
|
||||
|
||||
github-release)
|
||||
echo "🏷️ Creating GitHub release..."
|
||||
|
||||
# Create release
|
||||
if [ "$GENERATE_NOTES" = "true" ] && [ -z "$NOTES" ]; then
|
||||
RELEASE_DATA=$(gh release create "${TAG}" \
|
||||
--repo "${owner}/${repo}" \
|
||||
--title "${TAG}" \
|
||||
--generate-notes \
|
||||
$([ "$PRERELEASE" = "true" ] && echo "--prerelease") \
|
||||
$([ "$MAKE_LATEST" != "true" ] && echo "--latest=false") \
|
||||
--json url --jq '.url' 2>&1)
|
||||
else
|
||||
NOTES="${NOTES:-Release ${TAG}}"
|
||||
NOTES_FILE=$(mktemp)
|
||||
echo "$NOTES" > "$NOTES_FILE"
|
||||
RELEASE_DATA=$(gh release create "${TAG}" \
|
||||
--repo "${owner}/${repo}" \
|
||||
--title "${TAG}" \
|
||||
--notes-file "$NOTES_FILE" \
|
||||
$([ "$PRERELEASE" = "true" ] && echo "--prerelease") \
|
||||
$([ "$MAKE_LATEST" != "true" ] && echo "--latest=false") \
|
||||
--json url --jq '.url' 2>&1)
|
||||
rm -f "$NOTES_FILE"
|
||||
fi
|
||||
|
||||
release_url="$RELEASE_DATA"
|
||||
published=true
|
||||
|
||||
# Upload assets
|
||||
if [ "$ASSETS" != "[]" ]; then
|
||||
echo "$ASSETS" | jq -c '.[]' | while IFS= read -r asset; do
|
||||
ASSET_PATH=$(echo "$asset" | jq -r '.path')
|
||||
ASSET_NAME=$(echo "$asset" | jq -r '.name // .path | split("/")[-1]')
|
||||
if [ -f "$ASSET_PATH" ]; then
|
||||
gh release upload "${TAG}" "$ASSET_PATH" --repo "${owner}/${repo}" --clobber 2>&1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Build envelope ---
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"release_type": "${RELEASE_TYPE}",
|
||||
"version": "${TAG}",
|
||||
"published": ${published},
|
||||
"release_url": "${release_url:-}"
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
export AGENT_RELEASE=$(cat <<RELEASE
|
||||
{
|
||||
"version": "${TAG}",
|
||||
"tag": "${TAG}",
|
||||
"published": ${published},
|
||||
"assets": ${assets_list},
|
||||
"release_url": "${release_url:-}"
|
||||
}
|
||||
RELEASE
|
||||
)
|
||||
|
||||
SUMMARY="${RELEASE_TYPE} release ${TAG}: $([ "$published" = true ] && echo 'published' || echo 'dry run')"
|
||||
|
||||
# Suggestions
|
||||
SUGGESTIONS='[]'
|
||||
if [ "$published" = true ] && [ -n "$release_url" ]; then
|
||||
add_suggestion "release:create" "Release ${TAG} created" "{\"url\": \"${release_url}\"}" "medium"
|
||||
fi
|
||||
|
||||
write_envelope "release" "success" "$SUMMARY"
|
||||
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_url=${release_url:-}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,60 @@
|
||||
name: "Review"
|
||||
description: |
|
||||
Lightweight automated code review focused on structural issues, missing
|
||||
tests, dead code, and common patterns. Produces findings array in
|
||||
.agent/output.json for agent consumption.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
github-token:
|
||||
description: "GitHub token for API access"
|
||||
default: "${{ github.token }}"
|
||||
required: false
|
||||
max-findings:
|
||||
description: "Maximum findings to report"
|
||||
default: "30"
|
||||
required: false
|
||||
severity-threshold:
|
||||
description: "Minimum severity to report (error, warning, info)"
|
||||
default: "warning"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'review'"
|
||||
value: "review"
|
||||
status:
|
||||
description: "success | failure | partial"
|
||||
value: ${{ steps.review.outputs.status }}
|
||||
summary:
|
||||
description: "Review summary"
|
||||
value: ${{ steps.review.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: "Run Review"
|
||||
id: review
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
MAX_FINDINGS: ${{ inputs.max-findings }}
|
||||
SEVERITY_THRESHOLD: ${{ inputs.severity-threshold }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: |
|
||||
${{ github.action_path }}/run-review.sh
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Source the envelope helper
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "🔍 Running automated review..."
|
||||
|
||||
MAX_FINDINGS="${MAX_FINDINGS:-30}"
|
||||
SEVERITY_THRESHOLD="${SEVERITY_THRESHOLD:-warning}"
|
||||
owner="${GITHUB_REPOSITORY_OWNER:-}"
|
||||
repo="${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
findings_file=$(mktemp)
|
||||
review_start=$(date +%s)
|
||||
|
||||
# --- Helper: add a finding ---
|
||||
add_finding() {
|
||||
local severity="$1"
|
||||
local rule="$2"
|
||||
local file="$3"
|
||||
local line="${4:-0}"
|
||||
local message="$5"
|
||||
local fix="${6:-}"
|
||||
|
||||
if [ "$severity" = "info" ] && [ "$SEVERITY_THRESHOLD" = "error" ]; then
|
||||
return
|
||||
fi
|
||||
if [ "$severity" = "warning" ] && [ "$SEVERITY_THRESHOLD" = "error" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local finding
|
||||
finding=$(jq -n \
|
||||
--arg severity "$severity" \
|
||||
--arg rule "$rule" \
|
||||
--arg file "$file" \
|
||||
--argjson line "$line" \
|
||||
--arg message "$message" \
|
||||
--arg fix "$fix" \
|
||||
'{
|
||||
severity: $severity,
|
||||
rule: $rule,
|
||||
file: $file,
|
||||
line: ($line | tonumber),
|
||||
message: $message,
|
||||
suggested_fix: $fix
|
||||
}')
|
||||
|
||||
echo "$finding" >> "$findings_file"
|
||||
}
|
||||
|
||||
# --- Checks ---
|
||||
|
||||
# 1. Basic file structure violations
|
||||
check_file_structure() {
|
||||
# Check for TODO/FIXME without tracking issues
|
||||
while IFS=: read -r file line content; do
|
||||
local todo_match
|
||||
todo_match=$(echo "$content" | grep -oE '(TODO|FIXME|HACK|XXX)([^)]|$)' || true)
|
||||
if [ -n "$todo_match" ]; then
|
||||
add_finding "warning" "todo-without-issue" "${file#./}" "$line" \
|
||||
"TODO/FIXME found without linked issue. Consider creating a tracking issue." \
|
||||
"Create a GitHub issue and reference it: TODO(#issue-number)"
|
||||
fi
|
||||
done < <(git grep -nE '(TODO|FIXME|HACK|XXX)' -- ':!*.lock' ':!*lockb' ':!*.sum' ':!node_modules/*' ':!target/*' ':!.git/*' 2>/dev/null | head -20 || true)
|
||||
}
|
||||
|
||||
# 2. Check for missing tests
|
||||
check_missing_tests() {
|
||||
# Look for source files without corresponding test files
|
||||
for src_file in $(find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.rs' -o -name '*.py' -o -name '*.go' \) \
|
||||
-not -path './node_modules/*' \
|
||||
-not -path './target/*' \
|
||||
-not -path './.git/*' \
|
||||
-not -path '*__pycache__*' \
|
||||
-not -path '*/test*' \
|
||||
-not -path '*/spec*' \
|
||||
-not -path '*/vendor/*' \
|
||||
-not -name '*.test.*' \
|
||||
-not -name '*.spec.*' \
|
||||
-not -name '_test.go' \
|
||||
2>/dev/null | head -20); do
|
||||
local basename
|
||||
basename=$(basename "$src_file")
|
||||
local dir
|
||||
dir=$(dirname "$src_file")
|
||||
local stem="${basename%.*}"
|
||||
|
||||
local has_test=false
|
||||
case "$basename" in
|
||||
*.ts) [ -f "${dir}/${stem}.test.ts" ] || [ -f "${dir}/${stem}.spec.ts" ] || [ -f "${dir}/__tests__/${stem}.test.ts" ] && has_test=true ;;
|
||||
*.js) [ -f "${dir}/${stem}.test.js" ] || [ -f "${dir}/${stem}.spec.js" ] && has_test=true ;;
|
||||
*.rs) [ -f "${dir}/${stem}_test.rs" ] || [ -f "${dir}/tests/${stem}.rs" ] && has_test=true ;;
|
||||
*.py) [ -f "${dir}/test_${basename}" ] || [ -f "${dir}/${stem}_test.py" ] && has_test=true ;;
|
||||
*.go) [ -f "${dir}/${stem}_test.go" ] && has_test=true ;;
|
||||
esac
|
||||
|
||||
if [ "$has_test" = false ]; then
|
||||
add_finding "info" "missing-tests" "${src_file#./}" 0 \
|
||||
"Source file without corresponding test file" \
|
||||
"Create ${dir}/test_${basename} or ${dir}/${stem}.test.ts"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 3. Check for large files
|
||||
check_file_sizes() {
|
||||
while IFS= read -r file; do
|
||||
local size
|
||||
size=$(wc -l < "$file" 2>/dev/null || echo 0)
|
||||
if [ "$size" -gt 500 ]; then
|
||||
add_finding "warning" "large-file" "${file#./}" 0 \
|
||||
"File is ${size} lines. Consider splitting into smaller modules." \
|
||||
"Refactor into smaller files (< 300 lines each)"
|
||||
fi
|
||||
done < <(find . -type f \( -name '*.ts' -o -name '*.js' -o -name '*.rs' -o -name '*.py' -o -name '*.go' -o -name '*.tsx' -o -name '*.jsx' \) \
|
||||
-not -path './node_modules/*' \
|
||||
-not -path './target/*' \
|
||||
-not -path './.git/*' \
|
||||
2>/dev/null | head -30 || true)
|
||||
}
|
||||
|
||||
# 4. Check for hardcoded values (in JS/TS/Rust/Python)
|
||||
check_hardcoded() {
|
||||
# Detect hardcoded URLs/API keys/secrets in config-like files
|
||||
git grep -nE '(api_key|API_KEY|secret|SECRET|password|PASSWORD|token=\w{20,}|https?://.*:[^@]@)' \
|
||||
-- ':!*.lock' ':!*lockb' ':!node_modules/*' ':!target/*' ':!.git/*' \
|
||||
':!*.yaml' ':!*.yml' ':!*.env*' ':!*.example*' ':!*test*' \
|
||||
2>/dev/null | while IFS=: read -r file line content; do
|
||||
add_finding "error" "hardcoded-secret" "${file#./}" "$line" \
|
||||
"Potential hardcoded secret or credential" \
|
||||
"Use environment variables or a secrets manager"
|
||||
done || true
|
||||
}
|
||||
|
||||
# 5. Check for console.log left in production code
|
||||
check_debug_logs() {
|
||||
git grep -nE '(console\.log|println!|print\(|printf\()' \
|
||||
-- '*.ts' '*.js' '*.tsx' '*.jsx' '*.rs' '*.py' '*.go' \
|
||||
':!*test*' ':!*spec*' ':!node_modules/*' ':!target/*' ':!.git/*' \
|
||||
':!src/logger*' ':!src/logging*' ':!src/debug*' \
|
||||
2>/dev/null | while IFS=: read -r file line content; do
|
||||
add_finding "warning" "debug-log" "${file#./}" "$line" \
|
||||
"Debug log statement in non-test code" \
|
||||
"Remove or replace with proper logging framework"
|
||||
done || true
|
||||
}
|
||||
|
||||
# 6. Check for commented-out code
|
||||
check_commented_code() {
|
||||
git grep -nE '^\s*//\s*(function|class|def|const|let|var|import|export|fn|pub|impl)' \
|
||||
-- '*.ts' '*.js' '*.rs' '*.go' \
|
||||
':!node_modules/*' ':!target/*' ':!.git/*' \
|
||||
2>/dev/null | while IFS=: read -r file line content; do
|
||||
add_finding "warning" "commented-code" "${file#./}" "$line" \
|
||||
"Commented-out code detected" \
|
||||
"Remove dead code instead of commenting it out"
|
||||
done || true
|
||||
}
|
||||
|
||||
# 7. Check for TODO/FIXME in PR diff (if PR context available)
|
||||
check_pr_specific() {
|
||||
if [ -z "${PR_NUMBER:-}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local diff
|
||||
diff=$(gh api "repos/${owner}/${repo}/pulls/${PR_NUMBER}/files?per_page=100" \
|
||||
--jq '[.[] | select(.additions > 0) | {path, additions_url}]' 2>/dev/null || echo '[]')
|
||||
|
||||
if [ "$diff" = "[]" ] || [ "$diff" = "null" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Check PR files for the above patterns specifically in the diff
|
||||
echo "$diff" | jq -r '.[].path' | while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
# Look for added lines with TODO (rough heuristic using git blame)
|
||||
if git blame "$file" 2>/dev/null | head -5 | grep -q "00000000"; then
|
||||
: # New file — already caught by general scan
|
||||
fi
|
||||
fi
|
||||
done || true
|
||||
}
|
||||
|
||||
# 8. Check for focus-test (it.only/describe.only) in test files
|
||||
check_focused_tests() {
|
||||
git grep -nE '(it\.only|describe\.only|test\.only|fit\()' \
|
||||
-- '*test*' '*spec*' '*.test.*' '*.spec.*' \
|
||||
':!node_modules/*' ':!target/*' ':!.git/*' \
|
||||
2>/dev/null | while IFS=: read -r file line content; do
|
||||
add_finding "error" "focused-test" "${file#./}" "$line" \
|
||||
"Focused test (it.only/describe.only) found — will skip other tests" \
|
||||
"Remove .only to run the full test suite"
|
||||
done || true
|
||||
}
|
||||
|
||||
# --- Run all checks ---
|
||||
check_file_structure
|
||||
check_missing_tests
|
||||
check_file_sizes
|
||||
check_hardcoded
|
||||
check_debug_logs
|
||||
check_commented_code
|
||||
check_pr_specific
|
||||
check_focused_tests
|
||||
|
||||
# --- Compile findings ---
|
||||
ALL_FINDINGS=$(cat "$findings_file" 2>/dev/null | jq -s 'sort_by(.severity) | .[0:'"$MAX_FINDINGS"']' 2>/dev/null || echo '[]')
|
||||
|
||||
ERROR_COUNT=$(echo "$ALL_FINDINGS" | jq '[.[] | select(.severity == "error")] | length')
|
||||
WARNING_COUNT=$(echo "$ALL_FINDINGS" | jq '[.[] | select(.severity == "warning")] | length')
|
||||
INFO_COUNT=$(echo "$ALL_FINDINGS" | jq '[.[] | select(.severity == "info")] | length')
|
||||
|
||||
# Determine status
|
||||
if [ "$ERROR_COUNT" -gt 0 ]; then
|
||||
REVIEW_STATUS="failure"
|
||||
elif [ "$WARNING_COUNT" -gt 0 ]; then
|
||||
REVIEW_STATUS="partial"
|
||||
else
|
||||
REVIEW_STATUS="success"
|
||||
fi
|
||||
|
||||
SUMMARY="${ERROR_COUNT} errors, ${WARNING_COUNT} warnings, ${INFO_COUNT} info"
|
||||
|
||||
# Build outputs
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"summary": {
|
||||
"errors": ${ERROR_COUNT},
|
||||
"warnings": ${WARNING_COUNT},
|
||||
"info": ${INFO_COUNT},
|
||||
"total": $(echo "$ALL_FINDINGS" | jq 'length'),
|
||||
"truncated": $(echo "$ALL_FINDINGS" | jq "length > $MAX_FINDINGS")
|
||||
},
|
||||
"rules_applied": [
|
||||
"todo-without-issue",
|
||||
"missing-tests",
|
||||
"large-file",
|
||||
"hardcoded-secret",
|
||||
"debug-log",
|
||||
"commented-code",
|
||||
"focused-test"
|
||||
]
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
export AGENT_FINDINGS="$ALL_FINDINGS"
|
||||
|
||||
# Suggestions
|
||||
SUGGESTIONS='[]'
|
||||
if [ "$ERROR_COUNT" -gt 0 ]; then
|
||||
add_suggestion "issue:create" "${ERROR_COUNT} error(s) found in review" \
|
||||
"{\"labels\": [\"review\", \"automated\"], \"title\": \"Review findings: ${ERROR_COUNT} errors\"}" "medium"
|
||||
fi
|
||||
|
||||
review_end=$(date +%s)
|
||||
export AGENT_DURATION_MS=$(( (review_end - review_start) * 1000 ))
|
||||
|
||||
write_envelope "review" "$REVIEW_STATUS" "$SUMMARY"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$findings_file"
|
||||
|
||||
echo "status=${REVIEW_STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,155 @@
|
||||
name: "Rust CI"
|
||||
description: |
|
||||
Composite action for Rust projects: toolchain setup, caching, formatting,
|
||||
linting (clippy), testing, optional coverage (llvm-cov), and optional
|
||||
security audit. Outputs .agent/output.json.
|
||||
|
||||
author: "Heretek-AI"
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: "Directory containing Cargo.toml"
|
||||
default: "."
|
||||
required: false
|
||||
toolchain:
|
||||
description: "Rust toolchain (stable, nightly, or explicit version)"
|
||||
default: "stable"
|
||||
required: false
|
||||
components:
|
||||
description: "Additional Rust components to install"
|
||||
default: "clippy, rustfmt"
|
||||
required: false
|
||||
cache-workspaces:
|
||||
description: "Comma-separated workspace directories to cache"
|
||||
default: ""
|
||||
required: false
|
||||
system-dependencies:
|
||||
description: "Shell command to install system dependencies"
|
||||
default: ""
|
||||
required: false
|
||||
lint-command:
|
||||
description: "Override lint command (default: cargo clippy -- -D warnings)"
|
||||
default: ""
|
||||
required: false
|
||||
test-command:
|
||||
description: "Override test command (default: cargo test)"
|
||||
default: ""
|
||||
required: false
|
||||
test-continue-on-error:
|
||||
description: "Continue if tests fail"
|
||||
default: "false"
|
||||
required: false
|
||||
lint-continue-on-error:
|
||||
description: "Continue if lint fails"
|
||||
default: "false"
|
||||
required: false
|
||||
enable-coverage:
|
||||
description: "Enable code coverage via cargo-llvm-cov"
|
||||
default: "false"
|
||||
required: false
|
||||
coverage-path:
|
||||
description: "Path to coverage output for upload"
|
||||
default: "coverage/lcov.info"
|
||||
required: false
|
||||
enable-audit:
|
||||
description: "Enable cargo-audit security audit"
|
||||
default: "false"
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
agent_action:
|
||||
description: "Always 'ci-rust'"
|
||||
value: "ci-rust"
|
||||
status:
|
||||
description: "success | failure | partial"
|
||||
value: ${{ steps.envelope.outputs.status }}
|
||||
summary:
|
||||
description: "CI summary"
|
||||
value: ${{ steps.envelope.outputs.summary }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "Setup Rust Toolchain"
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
components: ${{ inputs.components }}
|
||||
|
||||
- name: "Setup Rust Cache"
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: ${{ inputs.cache-workspaces }}
|
||||
|
||||
- name: "Install System Dependencies"
|
||||
if: inputs.system-dependencies != ''
|
||||
shell: bash
|
||||
run: ${{ inputs.system-dependencies }}
|
||||
|
||||
- name: "Cargo Format"
|
||||
id: fmt
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
cargo fmt --check 2>&1 | tee .fmt-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Cargo Clippy"
|
||||
id: clippy
|
||||
continue-on-error: ${{ inputs.lint-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.lint-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
CMD="cargo clippy -- -D warnings"
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .clippy-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Cargo Test"
|
||||
id: test
|
||||
continue-on-error: ${{ inputs.test-continue-on-error == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
CMD="${{ inputs.test-command }}"
|
||||
if [ -z "$CMD" ]; then
|
||||
CMD="cargo test"
|
||||
fi
|
||||
eval "$CMD" 2>&1 | tee .test-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Cargo Audit"
|
||||
id: audit
|
||||
if: inputs.enable-audit == 'true'
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
cargo audit 2>&1 | tee .audit-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Code Coverage"
|
||||
id: coverage
|
||||
if: inputs.enable-coverage == 'true'
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
cargo install cargo-llvm-cov 2>/dev/null || true
|
||||
cargo llvm-cov --all-features --lcov --output-path ${{ inputs.coverage-path }} 2>&1 | tee .cov-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Generate Agent Envelope"
|
||||
id: envelope
|
||||
shell: bash
|
||||
env:
|
||||
FMT_EXIT: ${{ steps.fmt.outputs.exit_code }}
|
||||
CLIPPY_EXIT: ${{ steps.clippy.outputs.exit_code }}
|
||||
TEST_EXIT: ${{ steps.test.outputs.exit_code }}
|
||||
AUDIT_EXIT: ${{ steps.audit.outputs.exit_code }}
|
||||
COV_EXIT: ${{ steps.coverage.outputs.exit_code }}
|
||||
run: |
|
||||
${{ github.action_path }}/write-envelope.sh
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source "${{ github.action_path }}/../agent-envelope.sh"
|
||||
|
||||
echo "📝 Generating Rust CI envelope..."
|
||||
|
||||
# Parse exit codes
|
||||
FMT_OK=1; [ "${FMT_EXIT:-0}" = "0" ] 2>/dev/null && FMT_OK=0 || true
|
||||
CLIPPY_OK=1; [ "${CLIPPY_EXIT:-0}" = "0" ] 2>/dev/null && CLIPPY_OK=0 || true
|
||||
TEST_OK=1; [ "${TEST_EXIT:-0}" = "0" ] 2>/dev/null && TEST_OK=0 || true
|
||||
AUDIT_OK=1; [ "${AUDIT_EXIT:-0}" = "0" ] 2>/dev/null && AUDIT_OK=0 || true
|
||||
COV_OK=1; [ "${COV_EXIT:-0}" = "0" ] 2>/dev/null && COV_OK=0 || true
|
||||
|
||||
# Parse test output for pass/fail counts
|
||||
TEST_PASS=0
|
||||
TEST_FAIL=0
|
||||
if [ -f .test-output.txt ]; then
|
||||
TEST_PASS=$(grep -oP '\d+(?= passed)' .test-output.txt 2>/dev/null | paste -sd+ | bc 2>/dev/null || echo 0)
|
||||
TEST_FAIL=$(grep -oP '\d+(?= failed)' .test-output.txt 2>/dev/null | paste -sd+ | bc 2>/dev/null || echo 0)
|
||||
fi
|
||||
|
||||
# Parse audit findings
|
||||
AUDIT_VULNS=0
|
||||
if [ -f .audit-output.txt ]; then
|
||||
AUDIT_VULNS=$(grep -c "CVE-" .audit-output.txt 2>/dev/null || echo 0)
|
||||
fi
|
||||
|
||||
# Determine status
|
||||
ERRORS=0
|
||||
[ "$FMT_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$CLIPPY_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
[ "$TEST_OK" -eq 1 ] && ERRORS=$((ERRORS+1))
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
STATUS="failure"
|
||||
elif [ "$AUDIT_VULNS" -gt 0 ]; then
|
||||
STATUS="partial"
|
||||
else
|
||||
STATUS="success"
|
||||
fi
|
||||
|
||||
SUMMARY="rust-ci: "
|
||||
SUMMARY+="fmt=$([ "$FMT_OK" -eq 0 ] && echo '✓' || echo '✗') "
|
||||
SUMMARY+="clippy=$([ "$CLIPPY_OK" -eq 0 ] && echo '✓' || echo '✗') "
|
||||
SUMMARY+="test=${TEST_PASS}✓/${TEST_FAIL}✗ "
|
||||
if [ -n "${AUDIT_EXIT:-}" ]; then
|
||||
SUMMARY+="audit=${AUDIT_VULNS}vulns "
|
||||
fi
|
||||
|
||||
# Build outputs
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"toolchain": "${{ inputs.toolchain }}",
|
||||
"checks": {
|
||||
"fmt": {"passed": $([ "$FMT_OK" -eq 0 ] && echo true || echo false)},
|
||||
"clippy": {"passed": $([ "$CLIPPY_OK" -eq 0 ] && echo true || echo false)},
|
||||
"test": {"passed": $([ "$TEST_OK" -eq 0 ] && echo true || echo false), "passed_count": ${TEST_PASS}, "failed_count": ${TEST_FAIL}},
|
||||
"audit": {"passed": $([ "$AUDIT_OK" -eq 0 ] && echo true || echo false), "vulnerabilities": ${AUDIT_VULNS}},
|
||||
"coverage": {"passed": $([ "$COV_OK" -eq 0 ] && echo true || echo false), "enabled": $([ "${{ inputs.enable-coverage }}" = "true" ] && echo true || echo false)}
|
||||
}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
write_envelope "ci-rust" "$STATUS" "$SUMMARY"
|
||||
|
||||
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1,26 @@
|
||||
name: "Agent Context"
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-context:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Generate Agent Context"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/agent-context@v1
|
||||
|
||||
- name: "Read Agent Output for Logging"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
echo "### Agent Context" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '. | "\(.summary)"' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,50 @@
|
||||
name: "Check Status"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
wait-for-completion:
|
||||
description: "Max seconds to wait for all checks to complete"
|
||||
type: string
|
||||
default: "600"
|
||||
required: false
|
||||
poll-interval:
|
||||
description: "Seconds between polls"
|
||||
type: string
|
||||
default: "15"
|
||||
required: false
|
||||
secrets:
|
||||
github-token:
|
||||
description: "GitHub token"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
watch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Watch Check Status"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/check-status@v1
|
||||
with:
|
||||
github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }}
|
||||
wait-for-completion: ${{ inputs.wait-for-completion }}
|
||||
poll-interval: ${{ inputs.poll-interval }}
|
||||
|
||||
- name: "Read Agent Output"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
STATUS=$(cat .agent/output.json | jq -r '.status')
|
||||
SUMMARY=$(cat .agent/output.json | jq -r '.summary')
|
||||
echo "## Check Status: ${STATUS}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${SUMMARY}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Output merged status
|
||||
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "summary=${SUMMARY}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -0,0 +1,240 @@
|
||||
name: "Universal CI Check"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
rust:
|
||||
description: "Enable Rust checks (auto-detected by default)"
|
||||
type: string
|
||||
default: "auto"
|
||||
required: false
|
||||
javascript:
|
||||
description: "Enable JS/TS checks (auto-detected by default)"
|
||||
type: string
|
||||
default: "auto"
|
||||
required: false
|
||||
python:
|
||||
description: "Enable Python checks (auto-detected by default)"
|
||||
type: string
|
||||
default: "auto"
|
||||
required: false
|
||||
docker:
|
||||
description: "Enable Docker checks (auto-detected by default)"
|
||||
type: string
|
||||
default: "auto"
|
||||
required: false
|
||||
rust-toolchain:
|
||||
description: "Rust toolchain (stable/nightly)"
|
||||
type: string
|
||||
default: "stable"
|
||||
required: false
|
||||
node-version:
|
||||
description: "Node.js version for JS/TS projects"
|
||||
type: string
|
||||
default: "20"
|
||||
required: false
|
||||
python-version:
|
||||
description: "Python version for Python projects"
|
||||
type: string
|
||||
default: "3.12"
|
||||
required: false
|
||||
enable-coverage:
|
||||
description: "Enable code coverage (where supported)"
|
||||
type: string
|
||||
default: "false"
|
||||
required: false
|
||||
enable-audit:
|
||||
description: "Enable security audit (where supported)"
|
||||
type: string
|
||||
default: "false"
|
||||
required: false
|
||||
secrets:
|
||||
github-token:
|
||||
description: "GitHub token"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
# --- Auto-detection job ---
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_rust: ${{ steps.detect.outputs.has_rust }}
|
||||
has_js: ${{ steps.detect.outputs.has_js }}
|
||||
has_python: ${{ steps.detect.outputs.has_python }}
|
||||
has_docker: ${{ steps.detect.outputs.has_docker }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "1"
|
||||
|
||||
- name: "Detect Languages"
|
||||
id: detect
|
||||
shell: bash
|
||||
run: |
|
||||
# Rust
|
||||
HAS_RUST="false"
|
||||
if ls Cargo.toml 2>/dev/null; then HAS_RUST="true"; fi
|
||||
if [ "${{ inputs.rust }}" = "true" ]; then HAS_RUST="true"; fi
|
||||
if [ "${{ inputs.rust }}" = "false" ]; then HAS_RUST="false"; fi
|
||||
|
||||
# JS/TS
|
||||
HAS_JS="false"
|
||||
if ls package.json 2>/dev/null; then HAS_JS="true"; fi
|
||||
if [ "${{ inputs.javascript }}" = "true" ]; then HAS_JS="true"; fi
|
||||
if [ "${{ inputs.javascript }}" = "false" ]; then HAS_JS="false"; fi
|
||||
|
||||
# Python
|
||||
HAS_PYTHON="false"
|
||||
if ls pyproject.toml setup.py requirements.txt 2>/dev/null; then HAS_PYTHON="true"; fi
|
||||
if [ "${{ inputs.python }}" = "true" ]; then HAS_PYTHON="true"; fi
|
||||
if [ "${{ inputs.python }}" = "false" ]; then HAS_PYTHON="false"; fi
|
||||
|
||||
# Docker
|
||||
HAS_DOCKER="false"
|
||||
if ls Dockerfile docker-compose.yml compose.yml 2>/dev/null; then HAS_DOCKER="true"; fi
|
||||
if [ "${{ inputs.docker }}" = "true" ]; then HAS_DOCKER="true"; fi
|
||||
if [ "${{ inputs.docker }}" = "false" ]; then HAS_DOCKER="false"; fi
|
||||
|
||||
echo "has_rust=${HAS_RUST}" >> "$GITHUB_OUTPUT"
|
||||
echo "has_js=${HAS_JS}" >> "$GITHUB_OUTPUT"
|
||||
echo "has_python=${HAS_PYTHON}" >> "$GITHUB_OUTPUT"
|
||||
echo "has_docker=${HAS_DOCKER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "🔍 Detected: Rust=${HAS_RUST} JS/TS=${HAS_JS} Python=${HAS_PYTHON} Docker=${HAS_DOCKER}"
|
||||
|
||||
# --- Rust CI ---
|
||||
rust:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has_rust == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/rust-ci@v1
|
||||
with:
|
||||
toolchain: ${{ inputs.rust-toolchain }}
|
||||
enable-coverage: ${{ inputs.enable-coverage }}
|
||||
enable-audit: ${{ inputs.enable-audit }}
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output-rust
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
|
||||
# --- JavaScript/TypeScript CI ---
|
||||
javascript:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has_js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/js-ci@v1
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output-js
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
|
||||
# --- Python CI ---
|
||||
python:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has_python == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/python-ci@v1
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output-python
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
|
||||
# --- Docker check ---
|
||||
docker:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has_docker == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Dockerfile Lint (hadolint)"
|
||||
uses: hadolint/[email protected]
|
||||
continue-on-error: true
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: warning
|
||||
|
||||
- name: "Build Docker Image"
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
docker build -t ci-check:latest -f Dockerfile . 2>&1 | tee .docker-build-output.txt
|
||||
echo "exit_code=$?" >> $GITHUB_OUTPUT
|
||||
|
||||
# --- Merge results ---
|
||||
merge-results:
|
||||
needs: [detect, rust, javascript, python]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Download All Agent Outputs"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: agent-output-*
|
||||
merge-multiple: true
|
||||
path: .agent/results
|
||||
|
||||
- name: "Merge Envelopes"
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p .agent
|
||||
source .github/actions/agent-envelope.sh
|
||||
|
||||
CHECKS='[]'
|
||||
ALL_PASS=true
|
||||
|
||||
for f in .agent/results/*.json; do
|
||||
if [ -f "$f" ]; then
|
||||
STATUS=$(cat "$f" | jq -r '.status // "unknown"')
|
||||
SUMMARY=$(cat "$f" | jq -r '.summary // ""')
|
||||
ACTION=$(cat "$f" | jq -r '.agent_action // "unknown"')
|
||||
if [ "$STATUS" != "success" ]; then
|
||||
ALL_PASS=false
|
||||
fi
|
||||
CHECKS=$(echo "$CHECKS" | jq --arg a "$ACTION" --arg s "$STATUS" --arg sum "$SUMMARY" '. + [{"name": $a, "status": $s, "summary": $sum}]')
|
||||
fi
|
||||
done
|
||||
|
||||
export AGENT_CHECKS="$CHECKS"
|
||||
SUMMARY=$(echo "$CHECKS" | jq -r '[.[] | "\(.name)=\(.status)"] | join(", ")')
|
||||
[ "$ALL_PASS" = true ] && STATUS="success" || STATUS="failure"
|
||||
|
||||
write_envelope "ci-check" "$STATUS" "All checks: ${SUMMARY}"
|
||||
|
||||
- name: "Summarize"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
echo "## CI Check Results" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '.summary' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Table
|
||||
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '.checks[] | "| \(.name) | \(.status) |"' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Issue Triage"
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Triage Issue"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/issue-triage@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Read Agent Output"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
echo "### Issue Triage" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '. | "\(.summary)"' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "PR Summary"
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
summarize:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Generate PR Summary"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/pr-summary@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Read Agent Output"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
echo "### PR Summary" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '. | "\(.summary)"' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,91 @@
|
||||
name: "Pre-commit CI"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: .pre-commit-config.yaml
|
||||
|
||||
- name: "Install pre-commit"
|
||||
run: |
|
||||
pip install pre-commit
|
||||
pre-commit install-hooks
|
||||
|
||||
- name: "Run pre-commit"
|
||||
id: precommit
|
||||
continue-on-error: true
|
||||
run: |
|
||||
pre-commit run --all-files --show-diff-on-failure 2>&1 | tee .agent/pre-commit-output.txt
|
||||
|
||||
- name: "Generate Agent Envelope"
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir -p .agent
|
||||
|
||||
# Parse pre-commit output
|
||||
ERROR_COUNT=$(grep -c '^[A-Z]' .agent/pre-commit-output.txt 2>/dev/null || echo 0)
|
||||
FAILED_COUNT=$(grep -c 'Failed' .agent/pre-commit-output.txt 2>/dev/null || echo 0)
|
||||
PASSED_COUNT=$(grep -c 'Passed' .agent/pre-commit-output.txt 2>/dev/null || echo 0)
|
||||
|
||||
if [ "$FAILED_COUNT" -gt 0 ]; then
|
||||
STATUS="failure"
|
||||
SUMMARY="${FAILED_COUNT} hook(s) failed, ${PASSED_COUNT} passed"
|
||||
else
|
||||
STATUS="success"
|
||||
SUMMARY="${PASSED_COUNT} hook(s) passed"
|
||||
fi
|
||||
|
||||
AGENT_OUTPUTS=$(cat <<OUTPUTS
|
||||
{
|
||||
"hooks_run": ${PASSED_COUNT},
|
||||
"hooks_failed": ${FAILED_COUNT},
|
||||
"hooks_errors": ${ERROR_COUNT}
|
||||
}
|
||||
OUTPUTS
|
||||
export AGENT_OUTPUTS
|
||||
|
||||
source .github/actions/agent-envelope.sh
|
||||
write_envelope "pre-commit" "$STATUS" "$SUMMARY"
|
||||
|
||||
- name: "Upload Agent Output"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-output
|
||||
path: .agent/output.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
- name: "Summarize"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
echo "## Pre-commit Results" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '. | "\(.summary)"' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/pre-commit-output.txt >> $GITHUB_STEP_SUMMARY 2>/dev/null
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: "Fail if hooks failed"
|
||||
if: steps.precommit.outcome == 'failure'
|
||||
run: exit 1
|
||||
@@ -0,0 +1,122 @@
|
||||
name: "Universal Release"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release-type:
|
||||
description: "Release target: docker, flatpak, npm, github-release"
|
||||
type: string
|
||||
required: true
|
||||
version:
|
||||
description: "Semver version (e.g., v1.2.3)"
|
||||
type: string
|
||||
required: true
|
||||
dockerfile:
|
||||
description: "Path to Dockerfile (docker type)"
|
||||
type: string
|
||||
default: "./Dockerfile"
|
||||
required: false
|
||||
docker-image-name:
|
||||
description: "Docker image name (docker type)"
|
||||
type: string
|
||||
default: ""
|
||||
required: false
|
||||
docker-platforms:
|
||||
description: "Comma-separated platforms (docker type)"
|
||||
type: string
|
||||
default: "linux/amd64,linux/arm64"
|
||||
required: false
|
||||
flatpak-manifest:
|
||||
description: "Path to Flatpak manifest (flatpak type)"
|
||||
type: string
|
||||
default: ""
|
||||
required: false
|
||||
flatpak-branch:
|
||||
description: "Flatpak branch (flatpak type)"
|
||||
type: string
|
||||
default: "stable"
|
||||
required: false
|
||||
npm-dry-run:
|
||||
description: "Dry run for npm publish"
|
||||
type: string
|
||||
default: "false"
|
||||
required: false
|
||||
release-notes:
|
||||
description: "Custom release notes"
|
||||
type: string
|
||||
default: ""
|
||||
required: false
|
||||
generate-notes:
|
||||
description: "Auto-generate release notes from commits"
|
||||
type: string
|
||||
default: "true"
|
||||
required: false
|
||||
prerelease:
|
||||
description: "Mark as prerelease"
|
||||
type: string
|
||||
default: "false"
|
||||
required: false
|
||||
make-latest:
|
||||
description: "Mark as latest release"
|
||||
type: string
|
||||
default: "true"
|
||||
required: false
|
||||
assets:
|
||||
description: "JSON array of asset paths ({name, path} format)"
|
||||
type: string
|
||||
default: "[]"
|
||||
required: false
|
||||
|
||||
secrets:
|
||||
github-token:
|
||||
description: "GitHub token"
|
||||
required: false
|
||||
npm-token:
|
||||
description: "npm token (for npm releases)"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: "QEMU Setup (Docker)"
|
||||
if: inputs.release-type == 'docker'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: "Docker Buildx Setup (Docker)"
|
||||
if: inputs.release-type == 'docker'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "Docker Login"
|
||||
if: inputs.release-type == 'docker'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.github-token || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Run Release"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/release@v1
|
||||
with:
|
||||
release-type: ${{ inputs.release-type }}
|
||||
version: ${{ inputs.version }}
|
||||
github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }}
|
||||
dockerfile: ${{ inputs.dockerfile }}
|
||||
docker-image-name: ${{ inputs.docker-image-name || github.repository }}
|
||||
docker-platforms: ${{ inputs.docker-platforms }}
|
||||
flatpak-manifest: ${{ inputs.flatpak-manifest }}
|
||||
flatpak-branch: ${{ inputs.flatpak-branch }}
|
||||
npm-token: ${{ secrets.npm-token }}
|
||||
npm-dry-run: ${{ inputs.npm-dry-run }}
|
||||
release-notes: ${{ inputs.release-notes }}
|
||||
generate-notes: ${{ inputs.generate-notes }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
make-latest: ${{ inputs.make-latest }}
|
||||
assets: ${{ inputs.assets }}
|
||||
@@ -0,0 +1,45 @@
|
||||
name: "Automated Review"
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Run Review"
|
||||
uses: Heretek-AI/heretek-actions/.github/actions/review@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Summarize Review"
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .agent/output.json ]; then
|
||||
ERROR_COUNT=$(cat .agent/output.json | jq '.findings | map(select(.severity == "error")) | length')
|
||||
WARN_COUNT=$(cat .agent/output.json | jq '.findings | map(select(.severity == "warning")) | length')
|
||||
INFO_COUNT=$(cat .agent/output.json | jq '.findings | map(select(.severity == "info")) | length')
|
||||
|
||||
echo "## Review Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔴 Error | ${ERROR_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🟡 Warning | ${WARN_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔵 Info | ${INFO_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "$(cat .agent/output.json | jq '.findings | length')" -gt 0 ] && [ "$(cat .agent/output.json | jq '.findings | length')" -le 20 ]; then
|
||||
echo "### Findings" >> $GITHUB_STEP_SUMMARY
|
||||
cat .agent/output.json | jq -r '.findings[] | "- [\(.severity)] \(.file):\(.line) — \(.message)"' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Agent output
|
||||
.agent/output.json
|
||||
.agent/results/
|
||||
|
||||
# Secrets baseline (generated)
|
||||
.secrets.baseline
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
dist/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Rust
|
||||
target/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Generated
|
||||
*.log
|
||||
.coverage/
|
||||
@@ -0,0 +1,121 @@
|
||||
# Heretek Actions — Universal pre-commit hook configuration
|
||||
# https://pre-commit.com
|
||||
#
|
||||
# Install: pip install pre-commit && pre-commit install
|
||||
# Run all: pre-commit run --all-files
|
||||
# Auto-update: pre-commit autoupdate
|
||||
|
||||
repos:
|
||||
# --- Universal hooks (apply to all projects) ---
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=500]
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
|
||||
# --- Security scanning ---
|
||||
- repo: https://github.com/Yelp/detect-secrets
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: detect-secrets
|
||||
args: [--baseline, .secrets.baseline]
|
||||
stages: [pre-commit]
|
||||
exclude: (.*\.lock|.*\.lockb|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock)
|
||||
|
||||
# --- Agent-facing hooks ---
|
||||
- repo: https://github.com/Heretek-AI/heretek-actions
|
||||
rev: v1
|
||||
hooks:
|
||||
- id: agent-context
|
||||
name: "Agent Context"
|
||||
description: "Generate repo context file for AI agents"
|
||||
language: python
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
entry: python -m heretek_hooks.agent_context
|
||||
stages: [manual]
|
||||
|
||||
# --- Language-specific hooks (auto-detected) ---
|
||||
|
||||
# Rust
|
||||
- repo: https://github.com/doublify/pre-commit-rust
|
||||
rev: v1.0
|
||||
hooks:
|
||||
- id: cargo-check
|
||||
stages: [manual]
|
||||
- id: clippy
|
||||
stages: [manual]
|
||||
- id: fmt
|
||||
stages: [manual]
|
||||
|
||||
# JavaScript/TypeScript (requires node_modules)
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v9.0.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
types_or: [javascript, jsx, typescript, tsx]
|
||||
additional_dependencies:
|
||||
- eslint@9
|
||||
stages: [manual]
|
||||
exclude: (node_modules/|\.next/|dist/|build/)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, typescript, tsx, json, css, scss, markdown, yaml]
|
||||
stages: [manual]
|
||||
exclude: (node_modules/|\.next/|dist/|build/)
|
||||
|
||||
# Python
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
types_or: [python, pyi]
|
||||
stages: [manual]
|
||||
- id: ruff-format
|
||||
types_or: [python, pyi]
|
||||
stages: [manual]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.11.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
stages: [manual]
|
||||
additional_dependencies: [types-all]
|
||||
|
||||
# Shell
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: [--severity=warning]
|
||||
stages: [manual]
|
||||
exclude: (\.husky/)
|
||||
|
||||
# Docker
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.13.0-beta
|
||||
hooks:
|
||||
- id: hadolint
|
||||
stages: [manual]
|
||||
|
||||
# Commit message
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: []
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Heretek-AI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,405 @@
|
||||
# Heretek Actions
|
||||
|
||||
> Reusable GitHub Actions and workflows for **agent-driven development**.
|
||||
> Every action produces a standardized JSON envelope at `.agent/output.json` for easy agent consumption.
|
||||
|
||||
[](https://github.com/Heretek-AI/heretek-actions/actions/workflows/agent-context.yml)
|
||||
[](https://github.com/Heretek-AI/heretek-actions/actions/workflows/pre-commit-ci.yml)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **🤖 Agent-Facing Tooling** — Actions designed for AI agents to parse and act upon: PR summaries, issue triage, repo context, check status, and automated review
|
||||
- **🔄 Multi-Language CI** — Auto-detect your stack and run the right checks for Rust, JavaScript/TypeScript, Python, Docker, and web projects
|
||||
- **📋 Pre-commit Hooks** — Universal hook configuration via the Python `pre-commit` framework (no Node dependency)
|
||||
- **🚀 Universal Release** — Release to Docker, Flatpak, npm, or GitHub releases from a single workflow
|
||||
- **📦 Standardized Output** — Every action writes `.agent/output.json` following a [shared schema](.agent/schema.json) — **one file for agents to read**
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Agent Tooling (30 seconds)
|
||||
|
||||
Add this workflow to your repo:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/agent-tools.yml
|
||||
name: Agent Tools
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
context:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/agent-context.yml@v1
|
||||
triage:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/issue-triage.yml@v1
|
||||
pr-summary:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/pr-summary.yml@v1
|
||||
```
|
||||
|
||||
Then any **AI agent** can read `.agent/output.json` to understand your repo, triaged issues, and PR summaries — in one file.
|
||||
|
||||
### CI Checks
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
check:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/check.yml@v1
|
||||
```
|
||||
|
||||
Auto-detects your language (Rust, JS/TS, Python, Docker) and runs the appropriate checks.
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
Copy the [`.pre-commit-config.yaml`](.pre-commit-config.yaml) from this repo, or reference it:
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/Heretek-AI/heretek-actions
|
||||
rev: v1
|
||||
hooks:
|
||||
- id: agent-context
|
||||
stages: [manual]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent-Facing Actions
|
||||
|
||||
All agent actions emit `.agent/output.json` with the [Standard Envelope](#agent-integration). An agent reads one file and knows what to do.
|
||||
|
||||
| Action | Trigger | What it does | For Agents |
|
||||
|--------|---------|-------------|------------|
|
||||
| [`agent-context`](.github/actions/agent-context/) | push, dispatch | Scans repo structure, detects stack, queries GitHub API | Start here — agent reads context in seconds |
|
||||
| [`pr-summary`](.github/actions/pr-summary/) | pull_request | Analyzes PR diff, categorizes files, links issues, suggests reviewers | Agent creates PR body + structured JSON |
|
||||
| [`issue-triage`](.github/actions/issue-triage/) | issues.opened | Classifies issues, applies labels, detects duplicates | Agent reads classification, responds or closes |
|
||||
| [`check-status`](.github/actions/check-status/) | check_run (via workflow_call) | Watches all CI checks, produces merged status | Agent knows when to merge or investigate |
|
||||
| [`review`](.github/actions/review/) | pull_request | Lightweight structural analysis (TODOs, missing tests, hardcoded secrets, debug logs, focused tests) | Agent gets findings array and creates issues |
|
||||
|
||||
### `agent-context` — Understand Any Repo Fast
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/agent-context@v1
|
||||
```
|
||||
|
||||
**Output** (in `.agent/output.json`):
|
||||
```json
|
||||
{
|
||||
"outputs": {
|
||||
"name": "my-project",
|
||||
"stack": ["rust", "javascript"],
|
||||
"package_manager": "pnpm",
|
||||
"workspaces": ["crates/*", "packages/*"],
|
||||
"hooks": { "framework": "pre-commit", "config": ".pre-commit-config.yaml" },
|
||||
"issues": { "open": 12, "open_prs": 3 },
|
||||
"labels": ["bug", "enhancement", "question"]
|
||||
},
|
||||
"suggestions": [
|
||||
{ "type": "none", "reason": "Repo context collected" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Also writes `.claude/context.json` for Claude Code users.
|
||||
|
||||
### `pr-summary` — Structured PR Analysis
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/pr-summary@v1
|
||||
with:
|
||||
post-comment: "true"
|
||||
```
|
||||
|
||||
**Output** includes file statistics, change categories, linked issues, and suggested reviewers.
|
||||
|
||||
### `issue-triage` — Auto-Organize Issues
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/issue-triage@v1
|
||||
with:
|
||||
auto-label: "true"
|
||||
duplicate-threshold: "0.7"
|
||||
```
|
||||
|
||||
Detects issue types (bug, feature, question, docs, security, performance), applies labels, and flags potential duplicates via word-overlap similarity.
|
||||
|
||||
### `check-status` — CI Dashboard for Agents
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/check-status@v1
|
||||
with:
|
||||
wait-for-completion: "600"
|
||||
```
|
||||
|
||||
Polls check runs until complete, then tells the agent the merged result and whether to merge.
|
||||
|
||||
### `review` — Lightweight Automated Review
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/review@v1
|
||||
```
|
||||
|
||||
Scans for: TODO/FIXME without issues, missing test files, large files (>500 lines), hardcoded secrets, debug logs, commented-out code, and focused tests (`it.only`, `describe.only`).
|
||||
|
||||
---
|
||||
|
||||
## CI Composite Actions
|
||||
|
||||
| Action | Stack | What It Runs | Opt-Ins |
|
||||
|--------|-------|-------------|---------|
|
||||
| [`rust-ci`](.github/actions/rust-ci/) | 🦀 Rust | `cargo fmt`, `cargo clippy`, `cargo test` | coverage (`cargo-llvm-cov`), audit (`cargo-audit`) |
|
||||
| [`js-ci`](.github/actions/js-ci/) | 🟨 JS/TS | lint, typecheck (tsc --noEmit), test | — |
|
||||
| [`python-ci`](.github/actions/python-ci/) | 🐍 Python | lint (ruff), typecheck (mypy), test (pytest) | — |
|
||||
|
||||
### `rust-ci` Example
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/rust-ci@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
working-directory: ./crates/my-crate
|
||||
enable-coverage: "true"
|
||||
enable-audit: "true"
|
||||
```
|
||||
|
||||
### `js-ci` Example
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/js-ci@v1
|
||||
with:
|
||||
node-version: "22"
|
||||
package-manager: pnpm
|
||||
```
|
||||
|
||||
Auto-detects pnpm, yarn, bun, or npm from lock files.
|
||||
|
||||
### `python-ci` Example
|
||||
|
||||
```yaml
|
||||
- uses: Heretek-AI/heretek-actions/.github/actions/python-ci@v1
|
||||
with:
|
||||
python-version: "3.12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflows
|
||||
|
||||
### `check.yml` — Universal CI Orchestrator
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
check:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/check.yml@v1
|
||||
with:
|
||||
enable-coverage: "true"
|
||||
enable-audit: "true"
|
||||
```
|
||||
|
||||
Auto-detects languages in the repo and runs the matching CI actions. Configure with inputs to override detection.
|
||||
|
||||
### `release.yml` — Universal Release
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
release:
|
||||
uses: Heretek-AI/heretek-actions/.github/workflows/release.yml@v1
|
||||
with:
|
||||
release-type: docker
|
||||
version: v1.2.3
|
||||
dockerfile: ./Dockerfile
|
||||
docker-image-name: my-org/my-app
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
Supports `docker`, `flatpak`, `npm`, and `github-release` as release types.
|
||||
|
||||
---
|
||||
|
||||
## Agent Integration
|
||||
|
||||
### The Standard Envelope
|
||||
|
||||
Every Heretek Action writes `.agent/output.json` following this schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_action": "pr-summary",
|
||||
"version": "1.0",
|
||||
"status": "success",
|
||||
"summary": "3 files changed, 45 additions, 12 deletions",
|
||||
"outputs": { /* action-specific structured data */ },
|
||||
"suggestions": [
|
||||
{
|
||||
"type": "issue:create",
|
||||
"reason": "2 lint errors found in src/main.ts",
|
||||
"data": { "labels": ["lint", "automated"] },
|
||||
"priority": "medium"
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"findings": [],
|
||||
"release": null,
|
||||
"created_at": "2026-07-30T12:00:00Z",
|
||||
"repository": {
|
||||
"owner": "Heretek-AI",
|
||||
"repo": "heretek-actions",
|
||||
"sha": "abc123...",
|
||||
"ref": "refs/heads/main"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent Reading Pattern
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
with open(".agent/output.json") as f:
|
||||
envelope = json.load(f)
|
||||
|
||||
if envelope["status"] == "success":
|
||||
print(f"✅ {envelope['summary']}")
|
||||
else:
|
||||
print(f"❌ {envelope['summary']}")
|
||||
|
||||
for suggestion in envelope.get("suggestions", []):
|
||||
if suggestion["type"] == "issue:create":
|
||||
# Create GitHub issue
|
||||
create_issue(**suggestion["data"])
|
||||
elif suggestion["type"] == "pr:merge":
|
||||
# Merge the PR
|
||||
merge_pr(suggestion["data"]["pr_number"])
|
||||
```
|
||||
|
||||
### Suggestion Types
|
||||
|
||||
| Type | Meaning | Agent Action |
|
||||
|------|---------|-------------|
|
||||
| `issue:create` | Finding worth tracking | Create a GitHub issue |
|
||||
| `issue:label` | Issue needs a label | Add label to issue |
|
||||
| `issue:close` | Probable duplicate | Close or escalate |
|
||||
| `pr:merge` | All checks passed | Merge the PR |
|
||||
| `pr:review_request` | Large/complex change | Request human review |
|
||||
| `comment:post` | Needs communication | Post a comment |
|
||||
| `release:create` | Release completed | Notify stakeholders |
|
||||
| `none` | Informational only | No action needed |
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
This repo provides a universal [`.pre-commit-config.yaml`](.pre-commit-config.yaml) that works across languages:
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
|
||||
# Run on all files
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
**What's included:**
|
||||
- **Universal**: trailing whitespace, EOF fixer, YAML/JSON/TOML validation, merge conflict detection, private key detection
|
||||
- **Security**: `detect-secrets` with baseline support
|
||||
- **Rust**: `cargo check`, `clippy`, `fmt`
|
||||
- **JS/TS**: `eslint`, `prettier`
|
||||
- **Python**: `ruff`, `mypy`
|
||||
- **Shell**: `shellcheck`
|
||||
- **Docker**: `hadolint`
|
||||
- **Commits**: Conventional Commits enforcement
|
||||
|
||||
Language-specific hooks are staged as `manual` — only run if the tooling is present.
|
||||
|
||||
---
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/Heretek-AI/heretek-actions
|
||||
cd heretek-actions
|
||||
|
||||
# Setup pre-commit
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
|
||||
# Validate action.yml files
|
||||
actionlint
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
heretek-actions/
|
||||
├── .agent/
|
||||
│ └── schema.json
|
||||
├── .github/
|
||||
│ ├── actions/
|
||||
│ │ ├── agent-envelope.sh # Shared envelope writer
|
||||
│ │ ├── agent-context/
|
||||
│ │ ├── pr-summary/
|
||||
│ │ ├── issue-triage/
|
||||
│ │ ├── check-status/
|
||||
│ │ ├── review/
|
||||
│ │ ├── rust-ci/
|
||||
│ │ ├── js-ci/
|
||||
│ │ ├── python-ci/
|
||||
│ │ └── release/
|
||||
│ ├── workflows/
|
||||
│ │ ├── agent-context.yml
|
||||
│ │ ├── pr-summary.yml
|
||||
│ │ ├── issue-triage.yml
|
||||
│ │ ├── check-status.yml
|
||||
│ │ ├── review.yml
|
||||
│ │ ├── check.yml
|
||||
│ │ ├── pre-commit-ci.yml
|
||||
│ │ └── release.yml
|
||||
│ └── CODEOWNERS
|
||||
├── .pre-commit-config.yaml
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] **Phase 1**: Foundation — repo structure, schema, README shell
|
||||
- [x] **Phase 2**: Agent-facing actions — context, pr-summary, issue-triage, check-status, review
|
||||
- [x] **Phase 3**: Pre-commit hooks — universal `.pre-commit-config.yaml`, CI workflow
|
||||
- [x] **Phase 4**: CI actions — rust-ci, js-ci, python-ci composite actions
|
||||
- [x] **Phase 5**: Universal check.yml orchestrator with auto-detect
|
||||
- [x] **Phase 6**: Universal release workflow
|
||||
- [x] **Phase 7**: Documentation — README, per-action docs, agent integration guide
|
||||
- [ ] Live testing on reference repos
|
||||
- [ ] GitHub Marketplace listing
|
||||
- [ ] v1 release tag
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Built for agents, by agents. 🤖
|
||||
Reference in New Issue
Block a user