Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 68 additions & 21 deletions .just/cue-verify.just
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -191,27 +191,70 @@ cue-sync-from-github:
# Update .repo.toml using awk (portable across macOS and Linux)
GH_DESC_ESCAPED="${GH_DESC//\"/\\\"}"

# Use state-aware awk that only matches within [about] section
# State-aware awk that handles three cases for each key:
# 1. Active line (e.g. `topics = [...]`) -> replace in place
# 2. Commented line (e.g. `# topics = [...]`) -> replace in place
# 3. Missing line -> insert inside [about] block
# The `[#[:space:]]*` prefix tolerates a leading `#` and optional space so
# commented-out keys are still matched and rewritten. Missing keys are
# inserted at the end of the `[about]` block, *before* any trailing blank
# line that separates it from the next section. This is achieved by
# buffering blank lines while inside `[about]` and flushing them only
# after any pending missing-key insertions, so the inserted key lands
# adjacent to the preceding key (e.g. `license`) rather than after the
# blank line (which would visually attach it to the next `[section]`).
# Missing keys are flushed either when the next `[section]` begins or at
# EOF (END block), preserving the original field order of any other keys
# in [about]. See issue #165 for the failure modes this addresses.
awk -v desc="$GH_DESC_ESCAPED" -v topics="[$TOPICS_TOML]" '
/^\[about\]/ { in_about=1 }
/^\[/ && !/^\[about\]/ { in_about=0 }
in_about && /^description = / { print "description = \"" desc "\""; next }
in_about && /^topics = / { print "topics = " topics; next }
{ print }
function flush_blanks() { if (blanks != "") { printf "%s", blanks; blanks="" } }
/^\[about\]/ { in_about=1; flush_blanks(); print; next }
/^\[/ && !/^\[about\]/ {
if (in_about) {
if (!desc_written) { print "description = \"" desc "\""; desc_written=1 }
if (!topics_written) { print "topics = " topics; topics_written=1 }
}
in_about=0
flush_blanks()
print
next
}
in_about && /^[#[:space:]]*description[[:space:]]*=/ { flush_blanks(); print "description = \"" desc "\""; desc_written=1; next }
in_about && /^[#[:space:]]*topics[[:space:]]*=/ { flush_blanks(); print "topics = " topics; topics_written=1; next }
in_about && /^[[:space:]]*$/ { blanks=blanks "\n"; next }
{ flush_blanks(); print }
END {
if (in_about) {
if (!desc_written) { print "description = \"" desc "\""; desc_written=1 }
if (!topics_written) { print "topics = " topics; topics_written=1 }
}
flush_blanks()
}
' .repo.toml > .repo.toml.tmp && mv .repo.toml.tmp .repo.toml

# Validate the TOML is still valid after modification
# Validate the TOML is still valid after modification.
# NOTE: cue vet passing does NOT prove the sync wrote the right values
# (topics is optional in the CUE schema), so the backup is preserved
# until the trailing `just cue-verify` also passes. See issue #165.
echo "{{BLUE}}Validating modified TOML...{{NORMAL}}"
if cue vet .repo.toml docs/repo-toml.cue; then
# Validation passed - safe to remove backup
rm .repo.toml.backup
echo "{{GREEN}}Successfully synced from GitHub to .repo.toml{{NORMAL}}"
else
# Validation failed - restore from backup
if ! cue vet .repo.toml docs/repo-toml.cue; then
mv .repo.toml.backup .repo.toml
echo "{{RED}}Sync failed - TOML validation error. Restored from backup.{{NORMAL}}"
exit 1
fi

# Run full verification (this catches silent topics/description write
# failures that cue vet cannot, by comparing against the GitHub API).
echo ""
echo "{{BLUE}}Running full verification...{{NORMAL}}"
if ! just cue-verify; then
mv .repo.toml.backup .repo.toml
echo "{{RED}}Sync failed - verification mismatch. Restored from backup.{{NORMAL}}"
exit 1
fi

rm .repo.toml.backup
echo "{{GREEN}}Successfully synced from GitHub to .repo.toml{{NORMAL}}"
else
echo "{{YELLOW}}.repo.toml not found - creating from GitHub metadata{{NORMAL}}"

Expand All @@ -238,16 +281,20 @@ cue-sync-from-github:

# Validate the new TOML
echo "{{BLUE}}Validating new TOML...{{NORMAL}}"
if cue vet .repo.toml docs/repo-toml.cue; then
echo "{{GREEN}}Created .repo.toml from GitHub metadata{{NORMAL}}"
else
if ! cue vet .repo.toml docs/repo-toml.cue; then
rm .repo.toml
echo "{{RED}}Failed to create valid .repo.toml{{NORMAL}}"
exit 1
fi
fi

# Run full verification to confirm sync worked
echo ""
echo "{{BLUE}}Running full verification...{{NORMAL}}"
just cue-verify
# Mirror existing-file branch: clean up on failure (no backup to restore).
echo ""
echo "{{BLUE}}Running full verification...{{NORMAL}}"
if ! just cue-verify; then
rm .repo.toml
echo "{{RED}}Newly created .repo.toml failed verification - file removed.{{NORMAL}}"
exit 1
fi

echo "{{GREEN}}Successfully synced from GitHub to .repo.toml{{NORMAL}}"
fi
2 changes: 1 addition & 1 deletion .just/gh-process.just
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ sync:
git pull
git status --porcelain # stp

# PR create v6.7
# PR create v7.0
[group('Process')]
pr: _has_commits && pr_checks
#!/usr/bin/env bash
Expand Down
205 changes: 205 additions & 0 deletions .just/lib/cue_sync_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
# Test suite for cue-sync-from-github awk logic
#
# Verifies that the awk block in .just/cue-verify.just correctly handles
# missing, commented, and active keys in the [about] section of .repo.toml,
# and that inserted keys land *inside* the [about] block (before any trailing
# blank line), not after it (which would visually attach them to the next
# [section] header). See issue #165 and PR #175.
set -euo pipefail

# Color codes
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NORMAL='\033[0m'

readonly FIXTURES_DIR=".just/test/fixtures/cue_sync"
readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

passed=0
failed=0

# The awk program extracted from the cue-sync-from-github recipe in
# .just/cue-verify.just. Keep this in sync with the recipe. If the recipe
# changes, update both and add a test case covering the new behaviour.
run_awk() {
local input="$1" desc="$2" topics="$3"
awk -v desc="$desc" -v topics="[$topics]" '
function flush_blanks() { if (blanks != "") { printf "%s", blanks; blanks="" } }
/^\[about\]/ { in_about=1; flush_blanks(); print; next }
/^\[/ && !/^\[about\]/ {
if (in_about) {
if (!desc_written) { print "description = \"" desc "\""; desc_written=1 }
if (!topics_written) { print "topics = " topics; topics_written=1 }
}
in_about=0
flush_blanks()
print
next
}
in_about && /^[#[:space:]]*description[[:space:]]*=/ { flush_blanks(); print "description = \"" desc "\""; desc_written=1; next }
in_about && /^[#[:space:]]*topics[[:space:]]*=/ { flush_blanks(); print "topics = " topics; topics_written=1; next }
in_about && /^[[:space:]]*$/ { blanks=blanks "\n"; next }
{ flush_blanks(); print }
END {
if (in_about) {
if (!desc_written) { print "description = \"" desc "\""; desc_written=1 }
if (!topics_written) { print "topics = " topics; topics_written=1 }
}
flush_blanks()
}
' "$input"
}

# Assert helpers - count failures via a global flag
assert_eq() {
local label="$1" expected="$2" actual="$3"
if [[ "$expected" != "$actual" ]]; then
echo " ${RED}assertion failed:${NORMAL} $label"
echo " expected: $expected"
echo " actual: $actual"
return 1
fi
return 0
}

# Verify a given key line appears inside the [about] block, i.e. there is no
# blank line between it and the preceding non-blank [about] content, and it
# appears before the next [section] header.
# Args: output_file key_pattern
assert_key_inside_about() {
local output="$1" key_pat="$2"
local key_line section_line
key_line=$(grep -nE "$key_pat" "$output" | head -1 | cut -d: -f1 || true)
# Find the next non-[about] section header. grep -n emits "N:content";
# filter on the content after the colon so [about] itself is excluded.
section_line=$(grep -nE '^\[' "$output" | grep -v '^[0-9]*:\[about\]' | head -1 | cut -d: -f1 || true)
Comment on lines +75 to +78
if [[ -z "$key_line" ]]; then
echo -e " ${RED}assertion failed:${NORMAL} key matching /$key_pat/ not found"
return 1
fi
if [[ -n "$section_line" && "$key_line" -ge "$section_line" ]]; then
echo -e " ${RED}assertion failed:${NORMAL} key at line $key_line is at or after next section at line $section_line"
return 1
fi
# Find the nearest preceding non-blank line and confirm it is not separated
# by a blank line (i.e. the inserted key is adjacent to the prior key).
local prev_nonblank=0
local i=$((key_line - 1))
while [[ $i -gt 0 ]]; do
local line
line=$(sed -n "${i}p" "$output")
if [[ -n "$(echo "$line" | tr -d '[:space:]')" ]]; then
prev_nonblank=$i
break
fi
i=$((i - 1))
done
if [[ $((key_line - prev_nonblank)) -ne 1 ]]; then
echo -e " ${RED}assertion failed:${NORMAL} blank line(s) separate key (line $key_line) from preceding content (line $prev_nonblank)"
return 1
fi
return 0
}

run_test() {
local fixture="$1" desc="$2" topics="$3" key_pat="$4" expected_val="$5" label="$6"
local fixture_path="$REPO_ROOT/$FIXTURES_DIR/$fixture"
local result=0

if [[ ! -f "$fixture_path" ]]; then
echo -e "${RED}✗${NORMAL} $label - fixture not found: $fixture"
(( failed += 1 ))
return
fi

local workspace
workspace=$(mktemp -d)
cp "$fixture_path" "$workspace/.repo.toml"
Comment on lines +118 to +120

local output="$workspace/out.toml"
run_awk "$workspace/.repo.toml" "$desc" "$topics" > "$output" || result=$?

if [[ $result -ne 0 ]]; then
echo -e "${RED}✗${NORMAL} $label - awk exited $result"
rm -rf "$workspace"
(( failed += 1 ))
return
fi

local actual_val
actual_val=$(grep -oE "$key_pat" "$output" | head -1 || true)

local ok=true
assert_eq "value for $label" "$expected_val" "$actual_val" || ok=false
assert_key_inside_about "$output" "$key_pat" || ok=false

if [[ "$ok" == true ]]; then
echo -e "${GREEN}✓${NORMAL} $label"
(( passed += 1 ))
else
echo " --- output ---"
sed 's/^/ /' "$output"
echo " --- end ---"
(( failed += 1 ))
fi

rm -rf "$workspace"
}

main() {
echo -e "${BLUE}Running cue-sync-from-github tests...${NORMAL}"
echo

if [[ ! -d "$REPO_ROOT/$FIXTURES_DIR" ]]; then
echo -e "${YELLOW}No test fixtures found at $FIXTURES_DIR${NORMAL}"
echo "Tests skipped"
return 0
fi
Comment on lines +156 to +160

local desc="Automated GitHub workflow template: community standards, just recipes for PR lifecycle, AI reviews (Copilot/Claude), and template sync system"
local topics='"compliance", "github", "github-repository", "template", "template-generic-repo", "template-repository"'

# Case 1: missing description -> inserted inside [about], before blank line
run_test "missing_description.toml" "$desc" "$topics" \
'description = "[^"]*"' "description = \"$desc\"" \
"missing description inserted inside [about]"

# Case 2: commented description -> replaced in place, inside [about]
run_test "commented_description.toml" "$desc" "$topics" \
'description = "[^"]*"' "description = \"$desc\"" \
"commented description replaced inside [about]"

# Case 3: missing topics -> inserted inside [about], before blank line
run_test "missing_topics.toml" "$desc" "$topics" \
'topics = \[[^]]*\]' "topics = [$topics]" \
"missing topics inserted inside [about]"

# Case 4: commented topics -> replaced in place, inside [about]
# This is the headline fix for issue #165 and previously had no test.
run_test "commented_topics.toml" "$desc" "$topics" \
'topics = \[[^]]*\]' "topics = [$topics]" \
"commented topics replaced inside [about]"

# Case 5: happy path - both keys active -> replaced in place, inside [about]
# Regression guard against any future awk change silently breaking the
# pre-existing in-place replace behaviour. Asserts both keys in two calls
# since run_test takes a single key assertion.
run_test "active_keys.toml" "$desc" "$topics" \
'description = "[^"]*"' "description = \"$desc\"" \
"active description replaced inside [about]"
run_test "active_keys.toml" "$desc" "$topics" \
'topics = \[[^]]*\]' "topics = [$topics]" \
"active topics replaced inside [about]"

echo
echo -e "Results: ${GREEN}$passed passed${NORMAL}, ${RED}$failed failed${NORMAL}"

if [[ $failed -gt 0 ]]; then
exit 1
fi
}

main "$@"
5 changes: 5 additions & 0 deletions .just/lib/update_pr_body.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ if [[ "$state" == "$BEFORE_DONE" && ${#header_content[@]} -gt 0 ]]; then
fi
fi

# Trim trailing blank lines from footer (GitHub API + jq round-trip accumulation; issue #173)
while [[ ${#footer_content[@]} -gt 0 && -z "${footer_content[${#footer_content[@]}-1]}" ]]; do
unset 'footer_content[${#footer_content[@]}-1]'
done

# Output the updated PR body
# 1. Header content (everything before Done)
if [[ ${#header_content[@]} -gt 0 ]]; then
Expand Down
Loading