Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(upgrade): report which files a step could not upgrade, and recover by imorland · Pull Request #55 · flarum/cli · GitHub
Skip to content

fix(upgrade): report which files a step could not upgrade, and recover - #55

Merged
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience
Aug 1, 2026
Merged

fix(upgrade): report which files a step could not upgrade, and recover#55
imorland merged 1 commit into
3.xfrom
im/upgrade-resilience

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

A single unparseable file anywhere in an extension ended the whole upgrade:2.0 run:

Error occurred, and could not complete:
Unexpected token (2:25)
SyntaxError: Unexpected token (2:25)
at constructor (/…/node_modules/@babel/parser/lib/index.js:351:19)
at V8IntrinsicMixin.raise (/…/node_modules/@babel/parser/lib/index.js:3233:19)
… 8 more frames of our internals

Nothing there names the file. Preceding it, the terminal was handed a dump of the file's entire contents from a leftover debug console.log. And because the step's partial writes stayed on disk, the uncommitted-changes guard then refused to let the command be run again — so the author had to work out what to revert before they could even retry.

What it does now

Each step collects every file it could not handle, rolls back its own writes, and aborts with the file names and the reasons:

 => FAILED
js/src/forum/alsobroken.js
Could not parse: Unexpected token (2:11)
js/src/forum/broken.js
Could not parse: Unexpected token (2:25)
=> ERROR 2 files could not be upgraded by this step, so it made no changes.
Fix the problems above and run the command again — steps that already
completed are skipped, so it will resume from here.

Two decisions worth calling out:

Abort rather than skip the file and continue. A step that half-applied its transforms would leave the extension in a state nobody could reason about — some files migrated, some not, all committed together. Steps stay all-or-nothing.

Roll back the step's own writes. This is what makes the advice in the message true. Since every step starts behind the dirty-tree guard and each successful step commits its own work, anything uncommitted at the point of failure was written by the failing step — so checkout -- . is bounded to that step and cannot discard the author's work.

Collect all failures before aborting, so the author fixes everything the step found in one pass instead of rediscovering problems one re-run at a time.

Crash paths closed

Five places could kill the run. Four were found by reading; the fifth (the before() pre-pass) only showed up when driving the real binary.

LocationWasNow
advancedContent JSON parsecommand.error() — exits the processthrows with the parse reason
advancedContent JS/TS parseno handling at allthrows with the parse reason
re-parse after transformwarned with a full file dump, rethrewthrows, names what failed
generateCodewarned with a full file dump, rethrewthrows, names what failed
before() pre-passunprotectedcollected like the transform pass

Also

  • Removed a leftover debug console.log(code) in parseCode that printed whole files on any parse failure. The file name is now in the report, which is what's actually needed.
  • backend/phpunit.ts pointed authors at http://localhost:3000/extend/testing#model-factories — someone's dev server. Now https://docs.flarum.org/2.x/extend/testing#model-factories.

Testing

5 new unit tests for the failure collector, written red first. 181 tests pass; Prettier, ESLint and tsc clean.

Unit tests can't tell you whether the command behaves, so I drove the real binary against a fixture extension for each case: one broken file, two broken files, fix-and-resume (proving the run goes on through all 16 steps, which is what the error message promises), and a clean extension to confirm the normal path still formats and commits as before.

A single unparseable file anywhere in an extension ended the whole
upgrade run with a raw Babel stack trace and a dump of the file's
contents, naming neither the file nor anything the author could act on.
The step's partial writes were left on disk, so the uncommitted-changes
guard then refused to let the command be run again.
Each step now collects every file it could not handle, rolls back its own
writes, and aborts with the file names and the reasons. Aborting keeps a
step all-or-nothing: a half-applied transform leaves an extension in a
state that is hard to reason about. Since the rollback leaves the tree
clean and completed steps are already committed, re-running resumes from
the step that stopped.
Also removes a leftover debug console.log that printed whole files on any
parse failure, and fixes a docs link left pointing at localhost.
@imorland
imorland merged commit efdcda9 into 3.xAug 1, 2026
3 checks passed
@imorland
imorland deleted the im/upgrade-resilience branch August 1, 2026 07:15
Sign up for freeto 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.

1 participant

@imorland