Skip to content

Commit 4e15843

Browse files
committed
🍑 [just] gh-process v6.7 fixes bugs
Signed-off-by: Christopher Hicks <chicks.net@gmail.com>
1 parent a79d0f4 commit 4e15843

9 files changed

Lines changed: 319 additions & 138 deletions

.just/claude.just

100644100755
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
claude_settings_file := ".claude/settings.local.json"
44

5+
# Launch Claude Code in the repo root (with arguments optionally)
6+
[group('Claude Code')]
7+
claude *args:
8+
claude {{ args }}
9+
510
# Sort Claude Code permissions in canonical order
611
[group('Claude Code')]
712
claude_permissions_sort:

.just/compliance.just

100644100755
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# our own repo compliance check
2-
[group('Compliance')]
2+
[group('Testing/Compliance')]
33
compliance_check:
44
#!/usr/bin/env bash
55
set -euo pipefail # strict mode without tracing
@@ -86,3 +86,10 @@ compliance_check:
8686
else
8787
echo "{{RED}}You do NOT have an .editorconfig. Will your world explode when the tabs and spaces get together?{{NORMAL}}"
8888
fi
89+
90+
# Check for branch protection on main branch (repo-level or org-level rulesets)
91+
if gh api 'repos/{owner}/{repo}/rulesets' --jq '.[] | select(.enforcement == "active" and .target == "branch")' | grep -q .; then
92+
echo "{{GREEN}}[gh] You have branch protection enabled, keeping the main branch safe from accidents.{{NORMAL}}"
93+
else
94+
echo "{{RED}}[gh] You do NOT have branch protection enabled. Living dangerously, are we?{{NORMAL}}"
95+
fi

.just/copilot.just

100644100755
Lines changed: 179 additions & 92 deletions
Large diffs are not rendered by default.

.just/gh-process.just

100644100755
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ sync:
1313
git pull
1414
git status --porcelain # stp
1515

16-
# PR create v5.8
16+
# PR create v6.7
1717
[group('Process')]
1818
pr: _has_commits && pr_checks
1919
#!/usr/bin/env bash
@@ -216,7 +216,7 @@ release rel_version:
216216
217217
# watch GHAs then check for Copilot suggestions
218218
[group('Process')]
219-
pr_checks: && claude_review
219+
pr_checks: _on_a_pull_request && claude_review
220220
#!/usr/bin/env bash
221221
222222
# Use gh observer if installed, otherwise fall back to gh pr checks --watch
@@ -251,22 +251,23 @@ pr_checks: && claude_review
251251
COPILOT_COUNT_FILE="/tmp/copilot_count_${PR_REPO_OWNER}_${PR_REPO_NAME}_${PR_NUMBER}"
252252
253253
# Capture GraphQL output
254+
# Note: reviewThreads(first: 100) limits to 100 threads; see copilot_pick for comment.
254255
# shellcheck disable=SC2016
255256
COPILOT_JSON=$(gh api graphql \
256257
-F owner="$PR_REPO_OWNER" -F name="$PR_REPO_NAME" -F pr="$PR_NUMBER" \
257-
--jq '[ .data.repository.pullRequest.reviews.nodes.[] | select(.author.login=="copilot-pull-request-reviewer") | .comments.nodes.[] ]' \
258+
--jq '[ .data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | select(.author.login=="copilot-pull-request-reviewer") ]' \
258259
-f query='
259260
query($name: String!, $owner: String!, $pr: Int!) {
260261
repository(owner: $owner, name: $name) {
261262
pullRequest(number: $pr) {
262-
reviews(last: 20) {
263+
reviewThreads(first: 100) {
263264
nodes {
264-
author {
265-
resourcePath
266-
login
267-
}
265+
isResolved
268266
comments(first: 100) {
269267
nodes {
268+
author {
269+
login
270+
}
270271
body
271272
path
272273
originalLine
@@ -285,9 +286,9 @@ pr_checks: && claude_review
285286
286287
# Display output to maintain current behavior
287288
if [[ "$COPILOT_COUNT" -eq 0 ]]; then
288-
echo "🐈‍⬛ No Copilot suggestions - looks good!"
289+
echo "🐈‍⬛ No unresolved Copilot suggestions - looks good!"
289290
else
290-
echo "🐈‍⬛ Total Copilot suggestions: $COPILOT_COUNT"
291+
echo "🐈‍⬛ Unresolved Copilot suggestions: $COPILOT_COUNT"
291292
echo "$COPILOT_JSON" | jq '.'
292293
fi
293294
else
@@ -382,9 +383,9 @@ claude_review: _on_a_pull_request
382383
COPILOT_COUNT=$(cat "$COPILOT_COUNT_FILE")
383384
echo ""
384385
if [[ "$COPILOT_COUNT" -eq 0 ]]; then
385-
echo "🐈‍⬛ No Copilot suggestions - looks good!"
386+
echo "🐈‍⬛ No unresolved Copilot suggestions - looks good!"
386387
else
387-
echo "🐈‍⬛ Total Copilot suggestions: $COPILOT_COUNT"
388+
echo "🐈‍⬛ Unresolved Copilot suggestions: $COPILOT_COUNT"
388389
fi
389390
rm -f "$COPILOT_COUNT_FILE"
390391
fi

.just/lib/generate_checksums.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ CLEANED_FILES=(
2929
declare -a just_files
3030
while IFS= read -r file; do
3131
just_files+=("$file")
32-
done < <(git ls-files '.just/*.just' | sort)
32+
done < <(git ls-files '.just/*.just' ':(exclude).just/test/**' | sort)
3333

3434
# Get list of all .just/lib/*.sh files
3535
declare -a lib_files

.just/lib/install-prerequisites.sh

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# Usage: ./.just/lib/install-prerequisites.sh
55
#
6-
# This script checks for required tools (just, gh, shellcheck, markdownlint-cli2, jq, gum)
6+
# This script checks for required tools (just, gh, shellcheck, markdownlint-cli2, jq, gum, cue, gh-observer)
77
# and helps install them:
88
#
99
# - macOS: Automatically installs missing tools using Homebrew
@@ -66,6 +66,18 @@ else
6666
MISSING+=("gum")
6767
fi
6868

69+
if command -v cue &>/dev/null; then
70+
INSTALLED+=("cue")
71+
else
72+
MISSING+=("cue")
73+
fi
74+
75+
if gh extension list 2>/dev/null | grep -q 'gh-observer'; then
76+
INSTALLED+=("gh-observer")
77+
else
78+
MISSING+=("gh-observer")
79+
fi
80+
6981
# report what's already installed
7082
if [[ ${#INSTALLED[@]} -gt 0 ]]; then
7183
echo -e "${GREEN}Already installed:${NC}"
@@ -163,6 +175,29 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
163175
echo -e "${RED}Failed to install gum${NC}"
164176
fi
165177
;;
178+
cue)
179+
echo -e "${CYAN}Installing cue...${NC}"
180+
if brew install cue; then
181+
INSTALL_SUCCESS+=("cue")
182+
else
183+
INSTALL_FAILED+=("cue")
184+
echo -e "${RED}Failed to install cue${NC}"
185+
fi
186+
;;
187+
gh-observer)
188+
echo -e "${CYAN}Installing gh-observer...${NC}"
189+
if command -v gh &>/dev/null; then
190+
if gh extension install chicks-net/gh-observer; then
191+
INSTALL_SUCCESS+=("gh-observer")
192+
else
193+
INSTALL_FAILED+=("gh-observer")
194+
echo -e "${RED}Failed to install gh-observer${NC}"
195+
fi
196+
else
197+
INSTALL_FAILED+=("gh-observer")
198+
echo -e "${RED}Cannot install gh-observer: gh CLI is required${NC}"
199+
fi
200+
;;
166201
esac
167202
done
168203

@@ -249,6 +284,13 @@ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
249284
gum)
250285
echo -e "${CYAN}Install gum:${NC} See https://github.com/charmbracelet/gum#installation"
251286
;;
287+
cue)
288+
echo -e "${CYAN}Install cue:${NC} See https://github.com/cue-lang/cue#installation"
289+
;;
290+
gh-observer)
291+
echo -e "${CYAN}Install gh-observer:${NC} gh extension install chicks-net/gh-observer"
292+
echo -e "${YELLOW}(Requires gh CLI to be installed first)${NC}"
293+
;;
252294
esac
253295
done
254296

@@ -270,6 +312,8 @@ else
270312
echo " markdownlint-cli2: npm install -g markdownlint-cli2"
271313
echo " jq: https://stedolan.github.io/jq/download/"
272314
echo " gum: https://github.com/charmbracelet/gum#installation"
315+
echo " cue: https://github.com/cue-lang/cue#installation"
316+
echo " gh-observer: gh extension install chicks-net/gh-observer (requires gh CLI)"
273317
exit 1
274318
fi
275319

.just/lib/template_update.sh

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,29 +73,47 @@ fetch_manifest() {
7373
# Download a file with verification
7474
download_file() {
7575
local filepath="$1"
76+
local expected_checksum="${2:-}"
7677
local temp_file="${filepath}.tmp"
7778
local backup_file="${filepath}.pre-update-backup"
7879
local -r max=$MAX_RETRIES
7980
local attempt=1
81+
local err_file="${temp_file}.err"
82+
83+
# Ensure the target directory exists (defense in depth: derived repos
84+
# may receive files under paths whose parent dirs don't exist yet)
85+
mkdir -p "$(dirname "$filepath")" || return 1
8086

8187
# Backup existing file
8288
if [[ -f "$filepath" ]]; then
8389
cp "$filepath" "$backup_file"
8490
fi
8591

8692
while [[ $attempt -le $max ]]; do
87-
if curl -sSL -f "$TEMPLATE_URL/$filepath" -o "$temp_file" 2>/dev/null; then
93+
if curl -sSL -f "$TEMPLATE_URL/$filepath" -o "$temp_file" 2>"$err_file"; then
8894
# Verify it's not empty
8995
if [[ ! -s "$temp_file" ]]; then
9096
echo -e " ${RED}Downloaded file is empty${NORMAL}"
91-
rm -f "$temp_file"
97+
rm -f "$temp_file" "$err_file"
9298
[[ -f "$backup_file" ]] && mv "$backup_file" "$filepath"
9399
return 1
94100
fi
95101

102+
# Verify checksum if expected checksum provided
103+
if [[ -n "$expected_checksum" ]]; then
104+
local downloaded_checksum
105+
downloaded_checksum=$(compute_checksum "$temp_file")
106+
if [[ "$downloaded_checksum" != "$expected_checksum" ]]; then
107+
echo -e " ${RED}Checksum mismatch${NORMAL}"
108+
rm -f "$temp_file" "$err_file"
109+
[[ -f "$backup_file" ]] && mv "$backup_file" "$filepath"
110+
return 1
111+
fi
112+
fi
113+
96114
# Move into place and clean up
97115
mv "$temp_file" "$filepath"
98-
rm -f "$backup_file"
116+
rm -f "$backup_file" "$err_file"
99117

100118
# Make executable (except common.sh which is only sourced)
101119
if [[ "$(basename "$filepath")" != "common.sh" ]]; then
@@ -110,7 +128,10 @@ download_file() {
110128
((attempt++))
111129
else
112130
echo -e " ${RED}Download failed after $max attempts${NORMAL}"
113-
rm -f "$temp_file"
131+
if [[ -s "$err_file" ]]; then
132+
echo -e " ${RED}curl error: $(<"$err_file")${NORMAL}"
133+
fi
134+
rm -f "$temp_file" "$err_file"
114135
[[ -f "$backup_file" ]] && mv "$backup_file" "$filepath"
115136
return 1
116137
fi
@@ -121,9 +142,9 @@ download_file() {
121142
process_file() {
122143
local filepath="$1"
123144

124-
# Check if this is a cleaned file (should be skipped if missing)
145+
# Check if this is a cleaned file (or under a cleaned directory)
125146
local is_cleaned=false
126-
if jq -e --arg fp "$filepath" '.cleaned_files // [] | index($fp) != null' "$MANIFEST_FILE" >/dev/null 2>&1; then
147+
if jq -e --arg fp "$filepath" '.cleaned_files // [] | any(. as $p | $fp | startswith($p + "/") or $fp == $p)' "$MANIFEST_FILE" >/dev/null 2>&1; then
127148
is_cleaned=true
128149
fi
129150

@@ -150,7 +171,7 @@ process_file() {
150171
return
151172
fi
152173
echo -e " ${BLUE}${NORMAL} $filepath - new file, downloading"
153-
if download_file "$filepath"; then
174+
if download_file "$filepath" "$latest_checksum"; then
154175
echo -e " ${GREEN}Downloaded successfully${NORMAL}"
155176
((downloaded_new_count++)) || true
156177
else
@@ -195,7 +216,7 @@ process_file() {
195216
fi
196217

197218
echo -e " ${BLUE}${NORMAL} $filepath - updating${version_info}"
198-
if download_file "$filepath"; then
219+
if download_file "$filepath" "$latest_checksum"; then
199220
echo -e " ${GREEN}Updated successfully${NORMAL}"
200221
((updated_count++)) || true
201222
else

.just/repo-toml.just

100644100755
Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ repo_toml_generate:
77
#!/usr/bin/env bash
88
set -euo pipefail
99
10+
shell_quote() {
11+
printf '%q' "$1"
12+
}
13+
1014
# Validate .repo.toml exists and passes cue validation
1115
if [[ ! -f .repo.toml ]]; then
1216
echo "{{RED}}Error: .repo.toml not found{{NORMAL}}"
@@ -22,26 +26,28 @@ repo_toml_generate:
2226
# Export JSON once
2327
JSON=$(cue export .repo.toml)
2428
25-
# Extract all fields with safe defaults
26-
DESCRIPTION=$(echo "$JSON" | jq -r '.about.description // ""')
27-
LICENSE=$(echo "$JSON" | jq -r '.about.license // ""')
29+
# Extract all fields with @sh for safe shell escaping
30+
DESCRIPTION=$(echo "$JSON" | jq -r '.about.description // "" | @sh')
31+
LICENSE=$(echo "$JSON" | jq -r '.about.license // "" | @sh')
2832
TOPICS_JSON=$(echo "$JSON" | jq -r '.about.topics // []')
2933
TOPICS_ARRAY=$(echo "$TOPICS_JSON" | jq -r '.[]' | sort)
30-
TOPICS_CSV=$(echo "$TOPICS_ARRAY" | paste -sd ',' -)
34+
TOPICS_CSV=$(echo "$TOPICS_ARRAY" | grep -v '^$' | paste -sd ',' - || echo "")
3135
32-
GIT_SSH=$(echo "$JSON" | jq -r '.urls.git_ssh // ""')
33-
WEB_URL=$(echo "$JSON" | jq -r '.urls.web_url // ""')
36+
GIT_SSH=$(echo "$JSON" | jq -r '.urls.git_ssh // "" | @sh')
37+
WEB_URL=$(echo "$JSON" | jq -r '.urls.web_url // "" | @sh')
3438
3539
# Derive org and repo name from web URL
3640
# Example: https://github.com/fini-net/template-repo -> fini-net, template-repo
37-
ORG_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/([^/]+)/.*|\1|')
38-
REPO_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/[^/]+/(.+)|\1|')
41+
# Extract raw URL separately from @sh-quoted version to avoid fragile sed stripping
42+
WEB_URL_RAW=$(echo "$JSON" | jq -r '.urls.web_url // ""')
43+
ORG_NAME=$(echo "$WEB_URL_RAW" | sed -E 's|https://github.com/([^/]+)/.*|\1|')
44+
REPO_NAME=$(echo "$WEB_URL_RAW" | sed -E 's|https://github.com/[^/]+/(.+)|\1|')
3945
4046
# Validate URL parsing succeeded
4147
if [[ -z "$ORG_NAME" ]] || [[ -z "$REPO_NAME" ]]; then
4248
echo "{{RED}}Error: Failed to parse organization and repository name from web_url{{NORMAL}}"
4349
echo "{{YELLOW}}Expected format: https://github.com/org-name/repo-name{{NORMAL}}"
44-
echo "{{YELLOW}}Got: $WEB_URL{{NORMAL}}"
50+
echo "{{YELLOW}}Got: $WEB_URL_RAW{{NORMAL}}"
4551
exit 1
4652
fi
4753
@@ -52,34 +58,41 @@ repo_toml_generate:
5258
FLAG_STANDARD_RELEASE=$(echo "$JSON" | jq -r 'if .flags["standard-release"] == false then "false" else "true" end')
5359
5460
# Generate .just/repo-toml.sh with all content
61+
# @sh-quoted string values already include proper shell quoting (e.g., 'value')
62+
# Flag values are bare true/false strings (no quoting needed)
63+
# shell_quote-d values use printf %q for safe embedding
64+
QUOTED_TOPICS_CSV=$(shell_quote "$TOPICS_CSV")
65+
QUOTED_ORG_NAME=$(shell_quote "$ORG_NAME")
66+
QUOTED_REPO_NAME=$(shell_quote "$REPO_NAME")
67+
5568
{
5669
echo "# Auto-generated from .repo.toml by 'just repo_toml_generate'"
5770
echo "# DO NOT EDIT THIS FILE MANUALLY - regenerate with: just repo_toml_generate"
5871
echo ""
5972
echo "# About section"
60-
echo "REPO_DESCRIPTION=\"${DESCRIPTION}\""
61-
echo "REPO_LICENSE=\"${LICENSE}\""
62-
echo "REPO_TOPICS=\"${TOPICS_CSV}\""
73+
echo "REPO_DESCRIPTION=${DESCRIPTION}"
74+
echo "REPO_LICENSE=${LICENSE}"
75+
echo "REPO_TOPICS=${QUOTED_TOPICS_CSV}"
6376
echo ""
6477
echo "# URLs section"
65-
echo "REPO_GIT_SSH=\"${GIT_SSH}\""
66-
echo "REPO_WEB_URL=\"${WEB_URL}\""
78+
echo "REPO_GIT_SSH=${GIT_SSH}"
79+
echo "REPO_WEB_URL=${WEB_URL}"
6780
echo ""
6881
echo "# Derived values"
69-
echo "REPO_ORG_NAME=\"${ORG_NAME}\""
70-
echo "REPO_REPO_NAME=\"${REPO_NAME}\""
82+
echo "REPO_ORG_NAME=${QUOTED_ORG_NAME}"
83+
echo "REPO_REPO_NAME=${QUOTED_REPO_NAME}"
7184
echo ""
7285
echo "# Feature flags (strings: \"true\" or \"false\")"
73-
echo "FLAG_CLAUDE=\"${FLAG_CLAUDE}\""
74-
echo "FLAG_CLAUDE_REVIEW=\"${FLAG_CLAUDE_REVIEW}\""
75-
echo "FLAG_COPILOT_REVIEW=\"${FLAG_COPILOT_REVIEW}\""
76-
echo "FLAG_STANDARD_RELEASE=\"${FLAG_STANDARD_RELEASE}\""
86+
echo "FLAG_CLAUDE=${FLAG_CLAUDE}"
87+
echo "FLAG_CLAUDE_REVIEW=${FLAG_CLAUDE_REVIEW}"
88+
echo "FLAG_COPILOT_REVIEW=${FLAG_COPILOT_REVIEW}"
89+
echo "FLAG_STANDARD_RELEASE=${FLAG_STANDARD_RELEASE}"
7790
echo ""
7891
echo "# Topics as array (for iteration)"
7992
echo "REPO_TOPICS_ARRAY=("
8093
echo "$TOPICS_ARRAY" | while IFS= read -r topic; do
8194
if [[ -n "$topic" ]]; then
82-
echo " \"$topic\""
95+
echo " $(shell_quote "$topic")"
8396
fi
8497
done
8598
echo ")"

0 commit comments

Comments
 (0)