Repository navigation
feat: implement issue #518 — add-to-project backlog reconcile (workflow_dispatch + cron) #522
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
Merged
+269
−3
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| #!/usr/bin/env bash | ||
| # reconcile-backlog.sh — periodic/manual backlog reconcile for add-to-project. | ||
| # | ||
| # Why this exists (petry-projects/.github#518): the event-driven add-to-project | ||
| # path misses qualifying items in two structural cases — | ||
| # 1. issues/PRs created by the default GITHUB_TOKEN (app/github-actions): | ||
| # GitHub does not fire workflows from default-token events, so | ||
| # `issues: opened` never triggers the add; and | ||
| # 2. runs dropped under runner congestion (no retry). | ||
| # This scans each repo's OPEN issues/PRs + Ideas discussions and reconciles each | ||
| # with the board using the SAME gate + helpers as the event path | ||
| # (evaluate_noise_gate / reconcile_content_with_project / reconcile_discussion), | ||
| # so a missed item lands within one reconcile cycle regardless of author/token. | ||
| # Idempotent; honours DRY_RUN=1 (logs intended actions, mutates nothing). | ||
| # | ||
| # Env: | ||
| # PROJECT_ID ProjectV2 node ID of the Initiatives project (required) | ||
| # PROJECT_URL human-readable URL (logging only) | ||
| # REQUIRED_LABEL gate's required label (default: dev-lead) | ||
| # EXCLUDED_LABELS gate's excluded labels (default in add-issue-or-pr.sh) | ||
| # IDEAS_CATEGORY discussion category tracked as drafts (default: Ideas) | ||
| # RECON_REPOS space-separated owner/repo list to scan | ||
| # (default: the planner App installation's repositories) | ||
| # DRY_RUN "1" → log only | ||
| # GH_TOKEN planner App installation token (issues/PRs read + project write) | ||
| set -euo pipefail | ||
|
|
||
| _dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" | ||
| # shellcheck source=.github/scripts/add-to-project/add-issue-or-pr.sh | ||
| source "${_dir}/add-issue-or-pr.sh" # reconcile_content_with_project (+ lib.sh) | ||
| # shellcheck source=.github/scripts/add-to-project/reconcile-discussion.sh | ||
| source "${_dir}/reconcile-discussion.sh" # reconcile_discussion | ||
|
|
||
| : "${PROJECT_ID:?PROJECT_ID is required}" | ||
| ideas_category="${IDEAS_CATEGORY:-Ideas}" | ||
|
|
||
| # Resolve target repos: explicit RECON_REPOS, else the App installation's repos. | ||
| declare -a repos | ||
| if [ -n "${RECON_REPOS:-}" ]; then | ||
| read -r -a repos <<< "${RECON_REPOS}" | ||
| else | ||
| mapfile -t repos < <(gh api --paginate /installation/repositories --jq '.repositories[].full_name' 2>/dev/null) | ||
| fi | ||
| if [ "${#repos[@]}" -eq 0 ]; then | ||
| echo "::error::no repos to reconcile (set RECON_REPOS or check the App installation)" >&2 | ||
| exit 64 | ||
| fi | ||
|
|
||
| scanned=0 | ||
| echo "Reconciling ${#repos[@]} repo(s) into ${PROJECT_URL:-the project}${DRY_RUN:+ (DRY RUN)}" | ||
| for repo in "${repos[@]}"; do | ||
| [ -z "$repo" ] && continue | ||
| echo "::group::${repo}" | ||
|
|
||
| # Open issues AND PRs (the issues endpoint returns both; the gate applies to | ||
| # both identically). Pass labels as the objects array the gate expects. | ||
| while IFS=$'\t' read -r nid url labels; do | ||
| [ -z "$nid" ] && continue | ||
| scanned=$((scanned + 1)) | ||
| reconcile_content_with_project "$nid" "$url" "$labels" || true | ||
| done < <(gh api --paginate "repos/${repo}/issues?state=open&per_page=100" \ | ||
| --jq '.[] | [.node_id, .html_url, ([.labels[] | {name}] | @json)] | @tsv' 2>/dev/null) | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| # Open Ideas-category discussions → draft items. | ||
| while IFS=$'\t' read -r number title url; do | ||
| [ -z "$number" ] && continue | ||
| scanned=$((scanned + 1)) | ||
| reconcile_discussion "$number" "$title" "$url" "$ideas_category" || true | ||
| done < <(gh api graphql --paginate -f owner="${repo%%/*}" -f name="${repo##*/}" -f query=' | ||
| query($owner:String!,$name:String!,$endCursor:String){ | ||
| repository(owner:$owner,name:$name){ | ||
| discussions(first:100,after:$endCursor,states:OPEN){ | ||
| pageInfo{hasNextPage endCursor} | ||
| nodes{number title url category{name}} | ||
| } | ||
| } | ||
| }' --jq ".data.repository.discussions.nodes[] | select(.category.name==\"${ideas_category}\") | [(.number|tostring), .title, .url] | @tsv" 2>/dev/null) | ||
|
don-petry marked this conversation as resolved.
Resolved
|
||
|
|
||
| echo "::endgroup::" | ||
| done | ||
|
|
||
| echo "Reconcile complete — scanned ${scanned} item(s) across ${#repos[@]} repo(s)." | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Backlog reconcile for the Initiatives project (petry-projects/.github#518). | ||
| # | ||
| # The event-driven add-to-project automation misses qualifying items created by | ||
| # the default GITHUB_TOKEN (GitHub doesn't fire workflows from default-token | ||
| # events) and items whose run was dropped under congestion. This scheduled/ | ||
| # manual reconcile scans every installed repo's OPEN issues/PRs + Ideas | ||
| # discussions and reconciles them onto the board using the same gate + helpers | ||
| # as the event path — so missed items land within one cycle. | ||
| # | ||
| # Runs centrally in petry-projects/.github (the scripts live here); mints the | ||
| # petry-projects-planner App token for issues/PRs read + project write. | ||
| name: Add-to-Project Reconcile | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| dry_run: | ||
| description: "Log intended adds/removes without mutating the board" | ||
| type: boolean | ||
| default: true | ||
| repos: | ||
| description: "Optional space-separated owner/repo list (default: all App-installed repos)" | ||
| type: string | ||
| default: "" | ||
| schedule: | ||
| - cron: "0 7 * * *" # daily 07:00 UTC | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: add-to-project-reconcile | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| reconcile: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| fetch-depth: 1 | ||
| persist-credentials: false | ||
|
|
||
| - name: Mint App installation token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | ||
| with: | ||
| app-id: ${{ secrets.INITIATIVES_APP_ID }} | ||
| private-key: ${{ secrets.INITIATIVES_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
|
|
||
| - name: Reconcile backlog into the project | ||
| env: | ||
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | ||
| PROJECT_ID: PVT_kwDOD2inqs4BZq3- | ||
| PROJECT_URL: https://github.com/orgs/petry-projects/projects/1 | ||
| REQUIRED_LABEL: dev-lead | ||
| EXCLUDED_LABELS: compliance-audit,health-check,fleet-tracker,daily-report | ||
| IDEAS_CATEGORY: Ideas | ||
| RECON_REPOS: ${{ inputs.repos }} | ||
| # Manual runs default to dry-run; scheduled runs are live. | ||
| DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && inputs.dry_run) && '1' || '0' }} | ||
| run: bash .github/scripts/add-to-project/reconcile-backlog.sh |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| #!/usr/bin/env bats | ||
| # Tests for reconcile-backlog.sh — the periodic/manual backlog reconcile | ||
| # (petry-projects/.github#518). Runs the script with a tailored `gh` stub and | ||
| # DRY_RUN=1, and asserts it scans a repo's open issues/PRs + Ideas discussions | ||
| # and reconciles qualifying ones via the shared event-path helpers (logging the | ||
| # intended adds rather than mutating). | ||
|
|
||
| bats_require_minimum_version 1.5.0 | ||
|
|
||
| load 'helpers/setup' | ||
|
|
||
| setup() { | ||
| tt_make_tmpdir | ||
| # Tailored gh stub: returns one qualifying issue, one Ideas discussion, and | ||
| # "not found" for the draft-dedup lookup so reconcile takes the add path. | ||
| local bin="${TT_TMP}/bin"; mkdir -p "$bin" | ||
| cat > "$bin/gh" <<'STUB' | ||
| #!/usr/bin/env bash | ||
| args="$*" | ||
| case "$args" in | ||
| *"repos/"*"/issues"*) | ||
| # node_id <TAB> html_url <TAB> labels-objects-json (one qualifying issue) | ||
| printf 'I_node1\thttps://example.invalid/issues/1\t[{"name":"dev-lead"}]\n' ;; | ||
| *graphql*discussions*) | ||
| # number <TAB> title <TAB> url (one Ideas discussion) | ||
| printf '55\tShiny idea\thttps://example.invalid/discussions/55\n' ;; | ||
| *graphql*) | ||
| # find_project_item lookups (draft-dedup / remove path) → not found | ||
| printf '' ;; | ||
| *) printf '' ;; | ||
| esac | ||
| STUB | ||
| chmod +x "$bin/gh"; PATH="${bin}:${PATH}"; export PATH | ||
| export PROJECT_ID="PVT_test" PROJECT_URL="https://example.invalid/p/1" GH_TOKEN="t" | ||
| export RECON_REPOS="petry-projects/demo" # skip installation lookup | ||
| export DRY_RUN="1" | ||
| } | ||
| teardown() { tt_cleanup_tmpdir; } | ||
|
|
||
| run_recon() { run bash "${TT_SCRIPTS_DIR}/reconcile-backlog.sh"; } | ||
|
|
||
| @test "dry-run reconciles a qualifying issue (logs intended add, no mutation)" { | ||
| run_recon | ||
| [ "$status" -eq 0 ] | ||
| echo "$output" | grep -q '\[dry-run\] would add content I_node1 to project' | ||
| } | ||
|
|
||
| @test "dry-run reconciles an Ideas discussion as a draft" { | ||
| run_recon | ||
| [ "$status" -eq 0 ] | ||
| echo "$output" | grep -q '\[dry-run\] would add draft: \[Discussion #55\] Shiny idea' | ||
| } | ||
|
|
||
| @test "reports the repo count it scanned" { | ||
| run_recon | ||
| [ "$status" -eq 0 ] | ||
| echo "$output" | grep -q 'Reconciling 1 repo(s)' | ||
| echo "$output" | grep -q 'Reconcile complete' | ||
| } | ||
|
|
||
| @test "an excluded label disqualifies an issue (no add)" { | ||
| # override the stub to return a dev-lead + compliance-audit issue | ||
| cat > "${TT_TMP}/bin/gh" <<'STUB' | ||
| #!/usr/bin/env bash | ||
| case "$*" in | ||
| *"repos/"*"/issues"*) printf 'I_excl\thttps://example.invalid/issues/2\t[{"name":"dev-lead"},{"name":"compliance-audit"}]\n' ;; | ||
| *graphql*discussions*) printf '' ;; | ||
| *graphql*) printf '' ;; | ||
| esac | ||
| STUB | ||
| chmod +x "${TT_TMP}/bin/gh" | ||
| run_recon | ||
| [ "$status" -eq 0 ] | ||
| ! echo "$output" | grep -q 'would add content I_excl' | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.