-
Notifications
You must be signed in to change notification settings - Fork 11
fix(#705): paginate all open issues in pre-scribe backlog fetch #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,334 @@ | ||
| #!/usr/bin/env bash | ||
| # pre-scribe-test.sh — Test the backlog fetch and metadata logic in | ||
| # pre-scribe.sh. Tests the paginated issue fetch pipeline in isolation | ||
| # (the full script requires Drive credentials that are unavailable in | ||
| # the test environment). | ||
| # | ||
| # Run from the repo root: bash scripts/pre-scribe-test.sh | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| FAILURES=0 | ||
|
|
||
| TMPDIR="$(mktemp -d)" | ||
| trap 'rm -rf "${TMPDIR}"' EXIT | ||
|
|
||
| MOCK_BIN="${TMPDIR}/bin" | ||
| mkdir -p "${MOCK_BIN}" | ||
|
|
||
| # --- Mock gh --- | ||
| # Simulates `gh api --paginate` for the issues endpoint. Returns fixture | ||
| # data from a file, applying the --jq filter via real jq to exercise | ||
| # the filter expression end-to-end. | ||
| build_mock_gh() { | ||
| local fixture_file="$1" | ||
|
|
||
| cat > "${MOCK_BIN}/gh" <<'MOCKEOF' | ||
| #!/usr/bin/env bash | ||
| FIXTURE="FIXTURE_PLACEHOLDER" | ||
|
|
||
| if [[ "$1" == "api" ]]; then | ||
| shift | ||
| JQ_EXPR="" | ||
| while [[ $# -gt 0 ]]; do | ||
| case "$1" in | ||
| --paginate) shift ;; | ||
| --jq) JQ_EXPR="$2"; shift 2 ;; | ||
| *) shift ;; | ||
| esac | ||
| done | ||
|
|
||
| if [[ -n "${JQ_EXPR}" ]]; then | ||
| jq -r "${JQ_EXPR}" "${FIXTURE}" | ||
| else | ||
| cat "${FIXTURE}" | ||
| fi | ||
| exit 0 | ||
| fi | ||
|
|
||
| exit 0 | ||
| MOCKEOF | ||
|
|
||
| local escaped_fixture="${fixture_file//\//\\/}" | ||
| perl -pi -e "s/FIXTURE_PLACEHOLDER/${escaped_fixture}/g" "${MOCK_BIN}/gh" | ||
| chmod +x "${MOCK_BIN}/gh" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] test-coverage-gap The run_backlog_fetch function duplicates the gh api/jq pipeline from pre-scribe.sh rather than sourcing the actual script. This is a reasonable trade-off given Drive credential requirements, but future pipeline changes in pre-scribe.sh require a parallel update in the test. |
||
| } | ||
|
|
||
| # --- Backlog fetch pipeline --- | ||
| # Reproduces the exact command chain from pre-scribe.sh so we test the | ||
| # real pipeline without requiring Drive credentials. | ||
| run_backlog_fetch() { | ||
| local scribe_repo="$1" | ||
| local backlog_file="$2" | ||
|
|
||
| PATH="${MOCK_BIN}:${PATH}" \ | ||
| gh api --paginate "repos/${scribe_repo}/issues?state=open&per_page=100" \ | ||
| --jq '.[] | select(.pull_request == null) | {number, title, body, labels, milestone, url: .html_url}' \ | ||
| | jq -s '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \ | ||
| > "${backlog_file}" | ||
| } | ||
|
|
||
| # --- Test helpers --- | ||
| assert_eq() { | ||
| local test_name="$1" expected="$2" actual="$3" | ||
| if [[ "${expected}" != "${actual}" ]]; then | ||
| echo "FAIL: ${test_name} — expected '${expected}', got '${actual}'" | ||
| FAILURES=$((FAILURES + 1)) | ||
| return 1 | ||
| fi | ||
| return 0 | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 1: All issues included, PRs filtered out | ||
| # =================================================================== | ||
| test_pagination_filters_prs() { | ||
| local test_name="pagination-filters-prs" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| # Fixture: 3 issues + 1 pull request (has pull_request field) | ||
| cat > "${fixture}" <<'EOF' | ||
| [ | ||
| {"number": 1, "title": "Old bug", "body": "stale issue", "labels": [{"name": "bug"}], "milestone": null, "html_url": "https://github.com/o/r/issues/1"}, | ||
| {"number": 2, "title": "Feature request", "body": "add dark mode", "labels": [], "milestone": {"title": "v2"}, "html_url": "https://github.com/o/r/issues/2"}, | ||
| {"number": 3, "title": "A pull request", "body": "PR body", "labels": [], "milestone": null, "html_url": "https://github.com/o/r/pull/3", "pull_request": {"url": "https://api.github.com/repos/o/r/pulls/3"}}, | ||
| {"number": 4, "title": "Config issue", "body": "assignee config", "labels": [{"name": "enhancement"}], "milestone": null, "html_url": "https://github.com/o/r/issues/4"} | ||
| ] | ||
| EOF | ||
|
|
||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local count | ||
| count=$(jq 'length' "${backlog}") | ||
| if ! assert_eq "${test_name}: count" "3" "${count}"; then | ||
| echo " backlog: $(cat "${backlog}")" | ||
| return | ||
| fi | ||
|
|
||
| # Verify PR (number 3) is excluded | ||
| local has_pr | ||
| has_pr=$(jq '[.[] | select(.number == 3)] | length' "${backlog}") | ||
| if ! assert_eq "${test_name}: no PR" "0" "${has_pr}"; then return; fi | ||
|
|
||
| # Verify issues 1, 2, 4 are present | ||
| local has_1 has_2 has_4 | ||
| has_1=$(jq '[.[] | select(.number == 1)] | length' "${backlog}") | ||
| has_2=$(jq '[.[] | select(.number == 2)] | length' "${backlog}") | ||
| has_4=$(jq '[.[] | select(.number == 4)] | length' "${backlog}") | ||
| if ! assert_eq "${test_name}: issue 1" "1" "${has_1}"; then return; fi | ||
| if ! assert_eq "${test_name}: issue 2" "1" "${has_2}"; then return; fi | ||
| if ! assert_eq "${test_name}: issue 4" "1" "${has_4}"; then return; fi | ||
|
|
||
| # Verify url field is mapped from html_url | ||
| local url_1 | ||
| url_1=$(jq -r '.[] | select(.number == 1) | .url' "${backlog}") | ||
| if ! assert_eq "${test_name}: url mapped" "https://github.com/o/r/issues/1" "${url_1}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 2: Body truncation at 500 chars | ||
| # =================================================================== | ||
| test_body_truncation() { | ||
| local test_name="body-truncation" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| # Generate a body > 500 chars (600 'a' characters) | ||
| local long_body | ||
| long_body=$(printf 'a%.0s' $(seq 1 600)) | ||
|
|
||
| jq -n --arg body "${long_body}" \ | ||
| '[{"number": 10, "title": "Long body issue", "body": $body, "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/10"}]' \ | ||
| > "${fixture}" | ||
|
|
||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local body_len | ||
| body_len=$(jq -r '.[0].body | length' "${backlog}") | ||
| # 500 chars + 1 "…" character = 501 | ||
| if ! assert_eq "${test_name}: truncated length" "501" "${body_len}"; then | ||
| echo " actual body length: ${body_len}" | ||
| return | ||
| fi | ||
|
|
||
| # Verify the truncation marker | ||
| local ends_with | ||
| ends_with=$(jq -r '.[0].body | .[-1:]' "${backlog}") | ||
| if ! assert_eq "${test_name}: ends with ellipsis" "…" "${ends_with}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 3: Short body is NOT truncated | ||
| # =================================================================== | ||
| test_short_body_preserved() { | ||
| local test_name="short-body-preserved" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| cat > "${fixture}" <<'EOF' | ||
| [ | ||
| {"number": 20, "title": "Short issue", "body": "This is a short body.", "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/20"} | ||
| ] | ||
| EOF | ||
|
|
||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local body | ||
| body=$(jq -r '.[0].body' "${backlog}") | ||
| if ! assert_eq "${test_name}: body preserved" "This is a short body." "${body}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 4: Empty result (no open issues) | ||
| # =================================================================== | ||
| test_empty_issues() { | ||
| local test_name="empty-issues" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| echo '[]' > "${fixture}" | ||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local count | ||
| count=$(jq 'length' "${backlog}") | ||
| if ! assert_eq "${test_name}: empty array" "0" "${count}"; then | ||
| echo " backlog: $(cat "${backlog}")" | ||
| return | ||
| fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 5: Null body handled gracefully | ||
| # =================================================================== | ||
| test_null_body() { | ||
| local test_name="null-body" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| cat > "${fixture}" <<'EOF' | ||
| [ | ||
| {"number": 30, "title": "No body issue", "body": null, "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/30"} | ||
| ] | ||
| EOF | ||
|
|
||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local body | ||
| body=$(jq -r '.[0].body' "${backlog}") | ||
| if ! assert_eq "${test_name}: null body becomes empty" "" "${body}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 6: Metadata includes open_issue_total and backlog_truncated | ||
| # =================================================================== | ||
| test_metadata_fields() { | ||
| local test_name="metadata-fields" | ||
| local meta_file="${TMPDIR}/meta-${test_name}.json" | ||
| local issue_count=1879 | ||
|
|
||
| # Run the same jq command from pre-scribe.sh to generate metadata | ||
| jq -n \ | ||
| --arg cutoff "2026-08-06T09:00:00Z" \ | ||
| --arg notes_url "https://docs.google.com/document/d/abc" \ | ||
| --arg repo "mock-org/mock-repo" \ | ||
| --argjson doc_count 1 \ | ||
| --argjson issue_count "${issue_count}" \ | ||
| --argjson closed_count 50 \ | ||
| --argjson pr_count 10 \ | ||
| --argjson doc_path_count 25 \ | ||
| '{ | ||
| cutoff_date: $cutoff, | ||
| notes_url: $notes_url, | ||
| repo: $repo, | ||
| docs_downloaded: $doc_count, | ||
| backlog_issues: $issue_count, | ||
| open_issue_total: $issue_count, | ||
| backlog_truncated: false, | ||
| closed_issues: $closed_count, | ||
| open_prs: $pr_count, | ||
| repo_docs: $doc_path_count | ||
| }' > "${meta_file}" | ||
|
|
||
| # Verify new fields exist and have correct values | ||
| local total truncated | ||
| total=$(jq '.open_issue_total' "${meta_file}") | ||
| truncated=$(jq '.backlog_truncated' "${meta_file}") | ||
| if ! assert_eq "${test_name}: open_issue_total" "1879" "${total}"; then return; fi | ||
| if ! assert_eq "${test_name}: backlog_truncated" "false" "${truncated}"; then return; fi | ||
|
|
||
| # Verify existing fields still present | ||
| local backlog_issues | ||
| backlog_issues=$(jq '.backlog_issues' "${meta_file}") | ||
| if ! assert_eq "${test_name}: backlog_issues" "1879" "${backlog_issues}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # =================================================================== | ||
| # Test 7: Labels and milestone are preserved from REST API format | ||
| # =================================================================== | ||
| test_labels_milestone_preserved() { | ||
| local test_name="labels-milestone-preserved" | ||
| local fixture="${TMPDIR}/fixture-${test_name}.json" | ||
| local backlog="${TMPDIR}/backlog-${test_name}.json" | ||
|
|
||
| cat > "${fixture}" <<'EOF' | ||
| [ | ||
| { | ||
| "number": 40, | ||
| "title": "Labeled issue", | ||
| "body": "has labels and milestone", | ||
| "labels": [{"id": 1, "name": "bug", "color": "d73a4a"}, {"id": 2, "name": "high-priority", "color": "ff0000"}], | ||
| "milestone": {"id": 1, "title": "v2.0", "number": 3}, | ||
| "html_url": "https://github.com/o/r/issues/40" | ||
| } | ||
| ] | ||
| EOF | ||
|
|
||
| build_mock_gh "${fixture}" | ||
| run_backlog_fetch "o/r" "${backlog}" | ||
|
|
||
| local label_count label_name milestone_title | ||
| label_count=$(jq '.[0].labels | length' "${backlog}") | ||
| label_name=$(jq -r '.[0].labels[0].name' "${backlog}") | ||
| milestone_title=$(jq -r '.[0].milestone.title' "${backlog}") | ||
|
|
||
| if ! assert_eq "${test_name}: label count" "2" "${label_count}"; then return; fi | ||
| if ! assert_eq "${test_name}: label name" "bug" "${label_name}"; then return; fi | ||
| if ! assert_eq "${test_name}: milestone title" "v2.0" "${milestone_title}"; then return; fi | ||
|
|
||
| echo "PASS: ${test_name}" | ||
| } | ||
|
|
||
| # --- Run tests --- | ||
|
|
||
| test_pagination_filters_prs | ||
| test_body_truncation | ||
| test_short_body_preserved | ||
| test_empty_issues | ||
| test_null_body | ||
| test_metadata_fields | ||
| test_labels_milestone_preserved | ||
|
|
||
| echo "" | ||
| if [[ ${FAILURES} -gt 0 ]]; then | ||
| echo "${FAILURES} test(s) failed" | ||
| exit 1 | ||
| fi | ||
| echo "All tests passed" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,11 +39,14 @@ CLOSED_ISSUES_FILE="${WORK_DIR}/closed-issues.json" | |
| OPEN_PRS_FILE="${WORK_DIR}/open-prs.json" | ||
| REPO_DOCS_FILE="${WORK_DIR}/repo-docs-index.json" | ||
|
|
||
| # Open issues with bodies (truncated to 500 chars to keep context lean) | ||
| # Open issues with bodies (truncated to 500 chars to keep context lean). | ||
| # Use gh api --paginate to fetch ALL open issues, avoiding truncation on | ||
| # repos with >1000 open issues (see #705). The REST /issues endpoint | ||
| # includes pull requests, so we filter them out with --jq. | ||
| echo "Fetching open issues from ${SCRIBE_REPO}..." | ||
| gh issue list --repo "${SCRIBE_REPO}" --state open \ | ||
| --json number,title,body,labels,milestone,url --limit 1000 \ | ||
| | jq '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \ | ||
| gh api --paginate "repos/${SCRIBE_REPO}/issues?state=open&per_page=100" \ | ||
| --jq '.[] | select(.pull_request == null) | {number, title, body, labels, milestone, url: .html_url}' \ | ||
| | jq -s '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \ | ||
| > "${BACKLOG_FILE}" | ||
| ISSUE_COUNT=$(jq 'length' "${BACKLOG_FILE}") | ||
| echo "Fetched ${ISSUE_COUNT} open issues for backlog context." | ||
|
|
@@ -176,7 +179,7 @@ if [[ "${DOC_COUNT}" -eq 0 ]]; then | |
| --argjson closed_count "${CLOSED_COUNT}" \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] incomplete-metadata-update backlog_truncated is hardcoded to false and open_issue_total is set to the same value as backlog_issues. Since --paginate fetches all issues, truncation cannot occur, making these values correct. However, neither field can ever carry a different value — the new fields add no dynamic runtime information beyond what backlog_issues already provides. |
||
| --argjson pr_count "${PR_COUNT}" \ | ||
| --argjson doc_path_count "${DOC_PATH_COUNT}" \ | ||
| '{cutoff_date: $cutoff, notes_url: "", repo: $repo, docs_downloaded: $doc_count, backlog_issues: $issue_count, closed_issues: $closed_count, open_prs: $pr_count, repo_docs: $doc_path_count}' \ | ||
| '{cutoff_date: $cutoff, notes_url: "", repo: $repo, docs_downloaded: $doc_count, backlog_issues: $issue_count, open_issue_total: $issue_count, backlog_truncated: false, closed_issues: $closed_count, open_prs: $pr_count, repo_docs: $doc_path_count}' \ | ||
| > "${META_FILE}" | ||
| echo "Workspace: ${WORK_DIR}" | ||
| exit 0 | ||
|
|
@@ -316,6 +319,8 @@ jq -n \ | |
| repo: $repo, | ||
| docs_downloaded: $doc_count, | ||
| backlog_issues: $issue_count, | ||
| open_issue_total: $issue_count, | ||
| backlog_truncated: false, | ||
| closed_issues: $closed_count, | ||
| open_prs: $pr_count, | ||
| repo_docs: $doc_path_count | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] test-robustness
build_mock_gh escapes only forward slashes in the fixture path before Perl regex substitution. Characters special to Perl regex (., +, $) in mktemp paths could theoretically cause failures, though in practice Linux mktemp paths are safe.