Skip to content

feat: add Backstage 1.54.0 migration recipe (#155) - #160

Merged
schultzp2020 merged 20 commits into
mainfrom
feat/v1.54.0/migration-recipe
Nov 3, 2026
Merged

schultzp2020 merged 20 commits into
mainfrom
feat/v1.54.0/migration-recipe

Conversation

@schultzp2020

@schultzp2020 schultzp2020 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

@backstage/v1-54-0-migration-recipe is now the only published Backstage 1.54.0 artifact:

@backstage/v1-54-0-migration-recipe
  scripts/move-connections-backend-imports.ts        (#143)
  scripts/migrate-connections-query.ts               (#144)
  scripts/rename-root-connection-auth.ts             (#145)
  scripts/migrate-extension-config-schema-index.ts   (#146)
  scripts/migrate-extension-config-schema.ts         (#146)
  scripts/rename-openapi-validate-command-*.ts/.mjs  (#147)
  • The five standalone codemod packages are removed; their transform scripts are bundled into the recipe's scripts/ folder, each kept separate so users can still run one individually (see recipe README).
  • The workflow runs each script as its own local js-ast-grep/shell step instead of nesting registry codemod packages. This avoids the CLI 1.18.3 bug where nested workflow steps fail with unbound state.targetDirEncoded in the OpenAPI shell step.
  • All repository tooling is TypeScript in packages/scripts: every former bash script (quality gates, README generation, release automation, the recipe test runner) is now a cleye-based bin in the private @backstage/scripts workspace package, with node:test ports of every regression test. Entry points (root scripts, lint-staged, husky, CI/release workflows) call bins by name — no bash and no relative paths remain in the repo. The package is linted and formatted exactly like the codemods (type-aware oxlint, oxfmt, same strict tsconfig).
  • Testing is fully preserved and convention-based: every step's fixture suite lives in tests/<name>/ matching its scripts/<name>.ts, discovered by the run-recipe-tests bin from @backstage/scripts: jssg fixtures (language inferred from fixture extension), assert-fixtures.ts workflow-level app trees, fixture tsconfig.json type checks, *.test.mjs unit tests. Adding a step never changes a test command; a recipe's test script is just run-recipe-tests. The runner has --list plan mode and its own regression tests; the recipe layout is documented in CONTRIBUTING.md as the pattern for future versions.
  • Default aiFixup=false; the optional AI node is gated by if: params.aiFixup == true and remains untested/unapproved. Minor changeset included. Relates to feat: Backstage 1.54.0 migration - add v1.54.0 migration recipe workflow #155.

Evidence

  • End-to-end apply on a disposable tracked scratch app (package.json + .github/workflows/ci.yml + check.sh, each containing backstage-repo-tools repo schema openapi verify): all 8 steps succeed and all three files are rewritten to validate. The previously failing shell step now binds state.targetDirEncoded because state is set by the package.json step in the same workflow. ai-fixup node correctly skipped (condition not met: params.aiFixup == true).
  • Dry-run on the same target completes with all steps successful.
  • yarn test at the root (all workspaces) passes at 8e9a0c7: the scripts package's 13 node:test regressions (argv batching, paths with spaces, failure propagation, wiring), the recipe's 65 jssg fixtures, 27 workflow-level fixtures, tsc, unit tests and codemod workflow validate. The TS README generator reproduces the committed README byte-for-byte. yarn lint, yarn format and the full yarn check:changed gate (now itself a TS bin, also run by the pre-push hook) pass. Unix-only regression scenarios (sh/gh stubs) are skipped on Windows and run on CI's ubuntu runner.
  • yarn lint, yarn format, and the full yarn check:changed CI gate pass at 8e9a0c7.
  • Scripts are byte-identical to the released v0.2.0 package scripts except the ./command.js import path in the OpenAPI scripts, exercised by the apply above and the unit tests.
  • Optional feat: Backstage 1.54.0 migration - Migrate extension config.schema to Standard Schema #146 AI path deliberately untested; no provider or personal openai-codex used.

Merge notes

The five packages were already published at v0.2.0; removing them here stops further releases of the standalone packages — the recipe becomes the single 1.54.0 release artifact. Published registry entries remain until deprecated separately.

Full Backstage-app QA (running the recipe against a real upgraded app) is still open; the scratch-app apply proves the workflow mechanics and the previously failing state binding, and the restored fixtures cover each step's transform behavior.

@schultzp2020

Copy link
Copy Markdown
Contributor Author

Current blocker (CLI 1.18.3; five registry codemods at v0.2.0): recipe dry-run completes, but it does not execute shell steps. Apply runs steps 1–4; the final nested @backstage/rename-openapi-validate-command (#147) fails at shell interpolation with Variable identifier is not bound to anything by context: "state.targetDirEncoded". Its JSON step sets state.targetDirEncoded from the tracked root package.json. Published #147 succeeds standalone on the same tracked disposable target; a one-step nested registry repro still fails, and an instrumented local child confirms the initializer executes before the failure. This is observed nested state-resolution behavior, not a proven CLI bug; CLI scoping contract versus #147 workflow design remains unresolved.

Repro from codemods/v1.54.0/migration-recipe with a disposable target containing a tracked root package.json: yarn workspace @backstage/v1-54-0-migration-recipe exec codemod workflow run -w workflow.yaml --no-interactive --target <disposable>; compare codemod run @backstage/rename-openapi-validate-command --no-interactive --target <disposable>. No --allow-fs or skipped step. Full apply/repeat remains unverified; keep PR draft and do not merge/publish until resolved and retested. Optional AI path untested.

…n-recipe

# Conflicts:
#	README.md
#	yarn.lock
Run each Backstage 1.54.0 codemod script as a local js-ast-grep or
shell step in the recipe workflow instead of nesting registry codemod
packages, which failed in CLI 1.18.3 with an unbound
state.targetDirEncoded in the nested OpenAPI step.

- Move the five codemods' scripts into the recipe's scripts folder,
  each script kept separate and runnable
- Remove the five standalone packages so the recipe is the only
  published 1.54.0 artifact
- Verify with an end-to-end apply on a scratch app: package.json,
  GitHub Actions and shell scripts are rewritten
Restore the fixture suites from the five removed packages under
tests/<codemod-name>/ and wire them into the recipe's yarn test:

- Connections scripts run as jssg fixture tests (34+6+25 cases)
- Extension config schema fixtures are full app trees run through the
  entire recipe workflow via assert-fixtures.sh (27 fixtures; the diff
  also proves the other steps no-op on them), plus a tsc type check
- OpenAPI scripts keep their JSON/YAML/shell fixture suites and
  node --test unit tests for rewriteCommand and the shell walker

Path references in the moved test scripts are updated for the new
layout; script logic is unchanged.
Drop references to the CLI bug and the removed standalone packages
from the README; the local-scripts recipe layout is the pattern for
future Backstage migration recipes.
Replace the recipe's hardcoded test chain with
scripts/run-recipe-tests.sh, shared by every version recipe. Suites
are discovered by convention so adding a script or fixture suite
needs no test-command change:

- scripts/<name>.ts + tests/<name>/<case>/input.<ext> -> codemod jssg
  test, language inferred from the fixture extension
- tests/<name>/assert-fixtures.sh -> workflow fixture run
- tests/**/tsconfig.json -> tsc
- tests/**/*.test.mjs -> node --test
- codemod workflow validate

--list prints the plan without executing; the runner has its own CI
regression test (scripts/test-run-recipe-tests.sh) following the
existing scripts/test-*.sh convention. OpenAPI fixture dirs renamed to
match their script basenames so the mapping is uniform. Migration
recipe layout documented in CONTRIBUTING.md.
Convert run-recipe-tests from a root bash script referenced by
relative path into TypeScript in packages/recipe-test-runner, a
private workspace package exposing a run-recipe-tests bin. Recipes
declare it as a devDependency and their test script is simply
"run-recipe-tests" - no relative paths and no coupling to workspace
depth. Node 24 runs the .ts bin natively.

The regression test moves with it (test.ts, still run by CI via
scripts/test-recipe-test-runner.sh and root yarn test). Discovery
rules, --list plan mode and output are unchanged.
Replace every bash script with TypeScript bins in the private
@backstage/scripts workspace package (packages/scripts), using cleye
for CLI parsing and node:test for the regression tests:

- check-changed, format-staged, test-staged (quality gates and
  lint-staged hooks)
- generate-readme-codemods, sync-codemod-versions (generation/versioning)
- tag-and-publish, sync-readme-pr (release automation)
- run-recipe-tests (migration recipe test runner; the separate
  recipe-test-runner package is folded in and removed)

Entry points reference bins by name: root package.json scripts,
.lintstagedrc.json, .husky hooks and the CI/release/sync-readme
workflows. runYarn executes Yarn's JS entry directly when available
and falls back to a quoted shell invocation for corepack shims; git
and gh run as native executables with argument arrays. The recipe's
assert-fixtures suite is TypeScript too - no bash remains in the repo.

The package is linted and formatted exactly like the codemods:
type-aware oxlint with the same strict tsconfig, oxfmt over src/,
tests/ ignored like codemod fixtures. Regression tests port every
scripts/test-*.sh scenario (argv batching, paths with spaces, failure
propagation, hook and gh-stub lifecycles); Unix-only scenarios are
skipped on Windows and covered by CI.
@schultzp2020
schultzp2020 marked this pull request as ready for review October 27, 2026 21:01
Two bugs that only run on Linux (the scenarios are skipped on
Windows):

- sync-readme-pr inverted the staged-changes guard: git diff --cached
  --quiet exits 0 when nothing is staged, so the failure condition
  must trigger when it does NOT exit 0
- the pre-commit hook regression was missing the executable
  lint-staged shim in the fixture repo's node_modules/.bin and the
  git init/staging setup lint-staged requires; runSync failures now
  surface the bin's output in the assertion message
… regression

The fixture shim pointed at bin/lint-staged.mjs, but lint-staged's bin
is bin/lint-staged.js; resolve it from the installed manifest so the
shim survives bin renames.
The changed-codemod gate runs the recipe's 27 workflow-level fixtures
and outran the Test job's 15-minute timeout on 2-core runners (the
bash-era runs already brushed against it). Two changes:

- assert-fixtures runs fixtures concurrently (4 workers); fixtures are
  independent workflow runs, and each now has a 5-minute timeout that
  dumps the workflow log instead of hanging on a stuck CLI
- Test job timeout raised to 40 minutes with a comment
The assert-fixtures timeout resolved its promise on 'close', which only
fires once the stdout pipe drains - a killed shell leaves codemod
holding the pipe, so a hung workflow run stalled the whole suite with
no diagnostics (the 40-minute CI cancellation). Resolve on 'exit',
kill the process group on unix, and drop the per-fixture budget to
120s; the timeout now dumps the workflow log so the hang is visible.
The run step executed node -e with an unquoted parenthesized payload,
which is a syntax error under sh on Linux (cmd accepted it, so Windows
runs passed). The failed step then triggered the codemod CLI's
post-failure scheduler hang, stalling every workflow fixture on CI.
Double quotes are valid in both shells; verified with an sh parse check
and a full Windows apply.
The Linux shell-step fix removed the hang that needed the headroom;
fixture runs now finish in seconds.
- tag-and-publish aborted only via the original bash's set -e; the
  TypeScript port parsed git ls-remote output without checking status,
  so a failed remote lookup looked like an empty tag set and re-tagged
  already-released codemods. Abort on lookup failure; add regressions
  for the failure path and the create/push/idempotence success path.
- assert-fixtures created a unix process group only in the shell-Yarn
  branch; the JS-entry branch could ESRCH on timeout. Detach both
  branches and tolerate the exit race in the kill.
- runYarn's cmd.exe fallback expands %VAR% inside double quotes; fail
  loudly on percent-containing arguments rather than silently
  rewriting them (both spawn helpers).
- restore the failing-package hook regression: the pre-commit hook
  must fail when a package test fails, after running both packages.
run-recipe-tests now structurally validates workflow.yaml before any
suite runs: every js-ast-grep js_file and run-step CODEMOD_SCRIPT must
exist locally and registry codemod: steps are rejected. Every future
recipe inherits the check; recipes keep only recipe-specific tests.

The 1.54 recipe's workflow.test.mjs gains the recipe-specific coverage
from the review: exact step ordering, the state couplings (zod index
before its transform, package.json step before the shell step reading
targetDirEncoded) and the quoted node -e payload that keeps the run
step valid under both sh and cmd.
@schultzp2020
schultzp2020 merged commit 245a183 into main Nov 3, 2026
2 checks passed
@schultzp2020
schultzp2020 deleted the feat/v1.54.0/migration-recipe branch November 3, 2026 17:04
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.

1 participant