Skip to content

feat: Upgrade Backstage to 1.53.1 - #40

Merged
feanil merged 11 commits into
mainfrom
feanil/backstage_1_53_upgrade
Aug 14, 2026
Merged

feat: Upgrade Backstage to 1.53.1#40
feanil merged 11 commits into
mainfrom
feanil/backstage_1_53_upgrade

Conversation

@feanil

@feanil feanil commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Upgrades Backstage from 1.49.2 to 1.53.1, and syncs the parts of the
@backstage/create-app template that we had drifted from.

Template changes between the two versions:
https://backstage.github.io/upgrade-helper/?from=1.49.2&to=1.53.1

Commits

Commit What
chore yarn backstage-cli versions:bump — the upgrade itself
fix Blank line after import in the e2e test, for a newly enforced lint rule
build Pin jest to ~30.2.0; later 30.x regressed on Node 22
build Playwright timeouts, and porturl for the app dev server
build Yarn 4.4.1 → 4.13.0 and the npm minimal age gate
feat Persist user settings in the database
feat Expose the catalog to AI agents over MCP, read-only
docs Record the template config we intentionally do not enable

Breaking changes reviewed

Four packages were flagged as having breaking changes. None required code
changes here:

  • plugin-scaffolder-backend 3.2.0 → 4.0.2 only dropped deprecated old
    backend system types, which we never imported
  • backend-defaults 0.16 → 0.17.6 has none in that range
  • plugin-auth-node 0.7.0 only affects explicit SignInResolverFactoryOptions
    type parameters, which we do not use
  • frontend-plugin-api moved NavContentBlueprint and SignInPageBlueprint
    to @backstage/plugin-app-react, which we had already done in the previous
    upgrade

New behaviour worth a closer look

Yarn age gate. npmMinimalAgeGate: 3d refuses package versions published
less than three days ago, which blunts npm account-takeover attacks that
publish a malicious patch release. @backstage/* is preapproved so Backstage
releases still install on day one. Side effect: dependabot PRs bumping to a
brand new release will fail to install until it is three days old.

MCP is read-only. scaffolder is deliberately left out of
backend.actions.pluginSources, so no software template can be run by an
agent. The catalog source also ships register-entity and the destructive
unregister-entity, so the server filters on the readOnly attribute each
action declares rather than on action ids — a write action added upstream
later will not be silently exposed. Verified against a running backend,
tools/list returns only:

auth.who-am-i
catalog.get-catalog-entity
catalog.get-catalog-model-description
catalog.query-catalog-entities
catalog.validate-entity

Because filters are only configurable on named servers, the endpoint is
/api/mcp-actions/v1/readonly. The unnamed /api/mcp-actions/v1 returns 404.

auth.clientIdMetadataDocuments is enabled so clients can register themselves
and get a token through an interactive approval popup instead of everyone
sharing one static MCP_TOKEN. This currently accepts any client id and any
redirect URI
, which is the upstream default; see the commented patterns in
app-config.yaml if we want to narrow it.

User settings now live in Postgres. Previously the storage API fell back to
browser localStorage, so theme and starred entities did not follow a user
between devices. This needed both halves — the backend plugin alone does
nothing without the frontend module.

Config that is intentionally off

Both app-config.yaml and app-config.production.yaml were compared against
the full 1.53 template, not just the diff between template versions. Sections
we do not use are now copied in commented out with a reason, so a future
template diff shows only real gaps.

The one to look at is auth.providers.guest in the production config. The
template re-enables guest in production; we cannot adopt that as-is, because
packages/backend/src/index.ts still registers the allow-all permission
policy and guest would therefore get anonymous write access. Public
read-only browsing is something we do want for an open source community
portal, and it will land separately alongside a permission policy that
restricts guests to read.

Verification

yarn tsc, yarn test, yarn lint:all, yarn build:all,
yarn install --immutable and yarn build-image all pass.
config:check --strict --deprecated passes for both the development and
production config layers, and the backend was booted locally to confirm all
plugins wire up and the MCP tool list is what it claims to be.

Two pre-existing issues were left alone as out of scope: yarn prettier:check
fails on 24 files (it fails on main too), and the e2e test asserts on
'My Company Catalog' rather than this instance's title.

feanil and others added 9 commits August 14, 2026 09:54
Upgrades Backstage from 1.49.2 to 1.53.1.

Template changes between the two versions:
https://backstage.github.io/upgrade-helper/?from=1.49.2&to=1.53.1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lint rules shipped with @backstage/cli 0.36.4 now flag the missing
blank line after the import in the Playwright e2e test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The upstream template pinned `jest` and `@jest/environment-jsdom-abstract`
to `~30.2.0` because later 30.x releases regressed on Node 22, which is
one of the Node versions this repo supports.

See @backstage/create-app 0.8.3:
https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The frontend now takes longer to become interactive on a cold start, so
upstream raised the assertion and dev-server timeouts and switched the app
web server from a `port` check to a `url` check. Waiting on the URL means
Playwright waits for the dev server to actually serve a response rather
than just for the port to be bound.

See @backstage/create-app 0.9.0:
https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches the create-app template. `npmMinimalAgeGate: 3d` refuses to install
package versions published less than three days ago, which blunts the
npm account-takeover attacks that publish a malicious patch release and
rely on it being pulled in within hours. `@backstage/*` is preapproved so
that Backstage's own releases are still installable on the day they land.

Note this means dependabot PRs that bump a dependency to a brand new
release will fail to install until that release is three days old.

Yarn is resolved through corepack from the `packageManager` field, which
the Dockerfile already enables, so no image changes are needed.

See @backstage/create-app 0.8.4:
https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 1.53 create-app template adds a user-settings plugin on both sides,
which this repo was missing. It is two halves that only work together:

- `@backstage/plugin-user-settings-backend` serves the settings over HTTP
- `@backstage/plugin-app-module-user-settings` overrides the frontend
  `storageApiRef` with `UserSettingsStorage`, which talks to that backend

Without the frontend module the storage API falls back to `WebStorage`,
so things like the chosen theme, starred entities and sidebar pin state
live in browser localStorage and are lost when a user switches browser or
device. With both halves they are stored per-user in Postgres and synced
across open tabs via the signals plugin, which is already enabled here.

The frontend module needs no wiring in App.tsx; it is picked up by the
`app.packages: all` discovery already configured in app-config.yaml.

See @backstage/create-app 0.9.0:
https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the MCP feature set from the 1.53 create-app template so agents can
query the software catalog:

- `@backstage/plugin-mcp-actions-backend` serves the MCP endpoint
- `@backstage/plugin-auth` provides the OIDC consent page at
  /oauth2/authorize/:sessionId that clients hit when registering

The server is exposed at /api/mcp-actions/v1/readonly rather than the
default /api/mcp-actions/v1, because filters are only configurable on
named servers. The unnamed path returns 404.

Nothing reachable over MCP can mutate state. `scaffolder` is left out of
`backend.actions.pluginSources` so no software template can be run, and
the `catalog` source is filtered on the `readOnly` attribute that each
action declares, since that source also ships `register-entity` and the
destructive `unregister-entity`. Filtering on the attribute rather than
on action ids means a write action added upstream later is not silently
exposed. Verified against a running backend, tools/list returns only:

  auth.who-am-i, catalog.get-catalog-entity,
  catalog.get-catalog-model-description, catalog.query-catalog-entities,
  catalog.validate-entity

`auth.clientIdMetadataDocuments` is enabled so clients can register
themselves and get a token through an interactive approval popup instead
of everyone sharing one static MCP_TOKEN. Note this currently accepts any
client id and redirect URI; see the commented patterns in app-config.yaml
to narrow that.

See https://backstage.io/docs/ai/mcp-actions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comparing both config files against the 1.53 create-app template turned up
several sections we simply do not have. Absence is ambiguous: it does not
say whether we decided against something or never noticed it. Copy each one
in commented out with the reason, so a future template diff shows only real
gaps.

app-config.yaml:
- catalog.import, we rely on the defaults, which match the template values
- catalog.rules and catalog.locations, our entities come from the GitHub
  discovery providers rather than a static list, and catalog-extra is picked
  up by the `extraCatalogs` provider's catalogPath
- kubernetes, we have no cluster to surface and the plugins are not installed
- the GitHub Enterprise example, plus a note on why we authenticate with a
  GitHub App rather than the template's personal access token

app-config.production.yaml:
- auth.providers.guest, which the template re-enables in production. We must
  not: backstage.openedx.org is public and sign-in has to go through GitHub.
  This is the one divergence where silently following the template would be
  a security regression, so it is worth stating outright.
- the catalog.locations override, which only exists to replace the example
  entities that we never seed

No effective config change; verified with `config:check --strict` and by
diffing `config:print` output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream vendors the Yarn release at `.yarn/releases/yarn-4.13.0.cjs` and
points `yarnPath` at it, which gets integrity for free because the binary
is in git. We resolve Yarn through corepack instead, a deliberate choice
from 0c50d8f, and `packageManager: "yarn@4.13.0"` carried no hash, so
corepack would install whatever the registry served for that version.
That sat badly next to the `npmMinimalAgeGate` hardening added a few
commits ago, which exists to defend against exactly that.

Corepack verifies the `+sha512` suffix when it downloads, which is the
case that matters: a fresh CI or Docker build. Confirmed by pointing
COREPACK_HOME at an empty directory with a deliberately wrong hash and
watching it refuse with a mismatch error.

Also drop `COPY .yarn ./.yarn` from the Dockerfile. That line is only
meaningful upstream, where there is a vendored release to copy. Nothing
under `.yarn` is tracked here, and .dockerignore excludes the
`install-state.gz` that is the only thing which ever appears there, so it
copied an empty directory. Worse, it only worked at all because the
documented build order runs `yarn install` first and that creates the
directory as a side effect. On a clean checkout the build failed on a
missing path. Verified the image still builds without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@feanil
feanil force-pushed the feanil/backstage_1_53_upgrade branch from 2d6f1ef to 595ca49 Compare August 14, 2026 15:34
@feanil

feanil commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed against https://backstage.github.io/upgrade-helper/?from=1.49.2&to=1.53.1&yarnPlugin=0 and looks good to me.

@feanil
feanil marked this pull request as ready for review August 14, 2026 15:46
feanil and others added 2 commits August 14, 2026 14:29
`versions:bump` only moves version numbers. Everything the create-app
template owns has to be reconciled by hand, and this upgrade turned up two
ways of getting that wrong: diffing the old template against the new one
rather than against ours, which hides sections that were never adopted
here in the first place, and diffing `templates/default-app` when we are
on the new frontend system and want `templates/next-app`. Both made real
additions look like migration noise we had already dealt with.

Also record that type checks alone do not catch broken plugin wiring, that
`yarn test` hangs in watch mode without CI=1, and that a backend package
is usually half of a pair and always needs registering.

While here, correct two things this file had wrong:

- it described app-config.production.yaml as the production config, when
  production loads both files and merges them, so anything in
  app-config.yaml is live in production too
- catalog-extra/ is picked up by the `extraCatalogs` provider's catalogPath,
  not by a `catalog.locations` entry, which does not exist

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The command change with one of the updates.
@feanil
feanil merged commit c668835 into main Aug 14, 2026
2 checks passed
@feanil
feanil deleted the feanil/backstage_1_53_upgrade branch August 14, 2026 18:51
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