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:
John Smith
2026-07-30 10:58:06 -04:00
co-authored by Claude
commit c3633b2b14
33 changed files with 3796 additions and 0 deletions
+170
View File
@@ -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" }
}
}
}
}
+22
View File
@@ -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
+58
View File
@@ -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"
+157
View File
@@ -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
}
+60
View File
@@ -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"
+67
View File
@@ -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"
+177
View File
@@ -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
+40
View File
@@ -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"
+74
View File
@@ -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
+164
View File
@@ -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"
+154
View File
@@ -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"
+104
View File
@@ -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
+186
View File
@@ -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"
+60
View File
@@ -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
+267
View File
@@ -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"
+155
View File
@@ -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
+69
View File
@@ -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"
+26
View File
@@ -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
+50
View File
@@ -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
+240
View File
@@ -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
+27
View File
@@ -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
+30
View File
@@ -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
+91
View File
@@ -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
+122
View File
@@ -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 }}
+45
View File
@@ -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
View File
@@ -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/
+121
View File
@@ -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: []
+21
View File
@@ -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.
+405
View File
@@ -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.
[![Agent Context](https://github.com/Heretek-AI/heretek-actions/actions/workflows/agent-context.yml/badge.svg)](https://github.com/Heretek-AI/heretek-actions/actions/workflows/agent-context.yml)
[![Pre-commit CI](https://github.com/Heretek-AI/heretek-actions/actions/workflows/pre-commit-ci.yml/badge.svg)](https://github.com/Heretek-AI/heretek-actions/actions/workflows/pre-commit-ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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. 🤖