🧘 [just] gh-process v5.8 utilizes gh-observer, adds copilot features, and fixes bugs#32
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the repository’s just-based automation to align with gh-process v5.8 by adding template-sync tooling, repo metadata generation, Copilot/Claude helpers, and a few workflow fixes (notably PR body “Done” section markers and gh-observer support).
Changes:
- Add template synchronization system (checksum manifest, update/verify/diff recipes, and update logic scripts).
- Add
.repo.toml→ shell env generation plus expanded CUE schema/verification (includingstandard-releaseflag). - Enhance PR workflow automation: PR body markers + robust Done-section updater, checks watching via
gh observer, and Copilot/Claude review helpers.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
justfile |
Imports additional .just/* modules (template sync, repo-toml, hooks, Copilot/Claude). |
docs/repo-toml.cue |
Adds flags.standard-release to the .repo.toml schema. |
.just/template-sync.just |
Adds recipes for checksum generation, verification, template updates, and diffing. |
.just/shellcheck.just |
Moves recipe group to Testing/Compliance. |
.just/repo-toml.sh |
Adds generated repo metadata/flags shell exports. |
.just/repo-toml.just |
Adds generator/checker recipes for .just/repo-toml.sh from .repo.toml. |
.just/pr-hook.just |
Adds a pre-PR hook recipe (shellcheck disabled, sorts Claude permissions). |
.just/lib/update_pr_body.sh |
Adds robust PR body Done-section updater using HTML markers + fallback parsing. |
.just/lib/template_update.sh |
Adds core manifest-driven template update logic. |
.just/lib/template_sync_test.sh |
Adds fixture-driven tests for template update logic (with mocked curl). |
.just/lib/pr_body_test.sh |
Adds test runner for PR body update logic. |
.just/lib/install-prerequisites.sh |
Adds helper script to install/check required CLI tooling. |
.just/lib/generate_checksums.sh |
Adds script to generate versioned checksums manifest from git history. |
.just/lib/common.sh |
Adds shared checksum helper for template sync scripts. |
.just/gh-process.just |
Updates PR creation/body markers, adds check polling, gh observer usage, conditional release workflow, and enhanced Copilot/Claude review output. |
.just/cue-verify.just |
Moves group + adds GitHub metadata sync checks and cue-sync-from-github recipe. |
.just/copilot.just |
Adds Copilot suggestion picker, refresh, and rollback tooling. |
.just/claude.just |
Adds Claude settings permission sorting/check helpers. |
.just/CHECKSUMS.json |
Adds checksum manifest used for template synchronization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # watch GHAs then check for Copilot suggestions | ||
| [group('Process')] | ||
| pr_checks: _on_a_pull_request && claude_review | ||
| pr_checks: && claude_review | ||
| #!/usr/bin/env bash | ||
|
|
||
| gh pr checks --watch -i 5 | ||
| # Use gh observer if installed, otherwise fall back to gh pr checks --watch | ||
| if gh observer --help >/dev/null 2>&1; then |
There was a problem hiding this comment.
pr_checks no longer runs the _on_a_pull_request sanity check (it was removed from the recipe header). Since the body calls gh pr checks / gh observer and reads PR metadata, this can now fail or behave confusingly when run from a branch without an open PR. Add _on_a_pull_request back as a pre-dependency (or call it at the start of the script body) so the recipe fails fast with the existing clear message/exit code.
| ORG_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/([^/]+)/.*|\1|') | ||
| REPO_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/[^/]+/(.+)|\1|') |
There was a problem hiding this comment.
REPO_NAME parsing from web_url doesn’t handle a trailing slash: for https://github.com/org/repo/ it will capture repo/. Consider using the same extraction used in .just/cue-verify.just (strip an optional trailing slash) or normalize WEB_URL first, so REPO_REPO_NAME is stable.
| ORG_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/([^/]+)/.*|\1|') | |
| REPO_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/[^/]+/(.+)|\1|') | |
| ORG_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/([^/]+)/[^/]+/?$|\1|') | |
| REPO_NAME=$(echo "$WEB_URL" | sed -E 's|https://github.com/[^/]+/([^/]+)/?$|\1|') |
| # Extract line number from selection to match back to original comment | ||
| line_num=$(echo "$selected" | cut -d':' -f2 | cut -d' ' -f1) | ||
|
|
||
| # Validate line number is numeric | ||
| if ! [[ "$line_num" =~ ^[0-9]+$ ]]; then | ||
| echo "{{RED}}Error: Could not extract line number from selection{{NORMAL}}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Get the full suggestion data | ||
| suggestion_data=$(jq -r --arg line "$line_num" '.[] | select(.originalLine == ($line | tonumber))' "$copilot_comments") | ||
| file_path=$(echo "$suggestion_data" | jq -r '.path') | ||
| original_line=$(echo "$suggestion_data" | jq -r '.originalLine') | ||
| suggestion_body=$(echo "$suggestion_data" | jq -r '.body') |
There was a problem hiding this comment.
The selected suggestion is re-identified using only originalLine. If multiple Copilot comments share the same line number (especially across different files), suggestion_data will contain multiple JSON objects and file_path/suggestion_body become multi-line/ambiguous, which can lead to applying the wrong suggestion. Match by both path and originalLine (parse both from the gum choose entry) or select by array index.
| # Single line: use sed with proper escaping | ||
| # shellcheck disable=SC2016 | ||
| escaped_code=$(echo "$suggested_code" | sed 's|[][\\\.*^$()+?{|/]|\\&|g') | ||
| sed -i "${original_line}s|.*|${escaped_code}|" "$file_path" |
There was a problem hiding this comment.
sed -i is not portable between GNU sed and BSD sed (macOS requires -i ''). As written, applying a single-line suggestion will fail on macOS. Use a portable in-place edit approach (e.g., write to a temp file and mv, or use perl -pi -e) or branch on OS/sed flavor.
| sed -i "${original_line}s|.*|${escaped_code}|" "$file_path" | |
| sed "${original_line}s|.*|${escaped_code}|" "$file_path" > "${file_path}.tmp" | |
| mv "${file_path}.tmp" "$file_path" |
|
|
||
| for backup in "$BACKUP_DIR"/*.bak; do | ||
| if [[ -f "$backup" ]]; then | ||
| filename=$(basename "$backup") | ||
| timestamp=${filename##*_} | ||
| timestamp=${timestamp%.bak} | ||
| # Convert safe path back to original file path | ||
| safe_path=${filename%_*} | ||
| target_file=${safe_path//_/\/} |
There was a problem hiding this comment.
Backup filenames encode the target path by replacing / with _, but rollback reconstructs the path by replacing every _ with /. This is not reversible when the original path contains underscores, and can restore to the wrong location. Use an unambiguous encoding (e.g., base64/urlencode) or store the original path alongside the backup (manifest file, or include a delimiter/escape scheme).
| for backup in "$BACKUP_DIR"/*.bak; do | |
| if [[ -f "$backup" ]]; then | |
| filename=$(basename "$backup") | |
| timestamp=${filename##*_} | |
| timestamp=${timestamp%.bak} | |
| # Convert safe path back to original file path | |
| safe_path=${filename%_*} | |
| target_file=${safe_path//_/\/} | |
| # Decode the original backup target path from the encoded portion of the filename. | |
| # Filenames are expected to be of the form: <base64url-encoded-path>_<timestamp>.bak | |
| # where the path component has been encoded with URL-safe base64. | |
| decode_backup_target() { | |
| local encoded="$1" | |
| python3 - << 'PY' | |
| import sys | |
| import base64 | |
| def decode_base64url(s: str) -> str: | |
| # Add required padding for base64 decoding | |
| pad = (-len(s)) % 4 | |
| s_padded = s + ("=" * pad) | |
| return base64.urlsafe_b64decode(s_padded.encode("utf-8")).decode("utf-8") | |
| if __name__ == "__main__": | |
| try: | |
| s = sys.argv[1] | |
| sys.stdout.write(decode_base64url(s)) | |
| except Exception: | |
| # On any error, write nothing and exit with non-zero for the caller to handle. | |
| sys.exit(1) | |
| PY | |
| } | |
| for backup in "$BACKUP_DIR"/*.bak; do | |
| if [[ -f "$backup" ]]; then | |
| filename=$(basename "$backup") | |
| # Strip the .bak suffix first | |
| base_name=${filename%.bak} | |
| # Extract timestamp (text after the last underscore) | |
| timestamp=${base_name##*_} | |
| # Extract the encoded path (everything before the last underscore) | |
| encoded_path=${base_name%_*} | |
| # Decode the original target file path from the encoded portion | |
| if ! target_file=$(decode_backup_target "$encoded_path"); then | |
| echo "{{RED}}Warning: Skipping backup with invalid encoded path: $filename{{NORMAL}}" | |
| continue | |
| fi |
| while IFS= read -r filepath; do | ||
| if [[ ! -f "$filepath" ]]; then | ||
| # Check if this is a cleaned file | ||
| if jq -e --arg fp "$filepath" '.cleaned_files // [] | index($fp) != null' "$MANIFEST_FILE" >/dev/null 2>&1; then | ||
| echo " {{BLUE}}⊘{{NORMAL}} $filepath - removed by clean_template" | ||
| else | ||
| echo " {{YELLOW}}⊘{{NORMAL}} $filepath - not present locally" | ||
| all_match=false |
There was a problem hiding this comment.
Cleaned-file detection only checks for exact membership in .cleaned_files, but the manifest includes many paths under .just/test/... while .cleaned_files contains only the directory .just/test. This causes derived repos (like this one, which has no .just/test/) to report many files as missing and fail verification. Treat cleaned entries as path prefixes (skip any file where its path is under a cleaned directory).
| # Check if this is a cleaned file (should be skipped if missing) | ||
| local is_cleaned=false | ||
| if jq -e --arg fp "$filepath" '.cleaned_files // [] | index($fp) != null' "$MANIFEST_FILE" >/dev/null 2>&1; then | ||
| is_cleaned=true | ||
| fi | ||
|
|
||
| # Get versions array from manifest | ||
| local versions_json | ||
| versions_json=$(jq -r ".files[\"$filepath\"].versions // []" "$MANIFEST_FILE") | ||
|
|
||
| if [[ "$versions_json" == "[]" ]]; then | ||
| echo -e " ${YELLOW}⚠${NORMAL} $filepath - not in manifest, skipping" | ||
| return | ||
| fi | ||
|
|
||
| # Get latest version info | ||
| local latest_checksum latest_version | ||
| latest_checksum=$(echo "$versions_json" | jq -r '.[0].checksum') | ||
| latest_version=$(echo "$versions_json" | jq -r '.[0].version') | ||
|
|
||
| # Check if file exists locally | ||
| if [[ ! -f "$filepath" ]]; then | ||
| # If it's a cleaned file, skip it (intentionally removed) | ||
| if [[ "$is_cleaned" == true ]]; then | ||
| echo -e " ${GREEN}⊘${NORMAL} $filepath - removed by clean_template, skipping" | ||
| ((skipped_cleaned_count++)) || true | ||
| return | ||
| fi | ||
| echo -e " ${BLUE}↓${NORMAL} $filepath - new file, downloading" | ||
| if download_file "$filepath"; then | ||
| echo -e " ${GREEN}Downloaded successfully${NORMAL}" | ||
| ((downloaded_new_count++)) || true | ||
| else |
There was a problem hiding this comment.
template_update.sh treats cleaned_files as exact file paths, but the manifest includes many files under .just/test/... while .cleaned_files only lists the directory .just/test. This means missing fixture files will be treated as “new files” and download will be attempted (and likely fail due to missing parent dirs) and/or reintroduce cleaned content. Update the cleaned check to treat entries as directory prefixes and skip any file under a cleaned directory (and consider mkdir -p $(dirname "$filepath") before downloads for new files).
| if command -v sha256sum &>/dev/null; then | ||
| sha256sum "$file" | awk '{print $1}' | ||
| elif command -v shasum &>/dev/null; then | ||
| shasum -a 256 "$file" | awk '{print $1}' |
There was a problem hiding this comment.
compute_checksum returns an empty string when neither sha256sum nor shasum exists, which can lead to misleading results (comparisons against empty checksums). This should error out with a clear message (similar to .just/lib/common.sh) so verification doesn’t silently misbehave.
| shasum -a 256 "$file" | awk '{print $1}' | |
| shasum -a 256 "$file" | awk '{print $1}' | |
| else | |
| echo "{{RED}}Error: No SHA-256 checksum tool found (sha256sum or shasum){{NORMAL}}" >&2 | |
| echo "Please install coreutils (sha256sum) or a compatible shasum implementation and retry." >&2 | |
| exit 1 |
| printf '%s\n' \ | ||
| "# .repo.toml" \ | ||
| "" \ | ||
| "[about]" \ | ||
| "description = \"$GH_DESC\"" \ |
There was a problem hiding this comment.
When creating a new .repo.toml, the description line interpolates $GH_DESC without escaping quotes/newlines. If the GitHub description contains " (or other characters requiring escaping), the generated TOML will be invalid and the script will delete .repo.toml after failing validation. Escape the description (as you already do for the update-in-place path) or use a TOML-safe quoting strategy.
| printf '%s\n' \ | |
| "# .repo.toml" \ | |
| "" \ | |
| "[about]" \ | |
| "description = \"$GH_DESC\"" \ | |
| # Escape description for safe embedding in TOML basic string | |
| GH_DESC_ESCAPED="${GH_DESC//\"/\\\"}" | |
| GH_DESC_ESCAPED="${GH_DESC_ESCAPED//$'\n'/\\n}" | |
| printf '%s\n' \ | |
| "# .repo.toml" \ | |
| "" \ | |
| "[about]" \ | |
| "description = \"$GH_DESC_ESCAPED\"" \ |
| # Verify test directory exists | ||
| if [[ ! -d "$TEST_DIR" ]]; then | ||
| echo -e "${RED}Error: Test directory not found: $TEST_DIR${NORMAL}" >&2 | ||
| exit 1 |
There was a problem hiding this comment.
This test runner hard-fails if .just/test/fixtures/pr_bodies doesn’t exist. In this repo, .just/test isn’t present, so the script will always exit 1 if invoked. Consider skipping tests (exit 0 with a message) when fixtures are absent, consistent with .just/lib/template_sync_test.sh.
| # Verify test directory exists | |
| if [[ ! -d "$TEST_DIR" ]]; then | |
| echo -e "${RED}Error: Test directory not found: $TEST_DIR${NORMAL}" >&2 | |
| exit 1 | |
| # Verify test directory exists; if absent, skip tests | |
| if [[ ! -d "$TEST_DIR" ]]; then | |
| echo -e "${BLUE}PR body tests skipped: test directory not found: $TEST_DIR${NORMAL}" | |
| exit 0 |
Done
Meta
(Automated in
.just/gh-process.just.)