Skip to content

refactor: extract shared org-wide runner for update and upgrade commands#41553

Merged
pelikhan merged 4 commits into
mainfrom
copilot/refactor-update-organization-algorithm
Jun 26, 2026
Merged

refactor: extract shared org-wide runner for update and upgrade commands#41553
pelikhan merged 4 commits into
mainfrom
copilot/refactor-update-organization-algorithm

Conversation

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

runUpdateForOrg and runUpgradeForOrg duplicated the same structural algorithm — discover repos, filter, loop with rate limiting, report, apply/issue. This extracts that algorithm into a shared runCommandForOrg and rewires both commands through it.

New: pkg/cli/org_runner.go

Introduces orgRunCallbacks and runCommandForOrg. All structural concerns live here:

  • Input validation + glob filtering
  • SIGTERM/Ctrl-C signal handling (partial results reported on interrupt)
  • Per-repo scan loop with rate-limit awareness and cancellation (ScanFn; nil = include all discovered repos without scanning)
  • Stable sort by oldest-edit time, ties broken alphabetically
  • Preview report before any writes
  • Apply and issue loops that skip failed repos and only error when every repo fails

update_org.go

runUpdateForOrg becomes a thin wrapper: builds orgRunCallbacks with the update-specific ScanFn (wraps previewOrgRepoUpdates) and delegates to runCommandForOrg. Behavior unchanged.

upgrade_org.go

runUpgradeForOrg delegates with ScanFn = nil — no per-repo API scan needed. Upgrade org mode gains capabilities it previously lacked:

Before After
Signal handling ❌ exits abruptly ✅ shows partial results
Repo failure stops entire run skips, continues
Processing order arbitrary sorted by oldest edit

upgrade_org_test.go

TestRunUpgradeForOrgStopsOnFirstErrorTestRunUpgradeForOrgSkipsFailedRepos, updated to assert all repos are attempted and an error is only returned when every one fails.

Copilot AI and others added 2 commits June 25, 2026 23:47
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title refactor: extract shared org runner for update and upgrade commands refactor: extract shared org-wide runner for update and upgrade commands Jun 25, 2026
Copilot AI requested a review from pelikhan June 25, 2026 23:58
@pelikhan pelikhan marked this pull request as ready for review June 26, 2026 00:24
Copilot AI review requested due to automatic review settings June 26, 2026 00:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the org-wide update and upgrade CLI flows by extracting their shared “discover → filter → (optional scan) → sort → report → apply/issue with cancellation + rate-limit handling” algorithm into a new runCommandForOrg helper, and rewires both commands to use it.

Changes:

  • Added pkg/cli/org_runner.go with orgRunCallbacks and runCommandForOrg to centralize org discovery, cancellation, rate-limit handling, sorting, reporting, and per-repo error recovery.
  • Updated runUpdateForOrg and runUpgradeForOrg to delegate to runCommandForOrg (with ScanFn enabled for update and disabled for upgrade).
  • Updated upgrade org behavior/tests to continue past per-repo failures and only error when all repos fail; also includes workflow .lock.yml updates (model string normalization and a few run-command tweaks).
Show a summary per file
File Description
pkg/cli/org_runner.go New shared org-wide runner implementing discovery/scan/report/apply logic with cancellation and rate-limit handling.
pkg/cli/update_org.go Refactors org update mode into callbacks + shared runner; keeps per-repo preview logic in ScanFn.
pkg/cli/upgrade_org.go Refactors org upgrade mode into callbacks + shared runner (no scan phase).
pkg/cli/upgrade_org_test.go Updates org upgrade tests to assert “skip failures, only error if all fail”.
.github/workflows/unbloat-docs.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/spec-enforcer.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/smoke-pi.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/smoke-opencode.lock.yml Updates generated workflow lock run command (adds cd $GITHUB_WORKSPACE) and related generated output.
.github/workflows/smoke-crush.lock.yml Updates generated workflow lock run command (adds cd $GITHUB_WORKSPACE) and related generated output.
.github/workflows/smoke-antigravity.lock.yml Updates generated workflow lock run command (adds cd $GITHUB_WORKSPACE) and related generated output.
.github/workflows/schema-consistency-checker.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/poem-bot.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/lint-monster.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).
.github/workflows/issue-monster.lock.yml Updates generated workflow lock env (COPILOT_MODEL value normalization).

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 14/14 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment thread pkg/cli/update_org.go Outdated
if err != nil {
return err
}
orgUpdateLog.Printf("Previewing updates for repositories in %s", org)
Comment thread pkg/cli/update_org.go Outdated
Comment on lines +75 to +77
// scanFn previews a single repo and decides whether to include it.
// It also prints per-repo progress to stderr so the user can see which
// workflows are being inspected.
Comment thread pkg/cli/org_runner.go
Comment on lines +139 to +142
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Cancellation requested; stopping after %d/%d repositories", i, total)))
orgUpdateLog.Printf("Context canceled during scan at repo %d/%d: %v", i, total, ctx.Err())
stopped = true
break
COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
COPILOT_MODEL: copilot/gpt-5.4
COPILOT_MODEL: gpt-5.4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (360 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/41553-extract-shared-org-runner-with-callbacks.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-41553: Extract Shared Org-Wide Runner with Callback Struct

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 41553-extract-shared-org-runner-with-callbacks.md for PR #41553).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 66.3 AIC · ⌖ 9.88 AIC · ⊞ 8.4K ·

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test (modified): 1 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0 (Go mock libraries / missing build tags / no assertion messages)
Test File Classification Issues Detected
TestRunUpgradeForOrgSkipsFailedRepos pkg/cli/upgrade_org_test.go:199 ✅ Design

Go: 1 (*_test.go); JavaScript: 0. Other languages detected but not scored.

Note on assertion messages: require.Error(t, err) and assert.Contains(t, err.Error(), ...) both lack an explicit message argument — the guideline recommends descriptive context on all assertions. Minor observation only; not a blocking violation.

Verdict

Check passed. 0% implementation tests (threshold: 30%). The renamed test TestRunUpgradeForOrgSkipsFailedRepos (formerly TestRunUpgradeForOrgStopsOnFirstError) correctly updates behavioral coverage to match the new skip-failures contract: verifies all repos are attempted (not just the first), and that a meaningful error is returned when every repo fails. Error coverage is present and the function-variable substitution pattern (not testify/mock or gomock) is used appropriately.

🧪 Test quality analysis by Test Quality Sentinel · 42 AIC · ⌖ 21.7 AIC · ⊞ 8.4K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The updated test TestRunUpgradeForOrgSkipsFailedRepos correctly captures the new behavioral contract (skip failures, try all repos).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /improve-codebase-architecture, /zoom-out, and /tdd — requesting changes on two gaps before merging.

📋 Key Themes & Highlights

Positive Highlights

Clean extraction — the orgRunCallbacks struct + runCommandForOrg pattern elegantly removes ~220 lines of duplication while keeping both callers readable as thin wrappers.
Good upgrade capability uplift — signal handling and skip-on-error are real improvements over the old stop-on-first-error behaviour, and the PR description table is a nice way to surface the delta.
Test name communicates intentTestRunUpgradeForOrgSkipsFailedRepos reads as a spec, which is exactly the /tdd spirit.
Well-documented struct — every callback field has a doc comment explaining semantics, including the nil-means-skip contract for ScanFn.

Issues to Address

  1. orgUpdateLog in shared code (org_runner.go:140) — upgrade org debug logs appear under cli:update_org, confusing future diagnostics. Low effort to fix.
  2. Nil guards missing for required callbacks (org_runner.go:98) — SearchFn/ReportFn are called unconditionally; a nil value panics. Worth guarding early.
  3. --create-issue behavior change in upgrade is untested (upgrade_org.go:49) — the old code stopped on first error; the new shared loop skips and continues. This is the most important gap: the apply-path change has a test, the issue-path change does not.
  4. Sort order for upgrade not pinned by a test (upgrade_org_test.go:222) — alphabetical order is the effective guarantee; a test would prevent accidental regressions.
  5. createPR && createIssue both-true contract not documented (org_runner.go:213) — a comment or guard would help future callers.
  6. Log detail regression in update_org.go:73 — minor nit; the repo count was useful in debug output.

The blocking items are #2 (panic potential) and #3 (untested behavioral change). The others are non-blocking suggestions.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 104.7 AIC · ⌖ 12.9 AIC · ⊞ 6.5K

Comment thread pkg/cli/org_runner.go Outdated
// the report for the work completed so far.
if ctx.Err() != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Cancellation requested; stopping after %d/%d repositories", i, total)))
orgUpdateLog.Printf("Context canceled during scan at repo %d/%d: %v", i, total, ctx.Err())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] orgUpdateLog is defined in update_org.go as logger.New("cli:update_org"). Using it in this shared file means all upgrade org operations emit debug logs under the cli:update_org namespace, making it harder to isolate upgrade-specific output when debugging.

💡 Suggestion

Move — or add — a neutral shared logger in org_runner.go:

var orgLog = logger.New("cli:org_runner")

Then use orgLog throughout this file. update_org.go keeps orgUpdateLog for its own update-specific lines, and a future third org command won't silently inherit a misleading tag.

Comment thread pkg/cli/org_runner.go
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(discMsg))

repoPaths, err := cbs.SearchFn(ctx, org, verbose)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] SearchFn and ReportFn are called unconditionally but the struct doesn't enforce that they're non-nil. A caller that accidentally omits either field will get a nil-pointer panic at runtime; the optional fields (ScanFn, etc.) are nil-checked but required ones aren't.

💡 Suggestion

Add a guard just after the existing validation:

if cbs.SearchFn == nil {
    return errors.New("orgRunCallbacks.SearchFn must not be nil")
}
if cbs.ReportFn == nil {
    return errors.New("orgRunCallbacks.ReportFn must not be nil")
}

You could also add a doc comment on SearchFn and ReportFn marking them as required, and note that ApplyFn/IssueFn must be non-nil when createPR/createIssue are set — whichever approach fits the project's style.

Comment thread pkg/cli/org_runner.go
// the user see results even if the run is stopped early.
cbs.ReportFn(results, createPR || createIssue)

if !createPR && !createIssue {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/zoom-out] When both createPR and createIssue are true, the if createIssue branch runs and returns without ever reaching the apply loop — only issues are created. This is safe because callers enforce mutual exclusion upstream, but runCommandForOrg doesn't document or assert this invariant. A future caller could easily be surprised.

💡 Suggestion

Either add an early guard:

if createPR && createIssue {
    return errors.New("createPR and createIssue are mutually exclusive")
}

Or at minimum add a comment above this block:

// Callers are expected to enforce mutual exclusion; if both are set,
// only the issue path runs.

Making the contract explicit here helps the next reader understand the intent without tracing back to command-flag validation.

Comment thread pkg/cli/upgrade_org.go
ApplyFn: func(ctx context.Context, preview orgRepoPreview, v bool) error {
return runUpgradeForTargetRepoFn(ctx, preview.Repo, opts, v)
},
IssueFn: func(ctx context.Context, preview orgRepoPreview, v bool) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The old runUpgradeForOrg stopped on the first createIssue error (return err); the new shared loop skips failures and only returns an error when every repo fails. This is a behavioral change for --create-issue that isn't mentioned in the PR description's before/after table and has no corresponding test.

💡 Suggestion

Add a test mirroring TestRunUpgradeForOrgSkipsFailedRepos for the issue-creation path:

func TestRunUpgradeForOrgCreateIssueSkipsFailedRepos(t *testing.T) {
    // stub SearchFn, IssueFn to always fail
    // assert all repos attempted
    // assert error contains "failed to create issues in any repository"
}

The update org equivalent is TestRunUpgradeForOrgCreateIssueReturnsErrorWhenAllIssueCreatesFail — a symmetrical test for upgrade closes the coverage gap and documents the new semantics.

assert.Equal(t, []string{"octo/api"}, called, "should stop after first failure")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to upgrade any repository")
assert.Equal(t, []string{"octo/api", "octo/web"}, called, "should attempt all repos and skip failures")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The PR description's table lists sorted-by-oldest-edit as a new capability for upgrade, but there's no test for it. With ScanFn == nil, all repos get zero OldestEdit, so the sort falls back to alphabetical order — which is still a meaningful, testable guarantee.

💡 Suggested test
func TestRunUpgradeForOrgSortsAlphabetically(t *testing.T) {
    // ScanFn is nil → OldestEdit stays zero → ties broken alphabetically
    searchOrgAnyWorkflowReposFn = func(...) ([]string, error) {
        return []string{"octo/zoo", "octo/alpha", "octo/middle"}, nil
    }
    var called []string
    runUpgradeForTargetRepoFn = func(_ context.Context, repo string, _ upgradeOptions, _ bool) error {
        called = append(called, repo)
        return nil
    }
    ...
    _ = runUpgradeForOrg(context.Background(), "octo", nil, upgradeOptions{ctx: context.Background()}, true, false, false)
    assert.Equal(t, []string{"octo/alpha", "octo/middle", "octo/zoo"}, called)
}

This pins the deterministic ordering as a regression guard without needing real timestamps.

Comment thread pkg/cli/update_org.go Outdated
if err != nil {
return err
}
orgUpdateLog.Printf("Previewing updates for repositories in %s", org)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] The previous log line was "Previewing updates for %d repositories in %s", total, org — the count was useful for correlating log output with how many repos were scanned. The refactor drops it because total is no longer available at this call site.

💡 Suggestion (optional)

This is a low-severity nit. One option is to log the count inside runCommandForOrg after filtering (the runner already has len(repos) at that point). Alternatively, accept the regression as a trade-off of the extraction — the user-visible progress output still shows [n/total] per repo.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — one blocking issue in runCommandForOrg, plus a description inaccuracy.

Missing nil guards on required callbacks

runCommandForOrg calls cbs.SearchFn, cbs.ReportFn, cbs.ApplyFn, and cbs.IssueFn without any nil checks. The struct documentation marks only ScanFn as optional, leaving the other four implicitly required but unenforced. Both current callers set all callbacks correctly, so this will not panic today — but the abstraction is designed for reuse, and a future caller that omits a required field will trigger a runtime nil function-call panic with no helpful error context. Add four nil checks immediately after validateRepoGlobs (see inline comment on line 98).

Minor observation: upgrade sort is alphabetical, not by oldest edit

When ScanFn is nil (upgrade command), all orgRepoPreview entries are built with OldestEdit at its zero value. The sort comparator first branch — if a.OldestEdit.IsZero() && b.OldestEdit.IsZero() — always fires, giving purely alphabetical ordering, not oldest-edit ordering. Deterministic and harmless, but the PR description table entry "sorted by oldest edit" for upgrade is inaccurate; the actual ordering is alphabetical for any command using ScanFn = nil.

🔎 Code quality review by PR Code Quality Reviewer · 199 AIC · ⌖ 9.36 AIC · ⊞ 5.2K

Comment thread pkg/cli/org_runner.go
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(discMsg))

repoPaths, err := cbs.SearchFn(ctx, org, verbose)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing nil guards on required callbacks will cause runtime panics: cbs.SearchFn is called here without a nil check; the same applies to cbs.ReportFn (line 211), cbs.IssueFn (line 240, when createIssue=true), and cbs.ApplyFn (line 280, when createPR=true). Only ScanFn is documented as optional, but the other four are never validated before use. Both current callers are complete, so this will not panic today — but as this shared runner gains more callers, a missing callback will produce a cryptic nil function-call panic instead of a clear error.

💡 Suggested fix

Add a validation block immediately after the existing validateRepoGlobs check (line 83):

if cbs.SearchFn == nil {
    return errors.New("orgRunCallbacks.SearchFn is required")
}
if cbs.ReportFn == nil {
    return errors.New("orgRunCallbacks.ReportFn is required")
}
if createPR && cbs.ApplyFn == nil {
    return errors.New("orgRunCallbacks.ApplyFn is required when createPR is true")
}
if createIssue && cbs.IssueFn == nil {
    return errors.New("orgRunCallbacks.IssueFn is required when createIssue is true")
}

Alternatively, add // Required. to the struct field godocs for SearchFn, ReportFn, ApplyFn, and IssueFn to make the contract explicit at the declaration site.

Comment thread pkg/cli/org_runner.go
}

if len(results) == 0 {
if stopped {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial scan with non-zero results silently falls through to the apply/issue loop: when the scan is stopped early (stopped=true) due to a rate-limit-critical condition but len(results) > 0, the stopped flag is only checked inside if len(results) == 0. Code falls straight through to cbs.ReportFn and the createPR/createIssue apply loops with no indication that the scan was incomplete. In the rate-limit-critical case the context is NOT cancelled, so waitForOrgRateLimitFn in the apply loop is the only thing that prevents PR/issue creation — and if the limit has reset (mocked in tests, or on a long run straddling the reset window), PRs or issues will be created for only the partial subset of repos without any user warning.

💡 Suggested fix

Check stopped before entering the apply loops, not just inside the empty-results guard:

if len(results) == 0 {
    if stopped { /* NoResultsStopMsg */ }
    /* NoResultsMsg */
    return nil
}
// NEW: warn about partial scan even when results are non-empty
if stopped {
    fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Scan was stopped early; report and any subsequent operations cover only the repositories inspected so far"))
}

Alternatively, when stopped=true skip the apply/issue loops entirely (return after the report) to avoid acting on a partial scan.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Implemented a pr-finisher pass in commit 818c9d4: addressed actionable org-runner review feedback (required callback guards + neutral logger), updated update-org wording/log behavior, added missing upgrade-org coverage, and revalidated with make fmt, make lint, make test-unit, make test, make recompile, plus parallel validation.

@pelikhan pelikhan merged commit c38906b into main Jun 26, 2026
28 checks passed
@pelikhan pelikhan deleted the copilot/refactor-update-organization-algorithm branch June 26, 2026 01:34
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — nice refactor! Extracting the shared runCommandForOrg abstraction cleanly eliminates the structural duplication between runUpdateForOrg and runUpgradeForOrg, and the upgrade org command gets some meaningful improvements (signal handling, per-repo failure isolation, stable sort ordering) essentially for free.

The PR is well-packaged: an ADR in docs/adr/, updated and renamed tests in upgrade_org_test.go, and a thorough description with a before/after table. This looks ready for review. 🎉

Generated by ✅ Contribution Check · 181.2 AIC · ⌖ 20.4 AIC · ⊞ 6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants