@northbridge-security/secureai 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/README.md +122 -0
- package/.claude/commands/architect/clean.md +978 -0
- package/.claude/commands/architect/kiss.md +762 -0
- package/.claude/commands/architect/review.md +704 -0
- package/.claude/commands/catchup.md +90 -0
- package/.claude/commands/code.md +115 -0
- package/.claude/commands/commit.md +1218 -0
- package/.claude/commands/cover.md +1298 -0
- package/.claude/commands/fmea.md +275 -0
- package/.claude/commands/kaizen.md +312 -0
- package/.claude/commands/pr.md +503 -0
- package/.claude/commands/todo.md +99 -0
- package/.claude/commands/worktree.md +738 -0
- package/.claude/commands/wrapup.md +103 -0
- package/LICENSE +183 -0
- package/README.md +108 -0
- package/dist/cli.js +75634 -0
- package/docs/agents/devops-reviewer.md +889 -0
- package/docs/agents/kiss-simplifier.md +1088 -0
- package/docs/agents/typescript.md +8 -0
- package/docs/guides/README.md +109 -0
- package/docs/guides/agents.clean.arch.md +244 -0
- package/docs/guides/agents.clean.arch.ts.md +1314 -0
- package/docs/guides/agents.gotask.md +1037 -0
- package/docs/guides/agents.markdown.md +1209 -0
- package/docs/guides/agents.onepassword.md +285 -0
- package/docs/guides/agents.sonar.md +857 -0
- package/docs/guides/agents.tdd.md +838 -0
- package/docs/guides/agents.tdd.ts.md +1062 -0
- package/docs/guides/agents.typesript.md +1389 -0
- package/docs/guides/github-mcp.md +1075 -0
- package/package.json +130 -0
- package/packages/secureai-cli/src/cli.ts +21 -0
- package/tasks/README.md +880 -0
- package/tasks/aws.yml +64 -0
- package/tasks/bash.yml +118 -0
- package/tasks/bun.yml +738 -0
- package/tasks/claude.yml +183 -0
- package/tasks/docker.yml +420 -0
- package/tasks/docs.yml +127 -0
- package/tasks/git.yml +1336 -0
- package/tasks/gotask.yml +132 -0
- package/tasks/json.yml +77 -0
- package/tasks/markdown.yml +95 -0
- package/tasks/onepassword.yml +350 -0
- package/tasks/security.yml +102 -0
- package/tasks/sonar.yml +437 -0
- package/tasks/template.yml +74 -0
- package/tasks/vscode.yml +103 -0
- package/tasks/yaml.yml +121 -0
package/tasks/git.yml
ADDED
|
@@ -0,0 +1,1336 @@
|
|
|
1
|
+
# Git & GitHub Operations Tasks
|
|
2
|
+
# Tasks for Git operations and GitHub PR management
|
|
3
|
+
|
|
4
|
+
version: "3"
|
|
5
|
+
|
|
6
|
+
tasks:
|
|
7
|
+
default:
|
|
8
|
+
desc: "Show available Git tasks"
|
|
9
|
+
aliases: [help, h]
|
|
10
|
+
silent: true
|
|
11
|
+
cmds:
|
|
12
|
+
- |
|
|
13
|
+
# Color codes
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[0;33m'
|
|
16
|
+
BOLD='\033[1m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
echo -e "${BOLD}Git & GitHub Operations${NC}"
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Command Alias Description Examples"
|
|
22
|
+
echo "───────────────────────────────────────────────────────────────────────────────────────────────────"
|
|
23
|
+
echo -e "${BOLD}Repository Utilities:${NC}"
|
|
24
|
+
echo -e " ${GREEN}task git:repo:root${NC} ${YELLOW}root${NC} Get repository root path"
|
|
25
|
+
echo -e " ${GREEN}task git:repo:url${NC} ${YELLOW}url${NC} Get repository HTTPS URL"
|
|
26
|
+
echo -e " ${GREEN}task git:branch:current${NC} ${YELLOW}branch${NC} Get current branch name"
|
|
27
|
+
echo -e " ${GREEN}task git:branch:default${NC} ${YELLOW}main${NC} Get default branch (main/master)"
|
|
28
|
+
echo -e " ${GREEN}task git:branch:list${NC} ${YELLOW}bl${NC} List branches with local/remote status"
|
|
29
|
+
echo -e " ${GREEN}task git:branch:prune${NC} ${YELLOW}prune${NC} Delete local branches gone on remote"
|
|
30
|
+
echo -e " ${GREEN}task git:token${NC} ${YELLOW}token${NC} Get GitHub authentication token"
|
|
31
|
+
echo ""
|
|
32
|
+
echo -e "${BOLD}Pull Request Management:${NC}"
|
|
33
|
+
echo -e " ${GREEN}task git:pr:create${NC} ${YELLOW}pr${NC} Create/update draft PR from file FILE=\".pr.local.md\""
|
|
34
|
+
echo -e " ${GREEN}task git:pr:open${NC} ${YELLOW}pro${NC} Open current PR in browser"
|
|
35
|
+
echo -e " ${GREEN}task git:pr:list${NC} ${YELLOW}prl${NC} List pull requests LIMIT=10 STATE=open"
|
|
36
|
+
echo -e " ${GREEN}task git:pr:comments${NC} ${YELLOW}com${NC} Get all comments from current PR"
|
|
37
|
+
echo ""
|
|
38
|
+
echo -e "${BOLD}GitHub Actions:${NC}"
|
|
39
|
+
echo -e " ${GREEN}task git:runs:log${NC} ${YELLOW}logs${NC} Download workflow run logs STATE=all ALL=true"
|
|
40
|
+
echo -e " ${GREEN}task git:actions:pin${NC} ${YELLOW}pin${NC} Pin actions to SHAs UPGRADE=1 CHECK=1"
|
|
41
|
+
echo ""
|
|
42
|
+
echo -e "${BOLD}Security:${NC}"
|
|
43
|
+
echo -e " ${GREEN}task git:leaks${NC} ${YELLOW}leaks${NC} Scan for secrets with gitleaks"
|
|
44
|
+
echo -e " ${GREEN}task git:cve${NC} ${YELLOW}cve${NC} Download CVEs for repository MIN_SEVERITY=medium"
|
|
45
|
+
echo ""
|
|
46
|
+
echo -e "${BOLD}Usage Examples:${NC}"
|
|
47
|
+
echo -e " task git:pr:list LIMIT=20 STATE=open List 20 open PRs"
|
|
48
|
+
echo -e " task git:pr:create FILE=\"custom.md\" Create PR from custom file"
|
|
49
|
+
echo -e " task git:runs:log Download latest failed run"
|
|
50
|
+
echo -e " task git:runs:log STATE=all ALL=true Download all runs (any state)"
|
|
51
|
+
echo -e " task git:runs:log RUN_ID=12345678 Download specific run"
|
|
52
|
+
echo -e " task git:runs:log WORKFLOW=\"Tests\" STATE=all Download all test runs"
|
|
53
|
+
echo ""
|
|
54
|
+
|
|
55
|
+
cli:
|
|
56
|
+
internal: true
|
|
57
|
+
silent: true
|
|
58
|
+
desc: Verify that gh CLI is installed, and install if not found
|
|
59
|
+
cmds:
|
|
60
|
+
- |
|
|
61
|
+
# Check if gh is already installed
|
|
62
|
+
if command -v gh &> /dev/null; then
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# gh not found, install it silently
|
|
67
|
+
echo "Installing GitHub CLI..."
|
|
68
|
+
|
|
69
|
+
# Detect OS
|
|
70
|
+
OS="$(uname -s)"
|
|
71
|
+
case "$OS" in
|
|
72
|
+
Darwin)
|
|
73
|
+
# macOS - use Homebrew
|
|
74
|
+
if command -v brew &> /dev/null; then
|
|
75
|
+
brew install gh --quiet
|
|
76
|
+
else
|
|
77
|
+
echo "Error: Homebrew not found. Install from https://brew.sh"
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
;;
|
|
81
|
+
|
|
82
|
+
Linux)
|
|
83
|
+
# Linux - try different package managers
|
|
84
|
+
if command -v apt-get &> /dev/null; then
|
|
85
|
+
# Debian/Ubuntu
|
|
86
|
+
sudo mkdir -p -m 755 /etc/apt/keyrings
|
|
87
|
+
wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
|
|
88
|
+
sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
|
|
89
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
|
90
|
+
sudo apt-get update -qq
|
|
91
|
+
sudo apt-get install -qq -y gh
|
|
92
|
+
elif command -v dnf &> /dev/null; then
|
|
93
|
+
# Fedora/RHEL 9+
|
|
94
|
+
sudo dnf install -q -y 'dnf-command(config-manager)'
|
|
95
|
+
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
|
96
|
+
sudo dnf install -q -y gh
|
|
97
|
+
elif command -v yum &> /dev/null; then
|
|
98
|
+
# RHEL/CentOS 8
|
|
99
|
+
sudo yum install -q -y 'dnf-command(config-manager)'
|
|
100
|
+
sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
|
101
|
+
sudo yum install -q -y gh
|
|
102
|
+
else
|
|
103
|
+
echo "Error: No supported package manager found (apt-get, dnf, yum)"
|
|
104
|
+
echo "Install manually from https://github.com/cli/cli/releases"
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
;;
|
|
108
|
+
|
|
109
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
110
|
+
# Windows - try winget, then choco, then scoop
|
|
111
|
+
if command -v winget.exe &> /dev/null; then
|
|
112
|
+
winget.exe install --id GitHub.cli --silent --accept-package-agreements --accept-source-agreements
|
|
113
|
+
elif command -v choco &> /dev/null; then
|
|
114
|
+
choco install gh -y
|
|
115
|
+
elif command -v scoop &> /dev/null; then
|
|
116
|
+
scoop install gh
|
|
117
|
+
else
|
|
118
|
+
echo "Error: No supported package manager found (winget, chocolatey, scoop)"
|
|
119
|
+
echo "Install manually from https://github.com/cli/cli/releases"
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
;;
|
|
123
|
+
|
|
124
|
+
*)
|
|
125
|
+
echo "Error: Unsupported operating system: $OS"
|
|
126
|
+
echo "Install manually from https://github.com/cli/cli/releases"
|
|
127
|
+
exit 1
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
|
|
131
|
+
# Verify installation
|
|
132
|
+
if command -v gh &> /dev/null; then
|
|
133
|
+
echo "✓ GitHub CLI installed successfully"
|
|
134
|
+
else
|
|
135
|
+
echo "Error: Installation completed but gh command not found"
|
|
136
|
+
echo "Try closing and reopening your terminal"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
token:
|
|
141
|
+
desc: Get the authentication token
|
|
142
|
+
aliases: [token]
|
|
143
|
+
silent: true
|
|
144
|
+
cmds:
|
|
145
|
+
- |
|
|
146
|
+
# Source secrets if .env has 1Password references
|
|
147
|
+
source $(task op:export)
|
|
148
|
+
|
|
149
|
+
# Check if token is still encrypted (force a refresh)
|
|
150
|
+
if [[ "${GITHUB_TOKEN:-}" == *"op://"* ]]; then
|
|
151
|
+
source $(task op:export:force)
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Check if token is STILL encrypted (1Password reference not resolved)
|
|
155
|
+
if [[ "${GITHUB_TOKEN:-}" == *"op://"* ]]; then
|
|
156
|
+
echo -e "${RED}❌ Unable to decrypt authentication token${NC}" >&2
|
|
157
|
+
echo "1Password CLI may not be installed or configured" >&2
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Validate GitHub token is available
|
|
162
|
+
if [ -z "${GITHUB_TOKEN:-}" ]; then
|
|
163
|
+
echo -e "${RED}❌ GitHub token not found${NC}" >&2
|
|
164
|
+
echo "Set GITHUB_TOKEN in .env" >&2
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Output token for capture by calling task
|
|
169
|
+
echo "$GITHUB_TOKEN"
|
|
170
|
+
|
|
171
|
+
repo:root:
|
|
172
|
+
desc: Get the absolute path to the repository root
|
|
173
|
+
silent: true
|
|
174
|
+
aliases: [root]
|
|
175
|
+
cmds:
|
|
176
|
+
- |
|
|
177
|
+
ROOT_DIR=$(git rev-parse --show-toplevel)
|
|
178
|
+
if [ -z "$ROOT_DIR" ]; then
|
|
179
|
+
echo "Error: Not in a git repository"
|
|
180
|
+
exit 1
|
|
181
|
+
fi
|
|
182
|
+
echo "$ROOT_DIR"
|
|
183
|
+
|
|
184
|
+
repo:url:
|
|
185
|
+
silent: true
|
|
186
|
+
aliases: [url]
|
|
187
|
+
desc: Get repository HTTPS URL from git remote (converts SSH to HTTPS, git@git alias to github.com)
|
|
188
|
+
cmds:
|
|
189
|
+
- |
|
|
190
|
+
# Get remote URL
|
|
191
|
+
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
192
|
+
|
|
193
|
+
if [ -z "$REMOTE_URL" ]; then
|
|
194
|
+
echo "Error: No git remote 'origin' found" >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Convert SSH URL to HTTPS URL using sed (more reliable than bash regex)
|
|
199
|
+
# SSH formats:
|
|
200
|
+
# git@github.com:owner/repo.git
|
|
201
|
+
# git@git:owner/repo.git (alias for github.com)
|
|
202
|
+
# HTTPS formats:
|
|
203
|
+
# https://github.com/owner/repo.git
|
|
204
|
+
|
|
205
|
+
if echo "$REMOTE_URL" | grep -q '^git@'; then
|
|
206
|
+
# SSH format: git@host:owner/repo.git
|
|
207
|
+
# Extract host and path using sed
|
|
208
|
+
HOST=$(echo "$REMOTE_URL" | sed -E 's/^git@([^:]+):.*/\1/')
|
|
209
|
+
PATH=$(echo "$REMOTE_URL" | sed -E 's/^git@[^:]+:(.*)/\1/')
|
|
210
|
+
|
|
211
|
+
# Remove .git suffix if present
|
|
212
|
+
PATH="${PATH%.git}"
|
|
213
|
+
|
|
214
|
+
# Convert git@git alias to github.com
|
|
215
|
+
if [ "$HOST" = "git" ]; then
|
|
216
|
+
HOST="github.com"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
HTTPS_URL="https://${HOST}/${PATH}"
|
|
220
|
+
elif echo "$REMOTE_URL" | grep -q '^https\?://'; then
|
|
221
|
+
# Already HTTPS format
|
|
222
|
+
HTTPS_URL="${REMOTE_URL%.git}"
|
|
223
|
+
else
|
|
224
|
+
echo "Error: Unrecognized git remote URL format: $REMOTE_URL" >&2
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
echo "$HTTPS_URL"
|
|
229
|
+
|
|
230
|
+
branch:current:
|
|
231
|
+
desc: Get the current branch name
|
|
232
|
+
aliases: [branch]
|
|
233
|
+
silent: true
|
|
234
|
+
cmds:
|
|
235
|
+
- |
|
|
236
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
237
|
+
if [ -z "$CURRENT_BRANCH" ]; then
|
|
238
|
+
echo "Error: Not in a git repository" >&2
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
echo "$CURRENT_BRANCH"
|
|
242
|
+
|
|
243
|
+
branch:default:
|
|
244
|
+
desc: Get the default branch (main/master)
|
|
245
|
+
silent: true
|
|
246
|
+
aliases: [main]
|
|
247
|
+
cmds:
|
|
248
|
+
- |
|
|
249
|
+
# Get default branch (local-only, no network access needed)
|
|
250
|
+
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
|
|
251
|
+
|
|
252
|
+
# Fallback: try to detect from remote branches
|
|
253
|
+
if [ -z "$DEFAULT_BRANCH" ]; then
|
|
254
|
+
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
|
255
|
+
DEFAULT_BRANCH="main"
|
|
256
|
+
elif git show-ref --verify --quiet refs/remotes/origin/master; then
|
|
257
|
+
DEFAULT_BRANCH="master"
|
|
258
|
+
else
|
|
259
|
+
# Last resort: use gh CLI
|
|
260
|
+
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || echo "main")
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
if [ -z "$DEFAULT_BRANCH" ]; then
|
|
265
|
+
echo "Error: Unable to determine default branch" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
echo "$DEFAULT_BRANCH"
|
|
270
|
+
|
|
271
|
+
branch:prune:
|
|
272
|
+
desc: Delete local branches that no longer exist on remote
|
|
273
|
+
silent: true
|
|
274
|
+
aliases: [prune, clean-branches]
|
|
275
|
+
cmds:
|
|
276
|
+
- bun run ./src/tasks/git/branch-prune.ts
|
|
277
|
+
|
|
278
|
+
branch:list:
|
|
279
|
+
desc: List all branches with local/remote status
|
|
280
|
+
silent: true
|
|
281
|
+
aliases: [branches, bl]
|
|
282
|
+
cmds:
|
|
283
|
+
- |
|
|
284
|
+
# Color codes
|
|
285
|
+
GREEN='\033[0;32m'
|
|
286
|
+
YELLOW='\033[0;33m'
|
|
287
|
+
RED='\033[0;31m'
|
|
288
|
+
CYAN='\033[0;36m'
|
|
289
|
+
BOLD='\033[1m'
|
|
290
|
+
NC='\033[0m'
|
|
291
|
+
|
|
292
|
+
# Fetch latest remote info (silent)
|
|
293
|
+
git fetch --prune &>/dev/null || true
|
|
294
|
+
|
|
295
|
+
# Get current branch
|
|
296
|
+
CURRENT=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
297
|
+
|
|
298
|
+
# Get default branch
|
|
299
|
+
DEFAULT=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
|
|
300
|
+
|
|
301
|
+
# Collect local branches (strip * for current, + for worktree)
|
|
302
|
+
LOCAL_BRANCHES=$(git branch 2>/dev/null | sed 's/^[\*+] //' | sed 's/^ *//' | sort -u)
|
|
303
|
+
|
|
304
|
+
# Collect remote branches (strip origin/ prefix)
|
|
305
|
+
REMOTE_BRANCHES=$(git branch -r 2>/dev/null | grep -v '\->' | sed 's/^ *//' | sed 's|^origin/||' | sort -u)
|
|
306
|
+
|
|
307
|
+
# Combine and dedupe all branches
|
|
308
|
+
ALL_BRANCHES=$(printf "%s\n%s" "$LOCAL_BRANCHES" "$REMOTE_BRANCHES" | sort -u | grep -v '^$')
|
|
309
|
+
|
|
310
|
+
# Print header
|
|
311
|
+
printf "${BOLD}Branches${NC}\n\n"
|
|
312
|
+
printf "%-44s %-10s %-10s %s\n" "Branch" "Local" "Remote" "Status"
|
|
313
|
+
printf "────────────────────────────────────────────────────────────────────────────────\n"
|
|
314
|
+
|
|
315
|
+
# Counters
|
|
316
|
+
TOTAL=0
|
|
317
|
+
SYNCED=0
|
|
318
|
+
LOCAL_ONLY=0
|
|
319
|
+
REMOTE_ONLY=0
|
|
320
|
+
|
|
321
|
+
# Process each branch
|
|
322
|
+
echo "$ALL_BRANCHES" | while read -r branch; do
|
|
323
|
+
[ -z "$branch" ] && continue
|
|
324
|
+
|
|
325
|
+
# Check if exists locally
|
|
326
|
+
IS_LOCAL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
|
|
327
|
+
|
|
328
|
+
# Check if exists remotely
|
|
329
|
+
IS_REMOTE=$(echo "$REMOTE_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
|
|
330
|
+
|
|
331
|
+
# Determine indicators and status
|
|
332
|
+
if [ "$IS_LOCAL" = "yes" ] && [ "$IS_REMOTE" = "yes" ]; then
|
|
333
|
+
LOCAL_IND="${GREEN}✓${NC}"
|
|
334
|
+
REMOTE_IND="${GREEN}✓${NC}"
|
|
335
|
+
STATUS="${GREEN}synced${NC}"
|
|
336
|
+
elif [ "$IS_LOCAL" = "yes" ]; then
|
|
337
|
+
LOCAL_IND="${GREEN}✓${NC}"
|
|
338
|
+
REMOTE_IND="${RED}✗${NC}"
|
|
339
|
+
STATUS="${YELLOW}local only${NC}"
|
|
340
|
+
else
|
|
341
|
+
LOCAL_IND="${RED}✗${NC}"
|
|
342
|
+
REMOTE_IND="${GREEN}✓${NC}"
|
|
343
|
+
STATUS="${CYAN}remote only${NC}"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Mark current and default branches
|
|
347
|
+
MARKER=""
|
|
348
|
+
if [ "$branch" = "$CURRENT" ]; then
|
|
349
|
+
MARKER=" ${CYAN}← current${NC}"
|
|
350
|
+
elif [ "$branch" = "$DEFAULT" ]; then
|
|
351
|
+
MARKER=" ${YELLOW}← default${NC}"
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# Print row
|
|
355
|
+
printf "%-44s ${LOCAL_IND} ${REMOTE_IND} ${STATUS}${MARKER}\n" "$branch"
|
|
356
|
+
done
|
|
357
|
+
|
|
358
|
+
# Calculate summary (outside subshell)
|
|
359
|
+
TOTAL=$(echo "$ALL_BRANCHES" | grep -c '.' || echo 0)
|
|
360
|
+
SYNCED=0
|
|
361
|
+
LOCAL_ONLY=0
|
|
362
|
+
REMOTE_ONLY=0
|
|
363
|
+
|
|
364
|
+
echo "$ALL_BRANCHES" | while read -r branch; do
|
|
365
|
+
[ -z "$branch" ] && continue
|
|
366
|
+
IS_LOCAL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
|
|
367
|
+
IS_REMOTE=$(echo "$REMOTE_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
|
|
368
|
+
if [ "$IS_LOCAL" = "yes" ] && [ "$IS_REMOTE" = "yes" ]; then
|
|
369
|
+
SYNCED=$((SYNCED + 1))
|
|
370
|
+
elif [ "$IS_LOCAL" = "yes" ]; then
|
|
371
|
+
LOCAL_ONLY=$((LOCAL_ONLY + 1))
|
|
372
|
+
else
|
|
373
|
+
REMOTE_ONLY=$((REMOTE_ONLY + 1))
|
|
374
|
+
fi
|
|
375
|
+
echo "$SYNCED $LOCAL_ONLY $REMOTE_ONLY"
|
|
376
|
+
done | tail -1 | read SYNCED LOCAL_ONLY REMOTE_ONLY 2>/dev/null || true
|
|
377
|
+
|
|
378
|
+
# Recalculate to avoid subshell issue
|
|
379
|
+
SYNCED=$(echo "$ALL_BRANCHES" | while read -r b; do
|
|
380
|
+
[ -z "$b" ] && continue
|
|
381
|
+
IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
382
|
+
IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
383
|
+
[ "$IL" = "y" ] && [ "$IR" = "y" ] && echo "$b"
|
|
384
|
+
done | grep -c '.' || echo 0)
|
|
385
|
+
|
|
386
|
+
LOCAL_ONLY=$(echo "$ALL_BRANCHES" | while read -r b; do
|
|
387
|
+
[ -z "$b" ] && continue
|
|
388
|
+
IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
389
|
+
IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
390
|
+
[ "$IL" = "y" ] && [ "$IR" = "n" ] && echo "$b"
|
|
391
|
+
done | grep -c '.' || echo 0)
|
|
392
|
+
|
|
393
|
+
REMOTE_ONLY=$(echo "$ALL_BRANCHES" | while read -r b; do
|
|
394
|
+
[ -z "$b" ] && continue
|
|
395
|
+
IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
396
|
+
IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
|
|
397
|
+
[ "$IL" = "n" ] && [ "$IR" = "y" ] && echo "$b"
|
|
398
|
+
done | grep -c '.' || echo 0)
|
|
399
|
+
|
|
400
|
+
printf "\n${BOLD}Summary:${NC} $TOTAL branches ($SYNCED synced, $LOCAL_ONLY local only, $REMOTE_ONLY remote only)\n"
|
|
401
|
+
|
|
402
|
+
pr:list:
|
|
403
|
+
desc: "List pull requests (use LIMIT=n to change number shown, default 10)"
|
|
404
|
+
silent: true
|
|
405
|
+
deps: [cli]
|
|
406
|
+
dotenv: [".env"]
|
|
407
|
+
vars:
|
|
408
|
+
LIMIT: '{{.LIMIT | default "10"}}'
|
|
409
|
+
STATE: '{{.STATE | default "all"}}'
|
|
410
|
+
cmds:
|
|
411
|
+
- |
|
|
412
|
+
# Get and validate GitHub token (handles 1Password decryption)
|
|
413
|
+
export GH_TOKEN=$(task git:token)
|
|
414
|
+
|
|
415
|
+
# Get repository info
|
|
416
|
+
REPO=$(task git:repo:url)
|
|
417
|
+
|
|
418
|
+
# If that fails, extract from git remote
|
|
419
|
+
if [ -z "$REPO" ]; then
|
|
420
|
+
REPO=$(git remote get-url origin | sed -E 's|^(https://github.com/|git@github.com:)||' | sed 's|\.git$||')
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
echo -e "{{.CYAN}}Pull Requests for: $REPO{{.NC}}"
|
|
424
|
+
echo ""
|
|
425
|
+
|
|
426
|
+
# List PRs with state filter
|
|
427
|
+
STATE="{{.STATE}}"
|
|
428
|
+
STATE_FILTER=""
|
|
429
|
+
if [ "$STATE" != "all" ]; then
|
|
430
|
+
STATE_FILTER="--state $STATE"
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
# Show PRs in a nice table format
|
|
434
|
+
gh pr list $STATE_FILTER --limit {{.LIMIT}} --json number,title,state,author,createdAt,isDraft,headRefName,baseRefName --jq '.[] | "#\(.number) | " + (if .isDraft then "📝 " else "" end) + (.title | .[0:60]) + (if (.title | length) > 60 then "..." else "" end) + " | " + (.state | ascii_upcase) + " | " + .author.login + " | " + (.headRefName + " → " + .baseRefName) + " | " + (.createdAt | fromdateiso8601 | strftime("%Y-%m-%d"))' | column -t -s '|'
|
|
435
|
+
|
|
436
|
+
echo ""
|
|
437
|
+
echo -e "{{.YELLOW}}Total shown: {{.LIMIT}} (use LIMIT=n to change){{.NC}}"
|
|
438
|
+
echo -e "{{.YELLOW}}State filter: {{.STATE}} (use STATE=open|closed|merged|all){{.NC}}"
|
|
439
|
+
|
|
440
|
+
aliases: [prl]
|
|
441
|
+
|
|
442
|
+
pr:create:
|
|
443
|
+
desc: Create draft GitHub pull request from markdown file
|
|
444
|
+
aliases: [pr]
|
|
445
|
+
deps: [cli]
|
|
446
|
+
silent: true
|
|
447
|
+
vars:
|
|
448
|
+
PR_FILE: '{{.FILE | default ".pr.local.md"}}'
|
|
449
|
+
cmds:
|
|
450
|
+
- |
|
|
451
|
+
set -euo pipefail
|
|
452
|
+
|
|
453
|
+
# Color codes
|
|
454
|
+
GREEN='\033[0;32m'
|
|
455
|
+
YELLOW='\033[0;33m'
|
|
456
|
+
RED='\033[0;31m'
|
|
457
|
+
BOLD='\033[1m'
|
|
458
|
+
NC='\033[0m'
|
|
459
|
+
|
|
460
|
+
PR_FILE="{{.PR_FILE}}"
|
|
461
|
+
PR_URL=""
|
|
462
|
+
PR_NUMBER=""
|
|
463
|
+
PR_ACTION=""
|
|
464
|
+
|
|
465
|
+
# Validate PR file exists
|
|
466
|
+
if [ ! -f "$PR_FILE" ]; then
|
|
467
|
+
echo -e "${RED}❌ PR file not found: $PR_FILE${NC}" >&2
|
|
468
|
+
echo "Run /pr command first to generate PR content" >&2
|
|
469
|
+
exit 1
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
# Get and validate GitHub token (handles 1Password decryption)
|
|
473
|
+
export GH_TOKEN=$(task git:token)
|
|
474
|
+
|
|
475
|
+
# Ensure we're in a git repository
|
|
476
|
+
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|
477
|
+
echo -e "${RED}❌ Not in a git repository${NC}" >&2
|
|
478
|
+
exit 1
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
# Check if current branch has commits
|
|
482
|
+
if ! git log -1 > /dev/null 2>&1; then
|
|
483
|
+
echo -e "${RED}❌ No commits on current branch${NC}" >&2
|
|
484
|
+
echo "Make at least one commit before creating PR" >&2
|
|
485
|
+
exit 1
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
# Extract title (first line starting with # )
|
|
489
|
+
TITLE=$(grep "^# " "$PR_FILE" | head -n 1 | sed 's/^# //')
|
|
490
|
+
|
|
491
|
+
if [ -z "$TITLE" ]; then
|
|
492
|
+
echo -e "${RED}❌ No title found in PR file${NC}" >&2
|
|
493
|
+
echo "PR file must start with '# Title'" >&2
|
|
494
|
+
exit 1
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# Extract body (everything after first line, skip empty lines at start)
|
|
498
|
+
BODY=$(sed '1d' "$PR_FILE" | sed -e :a -e '/./,$!d;/^\n*$/{$d;N;};/\n$/ba')
|
|
499
|
+
|
|
500
|
+
if [ -z "$BODY" ]; then
|
|
501
|
+
echo -e "${RED}❌ No body content found in PR file${NC}" >&2
|
|
502
|
+
exit 1
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
echo -e "${GREEN}✓${NC} Title: $TITLE"
|
|
506
|
+
|
|
507
|
+
# Get current and default branches
|
|
508
|
+
CURRENT_BRANCH=$(task git:branch:current)
|
|
509
|
+
DEFAULT_BRANCH=$(task git:branch:default)
|
|
510
|
+
|
|
511
|
+
# Push branch to remote (gh pr create will handle this if needed)
|
|
512
|
+
# Try to push, but don't fail if SSH isn't configured - gh CLI will use HTTPS
|
|
513
|
+
|
|
514
|
+
if git push -u origin "$CURRENT_BRANCH" &>/dev/null; then
|
|
515
|
+
echo -e "${GREEN}✓${NC} Branch pushed to origin/$CURRENT_BRANCH"
|
|
516
|
+
else
|
|
517
|
+
echo -e "${YELLOW}⚠${NC} Git push failed (possibly SSH not configured)"
|
|
518
|
+
echo -e "${YELLOW}→${NC} Will rely on gh CLI to push via HTTPS"
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# Check if PR already exists for this branch
|
|
522
|
+
EXISTING_PR=$(gh pr list --head "$CURRENT_BRANCH" --base "$DEFAULT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
|
523
|
+
|
|
524
|
+
if [ -n "$EXISTING_PR" ]; then
|
|
525
|
+
# Update PR title and body
|
|
526
|
+
gh pr edit "$EXISTING_PR" \
|
|
527
|
+
--title "$TITLE" \
|
|
528
|
+
--body "$BODY" &>/dev/null
|
|
529
|
+
|
|
530
|
+
PR_NUMBER="$EXISTING_PR"
|
|
531
|
+
PR_ACTION="Updated"
|
|
532
|
+
else
|
|
533
|
+
# Create draft PR and capture output
|
|
534
|
+
PR_OUTPUT=$(gh pr create \
|
|
535
|
+
--base "$DEFAULT_BRANCH" \
|
|
536
|
+
--head "$CURRENT_BRANCH" \
|
|
537
|
+
--draft \
|
|
538
|
+
--title "$TITLE" \
|
|
539
|
+
--body "$BODY" 2>&1)
|
|
540
|
+
|
|
541
|
+
if [ $? -ne 0 ]; then
|
|
542
|
+
echo -e "${RED}❌ Failed to create PR${NC}" >&2
|
|
543
|
+
echo "$PR_OUTPUT" >&2
|
|
544
|
+
exit 1
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
# Extract PR number from output URL
|
|
548
|
+
PR_URL=$(echo "$PR_OUTPUT" | grep -o 'https://github.com/[^/]*/[^/]*/pull/[0-9]*' | head -1)
|
|
549
|
+
PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]*$')
|
|
550
|
+
PR_ACTION="Created"
|
|
551
|
+
fi
|
|
552
|
+
|
|
553
|
+
# Get PR URL for opening in browser
|
|
554
|
+
if [ -z "$PR_URL" ] && [ -n "$PR_NUMBER" ]; then
|
|
555
|
+
# Get URL from gh CLI
|
|
556
|
+
PR_URL=$(gh pr view "$PR_NUMBER" --json url --jq '.url' 2>/dev/null || echo "")
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
# Display success message with URL
|
|
560
|
+
if [ -n "$PR_URL" ]; then
|
|
561
|
+
echo -e "${GREEN}✓${NC} ${PR_ACTION}: $PR_URL"
|
|
562
|
+
|
|
563
|
+
# Open PR in browser (cross-platform)
|
|
564
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
565
|
+
# macOS
|
|
566
|
+
open "$PR_URL"
|
|
567
|
+
elif [ "$(uname)" = "Linux" ]; then
|
|
568
|
+
# Linux
|
|
569
|
+
if command -v xdg-open &> /dev/null; then
|
|
570
|
+
xdg-open "$PR_URL"
|
|
571
|
+
fi
|
|
572
|
+
elif [ "$(uname -o 2>/dev/null)" = "Msys" ] || [ "$(uname -o 2>/dev/null)" = "Cygwin" ]; then
|
|
573
|
+
# Windows (Git Bash or Cygwin)
|
|
574
|
+
start "$PR_URL"
|
|
575
|
+
fi
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
pr:open:
|
|
579
|
+
desc: Open current pull request in browser
|
|
580
|
+
aliases: [pro]
|
|
581
|
+
deps: [cli]
|
|
582
|
+
silent: true
|
|
583
|
+
cmds:
|
|
584
|
+
- |
|
|
585
|
+
set -euo pipefail
|
|
586
|
+
|
|
587
|
+
# Color codes
|
|
588
|
+
GREEN='\033[0;32m'
|
|
589
|
+
YELLOW='\033[0;33m'
|
|
590
|
+
RED='\033[0;31m'
|
|
591
|
+
BOLD='\033[1m'
|
|
592
|
+
NC='\033[0m'
|
|
593
|
+
|
|
594
|
+
# Get and validate GitHub token
|
|
595
|
+
export GH_TOKEN=$(task git:token)
|
|
596
|
+
|
|
597
|
+
# Get current and default branches
|
|
598
|
+
CURRENT_BRANCH=$(task git:branch:current)
|
|
599
|
+
DEFAULT_BRANCH=$(task git:branch:default)
|
|
600
|
+
|
|
601
|
+
# Check if PR exists for this branch
|
|
602
|
+
PR_NUMBER=$(gh pr list --head "$CURRENT_BRANCH" --base "$DEFAULT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
|
603
|
+
|
|
604
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
605
|
+
echo -e "${RED}❌ No pull request found for branch: $CURRENT_BRANCH${NC}" >&2
|
|
606
|
+
echo "" >&2
|
|
607
|
+
echo "Create a PR first:" >&2
|
|
608
|
+
echo -e " ${GREEN}task git:pr:create${NC}" >&2
|
|
609
|
+
echo "" >&2
|
|
610
|
+
exit 1
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
# Get PR URL
|
|
614
|
+
PR_URL=$(gh pr view "$PR_NUMBER" --json url --jq '.url' 2>/dev/null || echo "")
|
|
615
|
+
|
|
616
|
+
if [ -z "$PR_URL" ]; then
|
|
617
|
+
echo -e "${RED}❌ Failed to get PR URL${NC}" >&2
|
|
618
|
+
exit 1
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
echo -e "${GREEN}✓${NC} Opening: $PR_URL"
|
|
622
|
+
|
|
623
|
+
# Detect OS and open browser
|
|
624
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
625
|
+
open "$PR_URL"
|
|
626
|
+
elif [ "$(uname)" = "Linux" ]; then
|
|
627
|
+
if command -v xdg-open &> /dev/null; then
|
|
628
|
+
xdg-open "$PR_URL"
|
|
629
|
+
fi
|
|
630
|
+
elif [ "$(uname -o 2>/dev/null)" = "Msys" ] || [ "$(uname -o 2>/dev/null)" = "Cygwin" ]; then
|
|
631
|
+
start "$PR_URL"
|
|
632
|
+
fi
|
|
633
|
+
|
|
634
|
+
pr:comments:
|
|
635
|
+
desc: Retrieve all comments of the current pull request
|
|
636
|
+
aliases: [com]
|
|
637
|
+
silent: true
|
|
638
|
+
deps: [cli]
|
|
639
|
+
dotenv: [".env"]
|
|
640
|
+
vars:
|
|
641
|
+
REPO_ROOT:
|
|
642
|
+
sh: git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
643
|
+
LOG_DIR:
|
|
644
|
+
sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/comments"
|
|
645
|
+
cmds:
|
|
646
|
+
- |
|
|
647
|
+
# Color codes
|
|
648
|
+
GREEN='\033[0;32m'
|
|
649
|
+
RED='\033[0;31m'
|
|
650
|
+
NC='\033[0m'
|
|
651
|
+
|
|
652
|
+
# Get and validate GitHub token
|
|
653
|
+
export GH_TOKEN=$(task git:token)
|
|
654
|
+
|
|
655
|
+
# Get current branch and repository
|
|
656
|
+
CURRENT_BRANCH=$(task git:branch:current)
|
|
657
|
+
REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
|
|
658
|
+
|
|
659
|
+
# Find PR for current branch (gh pr view auto-detects PR for current branch)
|
|
660
|
+
PR_NUMBER=$(gh pr view --json number --jq '.number' 2>/dev/null || echo "")
|
|
661
|
+
|
|
662
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
663
|
+
echo -e "${RED}❌ No pull request found for branch: $CURRENT_BRANCH${NC}" >&2
|
|
664
|
+
exit 1
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
# Get PR details
|
|
668
|
+
PR_DATA=$(gh pr view "$PR_NUMBER" --json number,title,author,state)
|
|
669
|
+
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
|
670
|
+
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
|
671
|
+
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
|
|
672
|
+
|
|
673
|
+
# Create log directory
|
|
674
|
+
mkdir -p "{{.LOG_DIR}}"
|
|
675
|
+
|
|
676
|
+
# Generate timestamp
|
|
677
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
678
|
+
LOG_FILE="{{.LOG_DIR}}/${TIMESTAMP}.log"
|
|
679
|
+
|
|
680
|
+
# Fetch all comments (silent)
|
|
681
|
+
{
|
|
682
|
+
echo "=========================================="
|
|
683
|
+
echo "PR Comments Report"
|
|
684
|
+
echo "=========================================="
|
|
685
|
+
echo "Generated: $(date)"
|
|
686
|
+
echo "Repository: $REPO"
|
|
687
|
+
echo "PR #$PR_NUMBER: $PR_TITLE"
|
|
688
|
+
echo "Author: $PR_AUTHOR"
|
|
689
|
+
echo "State: $PR_STATE"
|
|
690
|
+
echo "Branch: $CURRENT_BRANCH"
|
|
691
|
+
echo ""
|
|
692
|
+
echo "=========================================="
|
|
693
|
+
echo "ISSUE COMMENTS (General PR Discussion)"
|
|
694
|
+
echo "=========================================="
|
|
695
|
+
echo ""
|
|
696
|
+
|
|
697
|
+
# Get issue comments
|
|
698
|
+
ISSUE_COMMENTS=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // []')
|
|
699
|
+
ISSUE_COUNT=$(echo "$ISSUE_COMMENTS" | jq 'length')
|
|
700
|
+
|
|
701
|
+
if [ "$ISSUE_COUNT" -gt 0 ]; then
|
|
702
|
+
echo "$ISSUE_COMMENTS" | jq -r '.[] | "[@\(.user.login)] \(.created_at)\n\(.body)\n\n---\n"'
|
|
703
|
+
else
|
|
704
|
+
echo "No general comments found."
|
|
705
|
+
echo ""
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
echo ""
|
|
709
|
+
echo "=========================================="
|
|
710
|
+
echo "REVIEW COMMENTS (Inline Code Comments)"
|
|
711
|
+
echo "=========================================="
|
|
712
|
+
echo ""
|
|
713
|
+
|
|
714
|
+
# Get review comments
|
|
715
|
+
REVIEW_COMMENTS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // []')
|
|
716
|
+
REVIEW_COUNT=$(echo "$REVIEW_COMMENTS" | jq 'length')
|
|
717
|
+
|
|
718
|
+
if [ "$REVIEW_COUNT" -gt 0 ]; then
|
|
719
|
+
echo "$REVIEW_COMMENTS" | jq -r '.[] | "[@\(.user.login)] \(.path):\(.line // .original_line // "??") - \(.created_at)\n\(.body)\n\n---\n"'
|
|
720
|
+
else
|
|
721
|
+
echo "No code review comments found."
|
|
722
|
+
echo ""
|
|
723
|
+
fi
|
|
724
|
+
|
|
725
|
+
echo ""
|
|
726
|
+
echo "=========================================="
|
|
727
|
+
echo "REVIEWS (Approve/Request Changes/Comment)"
|
|
728
|
+
echo "=========================================="
|
|
729
|
+
echo ""
|
|
730
|
+
|
|
731
|
+
# Get reviews
|
|
732
|
+
REVIEWS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null | jq -s 'add // []')
|
|
733
|
+
REVIEWS_COUNT=$(echo "$REVIEWS" | jq 'length')
|
|
734
|
+
|
|
735
|
+
if [ "$REVIEWS_COUNT" -gt 0 ]; then
|
|
736
|
+
echo "$REVIEWS" | jq -r '.[] | "[@\(.user.login)] \(.state) - \(.submitted_at)\n\(.body // "No comment")\n\n---\n"'
|
|
737
|
+
else
|
|
738
|
+
echo "No reviews found."
|
|
739
|
+
echo ""
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
echo ""
|
|
743
|
+
echo "=========================================="
|
|
744
|
+
echo "SUMMARY"
|
|
745
|
+
echo "=========================================="
|
|
746
|
+
echo "General comments: $ISSUE_COUNT"
|
|
747
|
+
echo "Code review comments: $REVIEW_COUNT"
|
|
748
|
+
echo "Reviews: $REVIEWS_COUNT"
|
|
749
|
+
echo "Total: $((ISSUE_COUNT + REVIEW_COUNT + REVIEWS_COUNT))"
|
|
750
|
+
} > "$LOG_FILE"
|
|
751
|
+
|
|
752
|
+
# Display summary - calculate counts from already-fetched data
|
|
753
|
+
ISSUE_COUNT=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // [] | length')
|
|
754
|
+
REVIEW_COUNT=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // [] | length')
|
|
755
|
+
REVIEWS_COUNT=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null | jq -s 'add // [] | length')
|
|
756
|
+
TOTAL=$((ISSUE_COUNT + REVIEW_COUNT + REVIEWS_COUNT))
|
|
757
|
+
|
|
758
|
+
echo -e "${GREEN}✓${NC} Retrieved $TOTAL comments for PR #$PR_NUMBER → $LOG_FILE"
|
|
759
|
+
|
|
760
|
+
# Open in VSCode if available (silent if not installed)
|
|
761
|
+
if command -v code &> /dev/null; then
|
|
762
|
+
code "$LOG_FILE" 2>/dev/null || true
|
|
763
|
+
elif command -v code-insiders &> /dev/null; then
|
|
764
|
+
code-insiders "$LOG_FILE" 2>/dev/null || true
|
|
765
|
+
fi
|
|
766
|
+
|
|
767
|
+
runs:log:
|
|
768
|
+
desc: Download GitHub Actions workflow run logs
|
|
769
|
+
aliases: [logs, rl]
|
|
770
|
+
silent: true
|
|
771
|
+
deps: [cli]
|
|
772
|
+
vars:
|
|
773
|
+
RUN_ID: '{{.RUN_ID | default ""}}'
|
|
774
|
+
WORKFLOW: '{{.WORKFLOW | default ""}}'
|
|
775
|
+
STATE: '{{.STATE | default "failure"}}'
|
|
776
|
+
ALL: '{{.ALL | default "false"}}'
|
|
777
|
+
REPO_ROOT:
|
|
778
|
+
sh: git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
779
|
+
BASE_LOG_DIR:
|
|
780
|
+
sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/runs"
|
|
781
|
+
cmds:
|
|
782
|
+
- |
|
|
783
|
+
set -euo pipefail
|
|
784
|
+
|
|
785
|
+
# Color codes
|
|
786
|
+
GREEN='\033[0;32m'
|
|
787
|
+
RED='\033[0;31m'
|
|
788
|
+
YELLOW='\033[0;33m'
|
|
789
|
+
NC='\033[0m'
|
|
790
|
+
|
|
791
|
+
# Get and validate GitHub token
|
|
792
|
+
export GH_TOKEN=$(task git:token)
|
|
793
|
+
|
|
794
|
+
# Get repository info
|
|
795
|
+
REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
|
|
796
|
+
|
|
797
|
+
STATE="{{.STATE}}"
|
|
798
|
+
DOWNLOAD_ALL="{{.ALL}}"
|
|
799
|
+
|
|
800
|
+
# Create timestamped directory for this download session
|
|
801
|
+
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
|
802
|
+
SESSION_DIR="{{.BASE_LOG_DIR}}/${TIMESTAMP}"
|
|
803
|
+
mkdir -p "$SESSION_DIR"
|
|
804
|
+
|
|
805
|
+
# Get current branch
|
|
806
|
+
CURRENT_BRANCH=$(task git:branch:current)
|
|
807
|
+
|
|
808
|
+
# If RUN_ID specified, download that specific run (ignore STATE and ALL)
|
|
809
|
+
if [ -n "{{.RUN_ID}}" ]; then
|
|
810
|
+
RUN_IDS=("{{.RUN_ID}}")
|
|
811
|
+
elif [ "$DOWNLOAD_ALL" = "true" ]; then
|
|
812
|
+
# Download all historical runs matching STATE (up to 100)
|
|
813
|
+
WORKFLOW_FILTER=""
|
|
814
|
+
if [ -n "{{.WORKFLOW}}" ]; then
|
|
815
|
+
WORKFLOW_FILTER="--workflow {{.WORKFLOW}}"
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
RUN_DATA=$(gh run list \
|
|
819
|
+
-b "$CURRENT_BRANCH" \
|
|
820
|
+
$WORKFLOW_FILTER \
|
|
821
|
+
--status completed \
|
|
822
|
+
--limit 100 \
|
|
823
|
+
--json databaseId,name,conclusion 2>/dev/null || echo "[]")
|
|
824
|
+
|
|
825
|
+
if [ "$RUN_DATA" = "[]" ] || [ -z "$RUN_DATA" ]; then
|
|
826
|
+
echo -e "${RED}❌ No workflow runs found${NC}" >&2
|
|
827
|
+
exit 1
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
# Filter by conclusion/state
|
|
831
|
+
if [ "$STATE" != "all" ]; then
|
|
832
|
+
RUN_IDS=($(echo "$RUN_DATA" | jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId"))
|
|
833
|
+
else
|
|
834
|
+
RUN_IDS=($(echo "$RUN_DATA" | jq -r '.[].databaseId'))
|
|
835
|
+
fi
|
|
836
|
+
|
|
837
|
+
if [ ${#RUN_IDS[@]} -eq 0 ]; then
|
|
838
|
+
echo -e "${YELLOW}⚠${NC} No runs found with STATE=$STATE, switching to STATE=all"
|
|
839
|
+
STATE="all"
|
|
840
|
+
RUN_IDS=($(echo "$RUN_DATA" | jq -r '.[].databaseId'))
|
|
841
|
+
fi
|
|
842
|
+
else
|
|
843
|
+
# Download latest run per workflow matching STATE
|
|
844
|
+
WORKFLOW_FILTER=""
|
|
845
|
+
if [ -n "{{.WORKFLOW}}" ]; then
|
|
846
|
+
WORKFLOW_FILTER="--workflow {{.WORKFLOW}}"
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
# Get all workflows in repo and latest run for each
|
|
850
|
+
RUN_IDS=()
|
|
851
|
+
|
|
852
|
+
if [ -n "{{.WORKFLOW}}" ]; then
|
|
853
|
+
# Single workflow specified
|
|
854
|
+
WORKFLOW_NAME="{{.WORKFLOW}}"
|
|
855
|
+
|
|
856
|
+
if [ "$STATE" != "all" ]; then
|
|
857
|
+
LATEST_RUN=$(gh run list \
|
|
858
|
+
--workflow "$WORKFLOW_NAME" \
|
|
859
|
+
-b "$CURRENT_BRANCH" \
|
|
860
|
+
--status completed \
|
|
861
|
+
--limit 10 \
|
|
862
|
+
--json databaseId,conclusion 2>/dev/null | \
|
|
863
|
+
jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId" | head -1)
|
|
864
|
+
else
|
|
865
|
+
LATEST_RUN=$(gh run list \
|
|
866
|
+
--workflow "$WORKFLOW_NAME" \
|
|
867
|
+
-b "$CURRENT_BRANCH" \
|
|
868
|
+
--status completed \
|
|
869
|
+
--limit 1 \
|
|
870
|
+
--json databaseId 2>/dev/null | \
|
|
871
|
+
jq -r '.[].databaseId')
|
|
872
|
+
fi
|
|
873
|
+
|
|
874
|
+
if [ -n "$LATEST_RUN" ]; then
|
|
875
|
+
RUN_IDS+=("$LATEST_RUN")
|
|
876
|
+
fi
|
|
877
|
+
else
|
|
878
|
+
# Get all workflows and process with while loop
|
|
879
|
+
while IFS= read -r WORKFLOW_NAME; do
|
|
880
|
+
[ -z "$WORKFLOW_NAME" ] && continue
|
|
881
|
+
|
|
882
|
+
if [ "$STATE" != "all" ]; then
|
|
883
|
+
LATEST_RUN=$(gh run list \
|
|
884
|
+
--workflow "$WORKFLOW_NAME" \
|
|
885
|
+
-b "$CURRENT_BRANCH" \
|
|
886
|
+
--status completed \
|
|
887
|
+
--limit 10 \
|
|
888
|
+
--json databaseId,conclusion 2>/dev/null | \
|
|
889
|
+
jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId" | head -1)
|
|
890
|
+
else
|
|
891
|
+
LATEST_RUN=$(gh run list \
|
|
892
|
+
--workflow "$WORKFLOW_NAME" \
|
|
893
|
+
-b "$CURRENT_BRANCH" \
|
|
894
|
+
--status completed \
|
|
895
|
+
--limit 1 \
|
|
896
|
+
--json databaseId 2>/dev/null | \
|
|
897
|
+
jq -r '.[].databaseId')
|
|
898
|
+
fi
|
|
899
|
+
|
|
900
|
+
if [ -n "$LATEST_RUN" ]; then
|
|
901
|
+
RUN_IDS+=("$LATEST_RUN")
|
|
902
|
+
fi
|
|
903
|
+
done < <(gh api "/repos/$REPO/actions/workflows" --jq '.workflows[].name' 2>/dev/null)
|
|
904
|
+
fi
|
|
905
|
+
|
|
906
|
+
# Auto-fallback if no runs found
|
|
907
|
+
if [ ${#RUN_IDS[@]} -eq 0 ]; then
|
|
908
|
+
if [ "$STATE" != "all" ]; then
|
|
909
|
+
echo -e "${YELLOW}⚠${NC} No runs found with STATE=$STATE, retrying with STATE=all"
|
|
910
|
+
STATE="all"
|
|
911
|
+
RUN_IDS=()
|
|
912
|
+
|
|
913
|
+
if [ -n "{{.WORKFLOW}}" ]; then
|
|
914
|
+
WORKFLOW_NAME="{{.WORKFLOW}}"
|
|
915
|
+
LATEST_RUN=$(gh run list \
|
|
916
|
+
--workflow "$WORKFLOW_NAME" \
|
|
917
|
+
-b "$CURRENT_BRANCH" \
|
|
918
|
+
--status completed \
|
|
919
|
+
--limit 1 \
|
|
920
|
+
--json databaseId 2>/dev/null | \
|
|
921
|
+
jq -r '.[].databaseId')
|
|
922
|
+
|
|
923
|
+
if [ -n "$LATEST_RUN" ]; then
|
|
924
|
+
RUN_IDS+=("$LATEST_RUN")
|
|
925
|
+
fi
|
|
926
|
+
else
|
|
927
|
+
while IFS= read -r WORKFLOW_NAME; do
|
|
928
|
+
[ -z "$WORKFLOW_NAME" ] && continue
|
|
929
|
+
|
|
930
|
+
LATEST_RUN=$(gh run list \
|
|
931
|
+
--workflow "$WORKFLOW_NAME" \
|
|
932
|
+
-b "$CURRENT_BRANCH" \
|
|
933
|
+
--status completed \
|
|
934
|
+
--limit 1 \
|
|
935
|
+
--json databaseId 2>/dev/null | \
|
|
936
|
+
jq -r '.[].databaseId')
|
|
937
|
+
|
|
938
|
+
if [ -n "$LATEST_RUN" ]; then
|
|
939
|
+
RUN_IDS+=("$LATEST_RUN")
|
|
940
|
+
fi
|
|
941
|
+
done < <(gh api "/repos/$REPO/actions/workflows" --jq '.workflows[].name' 2>/dev/null)
|
|
942
|
+
fi
|
|
943
|
+
fi
|
|
944
|
+
|
|
945
|
+
if [ ${#RUN_IDS[@]} -eq 0 ]; then
|
|
946
|
+
echo -e "${RED}❌ No completed runs found on branch: $CURRENT_BRANCH${NC}" >&2
|
|
947
|
+
exit 1
|
|
948
|
+
fi
|
|
949
|
+
fi
|
|
950
|
+
fi
|
|
951
|
+
|
|
952
|
+
# Download each run
|
|
953
|
+
DOWNLOADED_COUNT=0
|
|
954
|
+
TOTAL_LOGS=0
|
|
955
|
+
LAST_LOG_DIR=""
|
|
956
|
+
|
|
957
|
+
for RUN_ID in "${RUN_IDS[@]}"; do
|
|
958
|
+
# Get run details (silent)
|
|
959
|
+
RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
|
|
960
|
+
|
|
961
|
+
if [ -z "$RUN_INFO" ]; then
|
|
962
|
+
continue
|
|
963
|
+
fi
|
|
964
|
+
|
|
965
|
+
WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
|
|
966
|
+
WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
|
|
967
|
+
RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
|
|
968
|
+
CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
|
|
969
|
+
|
|
970
|
+
# Create log directory within session
|
|
971
|
+
LOG_DIR="$SESSION_DIR/${WORKFLOW_SLUG}-${RUN_ID}"
|
|
972
|
+
mkdir -p "$LOG_DIR"
|
|
973
|
+
|
|
974
|
+
# Download and extract logs (silent)
|
|
975
|
+
if gh api "/repos/$REPO/actions/runs/$RUN_ID/logs" > "$LOG_DIR/logs.zip" 2>/dev/null; then
|
|
976
|
+
# Extract logs (silent)
|
|
977
|
+
cd "$LOG_DIR"
|
|
978
|
+
if unzip -o -q logs.zip 2>/dev/null; then
|
|
979
|
+
rm logs.zip
|
|
980
|
+
|
|
981
|
+
# Save run metadata (silent)
|
|
982
|
+
echo "$RUN_INFO" | jq '.' > "run-metadata.json" 2>/dev/null
|
|
983
|
+
|
|
984
|
+
# Count log files
|
|
985
|
+
LOG_COUNT=$(find . -name "*.txt" -type f | wc -l | tr -d ' ')
|
|
986
|
+
TOTAL_LOGS=$((TOTAL_LOGS + LOG_COUNT))
|
|
987
|
+
DOWNLOADED_COUNT=$((DOWNLOADED_COUNT + 1))
|
|
988
|
+
LAST_LOG_DIR="$LOG_DIR"
|
|
989
|
+
|
|
990
|
+
# Show individual success if downloading multiple
|
|
991
|
+
if [ "$DOWNLOAD_ALL" = "true" ]; then
|
|
992
|
+
echo -e "${GREEN}✓${NC} Run #$RUN_NUMBER ($CONCLUSION): $LOG_COUNT logs"
|
|
993
|
+
fi
|
|
994
|
+
fi
|
|
995
|
+
cd - > /dev/null
|
|
996
|
+
fi
|
|
997
|
+
done
|
|
998
|
+
|
|
999
|
+
# Display summary and generate README
|
|
1000
|
+
if [ $DOWNLOADED_COUNT -eq 0 ]; then
|
|
1001
|
+
echo -e "${RED}❌ Failed to download any logs${NC}" >&2
|
|
1002
|
+
exit 1
|
|
1003
|
+
fi
|
|
1004
|
+
|
|
1005
|
+
# Always create README (for both single and multiple downloads)
|
|
1006
|
+
README_FILE="$SESSION_DIR/README.md"
|
|
1007
|
+
|
|
1008
|
+
{
|
|
1009
|
+
echo "# GitHub Actions Workflow Runs"
|
|
1010
|
+
echo ""
|
|
1011
|
+
echo "**Downloaded:** $(date)"
|
|
1012
|
+
echo "**Repository:** $REPO"
|
|
1013
|
+
echo "**Branch:** $(task git:branch:current)"
|
|
1014
|
+
echo "**Filter:** STATE=$STATE"
|
|
1015
|
+
echo ""
|
|
1016
|
+
echo "**Total Runs:** $DOWNLOADED_COUNT"
|
|
1017
|
+
echo "**Total Log Files:** $TOTAL_LOGS"
|
|
1018
|
+
echo ""
|
|
1019
|
+
echo "## Run Details"
|
|
1020
|
+
echo ""
|
|
1021
|
+
echo "| Run # | Workflow | Conclusion | Logs | Directory |"
|
|
1022
|
+
echo "|-------|----------|------------|------|-----------|"
|
|
1023
|
+
|
|
1024
|
+
# Generate table rows for each downloaded run
|
|
1025
|
+
for RUN_ID in "${RUN_IDS[@]}"; do
|
|
1026
|
+
RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
|
|
1027
|
+
|
|
1028
|
+
if [ -z "$RUN_INFO" ]; then
|
|
1029
|
+
continue
|
|
1030
|
+
fi
|
|
1031
|
+
|
|
1032
|
+
WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
|
|
1033
|
+
WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
|
|
1034
|
+
RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
|
|
1035
|
+
CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
|
|
1036
|
+
RUN_DIR="${WORKFLOW_SLUG}-${RUN_ID}"
|
|
1037
|
+
|
|
1038
|
+
# Count logs for this run
|
|
1039
|
+
RUN_LOG_COUNT=$(find "$SESSION_DIR/${RUN_DIR}" -name "*.txt" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
1040
|
+
|
|
1041
|
+
# Format conclusion with emoji
|
|
1042
|
+
case "$CONCLUSION" in
|
|
1043
|
+
success) CONCLUSION_DISPLAY="✅ success" ;;
|
|
1044
|
+
failure) CONCLUSION_DISPLAY="❌ failure" ;;
|
|
1045
|
+
cancelled) CONCLUSION_DISPLAY="🚫 cancelled" ;;
|
|
1046
|
+
skipped) CONCLUSION_DISPLAY="⏭️ skipped" ;;
|
|
1047
|
+
*) CONCLUSION_DISPLAY="$CONCLUSION" ;;
|
|
1048
|
+
esac
|
|
1049
|
+
|
|
1050
|
+
echo "| #$RUN_NUMBER | $WORKFLOW_NAME | $CONCLUSION_DISPLAY | $RUN_LOG_COUNT | [\`$RUN_DIR\`](./$RUN_DIR) |"
|
|
1051
|
+
done
|
|
1052
|
+
|
|
1053
|
+
echo ""
|
|
1054
|
+
echo "## Quick Links"
|
|
1055
|
+
echo ""
|
|
1056
|
+
|
|
1057
|
+
# Add links to each workflow directory
|
|
1058
|
+
for RUN_ID in "${RUN_IDS[@]}"; do
|
|
1059
|
+
RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
|
|
1060
|
+
|
|
1061
|
+
if [ -z "$RUN_INFO" ]; then
|
|
1062
|
+
continue
|
|
1063
|
+
fi
|
|
1064
|
+
|
|
1065
|
+
WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
|
|
1066
|
+
WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
|
|
1067
|
+
RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
|
|
1068
|
+
RUN_URL=$(echo "$RUN_INFO" | jq -r '.html_url')
|
|
1069
|
+
|
|
1070
|
+
echo "- **Run #$RUN_NUMBER** ($WORKFLOW_NAME): [GitHub]($RUN_URL) • [Logs](./${WORKFLOW_SLUG}-${RUN_ID})"
|
|
1071
|
+
done
|
|
1072
|
+
} > "$README_FILE"
|
|
1073
|
+
|
|
1074
|
+
# Display success message
|
|
1075
|
+
if [ $DOWNLOADED_COUNT -eq 1 ]; then
|
|
1076
|
+
RUN_ID="${RUN_IDS[0]}"
|
|
1077
|
+
RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
|
|
1078
|
+
RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
|
|
1079
|
+
CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
|
|
1080
|
+
|
|
1081
|
+
echo -e "${GREEN}✓${NC} Downloaded $TOTAL_LOGS log files for run #$RUN_NUMBER ($CONCLUSION) → $SESSION_DIR"
|
|
1082
|
+
else
|
|
1083
|
+
echo -e "${GREEN}✓${NC} Downloaded $DOWNLOADED_COUNT runs with $TOTAL_LOGS total log files → $SESSION_DIR"
|
|
1084
|
+
fi
|
|
1085
|
+
|
|
1086
|
+
# Open README in VSCode
|
|
1087
|
+
if command -v code &> /dev/null; then
|
|
1088
|
+
code "$README_FILE" 2>/dev/null || true
|
|
1089
|
+
elif command -v code-insiders &> /dev/null; then
|
|
1090
|
+
code-insiders "$README_FILE" 2>/dev/null || true
|
|
1091
|
+
fi
|
|
1092
|
+
|
|
1093
|
+
actions:pin:
|
|
1094
|
+
desc: Pin GitHub Actions to specific commit SHAs with version comments
|
|
1095
|
+
silent: true
|
|
1096
|
+
aliases: [pin]
|
|
1097
|
+
vars:
|
|
1098
|
+
UPGRADE_FLAG: "{{if .UPGRADE}}--upgrade{{end}}"
|
|
1099
|
+
CHECK_FLAG: "{{if .CHECK}}--check{{end}}"
|
|
1100
|
+
DRY_RUN_FLAG: "{{if .DRY_RUN}}--dry-run{{end}}"
|
|
1101
|
+
TARGET_PATH: '{{.TARGET_PATH | default ".github/workflows"}}'
|
|
1102
|
+
cmds:
|
|
1103
|
+
- |
|
|
1104
|
+
# Get and validate GitHub token
|
|
1105
|
+
export GITHUB_TOKEN=$(task git:token)
|
|
1106
|
+
|
|
1107
|
+
# Run pin-actions with authenticated API access
|
|
1108
|
+
ai-toolkit pin-actions {{.UPGRADE_FLAG}} {{.CHECK_FLAG}} {{.DRY_RUN_FLAG}} --target-path={{.TARGET_PATH}}
|
|
1109
|
+
|
|
1110
|
+
leaks:
|
|
1111
|
+
desc: Scan for secrets with gitleaks
|
|
1112
|
+
silent: true
|
|
1113
|
+
aliases: [leaks]
|
|
1114
|
+
cmds:
|
|
1115
|
+
- |
|
|
1116
|
+
GREEN='\033[0;32m'
|
|
1117
|
+
YELLOW='\033[0;33m'
|
|
1118
|
+
RED='\033[0;31m'
|
|
1119
|
+
NC='\033[0m'
|
|
1120
|
+
|
|
1121
|
+
echo -e "${YELLOW}🔍${NC} Running gitleaks secret scanner..."
|
|
1122
|
+
|
|
1123
|
+
# Check if gitleaks is installed, install if not
|
|
1124
|
+
if ! command -v gitleaks >/dev/null 2>&1; then
|
|
1125
|
+
echo -e "${YELLOW}⚠️${NC} gitleaks not installed, installing..."
|
|
1126
|
+
brew install gitleaks
|
|
1127
|
+
fi
|
|
1128
|
+
|
|
1129
|
+
# Temporarily set git remote to HTTPS format for gitleaks platform detection
|
|
1130
|
+
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
1131
|
+
RESTORED=false
|
|
1132
|
+
|
|
1133
|
+
if [ -n "$REMOTE_URL" ] && echo "$REMOTE_URL" | grep -q '^git@'; then
|
|
1134
|
+
# SSH format: convert to HTTPS temporarily
|
|
1135
|
+
HOST=$(echo "$REMOTE_URL" | sed -E 's/^git@([^:]+):.*/\1/')
|
|
1136
|
+
REPO_PATH=$(echo "$REMOTE_URL" | sed -E 's/^git@[^:]+:(.*)/\1/')
|
|
1137
|
+
REPO_PATH="${REPO_PATH%.git}"
|
|
1138
|
+
# Convert git@git alias to github.com
|
|
1139
|
+
if [ "$HOST" = "git" ]; then
|
|
1140
|
+
HOST="github.com"
|
|
1141
|
+
fi
|
|
1142
|
+
HTTPS_URL="https://${HOST}/${REPO_PATH}"
|
|
1143
|
+
|
|
1144
|
+
# Temporarily update remote for platform detection
|
|
1145
|
+
git remote set-url origin "$HTTPS_URL" 2>/dev/null && RESTORED=true
|
|
1146
|
+
fi
|
|
1147
|
+
|
|
1148
|
+
# Run gitleaks (will auto-detect platform from git remote)
|
|
1149
|
+
if gitleaks detect --verbose --redact; then
|
|
1150
|
+
echo -e "${GREEN}✓${NC} No secrets found by gitleaks"
|
|
1151
|
+
EXIT_CODE=0
|
|
1152
|
+
else
|
|
1153
|
+
echo -e "${RED}❌${NC} gitleaks found secrets!"
|
|
1154
|
+
EXIT_CODE=1
|
|
1155
|
+
fi
|
|
1156
|
+
|
|
1157
|
+
# Restore original remote URL
|
|
1158
|
+
if [ "$RESTORED" = "true" ]; then
|
|
1159
|
+
git remote set-url origin "$REMOTE_URL" 2>/dev/null
|
|
1160
|
+
fi
|
|
1161
|
+
|
|
1162
|
+
exit $EXIT_CODE
|
|
1163
|
+
|
|
1164
|
+
cve:
|
|
1165
|
+
desc: Download known CVEs for repository from Dependabot API
|
|
1166
|
+
aliases: [cve]
|
|
1167
|
+
silent: true
|
|
1168
|
+
deps: [cli]
|
|
1169
|
+
dotenv: [".env"]
|
|
1170
|
+
vars:
|
|
1171
|
+
MIN_SEVERITY: '{{.MIN_SEVERITY | default "medium"}}'
|
|
1172
|
+
REPO_ROOT:
|
|
1173
|
+
sh: git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
1174
|
+
LOG_DIR:
|
|
1175
|
+
sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/cve"
|
|
1176
|
+
cmds:
|
|
1177
|
+
- |
|
|
1178
|
+
# Color codes
|
|
1179
|
+
GREEN='\033[0;32m'
|
|
1180
|
+
RED='\033[0;31m'
|
|
1181
|
+
YELLOW='\033[0;33m'
|
|
1182
|
+
NC='\033[0m'
|
|
1183
|
+
|
|
1184
|
+
# Get and validate GitHub token
|
|
1185
|
+
export GH_TOKEN=$(task git:token)
|
|
1186
|
+
|
|
1187
|
+
# Validate severity level
|
|
1188
|
+
MIN_SEVERITY="{{.MIN_SEVERITY}}"
|
|
1189
|
+
case "$MIN_SEVERITY" in
|
|
1190
|
+
critical|high|medium|low) ;;
|
|
1191
|
+
*)
|
|
1192
|
+
echo -e "${RED}❌ Invalid MIN_SEVERITY: $MIN_SEVERITY${NC}" >&2
|
|
1193
|
+
echo "Valid values: critical, high, medium, low" >&2
|
|
1194
|
+
exit 1
|
|
1195
|
+
;;
|
|
1196
|
+
esac
|
|
1197
|
+
|
|
1198
|
+
# Get repository info
|
|
1199
|
+
REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
|
|
1200
|
+
|
|
1201
|
+
# Clean up old CVE logs
|
|
1202
|
+
if [ -d "{{.LOG_DIR}}" ]; then
|
|
1203
|
+
find "{{.LOG_DIR}}" -type f -name 'cve-*.*' -exec rm -f {} \; 2>/dev/null
|
|
1204
|
+
fi
|
|
1205
|
+
|
|
1206
|
+
# Create log directory
|
|
1207
|
+
mkdir -p "{{.LOG_DIR}}"
|
|
1208
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
1209
|
+
RAW_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}-raw.json"
|
|
1210
|
+
LOG_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}.json"
|
|
1211
|
+
SUMMARY_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}-summary.txt"
|
|
1212
|
+
|
|
1213
|
+
# Fetch vulnerabilities from Dependabot API (silent)
|
|
1214
|
+
VULNS_RAW=$(gh api "/repos/$REPO/dependabot/alerts" --paginate 2>&1)
|
|
1215
|
+
|
|
1216
|
+
if [ $? -ne 0 ]; then
|
|
1217
|
+
echo -e "${RED}❌ Failed to fetch CVEs from GitHub${NC}" >&2
|
|
1218
|
+
echo "$VULNS_RAW" >&2
|
|
1219
|
+
exit 1
|
|
1220
|
+
fi
|
|
1221
|
+
|
|
1222
|
+
# Save raw response
|
|
1223
|
+
echo "$VULNS_RAW" > "$RAW_FILE"
|
|
1224
|
+
|
|
1225
|
+
# Clean and process JSON
|
|
1226
|
+
VULNS_CLEAN=$(echo "$VULNS_RAW" | tr -d '\000-\010\013-\037')
|
|
1227
|
+
|
|
1228
|
+
# Filter and deduplicate vulnerabilities
|
|
1229
|
+
VULNS=$(echo "$VULNS_CLEAN" | jq --arg min_sev "$MIN_SEVERITY" '
|
|
1230
|
+
[.[] | select(.state=="open") | {
|
|
1231
|
+
cve: (.security_advisory.cve_id // null),
|
|
1232
|
+
pkg: (.security_vulnerability.package.name // "unknown"),
|
|
1233
|
+
ecosystem: (.security_vulnerability.package.ecosystem // "unknown"),
|
|
1234
|
+
sev: (.security_vulnerability.severity // "unknown"),
|
|
1235
|
+
current_version: (.security_vulnerability.vulnerable_version_range // null),
|
|
1236
|
+
fixed_version: (.security_vulnerability.first_patched_version.identifier // null),
|
|
1237
|
+
manifest_path: (.dependency.manifest_path // null),
|
|
1238
|
+
summary: (.security_advisory.summary // null),
|
|
1239
|
+
ghsa_id: (.security_advisory.ghsa_id // null),
|
|
1240
|
+
cvss_score: (.security_advisory.cvss.score // null),
|
|
1241
|
+
created_at: (.created_at // null)
|
|
1242
|
+
} | select(
|
|
1243
|
+
if $min_sev == "low" then true
|
|
1244
|
+
elif $min_sev == "medium" then (.sev == "medium" or .sev == "high" or .sev == "critical")
|
|
1245
|
+
elif $min_sev == "high" then (.sev == "high" or .sev == "critical")
|
|
1246
|
+
elif $min_sev == "critical" then .sev == "critical"
|
|
1247
|
+
else true end
|
|
1248
|
+
)] |
|
|
1249
|
+
group_by((.cve // .ghsa_id // "NO-ID") + ":" + .pkg) |
|
|
1250
|
+
map({
|
|
1251
|
+
cve: .[0].cve,
|
|
1252
|
+
pkg: .[0].pkg,
|
|
1253
|
+
ecosystem: .[0].ecosystem,
|
|
1254
|
+
sev: .[0].sev,
|
|
1255
|
+
current_version: .[0].current_version,
|
|
1256
|
+
fixed_version: .[0].fixed_version,
|
|
1257
|
+
summary: .[0].summary,
|
|
1258
|
+
ghsa_id: .[0].ghsa_id,
|
|
1259
|
+
cvss_score: .[0].cvss_score,
|
|
1260
|
+
manifest_paths: [.[] | .manifest_path] | unique | sort,
|
|
1261
|
+
count: length
|
|
1262
|
+
}) | sort_by(
|
|
1263
|
+
if .sev == "critical" then 0
|
|
1264
|
+
elif .sev == "high" then 1
|
|
1265
|
+
elif .sev == "medium" then 2
|
|
1266
|
+
elif .sev == "low" then 3
|
|
1267
|
+
else 4 end
|
|
1268
|
+
)' 2>/dev/null || echo "[]")
|
|
1269
|
+
|
|
1270
|
+
# Save filtered results
|
|
1271
|
+
echo "$VULNS" > "$LOG_FILE"
|
|
1272
|
+
|
|
1273
|
+
# Count vulnerabilities
|
|
1274
|
+
VULN_COUNT=$(echo "$VULNS" | jq 'length' 2>/dev/null || echo "0")
|
|
1275
|
+
CRITICAL_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "critical")] | length' 2>/dev/null || echo "0")
|
|
1276
|
+
HIGH_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "high")] | length' 2>/dev/null || echo "0")
|
|
1277
|
+
MEDIUM_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "medium")] | length' 2>/dev/null || echo "0")
|
|
1278
|
+
LOW_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "low")] | length' 2>/dev/null || echo "0")
|
|
1279
|
+
|
|
1280
|
+
# Create summary report
|
|
1281
|
+
{
|
|
1282
|
+
echo "CVE Security Report"
|
|
1283
|
+
echo "==================="
|
|
1284
|
+
echo "Repository: $REPO"
|
|
1285
|
+
echo "Date: $(date)"
|
|
1286
|
+
echo "Minimum severity: $MIN_SEVERITY and above"
|
|
1287
|
+
echo "Total vulnerabilities: $VULN_COUNT"
|
|
1288
|
+
echo ""
|
|
1289
|
+
|
|
1290
|
+
if [ "$VULN_COUNT" -gt "0" ]; then
|
|
1291
|
+
echo "Severity breakdown:"
|
|
1292
|
+
[ "$CRITICAL_COUNT" -gt 0 ] && echo " Critical: $CRITICAL_COUNT"
|
|
1293
|
+
[ "$HIGH_COUNT" -gt 0 ] && echo " High: $HIGH_COUNT"
|
|
1294
|
+
[ "$MEDIUM_COUNT" -gt 0 ] && echo " Medium: $MEDIUM_COUNT"
|
|
1295
|
+
[ "$LOW_COUNT" -gt 0 ] && echo " Low: $LOW_COUNT"
|
|
1296
|
+
echo ""
|
|
1297
|
+
echo "Vulnerabilities:"
|
|
1298
|
+
echo "───────────────────────────────────────────────────────────────"
|
|
1299
|
+
echo ""
|
|
1300
|
+
|
|
1301
|
+
echo "$VULNS" | jq -r '.[] |
|
|
1302
|
+
"CVE: \(.cve // .ghsa_id // "NO-ID")\n" +
|
|
1303
|
+
"Severity: \(.sev | ascii_upcase) (CVSS: \(.cvss_score // "N/A"))\n" +
|
|
1304
|
+
"Package: \(.ecosystem)/\(.pkg)\n" +
|
|
1305
|
+
"Locations (\(.count)):\n" +
|
|
1306
|
+
(.manifest_paths | map(" - " + .) | join("\n")) + "\n" +
|
|
1307
|
+
"Current: \(.current_version // "Unknown")\n" +
|
|
1308
|
+
"Fixed in: \(.fixed_version // "No fix available")\n" +
|
|
1309
|
+
"Summary: \(.summary // "No description available" | .[0:200])\n" +
|
|
1310
|
+
"───────────────────────────────────────────────────────────────\n"'
|
|
1311
|
+
else
|
|
1312
|
+
echo "No vulnerabilities found."
|
|
1313
|
+
fi
|
|
1314
|
+
} > "$SUMMARY_FILE"
|
|
1315
|
+
|
|
1316
|
+
# Display results
|
|
1317
|
+
if [ "$VULN_COUNT" -gt "0" ]; then
|
|
1318
|
+
echo -e "${RED}❌ Found $VULN_COUNT CVEs ($MIN_SEVERITY and above)${NC}"
|
|
1319
|
+
[ "$CRITICAL_COUNT" -gt 0 ] && echo " Critical: $CRITICAL_COUNT"
|
|
1320
|
+
[ "$HIGH_COUNT" -gt 0 ] && echo " High: $HIGH_COUNT"
|
|
1321
|
+
[ "$MEDIUM_COUNT" -gt 0 ] && echo " Medium: $MEDIUM_COUNT"
|
|
1322
|
+
[ "$LOW_COUNT" -gt 0 ] && echo " Low: $LOW_COUNT"
|
|
1323
|
+
echo ""
|
|
1324
|
+
echo -e "${GREEN}✓${NC} Summary: $SUMMARY_FILE"
|
|
1325
|
+
echo -e "${GREEN}✓${NC} Filtered data: $LOG_FILE"
|
|
1326
|
+
echo -e "${GREEN}✓${NC} Raw API response: $RAW_FILE"
|
|
1327
|
+
else
|
|
1328
|
+
echo -e "${GREEN}✓${NC} No CVEs found ($MIN_SEVERITY severity and above) → $SUMMARY_FILE"
|
|
1329
|
+
fi
|
|
1330
|
+
|
|
1331
|
+
# Open summary in VSCode
|
|
1332
|
+
if command -v code &> /dev/null; then
|
|
1333
|
+
code "$SUMMARY_FILE" 2>/dev/null || true
|
|
1334
|
+
elif command -v code-insiders &> /dev/null; then
|
|
1335
|
+
code-insiders "$SUMMARY_FILE" 2>/dev/null || true
|
|
1336
|
+
fi
|