-
Notifications
You must be signed in to change notification settings - Fork 23
feat(orch): reconcile-work-items — the tracker sweep for state written once and never re-read (VST-318) #1430
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
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
7cd2a2b
feat(orch): reconcile-work-items — the tracker sweep for state writte…
bmethod b9db13d
fix(orch): the jq check precedes its first use; the audit preflight g…
bmethod 5d7185e
fix(orch): the jq dependency check precedes every use
bmethod 36e1cfd
fix(orch): timestamps parse on BSD date and fail closed; the audit pr…
bmethod 196626f
fix(orch): jq scans materialize before their loops so a mid-stream fa…
bmethod 3d0453a
fix(orch): the reconcile wiring is prose-conditional with simple comm…
bmethod 4f28cf8
fix(orch): the PR probe counts only this repository's branch — a fork…
bmethod d15d8f8
fix(orch): PR probes scrub inherited GH_REPO/GITHUB_REPOSITORY redirects
bmethod 27f483e
fix(orch): reconcile-work-items resolves RECONCILE_* through project …
bmethod 100c3e5
docs(orch): RECONCILE_STALE_HOURS in the README configuration table
bmethod 2005625
fix(orch): the cache validation requires identifier/state fields per …
bmethod 24933da
fix(orch): a (one PR) bundle root with Done children is not container…
bmethod 37fca4c
fix(orch): the (one PR) marker matches case-insensitively, same as th…
bmethod 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
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,149 @@ | ||
| #!/usr/bin/env bash | ||
| # reconcile-work-items — report tracker state the workflows wrote once and | ||
| # never re-read. READ-ONLY: prints findings, mutates nothing; the caller | ||
| # decides what to fix. | ||
| # | ||
| # Three checks over the linear skill's cache (offline; sync first for | ||
| # freshness): | ||
| # container-parked a parent in a non-terminal state whose non-canceled | ||
| # children are all Done — the close that only merge-pr § 5 | ||
| # performs never ran | ||
| # started-stale an In Progress / In Review item untouched for longer | ||
| # than the threshold whose branch-named PR is merged (or | ||
| # unverifiable) — the item outlived its work | ||
| # done-unchecked a Done item whose description still carries unchecked | ||
| # `- [ ]` acceptance criteria — a partial-scope PR's | ||
| # `Closes` magic word closed it | ||
| # | ||
| # Exit codes: 0 nothing to reconcile; 1 findings; 2 config/collection error. | ||
| # | ||
| # RECONCILE_STALE_HOURS started-stale threshold (default 24) | ||
| # RECONCILE_GH_CLI PR-probe command (default gh); absent/failing gh | ||
| # degrades that item to "PR state unverified", never | ||
| # to silence | ||
| set -euo pipefail | ||
|
|
||
| config_error() { | ||
| echo "::error::reconcile-work-items: $*" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
|
|
||
| REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || config_error "not inside a git repository" | ||
| # shellcheck source=lib/vstack-env.sh | ||
| source "$SCRIPT_DIR/lib/vstack-env.sh" | ||
| vstack_load_project_env "$REPO_ROOT" | ||
|
|
||
| STALE_HOURS="${RECONCILE_STALE_HOURS:-24}" | ||
| case "$STALE_HOURS" in | ||
| "" | *[!0-9]* | 0*[0-9]) config_error "RECONCILE_STALE_HOURS must be a non-negative integer, got '$STALE_HOURS'" ;; | ||
|
bmethod marked this conversation as resolved.
bmethod marked this conversation as resolved.
bmethod marked this conversation as resolved.
bmethod marked this conversation as resolved.
bmethod marked this conversation as resolved.
|
||
| esac | ||
| GH_CLI="${RECONCILE_GH_CLI:-gh}" | ||
|
bmethod marked this conversation as resolved.
|
||
|
|
||
| command -v jq >/dev/null 2>&1 || config_error "jq is required" | ||
|
|
||
| CACHE="$REPO_ROOT/.cache/linear/issues.json" | ||
| [ -f "$CACHE" ] || config_error "no linear cache at $CACHE — run linear.sh sync first" | ||
| # Structural validation up front: an array of rows missing the fields the | ||
| # scans key on would otherwise be skipped silently and report clean. | ||
| jq -e 'type == "array" and all(.[]; | ||
| type == "object" | ||
| and (.identifier | type == "string") | ||
| and (.state | type == "object") | ||
| and (.state.name | type == "string") | ||
| and (.state.type | type == "string"))' "$CACHE" >/dev/null 2>&1 \ | ||
|
bmethod marked this conversation as resolved.
|
||
| || config_error "linear cache at $CACHE is not an array of issue rows with identifier and state.name/state.type" | ||
|
|
||
| findings=0 | ||
| note() { findings=$((findings + 1)); printf '%s\n' "$1"; } | ||
|
|
||
| # --- container-parked -------------------------------------------------------- | ||
| # Non-terminal parents whose non-canceled children (by parent identifier) are | ||
| # all completed. Trashed/archived rows never count as children or parents. | ||
| TMPD="$(mktemp -d)" || config_error "could not create a scratch directory" | ||
| trap 'rm -rf -- "$TMPD"' EXIT | ||
|
|
||
| jq -r ' | ||
| map(select((.trashed // false) == false and .archivedAt == null)) as $live | ||
| | ($live | map(select(.parent.identifier != null))) as $children | ||
| | ($children | group_by(.parent.identifier)) as $broods | ||
|
bmethod marked this conversation as resolved.
bmethod marked this conversation as resolved.
|
||
| | $broods[] | ||
| | .[0].parent.identifier as $pid | ||
| | (map(select(.state.type != "canceled"))) as $countable | ||
| | select(($countable | length) > 0 and ($countable | all(.state.type == "completed"))) | ||
|
bmethod marked this conversation as resolved.
|
||
| | ($live[] | select(.identifier == $pid)) as $parent | ||
| | select($parent.state.type != "completed" and $parent.state.type != "canceled") | ||
|
bmethod marked this conversation as resolved.
|
||
| # A "(one PR)" root is a single-PR bundle, not a container: its children go | ||
| # Done while the root stays open until ITS PR merges, so an open root with | ||
| # Done children is that contract working, not a parked container. | ||
| | select($parent.title | test("\\(one PR\\)"; "i") | not) | ||
| | [$parent.identifier, $parent.title, $parent.state.name] | @tsv | ||
| ' "$CACHE" >"$TMPD/containers" || config_error "container scan failed" | ||
| while IFS=$'\t' read -r pid ptitle pstate; do | ||
| [ -n "$pid" ] || continue | ||
| note "container-parked: $pid [$pstate] — every non-canceled child is Done; close it or say why it stays open ($ptitle)" | ||
| done <"$TMPD/containers" | ||
|
|
||
| # --- started-stale ----------------------------------------------------------- | ||
| now_epoch="$(date -u +%s)" || config_error "could not read the clock" | ||
| jq -r ' | ||
| .[] | select((.trashed // false) == false and .archivedAt == null) | ||
| | select(.state.type == "started") | ||
| | [.identifier, .title, .state.name, .updatedAt] | @tsv | ||
| ' "$CACHE" >"$TMPD/started" || config_error "started scan failed" | ||
| while IFS=$'\t' read -r iid ititle istate iupdated; do | ||
| [ -n "$iid" ] || continue | ||
| updated_epoch="$(date -u -d "$iupdated" +%s 2>/dev/null)" || updated_epoch="" | ||
| if [ -z "$updated_epoch" ]; then | ||
| # BSD date has no -d; parse the ISO stamp with -j -f (fractional part | ||
| # and zone dropped — whole-second precision is plenty for an hours | ||
| # threshold). | ||
| trimmed="${iupdated%%.*}" | ||
| trimmed="${trimmed%Z}" | ||
| updated_epoch="$(date -j -u -f "%Y-%m-%dT%H:%M:%S" "$trimmed" +%s 2>/dev/null)" || updated_epoch="" | ||
| fi | ||
| # A timestamp neither form can read must never silently shrink the check. | ||
| [ -n "$updated_epoch" ] || config_error "could not parse updatedAt '$iupdated' for $iid — the started-stale check cannot run" | ||
| age_hours=$(((now_epoch - updated_epoch) / 3600)) | ||
| [ "$age_hours" -ge "$STALE_HOURS" ] || continue | ||
| branch="$(printf '%s' "$iid" | tr '[:upper:]' '[:lower:]')" | ||
| pr_state="unverified" | ||
| if command -v "${GH_CLI%% *}" >/dev/null 2>&1; then | ||
|
bmethod marked this conversation as resolved.
|
||
| # This repo's branch only — a fork's same-named branch is someone | ||
| # else's PR and must not vouch for this item. | ||
| # The probes answer for THIS checkout: an inherited GH_REPO / | ||
| # GITHUB_REPOSITORY redirect would let another repository's same-named | ||
| # branch vouch for this item. | ||
| merged="$(env -u GH_REPO -u GITHUB_REPOSITORY $GH_CLI pr list --state merged --head "$branch" --json number,isCrossRepository --jq '[.[] | select(.isCrossRepository | not)] | length' 2>/dev/null)" || merged="" | ||
| open="$(env -u GH_REPO -u GITHUB_REPOSITORY $GH_CLI pr list --state open --head "$branch" --json number,isCrossRepository --jq '[.[] | select(.isCrossRepository | not)] | length' 2>/dev/null)" || open="" | ||
|
bmethod marked this conversation as resolved.
|
||
| if [ -n "$merged" ] && [ -n "$open" ]; then | ||
| if [ "$open" -gt 0 ]; then | ||
| continue # a live PR is a live item, whatever the clock says | ||
| elif [ "$merged" -gt 0 ]; then | ||
| pr_state="PR merged" | ||
| else | ||
| pr_state="no PR" | ||
| fi | ||
| fi | ||
| fi | ||
| note "started-stale: $iid [$istate] — untouched ${age_hours}h, $pr_state; finish the lifecycle or restate the scope ($ititle)" | ||
| done <"$TMPD/started" | ||
|
|
||
| # --- done-unchecked ---------------------------------------------------------- | ||
| jq -r ' | ||
| .[] | select((.trashed // false) == false and .archivedAt == null) | ||
| | select(.state.type == "completed") | ||
| | select((.description // "") | test("- \\[ \\]")) | ||
| | [.identifier, .title] | @tsv | ||
| ' "$CACHE" >"$TMPD/done" || config_error "done scan failed" | ||
| while IFS=$'\t' read -r iid ititle; do | ||
| [ -n "$iid" ] || continue | ||
| note "done-unchecked: $iid [Done] — the description still carries unchecked \`- [ ]\` acceptance criteria; reopen or check them off with evidence ($ititle)" | ||
| done <"$TMPD/done" | ||
|
|
||
| if [ "$findings" -gt 0 ]; then | ||
| echo "reconcile-work-items: $findings finding(s) — the tracker disagrees with the work" | ||
| exit 1 | ||
| fi | ||
| echo "reconcile-work-items: clean — every item's state matches its work" | ||
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,146 @@ | ||
| #!/usr/bin/env bash | ||
| # Pins for reconcile-work-items (vstack #1388 / VST-318): the read-only sweep | ||
| # reports the three write-without-read-back shapes and stays quiet on their | ||
| # healthy twins. Fully offline: fixture cache + stubbed PR probe. | ||
| set -euo pipefail | ||
|
|
||
| TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" | ||
| RW="$SKILL_DIR/scripts/reconcile-work-items" | ||
| TMP="$(mktemp -d)" | ||
| trap 'rm -rf "$TMP"' EXIT | ||
|
|
||
| PASS=0 | ||
| FAIL=0 | ||
| ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$1"; } | ||
| bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n %s\n' "$1" "${2:-}"; } | ||
|
|
||
| R="$TMP/repo" | ||
| mkdir -p "$R/.cache/linear" | ||
| git -C "$R" init -q -b main 2>/dev/null || git -C "$R" init -q | ||
|
|
||
| now="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" | ||
| old="$(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -j -u -v-3d +%Y-%m-%dT%H:%M:%S.000Z)" | ||
|
|
||
| issue() { # ID TITLE STATE_NAME STATE_TYPE UPDATED [PARENT] [DESC] | ||
| local parent="null" | ||
| [ -n "${6:-}" ] && parent="{\"identifier\":\"$6\"}" | ||
| jq -cn --arg id "$1" --arg t "$2" --arg sn "$3" --arg st "$4" --arg up "$5" --argjson p "$parent" --arg d "${7:-}" \ | ||
| '{identifier:$id, title:$t, state:{name:$sn,type:$st}, updatedAt:$up, parent:$p, description:$d, trashed:false, archivedAt:null}' | ||
| } | ||
|
|
||
| { | ||
| issue "T-1" "parked container" "Todo" "unstarted" "$now" | ||
| issue "T-2" "done child a" "Done" "completed" "$now" "T-1" | ||
| issue "T-3" "done child b" "Done" "completed" "$now" "T-1" | ||
| issue "T-4" "canceled child" "Canceled" "canceled" "$now" "T-1" | ||
| issue "T-5" "healthy container" "Todo" "unstarted" "$now" | ||
| issue "T-6" "done child" "Done" "completed" "$now" "T-5" | ||
| issue "T-7" "pending child" "In Progress" "started" "$now" "T-5" | ||
| issue "T-8" "closed container" "Done" "completed" "$now" | ||
| issue "T-9" "done child of closed" "Done" "completed" "$now" "T-8" | ||
| issue "T-10" "stale started merged" "In Review" "started" "$old" | ||
| issue "T-11" "fresh started" "In Progress" "started" "$now" | ||
| issue "T-12" "stale started live pr" "In Progress" "started" "$old" | ||
| issue "T-13" "done with open boxes" "Done" "completed" "$now" "" "did:\n- [x] one\n- [ ] two" | ||
| issue "T-14" "done all checked" "Done" "completed" "$now" "" "did:\n- [x] one\n- [x] two" | ||
| issue "T-15" "trashed parked" "Todo" "unstarted" "$now" | ||
| issue "T-16" "ship the widget (One PR)" "In Review" "started" "$now" | ||
| issue "T-17" "done bundle child" "Done" "completed" "$now" "T-16" | ||
| } | jq -s 'map(if .identifier == "T-15" then .trashed = true else . end)' >"$R/.cache/linear/issues.json" | ||
|
|
||
| cat >"$TMP/gh-stub" <<'STUB' | ||
| #!/usr/bin/env bash | ||
| # args: pr list --state STATE --head BRANCH --json number --jq length | ||
| # A leaked repo redirect must never reach the probe. | ||
| if [ -n "${GH_REPO:-}" ] || [ -n "${GITHUB_REPOSITORY:-}" ]; then | ||
| echo "gh-stub: GH_REPO/GITHUB_REPOSITORY leaked into the probe" >&2 | ||
| exit 9 | ||
| fi | ||
| state=""; head="" | ||
| while [ $# -gt 0 ]; do | ||
| case "$1" in | ||
| --state) state="$2"; shift ;; | ||
| --head) head="$2"; shift ;; | ||
| esac | ||
| shift | ||
| done | ||
| case "$head:$state" in | ||
| t-10:merged) echo 1 ;; | ||
| t-12:open) echo 1 ;; | ||
| *) echo 0 ;; | ||
| esac | ||
| STUB | ||
| chmod +x "$TMP/gh-stub" | ||
|
|
||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && GH_REPO=elsewhere/other GITHUB_REPOSITORY=elsewhere/other RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
|
|
||
| [ "$RC" -eq 1 ] && ok "findings exit 1" || bad "exit code" "rc=$RC out=$OUT" | ||
| case "$OUT" in *"container-parked: T-1"*) ok "the parked container is reported" ;; *) bad "parked container" "$OUT" ;; esac | ||
| # A "(one PR)" root with Done children is the single-PR bundle contract | ||
| # working, never a parked container. | ||
| case "$OUT" in *"container-parked: T-16"*) bad "one-PR bundle flagged as parked" "$OUT" ;; *) ok "a (One PR) bundle root is not container-parked (case-insensitive marker)" ;; esac | ||
| case "$OUT" in *"container-parked: T-5"*) bad "healthy container reported" "$OUT" ;; *) ok "a container with a pending child stays quiet" ;; esac | ||
| case "$OUT" in *"T-8"*) bad "closed container reported" "$OUT" ;; *) ok "a closed container stays quiet" ;; esac | ||
| case "$OUT" in *"started-stale: T-10"*"PR merged"*) ok "the stale started item with a merged PR is reported" ;; *) bad "stale merged" "$OUT" ;; esac | ||
| case "$OUT" in *"T-11"*) bad "fresh started reported" "$OUT" ;; *) ok "a fresh started item stays quiet" ;; esac | ||
| case "$OUT" in *"T-12"*) bad "live-PR started reported" "$OUT" ;; *) ok "a stale item with a live PR stays quiet" ;; esac | ||
| case "$OUT" in *"done-unchecked: T-13"*) ok "the Done item with open boxes is reported" ;; *) bad "done unchecked" "$OUT" ;; esac | ||
| case "$OUT" in *"T-14"*) bad "all-checked reported" "$OUT" ;; *) ok "a Done item with every box checked stays quiet" ;; esac | ||
| case "$OUT" in *"T-15"*) bad "trashed reported" "$OUT" ;; *) ok "a trashed row stays out of every check" ;; esac | ||
|
|
||
| # Clean fixture: only healthy rows -> exit 0 with the clean line. | ||
| jq '[.[] | select(.identifier == "T-5" or .identifier == "T-6" or .identifier == "T-7" or .identifier == "T-14" or .identifier == "T-11")]' \ | ||
| "$R/.cache/linear/issues.json" >"$R/.cache/linear/issues2.json" | ||
| mv "$R/.cache/linear/issues2.json" "$R/.cache/linear/issues.json" | ||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 0 ] && case "$OUT" in *"clean"*) true ;; *) false ;; esac \ | ||
| && ok "a healthy tracker exits 0 with the clean line" || bad "clean run" "rc=$RC out=$OUT" | ||
|
|
||
| # A malformed row inside an array-shaped cache: the scan must die loudly, | ||
| # never end early as a clean pass. | ||
| printf '[{"identifier":"T-BAD"}, 42]' >"$R/.cache/linear/issues.json" | ||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 2 ] && ok "a malformed cache row is a loud collection error" || bad "malformed row" "rc=$RC out=$OUT" | ||
|
|
||
| # Object-shaped but incomplete rows must not read as a clean tracker: a row | ||
| # without identifier/state carries nothing the scans can inspect. | ||
| printf '[{}]' >"$R/.cache/linear/issues.json" | ||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 2 ] && ok "an empty-object row is a config error, never clean" || bad "empty-object row" "rc=$RC out=$OUT" | ||
| printf '[{"identifier":"T-1","state":{"name":"Todo"}}]' >"$R/.cache/linear/issues.json" | ||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 2 ] && ok "a row missing state.type is a config error" || bad "missing state.type" "rc=$RC out=$OUT" | ||
|
|
||
| # Missing cache: loud config error, never a clean pass. | ||
| rm "$R/.cache/linear/issues.json" | ||
| OUT=""; RC=0 | ||
| OUT="$(cd "$R" && "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 2 ] && ok "a missing cache is a config error, never clean" || bad "missing cache" "rc=$RC out=$OUT" | ||
|
|
||
| # --- settings-file threshold ------------------------------------------------- | ||
| # RECONCILE_STALE_HOURS set in the project's vstack.settings.toml (not the | ||
| # environment) must reach the sweep: a 2h-old In Progress item is quiet at the | ||
| # 24h default and a finding at a 1h threshold. | ||
| R2="$TMP/settings-repo" | ||
| mkdir -p "$R2/.cache/linear" | ||
| git -C "$R2" init -q | ||
| TWO_H_AGO="$(date -u -d '2 hours ago' '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null || date -j -u -v-2H '+%Y-%m-%dT%H:%M:%S.000Z')" | ||
| cat >"$R2/.cache/linear/issues.json" <<JSON | ||
| [{"identifier":"VST-900","title":"stale candidate","state":{"name":"In Progress","type":"started"},"parent":null,"description":"","updatedAt":"$TWO_H_AGO"}] | ||
| JSON | ||
| RC=0 | ||
| OUT="$(cd "$R2" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| [ "$RC" -eq 0 ] && ok "default 24h threshold stays quiet at 2h" || bad "default threshold" "rc=$RC out=$OUT" | ||
| printf '[env]\nRECONCILE_STALE_HOURS = "1"\n' >"$R2/vstack.settings.toml" | ||
| RC=0 | ||
| OUT="$(cd "$R2" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? | ||
| { [ "$RC" -eq 1 ] && printf '%s' "$OUT" | grep -q "VST-900"; } && ok "settings-file RECONCILE_STALE_HOURS reaches the sweep" || bad "settings-file threshold" "rc=$RC out=$OUT" | ||
|
|
||
| printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" | ||
| [ "$FAIL" -eq 0 ] |
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
Oops, something went wrong.
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.