diff --git a/.changeset/auth-module-exports.md b/.changeset/auth-module-exports.md deleted file mode 100644 index 0b6dc46ac37a14..00000000000000 --- a/.changeset/auth-module-exports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-module-auth': patch ---- - -Export auth helper utilities for use by other CLI modules. Added per-instance config storage with `getInstanceConfig` and `updateInstanceConfig`. diff --git a/.changeset/cli-new-frontend-templates-default.md b/.changeset/cli-new-frontend-templates-default.md new file mode 100644 index 00000000000000..0513b367803147 --- /dev/null +++ b/.changeset/cli-new-frontend-templates-default.md @@ -0,0 +1,16 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: The CLI templates for frontend plugins have been renamed: + +- `new-frontend-plugin` → `frontend-plugin` +- `new-frontend-plugin-module` → `frontend-plugin-module` +- `frontend-plugin` (legacy) → `legacy-frontend-plugin` + +To smooth out this breaking change, the CLI now auto-detects which frontend system your app uses based on the dependencies in `packages/app/package.json`. When using the default templates (no explicit `templates` configuration): + +- Apps using `@backstage/frontend-defaults` will see the new frontend system templates (`frontend-plugin`, `frontend-plugin-module`) +- Apps using `@backstage/app-defaults` will see the legacy template (displayed as `frontend-plugin`) + +This means existing projects that haven't migrated to the new frontend system will continue to create legacy plugins by default, while new projects will get the new frontend system templates. If you have explicit template configuration in your `package.json`, it will be used as-is without any auto-detection. diff --git a/.changeset/cli-node-auth-api.md b/.changeset/cli-node-auth-api.md new file mode 100644 index 00000000000000..b4b943af5ae2de --- /dev/null +++ b/.changeset/cli-node-auth-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend. diff --git a/.changeset/new-frontend-system-default.md b/.changeset/new-frontend-system-default.md new file mode 100644 index 00000000000000..eeef22ce35df94 --- /dev/null +++ b/.changeset/new-frontend-system-default.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +**BREAKING**: The new frontend system is now the default template when creating a new Backstage app. The previous `--next` flag has been replaced with a `--legacy` flag that can be used to create an app using the old frontend system instead. diff --git a/.changeset/promote-translation-refs-stable-minor.md b/.changeset/promote-translation-refs-stable-minor.md new file mode 100644 index 00000000000000..6bf063bde7f06c --- /dev/null +++ b/.changeset/promote-translation-refs-stable-minor.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-react': minor +'@backstage/plugin-catalog': minor +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-search-react': minor +'@backstage/plugin-search': minor +--- + +Promoted the plugin's translation ref to the stable package entry point. It was previously only available through the alpha entry point. diff --git a/.changeset/promote-translation-refs-stable-patch.md b/.changeset/promote-translation-refs-stable-patch.md new file mode 100644 index 00000000000000..6cd963a3683f69 --- /dev/null +++ b/.changeset/promote-translation-refs-stable-patch.md @@ -0,0 +1,16 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-home': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-react': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-notifications': patch +'@backstage/plugin-org': patch +'@backstage/plugin-user-settings': patch +--- + +Promoted the plugin's translation ref to the stable package entry point. It was previously only available through the alpha entry point. diff --git a/.changeset/silver-snails-pull.md b/.changeset/silver-snails-pull.md new file mode 100644 index 00000000000000..99d900db65dbe0 --- /dev/null +++ b/.changeset/silver-snails-pull.md @@ -0,0 +1,56 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': patch +'@backstage/plugin-auth-backend-module-azure-easyauth-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-provider': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-openshift-provider': patch +'@backstage/cli-module-auth': patch +'@backstage/cli-module-new': patch +'@backstage/plugin-auth-backend-module-onelogin-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-github-provider': patch +'@backstage/plugin-auth-backend-module-gitlab-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-provider': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-auth-backend-module-okta-provider': patch +'@backstage/plugin-scaffolder-backend-module-github': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-user-settings-backend': patch +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +'@backstage/backend-plugin-api': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-mcp-actions-backend': patch +'@backstage/filter-predicates': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-defaults': patch +'@backstage/frontend-app-api': patch +'@backstage/plugin-permission-common': patch +'@backstage/core-compat-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-permission-node': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/catalog-model': patch +'@backstage/plugin-search-backend': patch +'@backstage/core-app-api': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-auth-backend': patch +'@backstage/repo-tools': patch +'@backstage/plugin-scaffolder': patch +'@backstage/cli-node': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-home': patch +'@backstage/plugin-app': patch +--- + +Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports. diff --git a/.changeset/solid-bats-flash.md b/.changeset/solid-bats-flash.md new file mode 100644 index 00000000000000..0a4918fcc2d3a6 --- /dev/null +++ b/.changeset/solid-bats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': patch +--- + +The Slack notification processor now uses the `MetricsService` to create metrics, providing plugin-scoped attribution. `{message}` unit has also been added. diff --git a/.changeset/tangy-toys-carry.md b/.changeset/tangy-toys-carry.md new file mode 100644 index 00000000000000..77d8243c7c8e1f --- /dev/null +++ b/.changeset/tangy-toys-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-dynamic-feature-loader': patch +--- + +Update the README of the `frontend-dynamic-feature-loader` package to mention the new `backstage-cli package bundle` command. diff --git a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md index 4852443ecf8b57..4a7e0ad233f4bd 100644 --- a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md +++ b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md @@ -1,4 +1,7 @@ -# Running the backend behind a Corporate Proxy +# Legacy: Running the backend behind a Corporate Proxy + +> [!NOTE] +> On Node.js 22.21.0 or later, you can use Node.js's built-in proxy support instead of the workarounds described here. See the [recommended proxy setup guide](../../../docs/tutorials/corporate-proxy.md) for details. This article helps you get your backend installation up and running making calls through corporate proxies. diff --git a/docs/auth/index--old.md b/docs/auth/index--old.md new file mode 100644 index 00000000000000..6ba50f7eeabbe0 --- /dev/null +++ b/docs/auth/index--old.md @@ -0,0 +1,549 @@ +--- +id: index--old +title: Authentication in Backstage +description: Introduction to authentication in Backstage +--- + +:::info +This documentation is written for the old frontend system. If you are on the [new frontend system](../frontend-system/index.md) you may want to read [its own article](./index.md) instead. +::: + +The authentication system in Backstage serves two distinct purposes: sign-in and +identification of users, as well as delegating access to third-party resources. It is possible to +configure Backstage to have any number of authentication providers, but only +one of these will typically be used for sign-in, with the rest being used to provide +access to external resources. + +:::note Note + +Identity management and the Sign-In page in Backstage will block external access by default, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). + +::: + +## Built-in Authentication Providers + +Backstage comes with many common authentication providers in the core library: + +- [Auth0](auth0/provider.md) +- [Atlassian](atlassian/provider.md) +- [Azure](microsoft/provider.md) +- [Azure Easy Auth](microsoft/azure-easyauth.md) +- [Bitbucket](bitbucket/provider.md) +- [Bitbucket Server](bitbucketServer/provider.md) +- [Cloudflare Access](cloudflare/provider.md) +- [GitHub](github/provider.md) +- [GitLab](gitlab/provider.md) +- [Google](google/provider.md) +- [Google IAP](google/gcp-iap-auth.md) +- [Okta](okta/provider.md) +- [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) +- [OneLogin](onelogin/provider.md) +- [OpenShift](openshift/provider.md) +- [VMware Cloud](vmware-cloud/provider.md) + +These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a +Backstage app in a similar way. + +## Configuring Authentication Providers + +Each built-in provider has a configuration block under the `auth` section of +`app-config.yaml`. For example, the GitHub provider: + +```yaml +auth: + environment: development + providers: + github: + development: + clientId: ${AUTH_GITHUB_CLIENT_ID} + clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} +``` + +See the documentation for a particular provider to see what configuration is +needed. + +The `providers` key may have several authentication providers if multiple +authentication methods are supported. Each provider may also have configuration +for different authentication environments (development, production, etc). This +allows a single auth backend to serve multiple environments, such as running a +local frontend against a deployed backend. The provider configuration matching +the local `auth.environment` setting will be selected. + +## Sign-In Configuration + +Using an authentication provider for sign-in is something you need to configure +both in the frontend app as well as the `auth` backend plugin. For information +on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). +The rest of this section will focus on how to configure sign-in for the frontend app. + +Sign-in is configured by providing a custom `SignInPage` app component. It will be +rendered before any other routes in the app and is responsible for providing the +identity of the current user. The `SignInPage` can render any number of pages and +components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` +callback prop, at which point the rest of the app is rendered. + +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, +which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. + +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, +and can be adapted to any of the built-in providers: + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-start */ +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPage } from '@backstage/core-components'; +/* highlight-add-end */ + +const app = createApp({ + /* highlight-add-start */ + components: { + SignInPage: props => ( + + ), + }, + /* highlight-add-end */ + // .. +}); +``` + +:::note Note + +You can configure sign-in to use a redirect flow with no pop-up by adding +`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` + +::: + +### Using Multiple Providers + +You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: + +```tsx title="packages/app/src/App.tsx" +const app = createApp({ + /* highlight-add-start */ + components: { + SignInPage: props => ( + + ), + }, + /* highlight-add-end */ + // .. +}); +``` + +### Conditionally Render Sign In Provider + +In the above example, you have both Guest and GitHub sign-in options; this is helpful for non-production, but in Production you will most likely not want to offer Guest access. You can easily use information from your config to help conditionally render the provider: + +```tsx title="packages/app/src/App.tsx" +import { + configApiRef, + githubAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; + +const app = createApp({ + components: { + SignInPage: props => { + const configApi = useApi(configApiRef); + if (configApi.getString('auth.environment') === 'development') { + return ( + + ); + } + return ( + + ); + }, + }, + // .. +}); +``` + +## Sign-In with Proxy Providers + +Some auth providers are so-called "proxy" providers, meaning they're meant to be used +behind an authentication proxy. Examples of these are +[Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), +[Azure EasyAuth](./microsoft/azure-easyauth.md), +[Cloudflare Access](./cloudflare/provider.md), +[Google Identity-Aware Proxy](./google/gcp-iap-auth.md) +and [OAuth2 Proxy](./oauth2-proxy/provider.md). + +When using a proxy provider, you'll end up wanting to use a different sign-in page, as +there is no need for further user interaction once you've signed in towards the proxy. +All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers +to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only +thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: + +```tsx title="packages/app/src/App.tsx" +import { ProxiedSignInPage } from '@backstage/core-components'; + +const app = createApp({ + components: { + SignInPage: props => , + }, + // .. +}); +``` + +If the provider in auth backend expects additional headers such as `x-provider-token`, there is now a way to configure that in `ProxiedSignInPage` using the optional `headers` prop. + +Example: + +```tsx + +``` + +Headers can also be returned in an async manner: + +```tsx + { + const someValue = await someFn(); + return { 'x-some-key': someValue }; + }} + /* highlight-end */ +/> +``` + +A downside of this method is that it can be cumbersome to set up for local development. +As a workaround for this, it's possible to dynamically select the sign-in page based on +what environment the app is running in and then use a different sign-in method for local +development, if one is needed at all. Depending on the exact setup, one might choose to +select the sign-in method based on the `process.env.NODE_ENV` environment variable, +by checking the `hostname` of the current location, or by accessing the configuration API +to read a configuration value. For example: + +```tsx title="packages/app/src/App.tsx" +const app = createApp({ + components: { + SignInPage: props => { + const configApi = useApi(configApiRef); + if (configApi.getString('auth.environment') === 'development') { + return ( + + ); + } + return ; + }, + }, + // .. +}); +``` + +When using multiple auth providers like this, it's important that you configure the different +sign-in resolvers so that they resolve to the same identity regardless of the method used. + +## Scaffolder Configuration (Software Templates) + +If you want to use the authentication capabilities of the [Repository Picker](../features/software-templates/writing-templates.md#the-repository-picker) inside your software templates, you will need to configure the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html) alongside your authentication provider. It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed. + +To set it up, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The example below sets up the `ScmAuthApi` for an already configured GitLab authentication provider: + +```ts title="packages/app/src/apis.ts" +createApiFactory({ + api: scmAuthApiRef, + deps: { + gitlabAuthApi: gitlabAuthApiRef, + }, + factory: ({ gitlabAuthApi }) => ScmAuth.forGitlab(gitlabAuthApi), +}); +``` + +In case you are using a custom authentication providers, you might need to add a [custom `ScmAuthApi` implementation](./index.md#custom-scmauthapi-implementation). + +## For Plugin Developers + +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers +to use, both to access the user identity as well as third-party resources. + +### Identity for Plugin Developers + +For plugin developers, there is one main touchpoint for accessing the user identity: the +`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. + +The `IdentityApi` gives access to the signed-in user's identity in the frontend. +It provides access to the user's entity reference, lightweight profile information, and +a Backstage token that identifies the user when making authenticated calls within Backstage. + +When making calls to backend plugins, we recommend that the `FetchApi` is used, which +is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will +automatically include a Backstage token in the request, meaning there is no need +to interact directly with the `IdentityApi`. + +### Accessing Third Party Resources + +A common pattern for talking to third-party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls, we keep the coupling between the frontend and +backend low and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to, for example, a session-based system +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third-party services is +through [Utility APIs](../api/utility-apis.md) for each service provider. These +are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a +full list of providers, see the +[@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. + +## Custom Authentication Provider + +There are generic authentication providers for OAuth2 and SAML. These can reduce +the amount of code needed to implement a custom authentication provider that +adheres to these standards. + +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has +a wide library of authentication strategies for different providers. See +[Add authentication provider](add-auth-provider.md) for details on adding a new +Passport-supported authentication method. + +## Custom ScmAuthApi Implementation + +The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`: + +```ts +ScmAuth.createDefaultApiFactory(); +``` + +If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. + +The first step is to remove the code that creates the default providers. + +```ts title="packages/app/src/apis.ts" +import { + ScmIntegrationsApi, + scmIntegrationsApiRef, + /* highlight-add-next-line */ + ScmAuth, +} from '@backstage/integration-react'; + +export const apis: AnyApiFactory[] = [ + /* highlight-add-next-line */ + ScmAuth.createDefaultApiFactory(), + // ... +]; +``` + +Then replace it with something like this, which will create an `ApiFactory` with only a GitHub provider. + +```ts title="packages/app/src/apis.ts" +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scmAuthApiRef, + deps: { + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ), + }); +``` + +If you use any custom authentication integrations, a new provider can be added to the `ApiFactory`. + +The first step is to create a new authentication ref, which follows the naming convention of `xxxAuthApiRef`. The example below is for a new GitHub enterprise integration which can be defined either inside the app itself if it's only used for this purpose or inside a common internal package for APIs, such as `@internal/apis`: + +```ts +const gheAuthApiRef: ApiRef = + createApiRef({ + id: 'internal.auth.ghe', + }); +``` + +This new API ref will only work if you define an API factory for it. For example: + +```ts +createApiFactory({ + api: gheAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider: { id: 'ghe', title: 'GitHub Enterprise', icon: () => null }, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), +}); +``` + +The new API ref is then used to add a new provider to the ApiFactory: + +```ts +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +``` + +Finally, you also need to add and configure another provider to the `auth-backend` using the provider ID, which in this example is `ghe`: + +```ts +import { providers } from '@backstage/plugin-auth-backend'; + +// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts +providerFactories: { + ghe: providers.github.create(), +}, +``` + +You can leverage the `authProvidersExtensionPoint` for this: + +```ts +// your-auth-plugin-module.ts +export const gheAuth = createBackendModule({ + // This ID must be exactly "auth" because that's the plugin it targets + pluginId: 'auth', + // This ID must be unique, but can be anything + moduleId: 'ghe-auth-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + logger: coreServices.logger, + }, + async init({ providers, logger }) { + providers.registerProvider({ + // This ID must match the actual provider config, e.g. addressing + // auth.providers.ghe means that this must be "ghe". + providerId: 'ghe', + factory: createOAuthProviderFactory({ + authenticator: githubAuthenticator, + signInResolverFactories: { + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); + +// backend index.ts +backend.add(gheAuth); +``` + +## Configuring token issuers + +By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued +Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. + +Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in +scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch +new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config +file: + +```yaml +auth: + keyStore: + provider: 'static' + static: + keys: + # Must be declared at least once and the first one will be used for signing + - keyId: 'primary' + publicKeyFile: /path/to/public.key + privateKeyFile: /path/to/private.key + algorithm: # Optional, algorithm used to generate the keys, defaults to ES256 + # More keys can be added so with future key rotations caches already know about it + - keyId: ... +``` + +The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. +You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following +steps: + +Generate a private key using the ES256 algorithm + +```sh +openssl ecparam -name prime256v1 -genkey -out private.ec.key +``` + +Convert it to PKCS#8 format + +```sh +openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key +``` + +Extract the public key + +```sh +openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key +``` diff --git a/docs/auth/index.md b/docs/auth/index.md index 389f824be3d39a..8a2ce1db90653f 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -4,15 +4,15 @@ title: Authentication in Backstage description: Introduction to authentication in Backstage --- -The authentication system in Backstage serves two distinct purposes: sign-in and -identification of users, as well as delegating access to third-party resources. It is possible to -configure Backstage to have any number of authentication providers, but only -one of these will typically be used for sign-in, with the rest being used to provide -access to external resources. +:::info +This documentation is written for [the new frontend system](../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index--old.md) instead. +::: + +The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only one of these will typically be used for sign-in, with the rest being used to provide access to external resources. :::note Note -Identity management and the Sign-In page in Backstage will block external access by default, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). +Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). ::: @@ -37,13 +37,11 @@ Backstage comes with many common authentication providers in the core library: - [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) -These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a -Backstage app in a similar way. +These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a Backstage app in a similar way. ## Configuring Authentication Providers -Each built-in provider has a configuration block under the `auth` section of -`app-config.yaml`. For example, the GitHub provider: +Each built-in provider has a configuration block under the `auth` section of `app-config.yaml`. For example, the GitHub provider: ```yaml auth: @@ -55,66 +53,66 @@ auth: clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} ``` -See the documentation for a particular provider to see what configuration is -needed. +See the documentation for a particular provider to see what configuration is needed. -The `providers` key may have several authentication providers if multiple -authentication methods are supported. Each provider may also have configuration -for different authentication environments (development, production, etc). This -allows a single auth backend to serve multiple environments, such as running a -local frontend against a deployed backend. The provider configuration matching -the local `auth.environment` setting will be selected. +The `providers` key may have several authentication providers if multiple authentication methods are supported. Each provider may also have configuration for different authentication environments (development, production, etc). This allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. ## Sign-In Configuration -Using an authentication provider for sign-in is something you need to configure -both in the frontend app as well as the `auth` backend plugin. For information -on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). -The rest of this section will focus on how to configure sign-in for the frontend app. +Using an authentication provider for sign-in is something you need to configure both in the frontend app as well as the `auth` backend plugin. For information on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). The rest of this section will focus on how to configure sign-in for the frontend app. -Sign-in is configured by providing a custom `SignInPage` app component. It will be -rendered before any other routes in the app and is responsible for providing the -identity of the current user. The `SignInPage` can render any number of pages and -components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` -callback prop, at which point the rest of the app is rendered. +Sign-in is configured by providing a custom `SignInPage` app component. It will be rendered before any other routes in the app and is responsible for providing the identity of the current user. The `SignInPage` can render any number of pages and components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` callback prop, at which point the rest of the app is rendered. -If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, -which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. -The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, -and can be adapted to any of the built-in providers: +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: ```tsx title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import { navModule } from './modules/nav'; + /* highlight-add-start */ import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { SignInPage } from '@backstage/core-components'; -/* highlight-add-end */ +import { createFrontendModule } from '@backstage/frontend-plugin-api'; -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), }, - /* highlight-add-end */ - // .. +}); +/* highlight-add-end */ + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` :::note Note -You can configure sign-in to use a redirect flow with no pop-up by adding -`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` +You can configure sign-in to use a redirect flow with no pop-up by adding `enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` ::: @@ -123,26 +121,44 @@ You can configure sign-in to use a redirect flow with no pop-up by adding You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: ```tsx title="packages/app/src/App.tsx" -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), }, - /* highlight-add-end */ - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` @@ -156,10 +172,14 @@ import { githubAuthApiRef, useApi, } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; -const app = createApp({ - components: { - SignInPage: props => { +const signInPage = SignInPageBlueprint.make({ + params: { + /* highlight-add-start */ + loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -177,47 +197,64 @@ const app = createApp({ /> ); } + return ( ); }, + /* highlight-add-end */ }, - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` ## Sign-In with Proxy Providers -Some auth providers are so-called "proxy" providers, meaning they're meant to be used -behind an authentication proxy. Examples of these are -[Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), -[Azure EasyAuth](./microsoft/azure-easyauth.md), -[Cloudflare Access](./cloudflare/provider.md), -[Google Identity-Aware Proxy](./google/gcp-iap-auth.md) -and [OAuth2 Proxy](./oauth2-proxy/provider.md). +Some auth providers are so-called "proxy" providers, meaning they're meant to be used behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), [Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). -When using a proxy provider, you'll end up wanting to use a different sign-in page, as -there is no need for further user interaction once you've signed in towards the proxy. -All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers -to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only -thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: +When using a proxy provider, you'll end up wanting to use a different sign-in page, as there is no need for further user interaction once you've signed in towards the proxy. All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: ```tsx title="packages/app/src/App.tsx" +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; import { ProxiedSignInPage } from '@backstage/core-components'; -const app = createApp({ - components: { - SignInPage: props => , +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + , }, - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], }); ``` @@ -249,18 +286,20 @@ Headers can also be returned in an async manner: /> ``` -A downside of this method is that it can be cumbersome to set up for local development. -As a workaround for this, it's possible to dynamically select the sign-in page based on -what environment the app is running in and then use a different sign-in method for local -development, if one is needed at all. Depending on the exact setup, one might choose to -select the sign-in method based on the `process.env.NODE_ENV` environment variable, -by checking the `hostname` of the current location, or by accessing the configuration API -to read a configuration value. For example: +A downside of this method is that it can be cumbersome to set up for local development. As a workaround for this, it's possible to dynamically select the sign-in page based on what environment the app is running in and then use a different sign-in method for local development, if one is needed at all. Depending on the exact setup, one might choose to select the sign-in method based on the `process.env.NODE_ENV` environment variable, by checking the `hostname` of the current location, or by accessing the configuration API to read a configuration value. For example: ```tsx title="packages/app/src/App.tsx" -const app = createApp({ - components: { - SignInPage: props => { +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { ProxiedSignInPage, SignInPage } from '@backstage/core-components'; +import { + createFrontendModule, + googleAuthApiRef, +} from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -275,93 +314,55 @@ const app = createApp({ /> ); } + return ; }, }, - // .. }); -``` - -When using multiple auth providers like this, it's important that you configure the different -sign-in resolvers so that they resolve to the same identity regardless of the method used. - -## Scaffolder Configuration (Software Templates) - -If you want to use the authentication capabilities of the [Repository Picker](../features/software-templates/writing-templates.md#the-repository-picker) inside your software templates, you will need to configure the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html) alongside your authentication provider. It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed. -To set it up, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The example below sets up the `ScmAuthApi` for an already configured GitLab authentication provider: - -```ts title="packages/app/src/apis.ts" -createApiFactory({ - api: scmAuthApiRef, - deps: { - gitlabAuthApi: gitlabAuthApiRef, - }, - factory: ({ gitlabAuthApi }) => ScmAuth.forGitlab(gitlabAuthApi), +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], }); ``` -In case you are using a custom authentication providers, you might need to add a [custom `ScmAuthApi` implementation](./index.md#custom-scmauthapi-implementation). +When using multiple auth providers like this, it's important that you configure the different sign-in resolvers so that they resolve to the same identity regardless of the method used. ## For Plugin Developers -The Backstage frontend core APIs provide a set of Utility APIs for plugin developers -to use, both to access the user identity as well as third-party resources. +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers to use, both to access the user identity as well as third-party resources. ### Identity for Plugin Developers -For plugin developers, there is one main touchpoint for accessing the user identity: the -`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. +For plugin developers, there is one main touchpoint for accessing the user identity: the `IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. -The `IdentityApi` gives access to the signed-in user's identity in the frontend. -It provides access to the user's entity reference, lightweight profile information, and -a Backstage token that identifies the user when making authenticated calls within Backstage. +The `IdentityApi` gives access to the signed-in user's identity in the frontend. It provides access to the user's entity reference, lightweight profile information, and a Backstage token that identifies the user when making authenticated calls within Backstage. -When making calls to backend plugins, we recommend that the `FetchApi` is used, which -is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will -automatically include a Backstage token in the request, meaning there is no need -to interact directly with the `IdentityApi`. +When making calls to backend plugins, we recommend that the `FetchApi` is used, which is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will automatically include a Backstage token in the request, meaning there is no need to interact directly with the `IdentityApi`. ### Accessing Third Party Resources -A common pattern for talking to third-party services in Backstage is -user-to-server requests, where short-lived OAuth Access Tokens are requested by -plugins to authenticate calls to external services. These calls can be made -either directly to the services or through a backend plugin or service. - -By relying on user-to-server calls, we keep the coupling between the frontend and -backend low and provide a much lower barrier for plugins to make use of third -party services. This is in comparison to, for example, a session-based system -where access tokens are stored server-side. Such a solution would require a much -deeper coupling between the auth backend plugin, its session storage, and other -backend plugins or separate services. A goal of Backstage is to make it as easy -as possible to create new plugins, and an auth solution based on user-to-server -OAuth helps in that regard. - -The method with which frontend plugins request access to third-party services is -through [Utility APIs](../api/utility-apis.md) for each service provider. These -are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a -full list of providers, see the -[@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. +A common pattern for talking to third-party services in Backstage is user-to-server requests, where short-lived OAuth Access Tokens are requested by plugins to authenticate calls to external services. These calls can be made either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls, we keep the coupling between the frontend and backend low and provide a much lower barrier for plugins to make use of third party services. This is in comparison to, for example, a session-based system where access tokens are stored server-side. Such a solution would require a much deeper coupling between the auth backend plugin, its session storage, and other backend plugins or separate services. A goal of Backstage is to make it as easy as possible to create new plugins, and an auth solution based on user-to-server OAuth helps in that regard. + +The method with which frontend plugins request access to third-party services is through [Utility APIs](../api/utility-apis.md) for each service provider. These are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a full list of providers, see the [@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. ## Custom Authentication Provider -There are generic authentication providers for OAuth2 and SAML. These can reduce -the amount of code needed to implement a custom authentication provider that -adheres to these standards. +There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that adheres to these standards. -Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has -a wide library of authentication strategies for different providers. See -[Add authentication provider](add-auth-provider.md) for details on adding a new -Passport-supported authentication method. +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See [Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. ## Custom ScmAuthApi Implementation -The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`: - -```ts -ScmAuth.createDefaultApiFactory(); -``` +The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` (Azure DevOps), `bitbucketServer` and `bitbucketCloud` and is created and registered automatically for you by the New Frontend System. If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. @@ -461,7 +462,7 @@ providerFactories: { }, ``` -You can leverage the `authProvidersExtensionPoint` for this: +In the new backend system you can leverage the `authProvidersExtensionPoint` for this: ```ts // your-auth-plugin-module.ts @@ -499,13 +500,9 @@ backend.add(gheAuth); ## Configuring token issuers -By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued -Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. +By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. -Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in -scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch -new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config -file: +Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config file: ```yaml auth: @@ -522,9 +519,7 @@ auth: - keyId: ... ``` -The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. -You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following -steps: +The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following steps: Generate a private key using the ES256 algorithm diff --git a/docs/deployment/index.md b/docs/deployment/index.md index 76c8c129326c2f..a5f3c9ac7a631c 100644 --- a/docs/deployment/index.md +++ b/docs/deployment/index.md @@ -32,6 +32,4 @@ This method is covered in [Building a Docker image](docker.md) and There are many ways to deploy Backstage! You can find more examples in the community contributed guides found [here](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/). -If you need to run Backstage behind a corporate proxy, this -[contributed guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md) -may help. +If you need to run Backstage behind a corporate proxy, see the [corporate proxy guide](../tutorials/corporate-proxy.md). diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 378c15e18ff3ae..d74123704e54d6 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -59,57 +59,3 @@ backend.start(); A list of all registered actions can be found under `/create/actions`. For local development you should be able to reach them at `http://localhost:3000/create/actions`. - -## Migrating from `fetch:cookiecutter` to `fetch:template` - -The `fetch:template` action is a new action with a similar API to -`fetch:cookiecutter` but no dependency on `cookiecutter`. There are two options -for migrating templates that use `fetch:cookiecutter` to use `fetch:template`: - -### Using `cookiecutterCompat` mode - -The new `fetch:template` action has a `cookiecutterCompat` flag which should -allow most templates built for `fetch:cookiecutter` to work without any changes. - -1. Update action name in `template.yaml`. The name should be changed from - `fetch:cookiecutter` to `fetch:template`. -2. Set `cookiecutterCompat` to `true` in the `fetch:template` step input in - `template.yaml`. - -```yaml title="template.yaml" -steps: - - id: fetchBase - name: Fetch Base - # highlight-remove-next-line - action: fetch:cookiecutter - # highlight-add-next-line - action: fetch:template - input: - url: ./skeleton - # highlight-add-next-line - cookiecutterCompat: true - values: -``` - -### Manual migration - -If you prefer, you can manually migrate your templates to avoid the need for -enabling cookiecutter compatibility mode, which will result in slightly less -verbose template variables expressions. - -1. Update action name in `template.yaml`. The name should be changed from - `fetch:cookiecutter` to `fetch:template`. -2. Update variable syntax in file names and content. `fetch:cookiecutter` - expects variables to be enclosed in `{{` `}}` and prefixed with - `cookiecutter.`, while `fetch:template` expects variables to be enclosed in - `${{` `}}` and prefixed with `values.`. For example, a reference to variable - `myInputVariable` would need to be migrated from - `{{ cookiecutter.myInputVariable }}` to `${{ values.myInputVariable }}`. -3. Replace uses of `jsonify` with `dump`. The - [`jsonify` filter](https://cookiecutter.readthedocs.io/en/latest/advanced/template_extensions.html#jsonify-extension) - is built in to `cookiecutter`, and is not available by default when using - `fetch:template`. The - [`dump` filter](https://mozilla.github.io/nunjucks/templating.html#dump) is - the equivalent filter in nunjucks, so an expression like - `{{ cookiecutter.myAwesomeList | jsonify }}` should be migrated to - `${{ values.myAwesomeList | dump }}`. diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index bbf35557cec3ea..c266fa1217118a 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -107,29 +107,6 @@ Default secrets are resolved from environment variables and accessible via `${{ **Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend. -## Disabling Docker in Docker situation (Optional) - -Software templates use the `fetch:template` action by default, which requires no -external dependencies and offers a -[Cookiecutter-compatible mode](https://backstage.io/docs/features/software-templates/builtin-actions#using-cookiecuttercompat-mode). -There is also a `fetch:cookiecutter` action, which uses -[Cookiecutter](https://github.com/cookiecutter/cookiecutter) directly for -templating. By default, the `fetch:cookiecutter` action will use the -[scaffolder-backend/Cookiecutter](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile) -docker image. - -If you are running Backstage from a Docker container and you want to avoid -calling a container inside a container, you can set up Cookiecutter in your own -image, this will use the local installation instead. - -You can do so by including the following lines in the last step of your -`Dockerfile`: - -```Dockerfile -RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install cookiecutter -``` - ## Customizing the ScaffolderPage with Grouping and Filtering Once you have more than a few software templates you may want to customize your diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 58da85c6eb0275..1b36fcb2dd9064 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -14,25 +14,12 @@ locations like GitHub or GitLab. When creating custom scaffolder actions, **use camelCase for action IDs** instead of kebab-case. Action IDs with dashes (like `fetch-component-id`) will cause template expressions like `${{ steps.fetch-component-id.output.componentId }}` to return `NaN` because the dashes are evaluated as subtraction operators in JavaScript expressions. -:::note - -See the [Writing Custom Actions guide](./writing-custom-actions.md#naming-conventions) and [Template Migration guide](./migrating-from-v1beta2-to-v1beta3.md#watch-out-for-dash-case) for more details. +See the [Writing Custom Actions guide](./writing-custom-actions.md#naming-conventions) for more details. ::: ## Prerequisites -:::note Note - -If you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to -use the templates feature. -One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage: -`export NODE_OPTIONS="${NODE_OPTIONS:-} --no-node-snapshot"` - -It's important to append to the existing `NODE_OPTIONS` value, if it's already set, rather than overwriting it, since some NodeJS Debugging tools may rely on this environment variable to work properly. - -::: - These docs assume you have already gone over the [Backstage Getting Started](../../getting-started) section and you are able to run Backstage locally or it has been deployed somewhere. ## Getting Started diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md deleted file mode 100644 index 91fc34f97840f5..00000000000000 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -id: migrating-from-v1beta2-to-v1beta3 -title: Migrating to v1beta3 templates -description: How to migrate your existing templates to beta3 syntax ---- - -# What's new? - -Well then, here we are! 🚀 - -Backstage has had many forms of templating languages throughout different -plugins and different systems. We've had `cookiecutter` syntax in templates, and -we also had `handlebars` templating in the `kind: Template`. Then we wanted to -remove the additional dependency on `cookiecutter` for Software Templates out of -the box, so we introduced `nunjucks` as an alternative in `fetch:template` -action which is based on the `jinja2` syntax so they're pretty similar. In an -effort to reduce confusion and unify on to one templating language, we're -officially deprecating support for `handlebars` templating in the -`kind: Template` entities with `apiVersion` `scaffolder.backstage.io/v1beta3` -and moving to using `nunjucks` instead. - -This provides us a lot of built in `filters` (`handlebars` helpers), that as -Template authors will give you much more flexibility out of the box, and also -open up sharing of filters in the Entity and the actual `skeleton` too, and -removing the slight differences between the two languages. - -We've also removed a lot of the built in helpers that we shipped with -`handlebars`, as they're now supported as first class citizens by either -`nunjucks` or the new `scaffolder` when using `scaffolder.backstage.io/v1beta3` -`apiVersion` - -The migration path is pretty simple, and we've removed some of the pain points -from writing the `handlebars` templates too. Let's go through what's new and how -to upgrade. - -## Add the Processor to the `plugin-catalog-backend` - -An important change is to add the required processor to your `packages/backend/src/plugins/catalog.ts` - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addProcessor(new ScaffolderEntitiesProcessor()); - const { processingEngine, router } = await builder.build(); - - // .. -} -``` - -## `backstage.io/v1beta2` -> `scaffolder.backstage.io/v1beta3` - -The most important change is that you'll need to switch over the `apiVersion` in -your templates to the new one. - -```yaml - kind: Template - # highlight-remove-next-line - apiVersion: backstage.io/v1beta2 - # highlight-add-next-line - apiVersion: scaffolder.backstage.io/v1beta3 -``` - -## `${{ }}` instead of `"{{ }}"` - -One really big readability issue and cause for confusion was the fact that with -`handlebars` and `yaml` you always had to wrap your templating strings in quotes -in `yaml` so that it didn't try to parse it as a `json` object and fail. This -was pretty annoying, as it also meant that all things look like strings. Now -that's no longer the case, you can now remove the `""` and take advantage of -writing nice `yaml` files that just work. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - description: 'This is {{ parameters.name }}' - # highlight-add-next-line - description: This is ${{ parameters.name }} - # highlight-remove-next-line - repoUrl: '{{ parameters.repoUrl }}' - # highlight-add-next-line - repoUrl: ${{ parameters.repoUrl }} -``` - -## No more `eq` or `not` helpers - -These helpers are no longer needed with the more expressive `api` that -`nunjucks` provides. You can simply use the built-in `nunjucks` and `jinja2` -style operators. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - if: '{{ eq parameters.value "backstage" }}' - # highlight-add-next-line - if: ${{ parameters.value === "backstage" }} -``` - -And then for the `not` - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - if: '{{ not parameters.value "backstage" }}' - # highlight-add-next-line - if: ${{ parameters.value !== "backstage" }} -``` - -Much better right? ✨ - -## No more `json` helper - -This helper is no longer needed, as we've added support for complex values and -supporting the additional primitive values now rather than everything being a -`string`. This means that now that you can pass around `parameters` and it -should all work as expected and keep the type that has been declared in the -input schema. - -```yaml -spec: - parameters: - test: - type: number - name: Test Number - address: - type: object - required: - - line1 - properties: - line1: - type: string - name: Line 1 - line2: - type: string - name: Line 2 - - steps: - - id: test step - action: run:something - input: - # highlight-remove-next-line - address: '{{ json parameters.address }}' - # highlight-add-next-line - address: ${{ parameters.address }} - # highlight-remove-next-line - test: '{{ parameters.test }}' - # highlight-add-next-line - test: ${{ parameters.test }} # this will now make sure that the type of test is a number 🙏 -``` - -## `parseRepoUrl` is now a `filter` - -All calls to `parseRepoUrl` are now a `jinja2` `filter`, which means you'll need -to update the syntax. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - repoUrl: '{{ parseRepoUrl parameters.repoUrl }}' - # highlight-add-next-line - repoUrl: ${{ parameters.repoUrl | parseRepoUrl }} -``` - -Now we have complex value support here too, expect that this `filter` will go -away in future versions and the `RepoUrlPicker` will return an object so -`parameters.repoUrl` will already be a -`{ host: string; owner: string; repo: string }` 🚀 - -## Links should be used instead of named outputs - -Previously, it was possible to provide links to the frontend using the named output `entityRef` and `remoteUrl`. -These should be moved to `links` under the `output` object instead. - -```yaml -output: - # highlight-remove-start - remoteUrl: {{ steps['publish'].output.remoteUrl }} - entityRef: {{ steps['register'].output.entityRef }} - # highlight-remove-end - # highlight-add-start - links: - - title: Repository - url: ${{ steps['publish'].output.remoteUrl }} - - title: Open in catalog - icon: catalog - entityRef: ${{ steps['register'].output.entityRef }} - # highlight-add-end -``` - -## Watch out for `dash-case` - -The nunjucks compiler can run into issues if the `id` fields in your template steps use dash characters, since these IDs translate directly to JavaScript object properties when accessed as output. One possible migration path is to use `camelCase` for your action IDs. - -```yaml - steps: - # highlight-remove-start - id: my-custom-action - ... - - id: publish-pull-request - input: - repoUrl: {{ steps.my-custom-action.output.repoUrl }} # Will not recognize 'my-custom-action' as a JS property since it contains dashes! - # highlight-remove-end - - steps: - # highlight-add-start - id: myCustomAction - ... - - id: publishPullRequest - input: - repoUrl: ${{ steps.myCustomAction.output.repoUrl }} - # highlight-add-end -``` - -Alternatively, it's possible to keep the `dash-case` syntax and use brackets for property access as you would in JavaScript: - -```yaml -input: - repoUrl: ${{ steps['my-custom-action'].output.repoUrl }} -``` - -### Summary - -Of course, we're always available on [discord](https://discord.gg/backstage-687207715902193673) if -you're stuck or something's not working as expected. You can also -[raise an issue](https://github.com/backstage/backstage/issues/new/choose) with -feedback or bugs! diff --git a/docs/features/software-templates/migrating-to-rjsf-v5.md b/docs/features/software-templates/migrating-to-rjsf-v5.md deleted file mode 100644 index d35eb166f0a8fd..00000000000000 --- a/docs/features/software-templates/migrating-to-rjsf-v5.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -id: migrating-to-rjsf-v5 -title: 'Migrating to react-jsonschema-form@v5' -description: Docs on migrating to `react-jsonschema-form`@v5 and the new designs ---- - -:::note Note - -If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc. - -::: - -## What's `react-jsonschema-form`? - -This library is core to the frontend part of the scaffolder plugin, and is responsible for rendering the form in which developers and end users fill out to meet the `jsonschema` requirement for the parameters section. - -Since the initial release of the `scaffolder` plugin, we we're on a pretty old version of `react-jsonschema-form` (v3), which has been pretty outdated as of late. The problem with us just bumping this library was that there are several breaking changes with the new v5 version, which we've tried pretty aggressively not to pass on to our end users for their templates and [Custom Field Extensions](https://backstage.io/docs/features/software-templates/writing-custom-field-extensions/). - -We're hoping that by duplicating the types from version 3 of `react-jsonschema-form` and making these the types that we will support even though the underlying library is v5, it should get us through all of the breaking changes without passing that down. - -## What's new? - -With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉 - -We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`. - -Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally. - -- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards. -- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper. -- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing. - - You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page. - -There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight. - -## How do I upgrade - -With the release of [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) these changes should have been made for you. We're hoping that it should be pretty transparent, and things just work as expected. Please reach out to us on [discord](https://discord.com/invite/MUpMjP2) or in a [issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&projects=&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+%3Ctitle%3E) if you're having issues. - -It's possible that if you have a hard dependency on any of the `@rjsf/*` libraries in your app, you'll need to bump these manually to the version that we currently support: `5.13.6` at the time of writing. There could be breaking changes that you will have to fix here however, which we think should be pretty simple, but they're things like changing imports from `@rjsf/core` to `@rjsf/utils`. - -```ts -/* highlight-remove-next-line */ -import { FieldValidation } from '@rjsf/core'; -/* highlight-add-next-line */ -import { FieldValidation } from '@rjsf/utils'; -``` - -## Escape hatch - -If for some reason the upgrade to [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) didn't go as planned, there's an escape hatch for use until the next mainline release in which we will try to get any issues fixed before removing the legacy code. - -We've moved some of the older exports to an `/alpha` export so you should be able switch to using the old library just in case. - -```tsx -/* highlight-remove-next-line */ -import { ScaffolderPage } from '@backstage/plugin-scaffolder'; -/* highlight-add-next-line */ -import { LegacyScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; -``` - -And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file: - -```tsx - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - ]} - /> - } -> - - - {/* ... other extensions */} - - - - {/* ... other layouts */} - - -``` - -And you can also update any of your `CustomFieldExtensions` to use the old helper like so: - -```ts -/* highlight-remove-next-line */ -import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder'; -/* highlight-add-next-line */ -import { createLegacyScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha'; - -export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( - /* highlight-remove-next-line */ - createScaffolderFieldExtension({ - /* highlight-add-next-line */ - createLegacyScaffolderFieldExtension({ - component: EntityNamePicker, - name: 'EntityNamePicker', - validation: entityNamePickerValidation, - }), -); -``` - -And in the component themselves, you might have to do the following: - -```tsx -/* highlight-remove-next-line */ -import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; -/* highlight-add-next-line */ -import { LegacyFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha'; - -export const EntityNamePicker = ( - /* highlight-remove-next-line */ - props: FieldExtensionComponentProps, - /* highlight-add-next-line */ - props: LegacyFieldExtensionComponentProps, -) => { - const { - onChange, - required, - schema: { title = 'Name', description = 'Unique name of the component' }, - rawErrors, - formData, - idSchema, - placeholder, - } = props; - // .. -}; -``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index f5ed9e48547086..94bf339595aa87 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -8,15 +8,6 @@ If you want to extend the functionality of the Scaffolder, you can do so by writing custom actions which can be used alongside our [built-in actions](./builtin-actions.md). -:::note Note - -When adding custom actions, the actions array will **replace the -built-in actions too**. Meaning, you will no longer be able to use them. -If you want to continue using the builtin actions, include them in the `actions` -array when registering your custom actions, as seen below. - -::: - ## Streamlining Custom Action Creation with Backstage CLI The creation of custom actions in Backstage has never been easier thanks to the Backstage CLI. This tool streamlines the @@ -56,7 +47,7 @@ its generated unit test. We will replace the existing placeholder code with our import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; -import { type z } from 'zod'; +import { type z } from 'zod/v3'; export const createNewFileAction = () => { return createTemplateAction({ diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 3fe4d82a1fa517..f2e8486a07fb6c 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -290,7 +290,7 @@ and type for your field props to preventing having to duplicate the definitions: ```tsx //packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx ... -import { z } from 'zod'; +import { z } from 'zod/v3'; import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder'; const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod( diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--old.md similarity index 56% rename from docs/features/techdocs/addons--new.md rename to docs/features/techdocs/addons--old.md index 170bd80f701fb3..1a22befc2eb074 100644 --- a/docs/features/techdocs/addons--new.md +++ b/docs/features/techdocs/addons--old.md @@ -1,11 +1,11 @@ --- -id: addons--new +id: addons--old title: TechDocs Addons description: How to find, use, or create TechDocs Addons. --- :::info -This documentation is written for [the new frontend system](../../frontend-system/index.md) which is still in alpha and is only supported by a small number of plugins. If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons.md) instead. +This documentation is written for [the old frontend system](./getting-started.md#adding-techdocs-frontend-plugin). If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./addons.md) instead. ::: ## Concepts @@ -50,8 +50,9 @@ representative of physical spaces in the TechDocs UI: ### Addon Registry The installation and configuration of Addons happens within a Backstage app's -frontend. Addons are imported from plugins and registered as a plugin extension which -are configured for both the TechDocs Reader page as well as the Entity docs page. +frontend. Addons are imported from plugins and added underneath a registry +component called ``. This registry can be configured for both +the TechDocs Reader page as well as the Entity docs page. Addons are rendered in the order in which they are registered. @@ -59,26 +60,62 @@ Addons are rendered in the order in which they are registered. To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` -Addons can then be installed as a module in your `App.tsx`: +Addons can be installed and configured in much the same way as extensions for +other Backstage plugins: by adding them underneath an extension registry +component (``) under the route representing the TechDocs Reader +page in your `App.tsx`: ```tsx // packages/app/src/App.tsx -import { createApp } from '@backstage/frontend-defaults'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { techDocsReportIssueAddonModule } from '@backstage/plugin-techdocs-module-addons-contrib/alpha'; +import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; // ... -const app = createApp({ - features: [ - // ... - techDocsReportIssueAddonModule, - // ...other techdocs addon modules - ], -}); +}> + + + {/* Other addons can be added here. */} + +; +``` + +If you are using a custom [TechDocs reader page](./how-to-guides.md#how-to-customize-the-techdocs-reader-page) your setup will be very similar, here's an example: + +```ts +}> + + + {/* Other addons can be added here. */} + + {techDocsPage} // This is your custom TechDocs reader page + +``` + +The process for configuring Addons on the documentation tab on the entity page +is very similar; instead of adding the `` registry under a +``, you'd add it as a child of ``: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityLayout } from '@backstage/plugin-catalog'; +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +// ... -export default app.createRoot(); + + + + + {/* Other addons can be added here. */} + + +; ``` Note that on the entity page, because the Catalog plugin is responsible for the @@ -89,12 +126,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered. Addons can, in principle, be provided by any plugin! To make it easier to discover available Addons, we've compiled a list of them here: -| Addon | Package/Plugin | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`techDocsExpandableNavigationAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsExpandableNavigationAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | -| [`techDocsReportIssueAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsReportIssueAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | -| [`techDocsTextSizeAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsTextSizeAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | -| [`techDocsLightBoxAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsLightBoxAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | +| Addon | Package/Plugin | Description | +| -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ExpandableNavigation.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ReportIssue.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.TextSize.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.LightBox.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | Got an Addon to contribute? Feel free to add a row above! @@ -105,32 +142,29 @@ specific locations within a TechDocs site. To package such a react component as an Addon, follow these steps: 1. Write the component in your plugin like any other component -2. Create the addon extension using the `TechDocsAddonBlueprint` -3. Create and export the addon module from your plugin +2. Create, provide, and export the component from your plugin ```ts // plugins/your-plugin/src/plugin.ts -import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; -import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; -import { CatGifComponent } from './addons'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { + createTechDocsAddonExtension, + TechDocsAddonLocations, +} from '@backstage/plugin-techdocs-react'; +import { CatGifComponent, CatGifComponentProps } from './addons'; // ... -const techDocsCatGifAddon = AddonBlueprint.make({ - name: 'cat-gif', - params: { +// You must "provide" your Addon, just like any extension, via your plugin. +export const CatGif = yourPlugin.provide( + // This function "creates" the Addon given a component and location. If your + // component can be configured via props, pass the prop type here too. + createTechDocsAddonExtension({ name: 'CatGif', location: TechDocsAddonLocations.Header, component: CatGifComponent, - }, -}); - -export const techDocsCatGifAddonModule = createFrontendModule({ - pluginId: 'techdocs', - extensions: [techDocsCatGifAddon], -}); + }), +); ``` ### Addons in the Content location @@ -147,8 +181,7 @@ provided by the Addon framework. ```tsx // plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx - -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; // This is a normal react component; in order to make it an Addon, you would diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 72e8aa39e8de17..9a72f508c353c1 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -5,7 +5,7 @@ description: How to find, use, or create TechDocs Addons. --- :::info -This documentation is written for [the old frontend system](./getting-started.md#adding-techdocs-frontend-plugin). If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./addons--new.md) instead. +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons--old.md) instead. ::: ## Concepts @@ -50,9 +50,8 @@ representative of physical spaces in the TechDocs UI: ### Addon Registry The installation and configuration of Addons happens within a Backstage app's -frontend. Addons are imported from plugins and added underneath a registry -component called ``. This registry can be configured for both -the TechDocs Reader page as well as the Entity docs page. +frontend. Addons are imported from plugins and registered as a plugin extension which +are configured for both the TechDocs Reader page as well as the Entity docs page. Addons are rendered in the order in which they are registered. @@ -60,62 +59,26 @@ Addons are rendered in the order in which they are registered. To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` -Addons can be installed and configured in much the same way as extensions for -other Backstage plugins: by adding them underneath an extension registry -component (``) under the route representing the TechDocs Reader -page in your `App.tsx`: +Addons can then be installed as a module in your `App.tsx`: ```tsx // packages/app/src/App.tsx -import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; -import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { createApp } from '@backstage/frontend-defaults'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { techDocsReportIssueAddonModule } from '@backstage/plugin-techdocs-module-addons-contrib/alpha'; // ... -}> - - - {/* Other addons can be added here. */} - -; -``` - -If you are using a custom [TechDocs reader page](./how-to-guides.md#how-to-customize-the-techdocs-reader-page) your setup will be very similar, here's an example: - -```ts -}> - - - {/* Other addons can be added here. */} - - {techDocsPage} // This is your custom TechDocs reader page - -``` - -The process for configuring Addons on the documentation tab on the entity page -is very similar; instead of adding the `` registry under a -``, you'd add it as a child of ``: - -```tsx -// packages/app/src/components/catalog/EntityPage.tsx - -import { EntityLayout } from '@backstage/plugin-catalog'; -import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; -import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; - -// ... +const app = createApp({ + features: [ + // ... + techDocsReportIssueAddonModule, + // ...other techdocs addon modules + ], +}); - - - - - {/* Other addons can be added here. */} - - -; +export default app.createRoot(); ``` Note that on the entity page, because the Catalog plugin is responsible for the @@ -126,12 +89,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered. Addons can, in principle, be provided by any plugin! To make it easier to discover available Addons, we've compiled a list of them here: -| Addon | Package/Plugin | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ExpandableNavigation.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ReportIssue.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.TextSize.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.LightBox.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | +| Addon | Package/Plugin | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`techDocsExpandableNavigationAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsExpandableNavigationAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [`techDocsReportIssueAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsReportIssueAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [`techDocsTextSizeAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsTextSizeAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [`techDocsLightBoxAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsLightBoxAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | Got an Addon to contribute? Feel free to add a row above! @@ -142,29 +105,32 @@ specific locations within a TechDocs site. To package such a react component as an Addon, follow these steps: 1. Write the component in your plugin like any other component -2. Create, provide, and export the component from your plugin +2. Create the addon extension using the `TechDocsAddonBlueprint` +3. Create and export the addon module from your plugin ```ts // plugins/your-plugin/src/plugin.ts -import { - createTechDocsAddonExtension, - TechDocsAddonLocations, -} from '@backstage/plugin-techdocs-react'; -import { CatGifComponent, CatGifComponentProps } from './addons'; +import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { CatGifComponent } from './addons'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; // ... -// You must "provide" your Addon, just like any extension, via your plugin. -export const CatGif = yourPlugin.provide( - // This function "creates" the Addon given a component and location. If your - // component can be configured via props, pass the prop type here too. - createTechDocsAddonExtension({ +const techDocsCatGifAddon = AddonBlueprint.make({ + name: 'cat-gif', + params: { name: 'CatGif', location: TechDocsAddonLocations.Header, component: CatGifComponent, - }), -); + }, +}); + +export const techDocsCatGifAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsCatGifAddon], +}); ``` ### Addons in the Content location @@ -181,7 +147,8 @@ provided by the Addon framework. ```tsx // plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx -import { useEffect } from 'react'; + +import React, { useEffect } from 'react'; import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; // This is a normal react component; in order to make it an Addon, you would diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index f6be53cb55991d..fa3d9148e2e1a8 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -208,10 +208,13 @@ Options: #### Publishing from behind a proxy -For users attempting to publish TechDocs content behind a proxy, the TechDocs CLI leverages `global-agent` to navigate the proxy to successfully connect to that location. To enable `global-agent`, the following variables need to be set prior to running the techdocs-cli command: +On Node.js 22.21.0+, set `NODE_USE_ENV_PROXY=1` along with `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` to route TechDocs publishing through a proxy. See the [corporate proxy guide](../../tutorials/corporate-proxy.md) for details. + +On older Node.js versions, the TechDocs CLI leverages `global-agent` to navigate the proxy. To enable `global-agent`, the following variables need to be set prior to running the techdocs-cli command: ```bash -export GLOBAL_AGENT_HTTPS_PROXY=${HTTP_PROXY} +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} ``` diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index cfef547afc90c4..d24e1622fd14a3 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -18,12 +18,12 @@ The create-app CLI requires Node.js Active LTS Release, see the [prerequisites d ::: ```sh -# The command bellow creates a Backstage App inside the current folder. +# The command below creates a Backstage App inside the current folder. # The name of the app-folder is the name that was provided when prompted. -npx @backstage/create-app@latest --next +npx @backstage/create-app@latest ``` -Using the `--next` flag will result in a Backstage app using the New Frontend System which will be further explained in the sections below. +This will create a Backstage app using the new frontend system which will be further explained in the sections below. ## The app instance diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 9f18067a55b692..ceba8fd8681818 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -899,39 +899,7 @@ It's encouraged that once you switch over to using the new frontend system, that This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system. -When creating a new Backstage app with `create-app` and using the `--next` flag you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`: - -```json -{ - ... - "scripts": { - ... - "new": "backstage-cli new" - }, - "backstage": { - "cli": { - "new": { - "globals": { - "license": "UNLICENSED" - }, - "templates": [ - "@backstage/cli-module-new/templates/new-frontend-plugin", - "@backstage/cli-module-new/templates/new-frontend-plugin-module", - "@backstage/cli-module-new/templates/backend-plugin", - "@backstage/cli-module-new/templates/backend-plugin-module", - "@backstage/cli-module-new/templates/plugin-web-library", - "@backstage/cli-module-new/templates/plugin-node-library", - "@backstage/cli-module-new/templates/plugin-common-library", - "@backstage/cli-module-new/templates/web-library", - "@backstage/cli-module-new/templates/node-library", - "@backstage/cli-module-new/templates/catalog-provider-module", - "@backstage/cli-module-new/templates/scaffolder-backend-module" - ] - } - } - } -} -``` +The `yarn new` command now defaults to the new frontend system templates for frontend plugins. If you have an older app that was created before this change, you can simply update the `@backstage/cli-module-new` package to get access to the new templates. ## Troubleshooting diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 861f454280d91c..d89eae87b08825 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -238,3 +238,11 @@ export const examplePlugin = createFrontendPlugin({ The `ExampleEntityContent` itself is again a regular React component where you can implement any functionality you want. To access the entity that the content is being rendered for, you can use the `useEntity` hook from `@backstage/plugin-catalog-react`. You can see a full list of APIs provided by the catalog React library in [the API reference](https://backstage.io/api/stable/modules/_backstage_plugin-catalog-react.index.html). For a more complete list of the different kinds of extensions that you can create for your plugin, see the [extension blueprints](./03-common-extension-blueprints.md) section. + +## Related topics + +The following guides cover cross-cutting concerns for building frontend plugins: + +- [Internationalization (i18n)](./07-internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. +- [Plugin Analytics](./08-analytics.md) — Instrumenting user interactions with the Analytics API using `AnalyticsImplementationBlueprint`. +- [Feature Flags](./09-feature-flags.md) — Defining and using feature flags via the `featureFlags` option of `createFrontendPlugin`. diff --git a/docs/frontend-system/building-plugins/07-internationalization.md b/docs/frontend-system/building-plugins/07-internationalization.md new file mode 100644 index 00000000000000..3e882cb8e20703 --- /dev/null +++ b/docs/frontend-system/building-plugins/07-internationalization.md @@ -0,0 +1,406 @@ +--- +id: internationalization +title: Internationalization +sidebar_label: Internationalization +description: Adding internationalization to plugins and apps +--- + +## Overview + +The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. + +## For a plugin developer + +When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example: + +```ts +import { createTranslationRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + indexPage: { + title: 'All your components', + createButtonTitle: 'Create new component', + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +And then use these messages in your components like: + +```tsx +import { useTranslationRef } from '@backstage/frontend-plugin-api'; + +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( + + + +); +``` + +You will see how the initial dictionary structure and nesting get converted into dot notation, so we encourage `camelCase` in key names and lean on the nesting structure to separate keys. + +### Guidelines for `i18n` messages and keys + +The API for `i18n` messages and keys can be pretty tricky to get right, as it's a pretty flexible API. We've put together some guidelines to help you get started that encourage good practices when thinking about translating plugins: + +#### Key names + +When defining messages it is recommended to use a nested structure that represents the semantic hierarchy in your translations. This allows for better organization and understanding of the structure. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + widgets: { + weather: { + title: 'Weather', + description: 'Shows the weather', + }, + calendar: { + title: 'Calendar', + description: 'Shows the calendar', + }, + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +Think about the semantic placement of content rather than the text content itself. Group related translations under a common prefix, and use nesting to represent relationships between different parts of your application. It's good to start grouping under extensions, page sections, or visual scopes and experiences. + +Translations should avoid using their own text content as key where possible, as this can lead to confusion if the translation changes. Instead prefer to use keys that describe the location or usage of the text. + +#### Common Key names + +This list is intended to grow over time, but below are some examples of common key names and patterns that we encourage you to use where possible: + +- `${page}.title` +- `${page}.subtitle` +- `${page}.description` + +- `${page}.header.title` + +#### Key reuse + +Reusing the same key in multiple places is discouraged. This helps prevent ambiguity, and instead keeps the usage of each key as clear as possible. Consider creating duplicate keys that are grouped under a semantic section instead. + +#### Flat keys + +Avoid a flat key structure at the root level, as it can lead to naming conflicts and make the translation file harder to manage and change evolve over time. Instead, group translations under a common prefix. + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + // this is BAD + title: 'My page', + subtitle: 'My subtitle', + // this is GOOD + dashboardPage: { + header: { + title: 'All your components', + subtitle: 'Create new component', + }, + }, + }, +}); +``` + +#### Plurals + +The `i18next` library, which is used as the underlying implementation, has built-in support for pluralization. You can use this feature as is described in [the documentation](https://www.i18next.com/translation-function/plurals). + +We encourage you to use this feature and avoid creating different key prefixes for pluralized content. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + cards: { + title_one: 'You have one card', + title_two: 'You have two cards', + title_other: 'You have many cards ({{count}})', + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +#### JSX Elements + +The translation API supports interpolation of JSX elements by passing them directly as values to the translation function. If any of the provided interpolation values are JSX elements, the translation function will return a JSX element instead of a string. + +For example, you might define the following messages: + +```ts title="define the message" +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + entityPage: { + redirect: { + message: 'The entity you are looking for has been moved to {{link}}.', + link: 'new location', + }, + }, + }, +}); +``` + +Which can be used within a component like this: + +```tsx title="use within a component" +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( +
+ {t('entityPage.redirect.message', { + link: {t('entityPage.redirect.link')}, + })} +
+); +``` + +The return type of the outer `t` function will be a `JSX.Element`, with the underlying value being a React fragment of the different parts of the message. + +## For an application developer + +As an app developer you can both override the default English messages of any plugin, and provide translations for additional languages. + +### Overriding messages + +To customize specific messages without adding new languages, create a translation extension using `TranslationBlueprint` from `@backstage/plugin-app-react` together with `createTranslationMessages` from `@backstage/frontend-plugin-api`: + +```ts +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +const catalogTranslations = TranslationBlueprint.make({ + name: 'catalog-overrides', + params: { + resource: createTranslationMessages({ + ref: catalogTranslationRef, + messages: { + 'indexPage.title': 'Service directory', + 'indexPage.createButtonTitle': 'Register new service', + }, + }), + }, +}); +``` + +Then install it as a feature in your app: + +```ts +import { createApp } from '@backstage/frontend-defaults'; + +const app = createApp({ + features: [catalogTranslations], +}); +``` + +You only need to include the keys you want to override — any missing keys fall back to the plugin's defaults. + +### Adding language translations + +To add support for additional languages, create a translation resource with lazy-loaded message files for each language, and install it using `TranslationBlueprint`: + +```ts +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +const userSettingsTranslations = TranslationBlueprint.make({ + name: 'user-settings-zh', + params: { + resource: createTranslationResource({ + ref: userSettingsTranslationRef, + translations: { + zh: () => import('./userSettings-zh'), + }, + }), + }, +}); +``` + +The translation messages can be defined using `createTranslationMessages` for type safety: + +```ts +// packages/app/src/translations/userSettings-zh.ts + +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +const zh = createTranslationMessages({ + ref: userSettingsTranslationRef, + full: false, // False means that this is a partial translation + messages: { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + }, +}); + +export default zh; +``` + +Or as a plain object export: + +```ts +// packages/app/src/translations/userSettings-zh.ts +export default { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + 'languageToggle.description': '切换语言', + 'themeToggle.title': '主题', + 'themeToggle.description': '切换主题', + 'themeToggle.select': '选择{{theme}}', + 'themeToggle.selectAuto': '选择自动主题', + 'themeToggle.names.auto': '自动', + 'themeToggle.names.dark': '暗黑', + 'themeToggle.names.light': '明亮', +}; +``` + +Install the translation extension in your app: + +```ts +import { createApp } from '@backstage/frontend-defaults'; + +const app = createApp({ + features: [userSettingsTranslations], +}); +``` + +Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly. + +### Using the CLI for full translation workflows + +When translating your app to other languages at scale — especially when working with external translation systems — the Backstage CLI provides `translations export` and `translations import` commands that automate the extraction and wiring of translation messages across all your plugin dependencies. + +#### Exporting default messages + +From your app package directory (e.g. `packages/app`), run: + +```bash +yarn backstage-cli translations export +``` + +This scans all frontend plugin dependencies (including transitive ones) for `TranslationRef` definitions and writes their default English messages as JSON files: + +```text +translations/ + manifest.json + messages/ + catalog.en.json + org.en.json + scaffolder.en.json + ... +``` + +Each `.en.json` file contains the flattened message keys and their default values: + +```json +{ + "indexPage.title": "All your components", + "indexPage.createButtonTitle": "Create new component", + "entityPage.notFound": "Entity not found" +} +``` + +#### Creating translations + +Copy the exported files and translate them for your target languages: + +```bash +cp translations/messages/catalog.en.json translations/messages/catalog.zh.json +``` + +Then edit `catalog.zh.json` with the translated strings. You only need to include the keys you want to translate — missing keys fall back to the English defaults at runtime. + +#### Generating wiring code + +Once you have translated files in place, run: + +```bash +yarn backstage-cli translations import +``` + +This generates a TypeScript module at `src/translations/resources.ts` that wires everything together: + +```ts +// This file is auto-generated by backstage-cli translations import +// Do not edit manually. + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +export default [ + createTranslationResource({ + ref: catalogTranslationRef, + translations: { + zh: () => import('../../translations/messages/catalog.zh.json'), + }, + }), +]; +``` + +Install the generated resources as features in your app: + +```ts +import { createApp } from '@backstage/frontend-defaults'; +import translationResources from './translations/resources'; + +const app = createApp({ + features: translationResources, +}); +``` + +#### Custom file patterns + +By default, message files use the pattern `messages/{id}.{lang}.json` (e.g. `messages/catalog.en.json`). You can change this with the `--pattern` option: + +```bash +yarn backstage-cli translations export --pattern '{lang}/{id}.json' +``` + +This produces a directory structure grouped by language instead: + +```text +translations/en/catalog.json +translations/zh/catalog.json +``` + +The pattern is stored in the manifest, so the `import` command automatically uses the same layout. + +#### Integration with external translation systems + +The exported JSON files are standard key-value pairs compatible with most external translation systems. A typical workflow looks like: + +1. Run `translations export` to generate the source English files +2. Upload the `.en.json` files to your translation system +3. Download the translated files back into the translations directory +4. Run `translations import` to regenerate the wiring code + +For full command reference, see the [CLI commands documentation](../../tooling/cli/03-commands.md#translations-export). diff --git a/docs/frontend-system/building-plugins/08-analytics.md b/docs/frontend-system/building-plugins/08-analytics.md new file mode 100644 index 00000000000000..0b51b5ae1ab1da --- /dev/null +++ b/docs/frontend-system/building-plugins/08-analytics.md @@ -0,0 +1,317 @@ +--- +id: analytics +title: Plugin Analytics +sidebar_label: Analytics +description: Measuring usage of your Backstage instance +--- + +Setting up, maintaining, and iterating on an instance of Backstage can be a +large investment. To help measure return on this investment, Backstage comes +with an event-based Analytics API that grants app integrators the flexibility to +collect and analyze Backstage usage in the analytics tool of their choice, while +providing plugin developers a standard interface for instrumenting key user +interactions. + +## Concepts + +- **Events** consist of, at a minimum, an `action` (like `click`) and a + `subject` (like `thing that was clicked on`). +- **Attributes** represent additional dimensional data (in the form of key/value + pairs) that may be provided on an event-by-event basis. To continue the above + example, the URL a user clicked to might look like `{ "to": "/a/page" }`. +- **Context** represents the broader context in which an event took place. By + default, information like `pluginId`, `extension`, and `routeRef` are + provided. + +This composition of events aims to allow analysis at different levels of detail, +enabling very granular questions (like "what is the most clicked on thing on a +particular route") as well as very high-level questions (like "what is the most +used plugin in my Backstage instance") to be answered. + +## Supported Analytics Tools + +While all that's needed to consume and forward these events to an analytics tool +is a concrete implementation of [AnalyticsApi][analytics-api-type], common +integrations are packaged and provided as plugins. Find your analytics tool of +choice below. + +| Analytics Tool | Support Status | +| ------------------------------------- | -------------- | +| [Google Analytics][ga] | Yes ✅ | +| [Google Analytics 4][ga4] | Yes ✅ | +| [New Relic Browser][newrelic-browser] | Community ✅ | +| [Matomo][matomo] | Community ✅ | +| [Quantum Metric][qm] | Community ✅ | +| [Generic HTTP][generic-http] | Community ✅ | + +To suggest an integration, please [open an issue][add-tool] for the analytics +tool your organization uses. Or jump to [Writing Integrations][int-howto] to +learn how to contribute the integration yourself! + +[ga]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga/README.md +[ga4]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga4/README.md +[newrelic-browser]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-newrelic-browser/README.md +[qm]: https://github.com/quantummetric/analytics-module-qm/blob/main/README.md +[matomo]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-matomo/README.md +[add-tool]: https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE +[int-howto]: #writing-integrations +[analytics-api-type]: https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.index.AnalyticsApi.html +[generic-http]: https://github.com/pfeifferj/backstage-plugin-analytics-generic/blob/main/README.md + +## Key Events + +The following table summarizes events that, depending on the plugins you have +installed, may be captured. + +| Action | Subject | Other Notes | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `navigate` | The URL of the page that was navigated to. | Fired immediately when route location changes (unless associated plugin/route data is ambiguous, in which case the event is fired after plugin/route data becomes known, immediately before the next event or document unload). The parameters of the current route will be included as attributes. | +| `click` | The text of the link that was clicked on. | The `to` attribute represents the URL clicked to. | +| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`). The `value` represents the number of minutes saved by running the template (based on the template's `backstage.io/time-saved` annotation, if available). | +| `search` | The search term entered in any search bar component. | The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | +| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided. | +| `not-found` | The path of the resource that resulted in a not found page | Fired by at least TechDocs. | + +If there is an event you'd like to see captured, please [open an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md&title=[Analytics%20Event]:%20THE+EVENT+TO+CAPTURE) describing the event you want to see and the questions it +would help you answer. Or jump to [Capturing Events](#capturing-events) to learn how +to contribute the instrumentation yourself! + +_OSS plugin maintainers: feel free to document your events in the table above._ + +## Writing Integrations + +Analytics event forwarding is implemented as a Backstage [Utility API](../utility-apis/01-index.md). The +provided API need only provide a single method `captureEvent`, which takes +an `AnalyticsEvent` object. + +A simple implementation using `AnalyticsImplementationBlueprint`: + +```ts +import { AnalyticsImplementationBlueprint } from '@backstage/plugin-app-react'; + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: {}, + factory() { + return { + captureEvent: event => { + window._AcmeAnalyticsQ.push(event); + }, + }; + }, + }), + }); +``` + +In reality, you would likely want to encapsulate instantiation logic and pull +some details from configuration. A more complete example might look like: + +```ts +import { + AnalyticsApi, + AnalyticsEvent, + configApiRef, +} from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/plugin-app-react'; +import { AcmeAnalytics } from 'acme-analytics'; + +class AcmeAnalyticsImpl implements AnalyticsApi { + private constructor(accountId: number) { + AcmeAnalytics.init(accountId); + } + + static fromConfig(config) { + const accountId = config.getString('app.analytics.acme.id'); + return new AcmeAnalyticsImpl(accountId); + } + + captureEvent(event: AnalyticsEvent) { + const { action, ...rest } = event; + AcmeAnalytics.send(action, rest); + } +} + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalyticsImpl.fromConfig(configApi), + }), + }); +``` + +If you are integrating with an analytics service (as opposed to an internal +tool), consider contributing your API implementation as a plugin! + +By convention, such packages should be named +`@backstage/analytics-module-[name]`, and any configuration should be keyed +under `app.analytics.[name]`. + +### Handling User Identity + +If the analytics platform you are integrating with has a first-class concept of +user identity, you can (optionally) choose to support this by the following this +convention: + +- Allow your implementation to be instantiated with the `identityApi` as one of + its dependencies. +- Use the `userEntityRef` resolved by `identityApi`'s `getBackstageIdentity()` + method as the basis for the user ID you send to your analytics platform. + +## Capturing Events + +To instrument an event in a component, start by retrieving an analytics tracker +using the `useAnalytics()` hook provided by `@backstage/frontend-plugin-api`. The +tracker includes a `captureEvent` method which takes an `action` and a `subject` +as arguments. + +```ts +import { useAnalytics } from '@backstage/frontend-plugin-api'; + +const analytics = useAnalytics(); +analytics.captureEvent('deploy', serviceName); +``` + +### Providing Extra Attributes + +Additional dimensional `attributes` as well as a numeric `value` can be provided +on a third `options` argument if/when relevant for the event: + +```ts +analytics.captureEvent('merge', pullRequestName, { + value: pullRequestAgeInMinutes, + attributes: { + org, + repo, + }, +}); +``` + +In the above example, an event resembling the following object would be +captured: + +```json +{ + "action": "merge", + "subject": "Name of Pull Request", + "value": 60, + "attributes": { + "org": "some-org", + "repo": "some-repo" + } +} +``` + +### Providing Context for Events + +The `attributes` option is good for capturing details available to you within +the component that you're instrumenting. For capturing metadata only available +further up the react tree, or to help app integrators aggregate distinct events +by some common value, use an ``. + +```tsx +import { AnalyticsContext, useAnalytics } from '@backstage/frontend-plugin-api'; + +const MyComponent = ({ value }) => { + const analytics = useAnalytics(); + const handleClick = () => analytics.captureEvent('check', value); + return ; +}; + +const MyWrapper = () => { + return ( + + + + ); +}; +``` + +In the above example, clicking on `` would result in an analytics +event resembling: + +```json +{ + "action": "check", + "subject": "Some Value", + "context": { + "segment": "xyz" + } +} +``` + +Note that, for brevity in the example above, the context keys provided by +Backstage core (`pluginId`, `extension`, and `routeRef`) have been omitted. In +reality, those details would be included alongside any additional context +provided by you. + +Analytics contexts can be nested; their values are merged down the react tree, +allowing keys to be overwritten. + +### Event Naming Considerations + +An event is split into its constituent parts to enable analysis at various +levels of granularity. In order to maintain this flexibility at analysis-time, +it's important to keep each of these levels of detail disaggregated. + +- Avoid providing an overly specific `action`. For example, instead of + `filterEntityTable`, consider just using `filter` as the action, and allowing + `EntityTable` to be specified as part of the event's `context` (most likely + automatically as part of the `extension` in which the `filter` event was + captured). + +- On the flip side, when adding `attributes` to or `context` around an event, + look at existing events and see if the data you are capturing matches the + intention, type, or even the content of _their_ `attributes` or `context`. + For instance, it's common for events that involve the Catalog to include an + `entityRef` contextual key. Using the same keys and values in your event will + ensure that events instrumented across plugins can easily be aggregated. + +### Unit Testing Event Capture + +The `@backstage/frontend-test-utils` package includes a `MockAnalyticsApi` implementation +that you can use in your unit tests to spy on and make assertions about any +analytics events captured. + +Use it like this: + +```tsx +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { analyticsApiRef } from '@backstage/frontend-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/frontend-test-utils'; + +describe('SomeComponent', () => { + it('should capture event on click', () => { + const apiSpy = new MockAnalyticsApi(); + + const { getByText } = render( + wrapInTestApp( + + + , + ), + ); + + fireEvent.click(getByText('some component text')); + + await waitFor(() => { + expect(apiSpy.getEvents()[0]).toMatchObject({ + action: 'expected action', + subject: 'expected subject', + attributes: { + foo: 'bar', + }, + }); + }); + }); +}); +``` diff --git a/docs/frontend-system/building-plugins/09-feature-flags.md b/docs/frontend-system/building-plugins/09-feature-flags.md new file mode 100644 index 00000000000000..06ee56be24e0d3 --- /dev/null +++ b/docs/frontend-system/building-plugins/09-feature-flags.md @@ -0,0 +1,78 @@ +--- +id: feature-flags +title: Feature Flags +sidebar_label: Feature Flags +description: Defining and using feature flags in plugins and apps +--- + +Backstage offers the ability to define feature flags inside a plugin or during application creation. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. + +This page describes the process of defining, setting and reading a feature flag. If you are looking for using feature flags specifically with software templates please see [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). + +## Defining a Feature Flag + +### In a plugin + +Feature flags are declared via the `featureFlags` option in `createFrontendPlugin`: + +```ts title="src/plugin.ts" +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; + +export const examplePlugin = createFrontendPlugin({ + pluginId: 'example', + featureFlags: [ + { + name: 'show-example-feature', + description: 'Enables the new beta dashboard view', + }, + ], + extensions: [ + // ... + ], +}); +``` + +Note that the `description` property is optional. If not provided, the default "Registered in {pluginId} plugin" message is shown. + +### In the application + +Defining a feature flag in the application is done by adding feature flags in the `featureFlags` array in the +`createApp()` function call: + +```ts title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; + +const app = createApp({ + // ... + featureFlags: [ + { + name: 'tech-radar', + description: 'Enables the tech radar plugin', + }, + ], + // ... +}); +``` + +## Enabling Feature Flags + +Feature flags are defaulted to off and can be updated by individual users in the backstage interface. These are set by navigating to the page under `Settings` > `Feature Flags`. + +The user's selection is saved in the user's browser local storage. Once a feature flag is toggled it may be required for a user to refresh the page to see the change. + +## Evaluating Feature Flag State + +You can query a feature flag using the [FeatureFlagsApi](https://backstage.io/api/stable/interfaces/_backstage_frontend-plugin-api.index.FeatureFlagsApi.html): + +```tsx +import { useApi, featureFlagsApiRef } from '@backstage/frontend-plugin-api'; + +function MyComponent() { + const featureFlagsApi = useApi(featureFlagsApiRef); + + if (featureFlagsApi.isActive('show-example-feature')) { + return ; + } + return ; +} +``` diff --git a/docs/getting-started/config/authentication--old.md b/docs/getting-started/config/authentication--old.md new file mode 100644 index 00000000000000..cc02eb2b7f045e --- /dev/null +++ b/docs/getting-started/config/authentication--old.md @@ -0,0 +1,213 @@ +--- +id: authentication--old +title: Authentication +description: How to setup authentication for your Backstage app +--- + +:::info +This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./authentication.md) instead. +::: + +Audience: Admins or Developers + +## Summary + +We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! + +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--old.md). + +:::note Note + +The default Backstage app comes with a guest Sign In Resolver. This resolver makes all users share a single "guest" identity and is only intended as a minimum requirement to quickly get up and running. You can read more about how [Sign In Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) play a role in creating a [Backstage User Identity](../../auth/identity-resolver.md#backstage-user-identity) for logged in users. + +::: + +## Setting up authentication + +For this tutorial we choose to use GitHub, a free service most of you might be familiar with, and we'll be using an OAuth app. For detailed options, see +[the GitHub auth provider documentation](../../auth/github/provider.md#create-an-oauth-app-on-github). + +Go to [https://github.com/settings/applications/new](https://github.com/settings/applications/new) to create your OAuth App. The "Homepage URL" should point to Backstage's frontend, in our tutorial it would be `http://localhost:3000`. The "Authorization callback URL" will point to the auth backend, which will most likely be `http://localhost:7007/api/auth/github/handler/frame`. + +![Screenshot of the GitHub OAuth creation page](../../assets/getting-started/gh-oauth.png) + +Take note of the `Client ID` and the `Client Secret` (clicking the "Generate a new client secret" button will get this value for you). Open `app-config.yaml`, and add them as `clientId` and `clientSecret` in this file. It should end up looking like this: + +```yaml title="app-config.yaml" +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + /* highlight-add-start */ + environment: development + /* highlight-add-end */ + providers: + # See https://backstage.io/docs/auth/guest/provider + guest: {} + /* highlight-add-start */ + github: + development: + clientId: YOUR CLIENT ID + clientSecret: YOUR CLIENT SECRET + /* highlight-add-end */ +``` + +## Add sign-in option to the frontend + +The next step is to change the sign-in page. For this, you'll actually need to write some code. + +Open `packages/app/src/App.tsx` and below the last `import` line, add: + +```typescript title="packages/app/src/App.tsx" +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +``` + +Search for `const app = createApp({` in this file, and replace: + +```tsx title="packages/app/src/App.tsx" +components: { + SignInPage: props => , +}, +``` + +with + +```tsx title="packages/app/src/App.tsx" +components: { + SignInPage: props => ( + + ), +}, +``` + +## Add sign-in resolver(s) + +Next we need to add the sign-in resolver to our configuration. Here's how: + +```yaml title="app-config.yaml" +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + # See https://backstage.io/docs/auth/guest/provider + guest: {} + github: + development: + clientId: YOUR CLIENT ID + clientSecret: YOUR CLIENT SECRET + /* highlight-add-start */ + signIn: + resolvers: + # Matches the GitHub username with the Backstage user entity name. + # See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers. + - resolver: usernameMatchingUserEntityName + /* highlight-add-end */ +``` + +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. + +Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. + +## Add the auth provider to the backend + +To add the auth provider to the backend, we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-github-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); +/* highlight-add-end */ +``` + +Restart Backstage from the terminal, by stopping it with `Ctrl+C`, and starting it with `yarn start`. You should be welcomed by a login prompt! If you try to login at this point you will get a "Failed to sign-in, unable to resolve user identity" message, read on as we'll fix that next. + +:::note Note + +Sometimes the frontend starts before the backend resulting in errors on the sign in page. Wait for the backend to start and then reload Backstage to proceed. + +::: + +## Adding a User + +The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. + +For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: + +1. First open the `/examples/org.yaml` file in your text editor of choice +2. At the bottom we'll add the following YAML: + + ```yaml + --- + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: YOUR GITHUB USERNAME + spec: + memberOf: [guests] + ``` + +3. Now make sure to replace the text "YOUR GITHUB USERNAME" with your actual GitHub User name. + +Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C`, and starting it with `yarn start`. You should now be able to log into Backstage and see items in your Catalog. + +To learn more about Authentication in Backstage, here are some docs you +could read: + +- [Authentication in Backstage](../../auth/index.md) +- [Using organizational data from GitHub](../../integrations/github/org.md) + +## Setting up a GitHub Integration + +The GitHub integration supports loading catalog entities from GitHub or GitHub Enterprise. Entities can be added to static catalog configuration, registered with the catalog-import plugin, or discovered from a GitHub organization. Users and Groups can also be loaded from an organization. While using [GitHub Apps](../../integrations/github/github-apps.md) might be the best way to set up integrations, for this tutorial you'll use a Personal Access Token. + +Create your Personal Access Token by opening [the GitHub token creation page](https://github.com/settings/tokens/new). Use a name to identify this token and put it in the notes field. Choose a number of days for expiration. If you have a hard time picking a number, we suggest to go for 7 days, it's a lucky number. + +![Screenshot of the GitHub Personal Access Token creation page](../../assets/getting-started/gh-pat.png) + +Set the scope to your likings. For this tutorial, selecting `repo` and `workflow` is required as the scaffolding job in this guide configures a GitHub actions workflow for the newly created project. + +For this tutorial, we will be writing the token to `app-config.local.yaml`. This file might not exist for you, so if it doesn't go ahead and create it alongside the `app-config.yaml` at the root of the project. This file should also be excluded in `.gitignore`, to avoid accidental committing of this file. More details on this file can be found in the [Static Configuration documentation](../../conf/index.md). + +In your `app-config.local.yaml` go ahead and add the following: + +```yaml title="app-config.local.yaml" +integrations: + github: + - host: github.com + token: ghp_urtokendeinfewinfiwebfweb # this should be the token from GitHub +``` + +That's settled. This information will be leveraged by other plugins. + +If you're looking for a more production way to manage this secret, then you can do the following with the token being stored in an environment variable called `GITHUB_TOKEN`. + +```yaml title="app-config.local.yaml" +integrations: + github: + - host: github.com + token: ${GITHUB_TOKEN} # this will use the environment variable GITHUB_TOKEN +``` + +:::note Note + +If you've updated the configuration for your integration, it's likely that the backend will need a restart to apply these changes. To do this, stop the running instance in your terminal with `Control-C`, then start it again with `yarn start`. Once the backend has restarted, retry the operation. + +::: + +Some helpful links, for if you want to learn more about: + +- [Other available integrations](../../integrations/index.md) +- [Using GitHub Apps instead of a Personal Access Token](../../integrations/github/github-apps.md#docsNav) diff --git a/docs/getting-started/config/authentication.md b/docs/getting-started/config/authentication.md index e25f357ff830ef..9df27692b83c61 100644 --- a/docs/getting-started/config/authentication.md +++ b/docs/getting-started/config/authentication.md @@ -4,6 +4,10 @@ title: Authentication description: How to setup authentication for your Backstage app --- +:::info +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./authentication--old.md) instead. +::: + Audience: Admins or Developers ## Summary @@ -50,37 +54,63 @@ auth: The next step is to change the sign-in page. For this, you'll actually need to write some code. -Open `packages/app/src/App.tsx` and below the last `import` line, add: +First let's add the packages we need, do this from the root: + +```shell +yarn --cwd packages/app add @backstage/core-plugin-api @backstage/plugin-app-react +``` + +Then open `packages/app/src/App.tsx` and below the last `import` line, add: ```typescript title="packages/app/src/App.tsx" import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +``` + +Now below this we are going to use the `SignInPageBlueprint` to create an extension, add this code block to do that: + +```tsx +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), + }, +}); ``` -Search for `const app = createApp({` in this file, and replace: +Search for the `createApp()` function call in this file, and replace: ```tsx title="packages/app/src/App.tsx" -components: { - SignInPage: props => , -}, +export default createApp({ + features: [catalogPlugin, navModule], +}); ``` with ```tsx title="packages/app/src/App.tsx" -components: { - SignInPage: props => ( - - ), -}, +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], +}); ``` ## Add sign-in resolver(s) @@ -107,7 +137,7 @@ auth: /* highlight-add-end */ ``` -What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get a "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. @@ -140,7 +170,7 @@ Sometimes the frontend starts before the backend resulting in errors on the sign The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. -For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: +For the sake of this guide we'll simply step you through adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: 1. First open the `/examples/org.yaml` file in your text editor of choice 2. At the bottom we'll add the following YAML: diff --git a/docs/getting-started/keeping-backstage-updated.md b/docs/getting-started/keeping-backstage-updated.md index c93c5200f6ba6b..a55f872f533329 100644 --- a/docs/getting-started/keeping-backstage-updated.md +++ b/docs/getting-started/keeping-backstage-updated.md @@ -151,12 +151,14 @@ down the number of duplicate packages. ## Proxy -The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) and `undici` to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. +On Node.js 22.21.0+, the Backstage CLI respects the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables when `NODE_USE_ENV_PROXY=1` is set. See the [corporate proxy guide](../tutorials/corporate-proxy.md) for full details. + +On older Node.js versions, the CLI falls back to [global-agent](https://www.npmjs.com/package/global-agent) and `undici` for proxy support, which require their own environment variables (prefixed with `GLOBAL_AGENT_`). This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. Additionally, yarn needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the other modules. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values. If you will always need proxy settings in all environments and situations, you can add `httpProxy` and `httpsProxy` values to [the yarnrc.yml file](https://yarnpkg.com/configuration/yarnrc). If some environments need it (say a developer workstation) but other environments do not (perhaps a CI build server running on AWS), then you may not want to update the yarnrc.yml file but just set environment variables `YARN_HTTP_PROXY` and `YARN_HTTPS_PROXY` in the environments/situations where you need to proxy. -**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the global agent proxy settings alone are sufficient. +**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the proxy settings alone are sufficient. ### Example Configuration @@ -164,9 +166,10 @@ If you will always need proxy settings in all environments and situations, you c export HTTP_PROXY=http://proxy.company.com:8080 export HTTPS_PROXY=https://secure-proxy.company.com:8080 export NO_PROXY=localhost,internal.company.com -export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} -export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} -export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} +export NODE_USE_ENV_PROXY=1 # Node.js 22.21.0+ +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} # Node.js < 22.21.0 export YARN_HTTP_PROXY=${HTTP_PROXY} # optional export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional ``` diff --git a/docs/golden-path/create-app/keeping-backstage-updated.md b/docs/golden-path/create-app/keeping-backstage-updated.md index de7d99b2b8692f..2692381737fd93 100644 --- a/docs/golden-path/create-app/keeping-backstage-updated.md +++ b/docs/golden-path/create-app/keeping-backstage-updated.md @@ -141,12 +141,14 @@ down the number of duplicate packages. ## Proxy -The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) and `undici` to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. +On Node.js 22.21.0+, the Backstage CLI respects the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables when `NODE_USE_ENV_PROXY=1` is set. See the [corporate proxy guide](../../tutorials/corporate-proxy.md) for full details. + +On older Node.js versions, the CLI falls back to [global-agent](https://www.npmjs.com/package/global-agent) and `undici` for proxy support, which require their own environment variables (prefixed with `GLOBAL_AGENT_`). This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. Additionally, `yarn` needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the other modules. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values. If you will always need proxy settings in all environments and situations, you can add `httpProxy` and `httpsProxy` values to [the yarnrc.yml file](https://yarnpkg.com/configuration/yarnrc). If some environments need it (say a developer workstation) but other environments do not (perhaps a CI build server running on AWS), then you may not want to update the yarnrc.yml file but just set environment variables `YARN_HTTP_PROXY` and `YARN_HTTPS_PROXY` in the environments/situations where you need to proxy. -**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the global agent proxy settings alone are sufficient. +**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the proxy settings alone are sufficient. ### Example Configuration @@ -154,9 +156,10 @@ If you will always need proxy settings in all environments and situations, you c export HTTP_PROXY=http://proxy.company.com:8080 export HTTPS_PROXY=https://secure-proxy.company.com:8080 export NO_PROXY=localhost,internal.company.com -export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} -export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} -export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} +export NODE_USE_ENV_PROXY=1 # Node.js 22.21.0+ +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} # Node.js < 22.21.0 export YARN_HTTP_PROXY=${HTTP_PROXY} # optional export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional ``` diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 4b188ce7f91c87..039805bcb42c61 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -23,7 +23,7 @@ import { createConditionFactory, createPermissionRule, } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const isInSystemRule = createPermissionRule({ name: 'IS_IN_SYSTEM', diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 1e1a73607ba205..afdb3b8c62c3db 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -126,7 +126,7 @@ import { createPermissionRule, } from '@backstage/plugin-permission-node'; import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Todo, TodoFilter } from './todos'; export const todoListPermissionResourceRef = createPermissionResourceRef< diff --git a/docs/plugins/add-to-directory.md b/docs/plugins/add-to-directory.md index 48affef489f9e1..a87062e207bb2e 100644 --- a/docs/plugins/add-to-directory.md +++ b/docs/plugins/add-to-directory.md @@ -4,6 +4,12 @@ title: Add to Directory description: Documentation on Adding Plugin to Plugin Directory --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The process for adding plugins to the directory described here is still current. + +::: + ## Adding a Plugin to the Directory To add a new plugin to the [plugin directory](https://backstage.io/plugins) create a file with the following pattern `.yaml` where `` is the name of your plugin. This file will go in [`microsite/data/plugins`](https://github.com/backstage/backstage/tree/master/microsite/data/plugins) with your plugin's information. Example: diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 3e8d5b4111409d..a758d63ebe98e3 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -4,6 +4,12 @@ title: Plugin Analytics description: Measuring usage of your Backstage instance. --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. For the new frontend system version, see [Plugin Analytics](../frontend-system/building-plugins/08-analytics.md). The concepts and events described here apply to both the old and new frontend systems. + +::: + Setting up, maintaining, and iterating on an instance of Backstage can be a large investment. To help measure return on this investment, Backstage comes with an event-based Analytics API that grants app integrators the flexibility to diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 2872fdcda9a7eb..91a3e048398914 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -4,6 +4,12 @@ title: Backend plugins description: Creating and Developing Backend plugins --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. While this page already describes the new backend system patterns, the canonical documentation for building backend plugins has moved to [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + This page describes the process of creating and managing backend plugins in your Backstage repository. diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index 35a6d8d599908c..5b5a91b708a7d8 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -4,6 +4,12 @@ title: Call Existing API description: Describes the various options that Backstage frontend plugins have, in communicating with service APIs that already exist --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The frontend code examples on this page use the old frontend system APIs (`discoveryApiRef`, `fetchApiRef` from `@backstage/core-plugin-api`). The same APIs are available in the new frontend system via `@backstage/frontend-plugin-api`. The general guidance on when to use direct requests vs. the proxy vs. a backend plugin remains valid for both systems. + +::: + This article describes the various options that Backstage frontend plugins have, in communicating with service APIs that already exist. Each section below describes a possible choice, and the circumstances under which it fits. diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index d6c9abc98f29f8..35da80a5b2866e 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -4,6 +4,12 @@ title: Composability System description: Documentation for the Backstage plugin composability APIs. --- +:::caution Legacy Documentation + +This page describes the composability system for the **old frontend system**, including `createRoutableExtension`, `createComponentExtension`, `RouteRef`, `ExternalRouteRef`, and component data. For the new frontend system, see [Extensions](../frontend-system/architecture/20-extensions.md), [Extension Blueprints](../frontend-system/architecture/23-extension-blueprints.md), and [Routes](../frontend-system/architecture/36-routes.md). + +::: + ## Summary This page describes the composability system that helps bring together content diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index ea0b088af7508b..8c97a3d30dd984 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -4,6 +4,12 @@ title: Create a Backstage Plugin description: Documentation on How to Create a Backstage Plugin --- +:::caution Legacy Documentation + +This page describes creating plugins for the **old frontend system**. For creating plugins using the new frontend system, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). For creating backend plugins, see [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + A Backstage Plugin adds functionality to Backstage. ## Create a Plugin diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index 2d0e34ba4b215d..25251e3db51cc4 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -4,6 +4,12 @@ title: Feature Flags description: Details the process of defining setting and reading a feature flag. --- +:::caution Legacy Documentation + +This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system version, see [Feature Flags](../frontend-system/building-plugins/09-feature-flags.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. + +::: + Backstage offers the ability to define feature flags inside a plugin or during application creation. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. This page describes the process of defining, setting and reading a feature flag. If you are looking for using feature flags specifically with software templates please see [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). diff --git a/docs/plugins/index.md b/docs/plugins/index.md index ef45152f55e59d..388c3b1177e480 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -1,9 +1,15 @@ --- id: index -title: Introduction to Plugins -description: Learn about integrating various infrastructure and software development tools into Backstage through plugins. +title: Introduction to Plugins (Legacy) +description: Legacy documentation for integrating various infrastructure and software development tools into Backstage through plugins using the old frontend system. --- +:::caution Legacy Documentation + +This section covers plugin development using the **old frontend system**. For new development, please refer to the [new frontend system](../frontend-system/index.md) and [new backend system](../backend-system/index.md) documentation. The content here is kept for reference and for maintaining existing plugins that have not yet been migrated. + +::: + Backstage orchestrates a cohesive single-page application by seamlessly integrating various plugins. Our vision for the plugin ecosystem champions flexibility, empowering you to incorporate a broad spectrum of infrastructure and software development tools into Backstage as plugins. Adherence to stringent [design guidelines](../dls/design.md) guarantees a consistent and intuitive user experience across the entire plugin landscape. diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 417b9324516553..1e3ebb2d2b716e 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -4,6 +4,12 @@ title: Integrate into the Software Catalog description: How to integrate a plugin into software catalog --- +:::caution Legacy Documentation + +This page describes integrating plugins into the Software Catalog using the **old frontend system** patterns (`EntitySwitch`, `EntityLayout`, `EntityLayout.Route`). For the new frontend system, entity page integrations are done using `EntityCardBlueprint` and `EntityContentBlueprint` — see [Common Extension Blueprints](../frontend-system/building-plugins/03-common-extension-blueprints.md). + +::: + > This is an advanced use case and currently is an experimental feature. Expect > API to change over time diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 7a7201d9e6d495..cbd9c89e14dedc 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -4,6 +4,12 @@ title: Integrating Search into a plugin description: How to integrate Search into a Backstage plugin --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. + +::: + The Backstage Search Platform was designed to give plugin developers the APIs and interfaces needed to offer search experiences within their plugins, while abstracting away (and instead empowering application integrators to choose) the diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md index 2770f767990208..67a6b962a0fd7c 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -4,6 +4,12 @@ title: Internationalization description: Documentation on adding internationalization to plugins and apps --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. For the new frontend system version, see [Internationalization](../frontend-system/building-plugins/07-internationalization.md). The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. + +::: + ## Overview The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. diff --git a/docs/plugins/new-backend-system.md b/docs/plugins/new-backend-system.md index cf983615b5d2a5..e7cef270abfd97 100644 --- a/docs/plugins/new-backend-system.md +++ b/docs/plugins/new-backend-system.md @@ -4,6 +4,12 @@ title: New Backend System description: Details of the new backend system --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The canonical documentation for the backend system has moved to the [Backend System](../backend-system/index.md) section, which includes more detailed and up-to-date guides for [building plugins and modules](../backend-system/building-plugins-and-modules/01-index.md), [architecture](../backend-system/architecture/01-index.md), and [core services](../backend-system/core-services/01-index.md). + +::: + ## Status The new backend system is released and ready for production use, and many plugins and modules have already been migrated. We recommend all plugins and deployments to migrate to the new system. diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index 7803d90bd5e9df..eab2ed200cc1df 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -4,6 +4,12 @@ title: Observability description: Adding Observability to Your Plugin --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. For new backend system logging, see the [Logger](../backend-system/core-services/logger.md) and [Root Logger](../backend-system/core-services/root-logger.md) core service documentation. For health checks, see [Root Health](../backend-system/core-services/root-health.md). + +::: + This article briefly describes the observability options that are available to a Backstage integrator. diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index 5ba1de4a7d3502..89318e7c5f8134 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -4,6 +4,12 @@ title: Plugin Development description: Documentation on Plugin Development --- +:::caution Legacy Documentation + +This page covers plugin development patterns for the **old frontend system**, including `createPlugin`, `createRoutableExtension`, and `RouteRef` from `@backstage/core-plugin-api`. For the new frontend system equivalents, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md) and [Routes](../frontend-system/architecture/36-routes.md). + +::: + Backstage plugins provide features to a Backstage App. Each plugin is treated as a self-contained web app and can include almost any diff --git a/docs/plugins/plugin-directory-audit.md b/docs/plugins/plugin-directory-audit.md index 9e82cb6de52ff8..b080463c829c6b 100644 --- a/docs/plugins/plugin-directory-audit.md +++ b/docs/plugins/plugin-directory-audit.md @@ -4,6 +4,12 @@ title: Plugin Directory Audit description: Details about the process for auditing plugins in the directory --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The audit process described here is still current. + +::: + ## Audit Process We have a simple process in place to audit the plugins in the Plugin Directory: diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index bd89fc6c36b46d..a764ea64d2ef72 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -4,6 +4,12 @@ title: Proxying description: Documentation on Proxying --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The proxy configuration and usage described here applies to both the old and new backend systems. For creating backend plugins and modules, see [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + This page describes how to configure and use the built-in HTTP proxy functionality in your Backstage backend. ## Overview diff --git a/docs/plugins/publish-private.md b/docs/plugins/publish-private.md deleted file mode 100644 index d04449d46569b4..00000000000000 --- a/docs/plugins/publish-private.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -id: publish-private -title: Publish private -description: Documentation on How to Publish private ---- - -## TODO diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 7cfaca45619491..3843359857c8f7 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -4,6 +4,12 @@ title: Structure of a Plugin description: Details about structure of a plugin --- +:::caution Legacy Documentation + +This page describes the structure of a plugin for the **old frontend system**. For the new frontend system, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). The general folder structure is similar, but the plugin wiring in `plugin.ts` differs significantly. + +::: + Nice, you have a new plugin! We'll soon see how we can develop it into doing great things. But first off, let's look at what we get out of the box. diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 1297d516f311cb..94015700fe4800 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -4,6 +4,12 @@ title: Testing with Jest description: Documentation on How to do unit testing with Jest --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The general testing principles described here still apply, but for system-specific testing guides, see [Testing Frontend Plugins](../frontend-system/building-plugins/02-testing.md) and [Testing Backend Plugins and Modules](../backend-system/building-plugins-and-modules/02-testing.md). + +::: + :::note Note You may want to consider migrating to Jest 30, to do this, you can follow this guide: [Migrating to Jest 30](../tutorials/jest30-migration.md) diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md index 552076ed665ce6..69b89a51c7542f 100644 --- a/docs/tooling/cli/04-templates.md +++ b/docs/tooling/cli/04-templates.md @@ -83,6 +83,8 @@ When defining the `templates` array it will override the default set of template "new": { "templates": [ "@backstage/cli-module-new/templates/frontend-plugin", + "@backstage/cli-module-new/templates/frontend-plugin-module", + "@backstage/cli-module-new/templates/legacy-frontend-plugin", "@backstage/cli-module-new/templates/backend-plugin", "@backstage/cli-module-new/templates/backend-plugin-module", "@backstage/cli-module-new/templates/plugin-web-library", @@ -90,6 +92,7 @@ When defining the `templates` array it will override the default set of template "@backstage/cli-module-new/templates/plugin-common-library", "@backstage/cli-module-new/templates/web-library", "@backstage/cli-module-new/templates/node-library", + "@backstage/cli-module-new/templates/cli-module", "@backstage/cli-module-new/templates/catalog-provider-module", "@backstage/cli-module-new/templates/scaffolder-backend-module" ] diff --git a/docs/tutorials/corporate-proxy.md b/docs/tutorials/corporate-proxy.md new file mode 100644 index 00000000000000..e4346e4f9479c7 --- /dev/null +++ b/docs/tutorials/corporate-proxy.md @@ -0,0 +1,33 @@ +--- +id: corporate-proxy +title: Running Backstage behind a Corporate Proxy +description: Guide on how to configure Backstage to work behind a corporate proxy using Node.js built-in proxy support. +--- + +# Running Backstage behind a Corporate Proxy + +Sometimes you have to run Backstage with no direct access to the public internet, except through a corporate proxy. The backend is most likely where you'll run into proxy issues, as it isn't helped by your browser or OS proxy settings. + +## Using Node.js built-in proxy support + +Starting with Node.js 22.21.0 and 24.5.0, Node.js has built-in support for proxy environment variables. When enabled, native `fetch()`, `node:http`, and `node:https` all respect the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables without any additional packages. See the [Node.js enterprise network configuration docs](https://nodejs.org/en/learn/http/enterprise-network-configuration#proxy-configuration) for full details. + +To enable the built-in proxy support, set the `NODE_USE_ENV_PROXY` environment variable along with your proxy settings: + +```sh +export HTTP_PROXY=http://username:password@proxy.example.net:8888 +export HTTPS_PROXY=http://username:password@proxy.example.net:8888 +export NO_PROXY=localhost,127.0.0.1,.internal.company.com +export NODE_USE_ENV_PROXY=1 +yarn start +``` + +### Compatibility with third-party fetch libraries + +Per [ADR014](../architecture-decisions/adr014-use-fetch.md), Backstage backend code should use native `fetch()`, which works with Node.js's proxy out of the box. Some core packages and many [community plugins](https://github.com/backstage/community-plugins/) still use `node-fetch` (see [ADR013](../architecture-decisions/adr013-use-node-fetch.md)) or `cross-fetch` (for isomorphic packages). Both libraries delegate to `node:http`/`node:https` internally and do **not** set a custom HTTP agent by default, which means Node.js's proxy works for them as well. + +The exception is code that explicitly passes a custom `agent` to its fetch calls (e.g. the Kubernetes plugins, which use `new https.Agent(...)` for TLS client certificates). In those cases, the custom agent takes precedence and the built-in proxy is bypassed. This is generally the desired behavior, since those agents are configured for direct connections to specific endpoints like cluster APIs. + +## Legacy approach + +If you are on a Node.js version older than 22.21.0, you can use third-party packages to add proxy support. See the [legacy proxy setup guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md) for instructions using `undici`, `global-agent`, and `proxy-agent`. diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 718a337b014b19..1cff303d548fef 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -237,7 +237,7 @@ const config: Config = { }, { from: '/docs/features/software-templates/testing-scaffolder-alpha', - to: '/docs/features/software-templates/migrating-to-rjsf-v5', + to: '/docs/features/software-templates/', }, { from: '/docs/auth/glossary', diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index ec4585b4077887..24472f4477cb5e 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -302,8 +302,6 @@ export default { 'features/software-templates/writing-custom-field-extensions', 'features/software-templates/writing-custom-step-layouts', 'features/software-templates/authorizing-scaffolder-template-details', - 'features/software-templates/migrating-to-rjsf-v5', - 'features/software-templates/migrating-from-v1beta2-to-v1beta3', 'features/software-templates/dry-run-testing', 'features/software-templates/experimental', 'features/software-templates/templating-extensions', @@ -410,60 +408,6 @@ export default { sidebarElementWithIndex({ label: 'Okta' }, ['integrations/okta/org']), ], ), - sidebarElementWithIndex( - { - label: 'Plugins', - description: 'Extend Backstage with custom functionality.', - }, - [ - 'plugins/index', - 'plugins/create-a-plugin', - 'plugins/plugin-development', - 'plugins/structure-of-a-plugin', - 'plugins/integrating-plugin-into-software-catalog', - 'plugins/integrating-search-into-plugins', - 'plugins/composability', - 'plugins/internationalization', - 'plugins/analytics', - 'plugins/feature-flags', - sidebarElementWithIndex( - { - label: 'OpenAPI', - description: - 'Work with OpenAPI specifications and generate clients.', - }, - [ - 'openapi/01-getting-started', - 'openapi/generate-client', - 'openapi/test-case-validation', - ], - ), - sidebarElementWithIndex( - { - label: 'Backends and APIs', - description: 'Build and manage backend services and APIs.', - }, - [ - 'plugins/proxying', - 'plugins/backend-plugin', - 'plugins/call-existing-api', - ], - ), - sidebarElementWithIndex( - { label: 'Testing', description: 'Testing plugins and modules.' }, - ['plugins/testing'], - ), - sidebarElementWithIndex( - { label: 'Publishing', description: 'Publishing your plugins.' }, - [ - 'plugins/publish-private', - 'plugins/add-to-directory', - 'plugins/plugin-directory-audit', - ], - ), - 'plugins/observability', - ], - ), sidebarElementWithIndex( { label: 'Configuration', @@ -597,6 +541,9 @@ export default { 'frontend-system/building-plugins/common-extension-blueprints', 'frontend-system/building-plugins/built-in-data-refs', 'frontend-system/building-plugins/migrating', + 'frontend-system/building-plugins/internationalization', + 'frontend-system/building-plugins/analytics', + 'frontend-system/building-plugins/feature-flags', ], ), sidebarElementWithIndex( @@ -666,6 +613,18 @@ export default { 'conf/user-interface/sidebar', ], ), + sidebarElementWithIndex( + { + label: 'OpenAPI', + description: + 'Work with OpenAPI specifications and generate clients.', + }, + [ + 'openapi/01-getting-started', + 'openapi/generate-client', + 'openapi/test-case-validation', + ], + ), ], ), sidebarElementWithIndex( @@ -719,6 +678,46 @@ export default { ), ], ), + sidebarElementWithIndex( + { + label: 'Plugins (Legacy)', + description: + 'Legacy plugin development documentation for the old frontend system. For new development, see the Frontend System and Backend System sections under Framework.', + }, + [ + 'plugins/index', + 'plugins/create-a-plugin', + 'plugins/plugin-development', + 'plugins/structure-of-a-plugin', + 'plugins/integrating-plugin-into-software-catalog', + 'plugins/integrating-search-into-plugins', + 'plugins/composability', + 'plugins/internationalization', + 'plugins/analytics', + 'plugins/feature-flags', + sidebarElementWithIndex( + { + label: 'Backends and APIs', + description: 'Build and manage backend services and APIs.', + }, + [ + 'plugins/proxying', + 'plugins/backend-plugin', + 'plugins/call-existing-api', + ], + ), + sidebarElementWithIndex( + { label: 'Testing', description: 'Testing plugins and modules.' }, + ['plugins/testing'], + ), + sidebarElementWithIndex( + { label: 'Publishing', description: 'Publishing your plugins.' }, + ['plugins/add-to-directory', 'plugins/plugin-directory-audit'], + ), + 'plugins/observability', + 'plugins/new-backend-system', + ], + ), sidebarElementWithIndex( { label: 'FAQ', description: 'Frequently asked questions and answers.' }, ['faq/index', 'faq/product', 'faq/technical'], diff --git a/mkdocs.yml b/mkdocs.yml index 618f77a59f8a03..e01ab2a4311c00 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,7 +80,6 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Templating Extensions: 'features/software-templates/templating-extensions.md' - - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' - Dry Run Testing: 'features/software-templates/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' @@ -141,7 +140,7 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - Plugins: + - Plugins (Legacy): - Intro to plugins: 'plugins/index.md' - Create a Backstage Plugin: 'plugins/create-a-plugin.md' - Plugin Development: 'plugins/plugin-development.md' @@ -162,9 +161,9 @@ nav: - Testing: - Testing with Jest: 'plugins/testing.md' - Publishing: - - Publish private: 'plugins/publish-private.md' - Add to Directory: 'plugins/add-to-directory.md' - Observability: 'plugins/observability.md' + - New Backend System: 'plugins/new-backend-system.md' - Configuration: - Static Configuration in Backstage: 'conf/index.md' - Reading Backstage Configuration: 'conf/reading.md' diff --git a/package.json b/package.json index c89dcefce6c1cc..0c969724d23e2f 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "typedoc": "^0.28.0", "typescript": "~5.7.0", "vite": "^7.1.5", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "packageManager": "yarn@4.8.1", "engines": { diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index fb4e29a3cea26f..7cb4e56a8f8f80 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -196,7 +196,7 @@ "winston-transport": "^4.5.0", "yauzl": "^3.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index 73b5126ab5cb91..01255db10c3639 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -25,7 +25,7 @@ import { } from '@backstage/backend-plugin-api'; import PromiseRouter from 'express-promise-router'; import { Router, json } from 'express'; -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { ActionsRegistryActionOptions, diff --git a/packages/backend-defaults/src/entrypoints/auditor/types.ts b/packages/backend-defaults/src/entrypoints/auditor/types.ts index 7827a31d1f8721..49a6de75eaa0eb 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/types.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @internal */ export const severityLogLevelMappingsSchema = z.record( diff --git a/packages/backend-defaults/src/entrypoints/auditor/utils.ts b/packages/backend-defaults/src/entrypoints/auditor/utils.ts index 719a227bf97d05..bf1e1f47560028 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/utils.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/utils.ts @@ -16,7 +16,7 @@ import type { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types'; /** diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts index f691ef78a072c2..c7f77e7a850f4d 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -17,7 +17,7 @@ import { JsonObject } from '@backstage/types'; import { CronTime } from 'cron'; import { Duration } from 'luxon'; -import { z } from 'zod'; +import { z } from 'zod/v3'; function isValidOptionalDurationString(d: string | undefined): boolean { try { diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 8f674cda44a977..f12b75d57f259d 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -66,7 +66,7 @@ "json-schema": "^0.4.0", "knex": "^3.0.0", "luxon": "^3.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 0f2c220609505d..72d9a2561e3f93 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnyZodObject } from 'zod'; +import { AnyZodObject } from 'zod/v3'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; @@ -11,7 +11,7 @@ import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @alpha (undocumented) export type ActionsRegistryActionContext = { diff --git a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts index a707e350e95b66..20b4d769ccd44d 100644 --- a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService, diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts index a17c82c6f29776..05f6879f0711b2 100644 --- a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService } from './LoggerService'; import { BackstageCredentials } from './AuthService'; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index c2d2268878447c..373f74fcbaad7d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -79,7 +79,7 @@ "text-extensions": "^2.4.0", "uuid": "^11.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md index b6bf9e883d79bf..4813f49c35a0ba 100644 --- a/packages/backend-test-utils/report-alpha.api.md +++ b/packages/backend-test-utils/report-alpha.api.md @@ -7,7 +7,7 @@ import { ActionsRegistryActionOptions } from '@backstage/backend-plugin-api/alph import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; import { ActionsServiceAction } from '@backstage/backend-plugin-api/alpha'; -import { AnyZodObject } from 'zod'; +import { AnyZodObject } from 'zod/v3'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; diff --git a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts index 94d844746f88d7..03edb42fad3909 100644 --- a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts +++ b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts @@ -19,7 +19,7 @@ import { } from '@backstage/backend-plugin-api'; import { InputError, NotFoundError } from '@backstage/errors'; import { JsonObject, JsonValue } from '@backstage/types'; -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { mockCredentials } from '../../services'; import { diff --git a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml index f5807537b3e6b4..f1847154ced74c 100644 --- a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml +++ b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml @@ -8,7 +8,7 @@ spec: lifecycle: experimental owner: team-c definition: | - import { z } from 'zod'; + import { z } from 'zod/v3'; import { publicProcedure, router } from '../trpc'; export const apiRouter = router({ diff --git a/packages/cli-internal/src/authIdentifiers.ts b/packages/cli-internal/src/authIdentifiers.ts new file mode 100644 index 00000000000000..6b72554a4fdaaa --- /dev/null +++ b/packages/cli-internal/src/authIdentifiers.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Returns the secret-store service key for a given auth instance. */ +export function getAuthInstanceService(instanceName: string): string { + return `backstage-cli:auth-instance:${instanceName}`; +} diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts index 0e69d450327c6f..281b998d92ddb7 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -25,3 +25,9 @@ export { OpaqueCommandLeafNode, isCommandNodeHidden, } from './InternalCommandNode'; +export { getAuthInstanceService } from './authIdentifiers'; +export { + getSecretStore, + resetSecretStore, + type SecretStore, +} from './secretStore'; diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-internal/src/secretStore.ts similarity index 88% rename from packages/cli-module-auth/src/lib/secretStore.ts rename to packages/cli-internal/src/secretStore.ts index a0f3c81d51484c..26c1e6ad2be033 100644 --- a/packages/cli-module-auth/src/lib/secretStore.ts +++ b/packages/cli-internal/src/secretStore.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import fs from 'fs-extra'; +import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -/** @public */ export type SecretStore = { get(service: string, account: string): Promise; set(service: string, account: string, secret: string): Promise; @@ -55,6 +54,15 @@ class KeytarSecretStore implements SecretStore { } } +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + class FileSecretStore implements SecretStore { private readonly baseDir: string; constructor() { @@ -74,23 +82,30 @@ class FileSecretStore implements SecretStore { } async get(service: string, account: string): Promise { const file = this.filePath(service, account); - if (!(await fs.pathExists(file))) return undefined; + if (!(await pathExists(file))) { + return undefined; + } return await fs.readFile(file, 'utf8'); } async set(service: string, account: string, secret: string): Promise { const file = this.filePath(service, account); - await fs.ensureDir(path.dirname(file)); + await fs.mkdir(path.dirname(file), { recursive: true }); await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 }); } async delete(service: string, account: string): Promise { const file = this.filePath(service, account); - await fs.remove(file); + try { + await fs.unlink(file); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } } } let singleton: SecretStore | undefined; -/** @public */ export async function getSecretStore(): Promise { if (!singleton) { const keytar = await loadKeytar(); diff --git a/packages/cli-module-actions/package.json b/packages/cli-module-actions/package.json index fe19a8a90da972..86c99973222a97 100644 --- a/packages/cli-module-actions/package.json +++ b/packages/cli-module-actions/package.json @@ -33,9 +33,10 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/cli-module-auth": "workspace:^", "@backstage/cli-node": "workspace:^", - "cleye": "^2.3.0" + "@backstage/errors": "workspace:^", + "cleye": "^2.3.0", + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/cli-module-actions/src/commands/execute.ts b/packages/cli-module-actions/src/commands/execute.ts index 4ff955096b1d83..f130c597ce5849 100644 --- a/packages/cli-module-actions/src/commands/execute.ts +++ b/packages/cli-module-actions/src/commands/execute.ts @@ -65,9 +65,9 @@ export default async ({ args, info }: CliCommandContext) => { process.exit(1); } - const { accessToken, instance } = await resolveAuth(instanceFlag); + const { accessToken, baseUrl } = await resolveAuth(instanceFlag); - const client = new ActionsClient(instance.baseUrl, accessToken); + const client = new ActionsClient(baseUrl, accessToken); const actions = await client.listForPlugin(actionId); const action = actions.find(a => a.id === actionId); diff --git a/packages/cli-module-actions/src/commands/list.ts b/packages/cli-module-actions/src/commands/list.ts index 601fa6d2c7e805..2049fc3e44f611 100644 --- a/packages/cli-module-actions/src/commands/list.ts +++ b/packages/cli-module-actions/src/commands/list.ts @@ -36,7 +36,7 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - const { accessToken, pluginSources, instance } = await resolveAuth( + const { accessToken, pluginSources, baseUrl } = await resolveAuth( instanceFlag, ); @@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - const client = new ActionsClient(instance.baseUrl, accessToken); + const client = new ActionsClient(baseUrl, accessToken); const actions = await client.list(pluginSources); if (!actions.length) { diff --git a/packages/cli-module-actions/src/commands/sourcesAdd.ts b/packages/cli-module-actions/src/commands/sourcesAdd.ts index 2a8135b63f8d5b..fee30076375408 100644 --- a/packages/cli-module-actions/src/commands/sourcesAdd.ts +++ b/packages/cli-module-actions/src/commands/sourcesAdd.ts @@ -15,12 +15,10 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -34,9 +32,10 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; - const instance = await getSelectedInstance(); - const existing = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const existing = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (existing.includes(pluginId)) { process.stderr.write( @@ -45,10 +44,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - await updateInstanceConfig(instance.name, 'pluginSources', [ - ...existing, - pluginId, - ]); + await auth.setMetadata('pluginSources', [...existing, pluginId]); process.stdout.write(`Added plugin source "${pluginId}".\n`); }; diff --git a/packages/cli-module-actions/src/commands/sourcesList.ts b/packages/cli-module-actions/src/commands/sourcesList.ts index 68b368efd5db2f..7557922c069723 100644 --- a/packages/cli-module-actions/src/commands/sourcesList.ts +++ b/packages/cli-module-actions/src/commands/sourcesList.ts @@ -15,18 +15,18 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { cli({ help: info }, undefined, args); - const instance = await getSelectedInstance(); - const sources = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const sources = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (!sources.length) { process.stderr.write('No plugin sources configured.\n'); diff --git a/packages/cli-module-actions/src/commands/sourcesRemove.ts b/packages/cli-module-actions/src/commands/sourcesRemove.ts index 731abe790dbabc..922522850315b3 100644 --- a/packages/cli-module-actions/src/commands/sourcesRemove.ts +++ b/packages/cli-module-actions/src/commands/sourcesRemove.ts @@ -15,12 +15,10 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -34,17 +32,17 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; - const instance = await getSelectedInstance(); - const existing = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const existing = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (!existing.includes(pluginId)) { process.stderr.write(`Plugin source "${pluginId}" is not configured.\n`); return; } - await updateInstanceConfig( - instance.name, + await auth.setMetadata( 'pluginSources', existing.filter(s => s !== pluginId), ); diff --git a/packages/cli-module-actions/src/lib/ActionsClient.test.ts b/packages/cli-module-actions/src/lib/ActionsClient.test.ts index 61526873c988f6..3aa2e06dc77653 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.test.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.test.ts @@ -15,9 +15,9 @@ */ import { ActionsClient } from './ActionsClient'; -import { httpJson } from '@backstage/cli-module-auth'; +import { httpJson } from './httpJson'; -jest.mock('@backstage/cli-module-auth', () => ({ +jest.mock('./httpJson', () => ({ httpJson: jest.fn(), })); diff --git a/packages/cli-module-actions/src/lib/ActionsClient.ts b/packages/cli-module-actions/src/lib/ActionsClient.ts index aa04902b2faa98..736a3b7f3754d1 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { httpJson } from '@backstage/cli-module-auth'; +import { httpJson } from './httpJson'; export type ActionDef = { id: string; diff --git a/packages/cli-module-actions/src/lib/httpJson.ts b/packages/cli-module-actions/src/lib/httpJson.ts new file mode 100644 index 00000000000000..0bea9dab94c786 --- /dev/null +++ b/packages/cli-module-actions/src/lib/httpJson.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ResponseError } from '@backstage/errors'; + +type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +export async function httpJson(url: string, init?: HttpInit): Promise { + const res = await fetch(url, { + ...init, + body: init?.body ? JSON.stringify(init.body) : undefined, + headers: { + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + if (!res.ok) { + throw await ResponseError.fromResponse(res); + } + return (await res.json()) as T; +} diff --git a/packages/cli-module-actions/src/lib/resolveAuth.test.ts b/packages/cli-module-actions/src/lib/resolveAuth.test.ts index f2702cf757666b..2aabefe9eaf8a7 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.test.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.test.ts @@ -15,99 +15,66 @@ */ import { resolveAuth } from './resolveAuth'; -import { - getSelectedInstance, - getInstanceConfig, - accessTokenNeedsRefresh, - refreshAccessToken, - getSecretStore, - type StoredInstance, -} from '@backstage/cli-module-auth'; +import { CliAuth } from '@backstage/cli-node'; -jest.mock('@backstage/cli-module-auth', () => ({ - getSelectedInstance: jest.fn(), - getInstanceConfig: jest.fn(), - accessTokenNeedsRefresh: jest.fn(), - refreshAccessToken: jest.fn(), - getSecretStore: jest.fn(), -})); - -const mockGetSelectedInstance = getSelectedInstance as jest.MockedFunction< - typeof getSelectedInstance ->; -const mockGetInstanceConfig = getInstanceConfig as jest.MockedFunction< - typeof getInstanceConfig ->; -const mockAccessTokenNeedsRefresh = - accessTokenNeedsRefresh as jest.MockedFunction< - typeof accessTokenNeedsRefresh - >; -const mockRefreshAccessToken = refreshAccessToken as jest.MockedFunction< - typeof refreshAccessToken ->; -const mockGetSecretStore = getSecretStore as jest.MockedFunction< - typeof getSecretStore ->; - -describe('resolveAuth', () => { - const mockInstance: StoredInstance = { - name: 'production', - baseUrl: 'https://backstage.example.com', - clientId: 'my-client', - issuedAt: Date.now(), - accessTokenExpiresAt: Date.now() + 3600_000, +jest.mock('@backstage/cli-node', () => { + const actual = jest.requireActual('@backstage/cli-node'); + return { + ...actual, + CliAuth: { create: jest.fn() }, }; +}); - const mockSecretStore = { - get: jest.fn(), - set: jest.fn(), - delete: jest.fn(), - }; +const mockCreate = CliAuth.create as jest.MockedFunction; +describe('resolveAuth', () => { beforeEach(() => { jest.clearAllMocks(); - mockGetSelectedInstance.mockResolvedValue(mockInstance); - mockAccessTokenNeedsRefresh.mockReturnValue(false); - mockGetSecretStore.mockResolvedValue(mockSecretStore); - mockSecretStore.get.mockResolvedValue('test-access-token'); - mockGetInstanceConfig.mockResolvedValue(['catalog', 'scaffolder']); }); it('resolves auth with the selected instance and stored token', async () => { + mockCreate.mockResolvedValue({ + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getMetadata: jest.fn().mockResolvedValue(['catalog', 'scaffolder']), + } as unknown as CliAuth); + const result = await resolveAuth(); - expect(mockGetSelectedInstance).toHaveBeenCalledWith(undefined); - expect(mockAccessTokenNeedsRefresh).toHaveBeenCalledWith(mockInstance); - expect(mockRefreshAccessToken).not.toHaveBeenCalled(); + expect(mockCreate).toHaveBeenCalledWith({ instanceName: undefined }); expect(result).toEqual({ - instance: mockInstance, + baseUrl: 'https://backstage.example.com', + instanceName: 'production', accessToken: 'test-access-token', pluginSources: ['catalog', 'scaffolder'], }); }); - it('passes instance name flag to getSelectedInstance', async () => { - await resolveAuth('staging'); - - expect(mockGetSelectedInstance).toHaveBeenCalledWith('staging'); - }); + it('passes instance name flag to CliAuth.create', async () => { + mockCreate.mockResolvedValue({ + getInstanceName: jest.fn().mockReturnValue('staging'), + getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'), + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getMetadata: jest.fn().mockResolvedValue([]), + } as unknown as CliAuth); - it('refreshes the access token when it is about to expire', async () => { - const refreshedInstance = { - ...mockInstance, - accessTokenExpiresAt: Date.now() + 7200_000, - }; - mockAccessTokenNeedsRefresh.mockReturnValue(true); - mockRefreshAccessToken.mockResolvedValue(refreshedInstance); - - const result = await resolveAuth(); + await resolveAuth('staging'); - expect(mockRefreshAccessToken).toHaveBeenCalledWith('production'); - expect(result.instance).toBe(refreshedInstance); + expect(mockCreate).toHaveBeenCalledWith({ instanceName: 'staging' }); }); - it('throws when no access token is stored', async () => { - mockSecretStore.get.mockResolvedValue(undefined); + it('throws when getAccessToken fails', async () => { + mockCreate.mockResolvedValue({ + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), + getAccessToken: jest + .fn() + .mockRejectedValue( + new Error('No access token found. Run "auth login" to authenticate.'), + ), + getMetadata: jest.fn().mockResolvedValue([]), + } as unknown as CliAuth); await expect(resolveAuth()).rejects.toThrow( 'No access token found. Run "auth login" to authenticate.', @@ -115,7 +82,12 @@ describe('resolveAuth', () => { }); it('returns empty plugin sources when none are configured', async () => { - mockGetInstanceConfig.mockResolvedValue(undefined); + mockCreate.mockResolvedValue({ + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getMetadata: jest.fn().mockResolvedValue(undefined), + } as unknown as CliAuth); const result = await resolveAuth(); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.ts b/packages/cli-module-actions/src/lib/resolveAuth.ts index 11fb6465451135..568bd4cdcdfd9e 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.ts @@ -14,35 +14,27 @@ * limitations under the License. */ -import { - getSelectedInstance, - getInstanceConfig, - accessTokenNeedsRefresh, - refreshAccessToken, - getSecretStore, - type StoredInstance, -} from '@backstage/cli-module-auth'; +import { CliAuth } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export async function resolveAuth(instanceFlag?: string): Promise<{ - instance: StoredInstance; + baseUrl: string; + instanceName: string; accessToken: string; pluginSources: string[]; }> { - let instance = await getSelectedInstance(instanceFlag); - - if (accessTokenNeedsRefresh(instance)) { - instance = await refreshAccessToken(instance.name); - } - - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } - - const pluginSources = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); + const pluginSources = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); - return { instance, accessToken, pluginSources }; + return { + baseUrl: auth.getBaseUrl(), + instanceName: auth.getInstanceName(), + accessToken, + pluginSources, + }; } diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index 94169e998eb81d..b0e2564661adc4 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -19,6 +19,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "bin": "bin/backstage-cli-module-auth", "files": [ "dist", "bin" @@ -50,6 +51,5 @@ }, "optionalDependencies": { "keytar": "^7.9.0" - }, - "bin": "bin/backstage-cli-module-auth" + } } diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md index a3b98e2f02d0f7..510bcaabe72f3a 100644 --- a/packages/cli-module-auth/report.api.md +++ b/packages/cli-module-auth/report.api.md @@ -5,67 +5,9 @@ ```ts import { CliModule } from '@backstage/cli-node'; -// @public (undocumented) -export function accessTokenNeedsRefresh(instance: StoredInstance): boolean; - // @public (undocumented) const _default: CliModule; export default _default; -// @public (undocumented) -export function getInstanceConfig( - instanceName: string, - key: string, -): Promise; - -// @public (undocumented) -export function getSecretStore(): Promise; - -// @public (undocumented) -export function getSelectedInstance( - instanceName?: string, -): Promise; - -// @public (undocumented) -export type HttpInit = { - headers?: Record; - method?: string; - body?: any; - signal?: AbortSignal; -}; - -// @public (undocumented) -export function httpJson(url: string, init?: HttpInit): Promise; - -// @public (undocumented) -export function refreshAccessToken( - instanceName: string, -): Promise; - -// @public (undocumented) -export type SecretStore = { - get(service: string, account: string): Promise; - set(service: string, account: string, secret: string): Promise; - delete(service: string, account: string): Promise; -}; - -// @public (undocumented) -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; - -// @public (undocumented) -export function updateInstanceConfig( - instanceName: string, - key: string, - value: unknown, -): Promise; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-module-auth/src/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts index b16060f956b08c..6bb875114718f7 100644 --- a/packages/cli-module-auth/src/commands/login.ts +++ b/packages/cli-module-auth/src/commands/login.ts @@ -27,7 +27,7 @@ import { getInstanceByName, StoredInstance, } from '../lib/storage'; -import { getSecretStore } from '../lib/secretStore'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import crypto from 'node:crypto'; import fs from 'fs-extra'; import path from 'node:path'; @@ -321,7 +321,7 @@ async function persistInstance(options: { const { instanceName, backendBaseUrl, clientId, token } = options; const secretStore = await getSecretStore(); await withMetadataLock(async () => { - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); await secretStore.set(service, 'accessToken', token.access_token); if (token.refresh_token) { await secretStore.set(service, 'refreshToken', token.refresh_token); diff --git a/packages/cli-module-auth/src/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts index f79ed2ef3559b5..a02fb3580d5686 100644 --- a/packages/cli-module-auth/src/commands/logout.ts +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import type { CliCommandContext } from '@backstage/cli-node'; -import { getSecretStore } from '../lib/secretStore'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import { removeInstance, withMetadataLock, @@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => { await withMetadataLock(async () => { const instance = await getInstanceByName(instanceName); const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; if (refreshToken) { diff --git a/packages/cli-module-auth/src/commands/printToken.ts b/packages/cli-module-auth/src/commands/printToken.ts index 39a78c1832d5a4..45b4cfd9f6982a 100644 --- a/packages/cli-module-auth/src/commands/printToken.ts +++ b/packages/cli-module-auth/src/commands/printToken.ts @@ -15,10 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; -import { getSelectedInstance } from '../lib/storage'; -import { getSecretStore } from '../lib/secretStore'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { @@ -37,18 +34,8 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - let instance = await getSelectedInstance(instanceFlag); - - if (accessTokenNeedsRefresh(instance)) { - instance = await refreshAccessToken(instance.name); - } - - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); process.stdout.write(`${accessToken}\n`); }; diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts index e1b7c62f72b4c3..275b6ca66db884 100644 --- a/packages/cli-module-auth/src/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -15,11 +15,8 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; import { httpJson } from '../lib/http'; -import { getSelectedInstance } from '../lib/storage'; -import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; -import { getSecretStore } from '../lib/secretStore'; export default async ({ args, info }: CliCommandContext) => { const { @@ -38,23 +35,13 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - let instance = await getSelectedInstance(instanceFlag); + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); - if (accessTokenNeedsRefresh(instance)) { - process.stdout.write('Refreshing access token...\n'); - instance = await refreshAccessToken(instance.name); - } - const authBase = new URL('/api/auth', instance.baseUrl) + const authBase = new URL('/api/auth', auth.getBaseUrl()) .toString() .replace(/\/$/, ''); - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } - const userinfo = await httpJson<{ claims: { sub: string; ent: string[] } }>( `${authBase}/v1/userinfo`, { diff --git a/packages/cli-module-auth/src/index.ts b/packages/cli-module-auth/src/index.ts index 38567e2174d89f..fef74ec8a59313 100644 --- a/packages/cli-module-auth/src/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -52,17 +52,3 @@ export default createCliModule({ }); }, }); - -/** @public */ -export { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, - type StoredInstance, -} from './lib/storage'; -/** @public */ -export { accessTokenNeedsRefresh, refreshAccessToken } from './lib/auth'; -/** @public */ -export { getSecretStore, type SecretStore } from './lib/secretStore'; -/** @public */ -export { httpJson, type HttpInit } from './lib/http'; diff --git a/packages/cli-module-auth/src/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts index 5ba4ae0295924c..18e7658e9228e1 100644 --- a/packages/cli-module-auth/src/lib/auth.test.ts +++ b/packages/cli-module-auth/src/lib/auth.test.ts @@ -16,15 +16,15 @@ import { accessTokenNeedsRefresh, refreshAccessToken } from './auth'; import * as storage from './storage'; -import * as secretStore from './secretStore'; +import * as internalCli from '@internal/cli'; import * as http from './http'; jest.mock('./storage'); -jest.mock('./secretStore'); +jest.mock('@internal/cli'); jest.mock('./http'); const mockStorage = storage as jest.Mocked; -const mockSecretStore = secretStore as jest.Mocked; +const mockInternalCli = internalCli as jest.Mocked; const mockHttp = http as jest.Mocked; describe('auth', () => { @@ -95,7 +95,10 @@ describe('auth', () => { beforeEach(() => { jest.clearAllMocks(); - mockSecretStore.getSecretStore.mockResolvedValue(mockSecretStoreInstance); + mockInternalCli.getSecretStore.mockResolvedValue(mockSecretStoreInstance); + mockInternalCli.getAuthInstanceService.mockImplementation( + (name: string) => `backstage-cli:auth-instance:${name}`, + ); }); it('should successfully refresh access token', async () => { diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index 0344d4636e95ca..1ad108cbf1a8db 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { - StoredInstance, + type StoredInstance, upsertInstance, withMetadataLock, getInstanceByName, } from './storage'; -import { getSecretStore } from './secretStore'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import { httpJson } from './http'; const TokenResponseSchema = z.object({ @@ -31,12 +31,11 @@ const TokenResponseSchema = z.object({ refresh_token: z.string().min(1).optional(), }); -/** @public */ export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { - return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; // 2 minutes before expiration + // 2 minutes before expiration + return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; } -/** @public */ export async function refreshAccessToken( instanceName: string, ): Promise { @@ -45,7 +44,7 @@ export async function refreshAccessToken( return withMetadataLock(async () => { const instance = await getInstanceByName(instanceName); - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; if (!refreshToken) { throw new Error( diff --git a/packages/cli-module-auth/src/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts index 4861a0772232df..4daaf823fbbb57 100644 --- a/packages/cli-module-auth/src/lib/http.ts +++ b/packages/cli-module-auth/src/lib/http.ts @@ -16,7 +16,6 @@ import { ResponseError } from '@backstage/errors'; -/** @public */ export type HttpInit = { headers?: Record; method?: string; @@ -24,7 +23,6 @@ export type HttpInit = { signal?: AbortSignal; }; -/** @public */ export async function httpJson(url: string, init?: HttpInit): Promise { const res = await fetch(url, { ...init, diff --git a/packages/cli-module-auth/src/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts index 6729f7c6a5ac95..f27d364c7c6edb 100644 --- a/packages/cli-module-auth/src/lib/secretStore.test.ts +++ b/packages/cli-module-auth/src/lib/secretStore.test.ts @@ -21,7 +21,7 @@ jest.mock('keytar', () => { import fs from 'fs-extra'; import path from 'node:path'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { getSecretStore, resetSecretStore } from './secretStore'; +import { getSecretStore, resetSecretStore } from '@internal/cli'; const mockDir = createMockDirectory(); diff --git a/packages/cli-module-auth/src/lib/storage.test.ts b/packages/cli-module-auth/src/lib/storage.test.ts index 9264c6bb89a552..042b0eb72a22c0 100644 --- a/packages/cli-module-auth/src/lib/storage.test.ts +++ b/packages/cli-module-auth/src/lib/storage.test.ts @@ -22,8 +22,8 @@ import { getAllInstances, getSelectedInstance, getInstanceByName, - getInstanceConfig, - updateInstanceConfig, + getInstanceMetadata, + updateInstanceMetadata, upsertInstance, removeInstance, setSelectedInstance, @@ -359,65 +359,65 @@ describe('storage', () => { }); }); - describe('getInstanceConfig', () => { - it('should return undefined when no config set', async () => { + describe('getInstanceMetadata', () => { + it('should return undefined when no metadata set', async () => { await upsertInstance(mockInstance1); - const result = await getInstanceConfig('production', 'someKey'); + const result = await getInstanceMetadata('production', 'someKey'); expect(result).toBeUndefined(); }); - it('should return config value for a key', async () => { + it('should return metadata value for a key', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'myKey', 'myValue'); + await updateInstanceMetadata('production', 'myKey', 'myValue'); - const result = await getInstanceConfig('production', 'myKey'); + const result = await getInstanceMetadata('production', 'myKey'); expect(result).toBe('myValue'); }); it('should throw NotFoundError for unknown instance', async () => { - await expect(getInstanceConfig('nonexistent', 'key')).rejects.toThrow( + await expect(getInstanceMetadata('nonexistent', 'key')).rejects.toThrow( NotFoundError, ); }); }); - describe('updateInstanceConfig', () => { - it('should set a config value', async () => { + describe('updateInstanceMetadata', () => { + it('should set a metadata value', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key1', 'value1'); - const result = await getInstanceConfig('production', 'key1'); + const result = await getInstanceMetadata('production', 'key1'); expect(result).toBe('value1'); }); - it('should preserve existing config keys', async () => { + it('should preserve existing metadata keys', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); - await updateInstanceConfig('production', 'key2', 'value2'); + await updateInstanceMetadata('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key2', 'value2'); - const result1 = await getInstanceConfig('production', 'key1'); - const result2 = await getInstanceConfig('production', 'key2'); + const result1 = await getInstanceMetadata('production', 'key1'); + const result2 = await getInstanceMetadata('production', 'key2'); expect(result1).toBe('value1'); expect(result2).toBe('value2'); }); it('should throw NotFoundError for unknown instance', async () => { await expect( - updateInstanceConfig('nonexistent', 'key', 'value'), + updateInstanceMetadata('nonexistent', 'key', 'value'), ).rejects.toThrow(NotFoundError); }); - it('should remove instance along with its config', async () => { + it('should remove instance along with its metadata', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key1', 'value1'); await removeInstance('production'); const { instances } = await getAllInstances(); expect(instances.find(i => i.name === 'production')).toBeUndefined(); await upsertInstance(mockInstance1); - const result = await getInstanceConfig('production', 'key1'); + const result = await getInstanceMetadata('production', 'key1'); expect(result).toBeUndefined(); }); }); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index 9d2551a4f9743d..c5d275149c318b 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -20,7 +20,17 @@ import os from 'node:os'; import path from 'node:path'; import lockfile from 'proper-lockfile'; import YAML from 'yaml'; -import { z } from 'zod'; +import { z } from 'zod/v3'; + +export type StoredInstance = { + name: string; + baseUrl: string; + clientId: string; + issuedAt: number; + accessTokenExpiresAt: number; + selected?: boolean; + metadata?: Record; +}; const METADATA_FILE = 'auth-instances.yaml'; @@ -36,20 +46,9 @@ const storedInstanceSchema = z.object({ issuedAt: z.number().int().nonnegative(), accessTokenExpiresAt: z.number().int().nonnegative(), selected: z.boolean().optional(), - config: z.record(z.string(), z.unknown()).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), }); -/** @public */ -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; - const authYamlSchema = z.object({ instances: z.array(storedInstanceSchema).default([]), }); @@ -97,9 +96,11 @@ export async function getAllInstances(): Promise<{ selected: StoredInstance | undefined; }> { const { instances } = await readAll(); + if (instances.length === 0) { + return { instances: [], selected: undefined }; + } const selected = instances.find(i => i.selected) ?? instances[0]; return { - // Normalize selection prop instances: instances.map(i => ({ ...i, selected: i.name === selected.name, @@ -108,7 +109,6 @@ export async function getAllInstances(): Promise<{ }; } -/** @public */ export async function getSelectedInstance( instanceName?: string, ): Promise { @@ -171,17 +171,15 @@ export async function setSelectedInstance(name: string): Promise { }); } -/** @public */ -export async function getInstanceConfig( +export async function getInstanceMetadata( instanceName: string, key: string, -): Promise { +): Promise { const instance = await getInstanceByName(instanceName); - return instance.config?.[key] as T | undefined; + return instance.metadata?.[key]; } -/** @public */ -export async function updateInstanceConfig( +export async function updateInstanceMetadata( instanceName: string, key: string, value: unknown, @@ -194,7 +192,7 @@ export async function updateInstanceConfig( } data.instances[idx] = { ...data.instances[idx], - config: { ...data.instances[idx].config, [key]: value }, + metadata: { ...data.instances[idx].metadata, [key]: value }, }; await writeAll(data); }); diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index d13fa4bac06c5c..9b32eca0fee0f6 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -34,6 +34,11 @@ "postpack": "backstage-cli package postpack", "test": "backstage-cli package test" }, + "jest": { + "coveragePathIgnorePatterns": [ + "/__fixtures__/" + ] + }, "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", diff --git a/packages/cli-module-new/src/lib/defaultTemplates.ts b/packages/cli-module-new/src/lib/defaultTemplates.ts index e8b8e7eb70c4a9..9bfd7f74edde74 100644 --- a/packages/cli-module-new/src/lib/defaultTemplates.ts +++ b/packages/cli-module-new/src/lib/defaultTemplates.ts @@ -16,6 +16,8 @@ export const defaultTemplates = [ '@backstage/cli-module-new/templates/frontend-plugin', + '@backstage/cli-module-new/templates/frontend-plugin-module', + '@backstage/cli-module-new/templates/legacy-frontend-plugin', '@backstage/cli-module-new/templates/backend-plugin', '@backstage/cli-module-new/templates/backend-plugin-module', '@backstage/cli-module-new/templates/plugin-web-library', diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts index 6a683fb31c8d64..13bae23f3c7ce3 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import fs from 'fs-extra'; import recursiveReaddir from 'recursive-readdir'; import { resolve as resolvePath, relative as relativePath } from 'node:path'; diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts index a89d0c3905bbbc..454ece648acab4 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts @@ -360,6 +360,141 @@ describe('loadPortableTemplateConfig', () => { ); }); + it('should filter out legacy frontend template for new frontend system apps', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({}), + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/frontend-defaults': '^0.1.0', + }, + }), + }, + }, + node_modules: Object.fromEntries( + defaultTemplates.map(t => { + // Match the real behavior: both frontend-plugin and legacy-frontend-plugin + // have the same template name "frontend-plugin" + const name = t.endsWith('/legacy-frontend-plugin') + ? 'frontend-plugin' + : basename(t); + return [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` }, + ]; + }), + ), + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(true); + + const templateNames = config.templatePointers.map(t => t.name); + expect(templateNames).toContain('frontend-plugin'); + expect(templateNames).toContain('frontend-plugin-module'); + expect(templateNames).toContain('backend-plugin'); + // Legacy template should be filtered out + expect(templateNames).not.toContain('legacy-frontend-plugin'); + + // The frontend-plugin in the list should be from the new template, not legacy + const frontendPlugin = config.templatePointers.find( + t => t.name === 'frontend-plugin', + ); + expect(frontendPlugin?.target).toContain('/frontend-plugin/'); + expect(frontendPlugin?.target).not.toContain('/legacy-frontend-plugin/'); + }); + + it('should filter out new frontend templates for legacy frontend system apps', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({}), + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/app-defaults': '^0.1.0', + '@backstage/core-app-api': '^0.1.0', + }, + }), + }, + }, + node_modules: Object.fromEntries( + defaultTemplates.map(t => { + const name = t.endsWith('/legacy-frontend-plugin') + ? 'frontend-plugin' + : basename(t); + return [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` }, + ]; + }), + ), + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(true); + + const templateNames = config.templatePointers.map(t => t.name); + // Legacy template should be present (shown as "frontend-plugin") + expect(templateNames).toContain('frontend-plugin'); + expect(templateNames).toContain('backend-plugin'); + // New frontend templates should be filtered out + expect(templateNames).not.toContain('frontend-plugin-module'); + + // The frontend-plugin in the list should be from the legacy template + const frontendPlugin = config.templatePointers.find( + t => t.name === 'frontend-plugin', + ); + expect(frontendPlugin?.target).toContain('/legacy-frontend-plugin/'); + }); + + it('should not filter templates when using explicit configuration', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['./my-frontend-plugin', './my-backend-plugin'], + }, + }, + }, + }), + // Even with a new frontend system app, explicit templates aren't filtered + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/frontend-defaults': '^0.1.0', + }, + }), + }, + }, + 'my-frontend-plugin': { + [TEMPLATE_FILE_NAME]: 'name: frontend-plugin\nrole: frontend-plugin\n', + }, + 'my-backend-plugin': { + [TEMPLATE_FILE_NAME]: 'name: backend-plugin\nrole: backend-plugin\n', + }, + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(false); + expect(config.templatePointers).toHaveLength(2); + expect(config.templatePointers.map(t => t.name)).toEqual([ + 'frontend-plugin', + 'backend-plugin', + ]); + }); + it('should handle missing backstage.new configuration', async () => { mockDir.setContent({ 'package.json': JSON.stringify({}), diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts index 50ffda751649e7..fd5a92bb91cf12 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts @@ -15,9 +15,8 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath, dirname, isAbsolute } from 'node:path'; +import { resolve as resolvePath, dirname, isAbsolute, join } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; - import { defaultTemplates } from '../defaultTemplates'; import { PortableTemplateConfig, @@ -25,10 +24,64 @@ import { TEMPLATE_FILE_NAME, } from '../types'; import { parse as parseYaml } from 'yaml'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { fromZodError } from 'zod-validation-error/v3'; import { ForwardedError } from '@backstage/errors'; +type FrontendSystem = 'new' | 'legacy' | 'unknown'; + +async function detectFrontendSystem(basePath: string): Promise { + const appPkgPath = join(basePath, 'packages', 'app', 'package.json'); + + try { + const appPkgJson = await fs.readJson(appPkgPath); + const deps = { + ...appPkgJson.dependencies, + ...appPkgJson.devDependencies, + }; + + if ( + deps['@backstage/frontend-defaults'] || + deps['@backstage/frontend-app-api'] + ) { + return 'new'; + } + if (deps['@backstage/app-defaults'] || deps['@backstage/core-app-api']) { + return 'legacy'; + } + } catch { + // App package doesn't exist or can't be read + } + + return 'unknown'; +} + +// Templates to exclude based on frontend system detection (by path, not name) +const newFrontendTemplates = [ + '@backstage/cli-module-new/templates/frontend-plugin', + '@backstage/cli-module-new/templates/frontend-plugin-module', +]; +const legacyFrontendTemplates = [ + '@backstage/cli-module-new/templates/legacy-frontend-plugin', +]; + +function filterTemplateEntriesForFrontendSystem( + entries: Array<{ pointer: PortableTemplatePointer; rawPointer: string }>, + frontendSystem: FrontendSystem, +): Array<{ pointer: PortableTemplatePointer; rawPointer: string }> { + if (frontendSystem === 'unknown') { + return entries; + } + + if (frontendSystem === 'new') { + // Filter out legacy frontend templates + return entries.filter(e => !legacyFrontendTemplates.includes(e.rawPointer)); + } + + // Legacy system - filter out new frontend templates + return entries.filter(e => !newFrontendTemplates.includes(e.rawPointer)); +} + const defaults = { license: 'Apache-2.0', version: '0.1.0', @@ -105,7 +158,9 @@ export async function loadPortableTemplateConfig( const config = parsed.data.backstage?.cli?.new; const basePath = dirname(pkgPath); - const templatePointerEntries = await Promise.all( + const isUsingDefaultTemplates = !config?.templates; + + let templatePointerEntries = await Promise.all( (config?.templates ?? defaultTemplates).map(async rawPointer => { try { const templatePath = resolveLocalTemplatePath(rawPointer, basePath); @@ -121,6 +176,17 @@ export async function loadPortableTemplateConfig( }), ); + // Auto-filter frontend templates based on detected frontend system. + // This must happen before the conflict check since both the new and legacy + // frontend plugin templates have the same name, but only one will be shown. + if (isUsingDefaultTemplates) { + const frontendSystem = await detectFrontendSystem(basePath); + templatePointerEntries = filterTemplateEntriesForFrontendSystem( + templatePointerEntries, + frontendSystem, + ); + } + const templateNameConflicts = new Map(); for (const { pointer, rawPointer } of templatePointerEntries) { const conflict = templateNameConflicts.get(pointer.name); @@ -143,7 +209,7 @@ export async function loadPortableTemplateConfig( ); return { - isUsingDefaultTemplates: !config?.templates, + isUsingDefaultTemplates, templatePointers: templatePointerEntries.map(({ pointer }) => pointer), license: overrides.license ?? config?.globals?.license ?? defaults.license, version: overrides.version ?? config?.globals?.version ?? defaults.version, diff --git a/packages/cli-module-new/templates/backend-plugin/package.json.hbs b/packages/cli-module-new/templates/backend-plugin/package.json.hbs index 4e23376c566733..eb08e2a54b0c20 100644 --- a/packages/cli-module-new/templates/backend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/backend-plugin/package.json.hbs @@ -29,7 +29,7 @@ "@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}", "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", - "zod": "{{versionQuery 'zod' '3.25.76'}}" + "zod": "{{versionQuery 'zod' '^3.25.76 || ^4.0.0'}}" }, "devDependencies": { "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", diff --git a/packages/cli-module-new/templates/backend-plugin/src/router.ts b/packages/cli-module-new/templates/backend-plugin/src/router.ts index fba8a24cb29348..0fbbc54cc36512 100644 --- a/packages/cli-module-new/templates/backend-plugin/src/router.ts +++ b/packages/cli-module-new/templates/backend-plugin/src/router.ts @@ -1,6 +1,6 @@ import { HttpAuthService } from '@backstage/backend-plugin-api'; import { InputError } from '@backstage/errors'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import express from 'express'; import Router from 'express-promise-router'; import { todoListServiceRef } from './services/TodoListService'; diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/.eslintrc.js.hbs b/packages/cli-module-new/templates/frontend-plugin-module/.eslintrc.js.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/.eslintrc.js.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/.eslintrc.js.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/README.md.hbs b/packages/cli-module-new/templates/frontend-plugin-module/README.md.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/README.md.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/README.md.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/package.json.hbs b/packages/cli-module-new/templates/frontend-plugin-module/package.json.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/package.json.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/package.json.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/portable-template.yaml b/packages/cli-module-new/templates/frontend-plugin-module/portable-template.yaml similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/portable-template.yaml rename to packages/cli-module-new/templates/frontend-plugin-module/portable-template.yaml diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/index.ts.hbs b/packages/cli-module-new/templates/frontend-plugin-module/src/index.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/index.ts.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/src/index.ts.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/module.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin-module/src/module.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/module.tsx.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/src/module.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/setupTests.ts b/packages/cli-module-new/templates/frontend-plugin-module/src/setupTests.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/setupTests.ts rename to packages/cli-module-new/templates/frontend-plugin-module/src/setupTests.ts diff --git a/packages/cli-module-new/templates/frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/frontend-plugin/README.md.hbs index 5eae32b7dc26ec..3f1ff3e4f7c1d9 100644 --- a/packages/cli-module-new/templates/frontend-plugin/README.md.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/README.md.hbs @@ -6,7 +6,14 @@ _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). +Your plugin has been added to the app in this repository, meaning you'll be able +to access it by running `yarn start` in the root directory, and then navigating +to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). + +This plugin is built with Backstage's [new frontend +system](https://backstage.io/docs/frontend-system/architecture/index), and you +can find more information about building plugins in the [plugin builder +documentation](https://backstage.io/docs/frontend-system/building-plugins/index). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/packages/cli-module-new/templates/new-frontend-plugin/dev/index.tsx b/packages/cli-module-new/templates/frontend-plugin/dev/index.tsx similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/dev/index.tsx rename to packages/cli-module-new/templates/frontend-plugin/dev/index.tsx diff --git a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs index 9499d11b182d25..955c6077b84514 100644 --- a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", - "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", + "@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}", "@backstage/theme": "{{versionQuery '@backstage/theme'}}", "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", @@ -31,22 +31,18 @@ "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "peerDependencies": { - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", - "@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}", - "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", + "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", + "@backstage/frontend-defaults": "{{versionQuery '@backstage/frontend-defaults'}}", + "@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}", "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}", - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml index 3afb60eb223fd5..a70ce316f1503a 100644 --- a/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml +++ b/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml @@ -3,4 +3,3 @@ role: frontend-plugin description: A new frontend plugin values: pluginVar: '{{ camelCase pluginId }}Plugin' - extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index c7c20863c2fa45..d1a8a93d0b95d9 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -5,7 +5,7 @@ import { screen } from '@testing-library/react'; import { registerMswTestHooks, renderInTestApp, -} from '@backstage/test-utils'; +} from '@backstage/frontend-test-utils'; describe('ExampleComponent', () => { const server = setupServer(); diff --git a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 820e54fa377363..c9b553287f1ce3 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,4 +1,4 @@ -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/frontend-test-utils'; import { ExampleFetchComponent } from './ExampleFetchComponent'; describe('ExampleFetchComponent', () => { diff --git a/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs index be4881efafc5db..0d6810ad6b94e7 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs @@ -1 +1 @@ -export { {{ pluginVar }}, {{ extensionName }} } from './plugin'; +export { {{ pluginVar }} as default } from './plugin'; diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/plugin.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/plugin.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/plugin.tsx.hbs rename to packages/cli-module-new/templates/frontend-plugin/src/plugin.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/routes.ts b/packages/cli-module-new/templates/frontend-plugin/src/routes.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/routes.ts rename to packages/cli-module-new/templates/frontend-plugin/src/routes.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/.eslintrc.js.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/.eslintrc.js.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/.eslintrc.js.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/.eslintrc.js.hbs diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs new file mode 100644 index 00000000000000..5eae32b7dc26ec --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs @@ -0,0 +1,13 @@ +# {{pluginId}} + +Welcome to the {{pluginId}} plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/packages/cli-module-new/templates/frontend-plugin/dev/index.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/dev/index.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/dev/index.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/dev/index.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs similarity index 72% rename from packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs index 4a37a4fa633a27..9499d11b182d25 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", - "@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}", + "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", "@backstage/theme": "{{versionQuery '@backstage/theme'}}", "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", @@ -31,17 +31,22 @@ "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "peerDependencies": { - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", - "@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}", + "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", + "@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}", + "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}", - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml new file mode 100644 index 00000000000000..d69a1f35df8e53 --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml @@ -0,0 +1,6 @@ +name: frontend-plugin +role: frontend-plugin +description: A new frontend plugin (legacy system) +values: + pluginVar: '{{ camelCase pluginId }}Plugin' + extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs similarity index 94% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index d1a8a93d0b95d9..c7c20863c2fa45 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -5,7 +5,7 @@ import { screen } from '@testing-library/react'; import { registerMswTestHooks, renderInTestApp, -} from '@backstage/frontend-test-utils'; +} from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/index.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/index.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/index.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/index.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs similarity index 91% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index c9b553287f1ce3..820e54fa377363 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,4 +1,4 @@ -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; import { ExampleFetchComponent } from './ExampleFetchComponent'; describe('ExampleFetchComponent', () => { diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/index.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/index.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/index.ts diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs new file mode 100644 index 00000000000000..be4881efafc5db --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs @@ -0,0 +1 @@ +export { {{ pluginVar }}, {{ extensionName }} } from './plugin'; diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/plugin.test.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.test.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/plugin.test.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.test.ts.hbs diff --git a/packages/cli-module-new/templates/frontend-plugin/src/plugin.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/src/plugin.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.ts.hbs diff --git a/packages/cli-module-new/templates/frontend-plugin/src/routes.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/routes.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/src/routes.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/routes.ts.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/setupTests.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/setupTests.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/setupTests.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/setupTests.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs deleted file mode 100644 index 3f1ff3e4f7c1d9..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs +++ /dev/null @@ -1,20 +0,0 @@ -# {{pluginId}} - -Welcome to the {{pluginId}} plugin! - -_This plugin was created through the Backstage CLI_ - -## Getting started - -Your plugin has been added to the app in this repository, meaning you'll be able -to access it by running `yarn start` in the root directory, and then navigating -to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). - -This plugin is built with Backstage's [new frontend -system](https://backstage.io/docs/frontend-system/architecture/index), and you -can find more information about building plugins in the [plugin builder -documentation](https://backstage.io/docs/frontend-system/building-plugins/index). - -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml deleted file mode 100644 index a70ce316f1503a..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: frontend-plugin -role: frontend-plugin -description: A new frontend plugin -values: - pluginVar: '{{ camelCase pluginId }}Plugin' diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs deleted file mode 100644 index 0d6810ad6b94e7..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export { {{ pluginVar }} as default } from './plugin'; diff --git a/packages/cli-module-translations/src/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts index 13a94f3a0ac38d..03d9044753c3ec 100644 --- a/packages/cli-module-translations/src/lib/extractTranslations.test.ts +++ b/packages/cli-module-translations/src/lib/extractTranslations.test.ts @@ -62,7 +62,8 @@ describe('extractTranslations', () => { resolvePath(__dirname, '../../../../tsconfig.json'), ); - // The main entry of org plugin exports components but no translation ref + // The main entry of org plugin exports components and a translation ref; + // only the translation ref should be extracted. const sourceFile = project.addSourceFileAtPath( resolvePath(__dirname, '../../../..', 'plugins/org/src/index.ts'), ); @@ -73,7 +74,13 @@ describe('extractTranslations', () => { '.', ); - expect(refs).toHaveLength(0); + expect(refs).toHaveLength(1); + expect(refs[0]).toMatchObject({ + id: 'org', + packageName: '@backstage/plugin-org', + exportPath: '.', + exportName: 'orgTranslationRef', + }); }); it('extracts from the test fixtures translation ref', () => { diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 483b0eccff3bac..9c997c02aafea6 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -42,16 +42,21 @@ "commander": "^12.0.0", "fs-extra": "^11.2.0", "pirates": "^4.0.6", + "proper-lockfile": "^4.1.2", "semver": "^7.5.3", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", + "@types/proper-lockfile": "^4", "@types/yarnpkg__lockfile": "^1.1.4" }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, "peerDependencies": { "@swc/core": "^1.15.6" }, diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 2fac82080be19b..9f04b80ddf8577 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,6 +86,21 @@ export interface BackstagePackageJson { version: string; } +// @public +export class CliAuth { + static create(options?: CliAuthCreateOptions): Promise; + getAccessToken(): Promise; + getBaseUrl(): string; + getInstanceName(): string; + getMetadata(key: string): Promise; + setMetadata(key: string, value: unknown): Promise; +} + +// @public +export interface CliAuthCreateOptions { + instanceName?: string; +} + // @public export interface CliCommand { deprecated?: boolean; diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts new file mode 100644 index 00000000000000..4b7cb61f5a89c8 --- /dev/null +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CliAuth } from './CliAuth'; +import * as storage from './storage'; +import * as secretStoreModule from './secretStore'; +import * as httpModule from './httpJson'; + +jest.mock('./storage'); +jest.mock('./secretStore'); +jest.mock('./httpJson'); + +const mockStorage = storage as jest.Mocked; +const mockSecretStoreModule = secretStoreModule as jest.Mocked< + typeof secretStoreModule +>; +const mockHttp = httpModule as jest.Mocked; + +describe('CliAuth', () => { + const now = Date.now(); + const mockInstance = { + name: 'production', + baseUrl: 'https://backstage.example.com', + clientId: 'prod-client', + issuedAt: now, + accessTokenExpiresAt: now + 3600_000, + }; + + const mockSecretStore = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockStorage.getSelectedInstance.mockResolvedValue(mockInstance); + mockSecretStoreModule.getSecretStore.mockResolvedValue(mockSecretStore); + mockStorage.accessTokenNeedsRefresh.mockReturnValue(false); + mockSecretStore.get.mockResolvedValue('test-access-token'); + }); + + describe('create', () => { + it('resolves the currently selected instance by default', async () => { + const auth = await CliAuth.create(); + + expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith(undefined); + expect(auth.getInstanceName()).toBe('production'); + expect(auth.getBaseUrl()).toBe('https://backstage.example.com'); + }); + + it('resolves a named instance when specified', async () => { + await CliAuth.create({ instanceName: 'staging' }); + + expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith('staging'); + }); + + it('throws when no instance can be found', async () => { + mockStorage.getSelectedInstance.mockRejectedValue( + new Error( + 'No instances found. Run "auth login" to authenticate first.', + ), + ); + + await expect(CliAuth.create()).rejects.toThrow( + 'No instances found. Run "auth login" to authenticate first.', + ); + }); + }); + + describe('getAccessToken', () => { + it('returns a stored access token when it is still valid', async () => { + const auth = await CliAuth.create(); + const token = await auth.getAccessToken(); + + expect(token).toBe('test-access-token'); + expect(mockSecretStore.get).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'accessToken', + ); + expect(mockHttp.httpJson).not.toHaveBeenCalled(); + }); + + it('throws when no access token is stored', async () => { + mockSecretStore.get.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'No access token found. Run "auth login" to authenticate.', + ); + }); + + it('refreshes the token when it is about to expire', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'refreshToken') return 'old-refresh-token'; + if (account === 'accessToken') return 'new-access-token'; + return undefined; + }, + ); + + mockHttp.httpJson.mockResolvedValue({ + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'new-refresh-token', + }); + + const auth = await CliAuth.create(); + const token = await auth.getAccessToken(); + + expect(token).toBe('new-access-token'); + expect(mockHttp.httpJson).toHaveBeenCalledWith( + 'https://backstage.example.com/api/auth/v1/token', + expect.objectContaining({ + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: 'old-refresh-token', + }, + }), + ); + expect(mockSecretStore.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'accessToken', + 'new-access-token', + ); + expect(mockSecretStore.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'refreshToken', + 'new-refresh-token', + ); + expect(mockStorage.updateInstance).toHaveBeenCalledWith('production', { + issuedAt: expect.any(Number), + accessTokenExpiresAt: expect.any(Number), + }); + }); + + it('throws when refresh token is missing and access token has expired', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'Access token is expired and no refresh token is available', + ); + }); + + it('throws when the token response is malformed', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'refreshToken') return 'refresh-token'; + return undefined; + }, + ); + + mockHttp.httpJson.mockResolvedValue({ + token_type: 'Bearer', + expires_in: 3600, + }); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'Invalid token response', + ); + }); + }); + + describe('getMetadata / setMetadata', () => { + it('returns a metadata value from the instance', async () => { + mockStorage.getInstanceMetadata.mockResolvedValue([ + 'catalog', + 'scaffolder', + ]); + + const auth = await CliAuth.create(); + const sources = await auth.getMetadata('pluginSources'); + + expect(sources).toEqual(['catalog', 'scaffolder']); + expect(mockStorage.getInstanceMetadata).toHaveBeenCalledWith( + 'production', + 'pluginSources', + ); + }); + + it('returns undefined for missing metadata keys', async () => { + mockStorage.getInstanceMetadata.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + const value = await auth.getMetadata('nonexistent'); + + expect(value).toBeUndefined(); + }); + + it('writes a metadata value to the instance store', async () => { + mockStorage.updateInstanceMetadata.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + await auth.setMetadata('pluginSources', ['catalog']); + + expect(mockStorage.updateInstanceMetadata).toHaveBeenCalledWith( + 'production', + 'pluginSources', + ['catalog'], + ); + }); + }); +}); diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts new file mode 100644 index 00000000000000..6f1fae221e66ea --- /dev/null +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -0,0 +1,163 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + type StoredInstance, + getSelectedInstance, + getInstanceMetadata, + updateInstanceMetadata, + updateInstance, + accessTokenNeedsRefresh, +} from './storage'; +import { getSecretStore, type SecretStore } from './secretStore'; +import { getAuthInstanceService } from './authIdentifiers'; +import { httpJson } from './httpJson'; +import { z } from 'zod/v3'; + +const TokenResponseSchema = z.object({ + access_token: z.string().min(1), + token_type: z.string().min(1), + expires_in: z.number().positive().finite(), + refresh_token: z.string().min(1).optional(), +}); + +/** + * Options for creating a {@link CliAuth} instance. + * + * @public + */ +export interface CliAuthCreateOptions { + /** + * An explicit instance name to resolve. When omitted the currently + * selected instance is used. + */ + instanceName?: string; +} + +/** + * Manages authentication state for Backstage CLI commands. + * + * Reads the currently selected (or explicitly named) auth instance from + * the on-disk instance store, transparently refreshes expired access + * tokens, and exposes helpers that other CLI modules need to talk to a + * Backstage backend. + * + * @public + */ +export class CliAuth { + readonly #secretStore: SecretStore; + #instance: StoredInstance; + + /** + * Resolve the current auth instance and return a ready-to-use + * {@link CliAuth} object. Throws when no instance can be found. + */ + static async create(options?: CliAuthCreateOptions): Promise { + const instance = await getSelectedInstance(options?.instanceName); + const secretStore = await getSecretStore(); + return new CliAuth(instance, secretStore); + } + + private constructor(instance: StoredInstance, secretStore: SecretStore) { + this.#instance = instance; + this.#secretStore = secretStore; + } + + /** Returns the name of the resolved auth instance. */ + getInstanceName(): string { + return this.#instance.name; + } + + /** Returns the base URL of the resolved auth instance. */ + getBaseUrl(): string { + return this.#instance.baseUrl; + } + + /** + * Returns a valid access token, refreshing it first if the current + * token is expired or about to expire. + */ + async getAccessToken(): Promise { + if (accessTokenNeedsRefresh(this.#instance)) { + await this.#refreshAccessToken(); + } + + const service = getAuthInstanceService(this.#instance.name); + const token = await this.#secretStore.get(service, 'accessToken'); + if (!token) { + throw new Error( + 'No access token found. Run "auth login" to authenticate.', + ); + } + return token; + } + + /** + * Reads a per-instance metadata value previously stored by the + * auth module (e.g. `pluginSources`). + */ + async getMetadata(key: string): Promise { + return getInstanceMetadata(this.#instance.name, key); + } + + /** + * Writes a per-instance metadata value to the on-disk instance store. + */ + async setMetadata(key: string, value: unknown): Promise { + return updateInstanceMetadata(this.#instance.name, key, value); + } + + async #refreshAccessToken(): Promise { + const service = getAuthInstanceService(this.#instance.name); + const refreshToken = + (await this.#secretStore.get(service, 'refreshToken')) ?? ''; + if (!refreshToken) { + throw new Error( + 'Access token is expired and no refresh token is available', + ); + } + + const response = await httpJson( + `${this.#instance.baseUrl}/api/auth/v1/token`, + { + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: refreshToken, + }, + signal: AbortSignal.timeout(30_000), + }, + ); + + const parsed = TokenResponseSchema.safeParse(response); + if (!parsed.success) { + throw new Error(`Invalid token response: ${parsed.error.message}`); + } + const token = parsed.data; + + await this.#secretStore.set(service, 'accessToken', token.access_token); + if (token.refresh_token) { + await this.#secretStore.set(service, 'refreshToken', token.refresh_token); + } + const issuedAt = Date.now(); + const accessTokenExpiresAt = Date.now() + token.expires_in * 1000; + this.#instance = { ...this.#instance, issuedAt, accessTokenExpiresAt }; + await updateInstance(this.#instance.name, { + issuedAt, + accessTokenExpiresAt, + }); + } +} diff --git a/packages/cli-node/src/auth/authIdentifiers.ts b/packages/cli-node/src/auth/authIdentifiers.ts new file mode 100644 index 00000000000000..16ddb5b437fab4 --- /dev/null +++ b/packages/cli-node/src/auth/authIdentifiers.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @internal */ +export function getAuthInstanceService(instanceName: string): string { + return `backstage-cli:auth-instance:${instanceName}`; +} diff --git a/packages/cli-node/src/auth/httpJson.ts b/packages/cli-node/src/auth/httpJson.ts new file mode 100644 index 00000000000000..832178befb7afb --- /dev/null +++ b/packages/cli-node/src/auth/httpJson.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ResponseError } from '@backstage/errors'; + +/** @internal */ +export type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +/** @internal */ +export async function httpJson(url: string, init?: HttpInit): Promise { + const res = await fetch(url, { + ...init, + body: init?.body ? JSON.stringify(init.body) : undefined, + headers: { + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + if (!res.ok) { + throw await ResponseError.fromResponse(res); + } + return (await res.json()) as T; +} diff --git a/packages/cli-node/src/auth/index.ts b/packages/cli-node/src/auth/index.ts new file mode 100644 index 00000000000000..d6a1b08d657baa --- /dev/null +++ b/packages/cli-node/src/auth/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CliAuth, type CliAuthCreateOptions } from './CliAuth'; diff --git a/packages/cli-node/src/auth/secretStore.ts b/packages/cli-node/src/auth/secretStore.ts new file mode 100644 index 00000000000000..70ef5f2f725f47 --- /dev/null +++ b/packages/cli-node/src/auth/secretStore.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +/** @internal */ +export type SecretStore = { + get(service: string, account: string): Promise; + set(service: string, account: string, secret: string): Promise; + delete(service: string, account: string): Promise; +}; + +async function loadKeytar(): Promise { + try { + // eslint-disable-next-line import/no-extraneous-dependencies, @backstage/no-undeclared-imports + const keytar = require('keytar') as typeof import('keytar'); + if (keytar && typeof keytar.getPassword === 'function') { + return keytar; + } + } catch { + // keytar not available + } + return undefined; +} + +class KeytarSecretStore implements SecretStore { + private readonly keytar: typeof import('keytar'); + constructor(keytar: typeof import('keytar')) { + this.keytar = keytar; + } + async get(service: string, account: string): Promise { + const result = await this.keytar.getPassword(service, account); + return result ?? undefined; + } + async set(service: string, account: string, secret: string): Promise { + await this.keytar.setPassword(service, account, secret); + } + async delete(service: string, account: string): Promise { + await this.keytar.deletePassword(service, account); + } +} + +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + +class FileSecretStore implements SecretStore { + private readonly baseDir: string; + constructor() { + const root = + process.env.XDG_DATA_HOME || + (process.platform === 'win32' + ? process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + : path.join(os.homedir(), '.local', 'share')); + this.baseDir = path.join(root, 'backstage-cli', 'auth-secrets'); + } + private filePath(service: string, account: string): string { + return path.join( + this.baseDir, + encodeURIComponent(service), + `${encodeURIComponent(account)}.secret`, + ); + } + async get(service: string, account: string): Promise { + const file = this.filePath(service, account); + if (!(await pathExists(file))) { + return undefined; + } + return await fs.readFile(file, 'utf8'); + } + async set(service: string, account: string, secret: string): Promise { + const file = this.filePath(service, account); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 }); + } + async delete(service: string, account: string): Promise { + const file = this.filePath(service, account); + try { + await fs.unlink(file); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } + } +} + +let singleton: SecretStore | undefined; + +/** @internal */ +export async function getSecretStore(): Promise { + if (!singleton) { + const keytar = await loadKeytar(); + if (keytar) { + singleton = new KeytarSecretStore(keytar); + } else { + singleton = new FileSecretStore(); + } + } + return singleton; +} + +/** + * Reset the singleton instance (for testing purposes only) + * @internal + */ +export function resetSecretStore(): void { + singleton = undefined; +} diff --git a/packages/cli-node/src/auth/storage.ts b/packages/cli-node/src/auth/storage.ts new file mode 100644 index 00000000000000..c58bd94505c77c --- /dev/null +++ b/packages/cli-node/src/auth/storage.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NotFoundError } from '@backstage/errors'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import lockfile from 'proper-lockfile'; +import YAML from 'yaml'; +import { z } from 'zod/v3'; + +const METADATA_FILE = 'auth-instances.yaml'; + +const INSTANCE_NAME_PATTERN = /^[a-zA-Z0-9._:@-]+$/; + +const storedInstanceSchema = z.object({ + name: z + .string() + .min(1) + .regex(INSTANCE_NAME_PATTERN, 'Instance name contains invalid characters'), + baseUrl: z.string().url(), + clientId: z.string().min(1), + issuedAt: z.number().int().nonnegative(), + accessTokenExpiresAt: z.number().int().nonnegative(), + selected: z.boolean().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}); + +const authYamlSchema = z.object({ + instances: z.array(storedInstanceSchema).default([]), +}); + +export type StoredInstance = { + name: string; + baseUrl: string; + clientId: string; + issuedAt: number; + accessTokenExpiresAt: number; + selected?: boolean; + metadata?: Record; +}; + +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + +/** @internal */ +export function getMetadataFilePath(): string { + const root = + process.env.XDG_CONFIG_HOME || + (process.platform === 'win32' + ? process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + : path.join(os.homedir(), '.config')); + + return path.join(root, 'backstage-cli', METADATA_FILE); +} + +/** @internal */ +export async function readAll(): Promise<{ instances: StoredInstance[] }> { + const file = getMetadataFilePath(); + if (!(await pathExists(file))) { + return { instances: [] }; + } + const text = await fs.readFile(file, 'utf8'); + if (!text.trim()) { + return { instances: [] }; + } + try { + const doc = YAML.parse(text); + const parsed = authYamlSchema.safeParse(doc); + if (parsed.success) { + return parsed.data; + } + return { instances: [] }; + } catch { + return { instances: [] }; + } +} + +async function writeAll(data: { instances: StoredInstance[] }): Promise { + const file = getMetadataFilePath(); + await fs.mkdir(path.dirname(file), { recursive: true }); + const yaml = YAML.stringify(authYamlSchema.parse(data), { indentSeq: false }); + await fs.writeFile(file, yaml, { encoding: 'utf8', mode: 0o600 }); +} + +async function withMetadataLock(fn: () => Promise): Promise { + const file = getMetadataFilePath(); + await fs.mkdir(path.dirname(file), { recursive: true }); + if (!(await pathExists(file))) { + await fs.writeFile(file, '', { encoding: 'utf8', mode: 0o600 }); + } + const release = await lockfile.lock(file, { + retries: { retries: 5, factor: 1.5, minTimeout: 100, maxTimeout: 1000 }, + }); + try { + return await fn(); + } finally { + await release(); + } +} + +/** @internal */ +export async function getAllInstances(): Promise<{ + instances: StoredInstance[]; + selected: StoredInstance | undefined; +}> { + const { instances } = await readAll(); + if (instances.length === 0) { + return { instances: [], selected: undefined }; + } + const selected = instances.find(i => i.selected) ?? instances[0]; + return { + instances: instances.map(i => ({ + ...i, + selected: i.name === selected.name, + })), + selected, + }; +} + +/** @internal */ +export async function getSelectedInstance( + instanceName?: string, +): Promise { + if (instanceName) { + return await getInstanceByName(instanceName); + } + const { selected } = await getAllInstances(); + if (!selected) { + throw new Error( + 'No instances found. Run "auth login" to authenticate first.', + ); + } + return selected; +} + +/** @internal */ +export async function getInstanceByName(name: string): Promise { + const { instances } = await readAll(); + const instance = instances.find(i => i.name === name); + if (!instance) { + throw new NotFoundError(`Instance '${name}' not found`); + } + return instance; +} + +/** @internal */ +export async function getInstanceMetadata( + instanceName: string, + key: string, +): Promise { + const instance = await getInstanceByName(instanceName); + return instance.metadata?.[key]; +} + +/** @internal */ +export async function updateInstanceMetadata( + instanceName: string, + key: string, + value: unknown, +): Promise { + return withMetadataLock(async () => { + const data = await readAll(); + const idx = data.instances.findIndex(i => i.name === instanceName); + if (idx === -1) { + throw new NotFoundError(`Instance '${instanceName}' not found`); + } + data.instances[idx] = { + ...data.instances[idx], + metadata: { ...data.instances[idx].metadata, [key]: value }, + }; + await writeAll(data); + }); +} + +/** @internal */ +export async function updateInstance( + instanceName: string, + updates: Partial>, +): Promise { + return withMetadataLock(async () => { + const data = await readAll(); + const idx = data.instances.findIndex(i => i.name === instanceName); + if (idx === -1) { + throw new NotFoundError(`Instance '${instanceName}' not found`); + } + data.instances[idx] = { ...data.instances[idx], ...updates }; + await writeAll(data); + }); +} + +/** @internal */ +export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { + // 2 minutes before expiration + return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; +} diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 8831753a820a03..a824673c1e0651 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './auth'; export * from './cache'; export * from './cli-module'; export * from './concurrency'; diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 7761b0d4cc85c5..2987a7cbda4fb1 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { PackageRole, PackageRoleInfo } from './types'; const packageRoleInfos: PackageRoleInfo[] = [ diff --git a/packages/cli-node/src/yarn/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts index 65383edd0c825a..fb592e6898e3c2 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; -import z from 'zod'; +import { z } from 'zod/v3'; import { targetPaths } from '@backstage/cli-common'; const yarnRcSchema = z.object({ diff --git a/packages/cli/package.json b/packages/cli/package.json index 4ba287a9efec9f..d34a1860cab5d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,11 +41,6 @@ "ext": "ts", "watch": "./src" }, - "jest": { - "coveragePathIgnorePatterns": [ - "/__fixtures__/" - ] - }, "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/cli-defaults": "workspace:^", diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index c37d4172a4e3b9..71c192e9577914 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -58,7 +58,7 @@ "prop-types": "^15.7.2", "react-use": "^17.2.4", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index e5e32e34575219..bc3e0c20fb3eaf 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -18,7 +18,7 @@ import { BackstageIdentityResponse, ProfileInfo, } from '@backstage/core-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @internal */ export type SamlSession = { diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 3f8622890bd0ea..b1bba120274b04 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -15,7 +15,7 @@ */ import { withLogCollector } from '@backstage/test-utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthSessionStore } from './AuthSessionStore'; import { SessionManager } from './types'; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts index e1fb80d41f8ab6..fc3030a7a6c40e 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema } from 'zod'; +import type { ZodSchema } from 'zod/v3'; import { MutableSessionManager, SessionScopesFunc, diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 6ee93be87581cd..e82140d5e201b3 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -40,7 +40,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "lodash": "^4.17.21", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", @@ -56,7 +56,7 @@ "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.30.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", diff --git a/packages/core-components/package.json b/packages/core-components/package.json index ecc27a6cc53502..b94cc13f2dddde 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -95,7 +95,7 @@ "rehype-sanitize": "^5.0.0", "remark-gfm": "^3.0.1", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/app-defaults": "workspace:^", diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index f373dd56ff0b2a..76adba09ecc717 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const coreComponentsTranslationRef: TranslationRef< 'core-components', { diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index d487cc00b0c5c2..ac6dc5ca15feb3 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -54,6 +54,7 @@ import { SVGProps } from 'react'; import { TabProps } from '@material-ui/core/Tab'; import { Theme } from '@material-ui/core/styles'; import { TooltipProps } from '@material-ui/core/Tooltip'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { WithStyles } from '@material-ui/core/styles'; // @public @@ -222,6 +223,69 @@ export interface CopyTextButtonProps { tooltipText?: string; } +// @public (undocumented) +export const coreComponentsTranslationRef: TranslationRef< + 'core-components', + { + readonly 'table.filter.title': 'Filters'; + readonly 'table.filter.placeholder': 'All results'; + readonly 'table.filter.clearAll': 'Clear all'; + readonly 'table.body.emptyDataSourceMessage': 'No records to display'; + readonly 'table.header.actions': 'Actions'; + readonly 'table.toolbar.search': 'Filter'; + readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}'; + readonly 'table.pagination.firstTooltip': 'First Page'; + readonly 'table.pagination.labelRowsSelect': 'rows'; + readonly 'table.pagination.lastTooltip': 'Last Page'; + readonly 'table.pagination.nextTooltip': 'Next Page'; + readonly 'table.pagination.previousTooltip': 'Previous Page'; + readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; + readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; + readonly 'emptyState.missingAnnotation.readMore': 'Read more'; + readonly 'signIn.title': 'Sign In'; + readonly 'signIn.loginFailed': 'Login failed'; + readonly 'signIn.customProvider.title': 'Custom User'; + readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.'; + readonly 'signIn.customProvider.userId': 'User ID'; + readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; + readonly 'signIn.customProvider.continue': 'Continue'; + readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; + readonly 'signIn.guestProvider.title': 'Guest'; + readonly 'signIn.guestProvider.enter': 'Enter'; + readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.'; + readonly skipToContent: 'Skip to content'; + readonly 'copyTextButton.tooltipText': 'Text copied to clipboard'; + readonly 'simpleStepper.finish': 'Finish'; + readonly 'simpleStepper.reset': 'Reset'; + readonly 'simpleStepper.next': 'Next'; + readonly 'simpleStepper.skip': 'Skip'; + readonly 'simpleStepper.back': 'Back'; + readonly 'errorPage.title': 'Looks like someone dropped the mic!'; + readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}'; + readonly 'errorPage.goBack': 'Go back'; + readonly 'errorPage.showMoreDetails': 'Show more details'; + readonly 'errorPage.showLessDetails': 'Show less details'; + readonly 'supportConfig.default.title': 'Support Not Configured'; + readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key'; + readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; + readonly 'oauthRequestDialog.message': 'Sign-in to allow {{appTitle}} access to {{provider}} APIs and identities.'; + readonly 'oauthRequestDialog.title': 'Login Required'; + readonly 'oauthRequestDialog.authRedirectTitle': 'This will trigger a http redirect to OAuth Login.'; + readonly 'oauthRequestDialog.login': 'Log in'; + readonly 'oauthRequestDialog.rejectAll': 'Reject All'; + readonly 'supportButton.title': 'Support'; + readonly 'supportButton.close': 'Close'; + readonly 'alertDisplay.message_one': '({{ count }} newer message)'; + readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; + readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; + readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; + readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; + readonly 'logViewer.searchField.placeholder': 'Search'; + readonly 'logViewer.downloadBtn.tooltip': 'Download logs'; + } +>; + // @public export function CreateButton(props: CreateButtonProps): JSX_2.Element | null; diff --git a/packages/core-components/src/alpha.ts b/packages/core-components/src/alpha.ts index e1f7678bae75d4..6955ce55eb234e 100644 --- a/packages/core-components/src/alpha.ts +++ b/packages/core-components/src/alpha.ts @@ -13,4 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './translation'; +import { coreComponentsTranslationRef as _coreComponentsTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/core-components` instead. + */ +export const coreComponentsTranslationRef = _coreComponentsTranslationRef; diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3c5e708360005f..20068585998388 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,3 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; +export { coreComponentsTranslationRef } from './translation'; diff --git a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts index 178714ecf90d91..abc9cabac6946e 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TypeOf } from 'zod'; +import type { TypeOf } from 'zod/v3'; import { ProxiedSession, proxiedSessionSchema } from './types'; describe('types', () => { diff --git a/packages/core-components/src/layout/ProxiedSignInPage/types.ts b/packages/core-components/src/layout/ProxiedSignInPage/types.ts index dd4230cd8b932a..bf237aabc7b66f 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/types.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/types.ts @@ -18,7 +18,7 @@ import { BackstageIdentityResponse, ProfileInfo, } from '@backstage/core-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const proxiedSessionSchema = z.object({ providerInfo: z.object({}).catchall(z.unknown()).optional(), diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index 1c063195c955f1..d6012273e9916b 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const coreComponentsTranslationRef = createTranslationRef({ id: 'core-components', messages: { diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 8af605aa7298e3..f97e7b803cf949 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -55,7 +55,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "history": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/create-app/cli-report.md b/packages/create-app/cli-report.md index 88e19d05c6560a..f9286a628cbfa8 100644 --- a/packages/create-app/cli-report.md +++ b/packages/create-app/cli-report.md @@ -8,7 +8,7 @@ Usage: backstage-create-app [options] Options: - --next + --legacy --path [directory] --skip-install --template-path [directory] diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index c38186b1d814d7..a032f9bb3b8bc6 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -69,7 +69,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/default-app'), + findOwnPaths(__dirname).resolve('templates/next-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), @@ -85,20 +85,20 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/default-app'), + findOwnPaths(__dirname).resolve('templates/next-app'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); expect(buildAppMock).toHaveBeenCalled(); }); - it('should call expected tasks when `--next` is supplied', async () => { - const cmd = { next: true } as unknown as Command; + it('should call expected tasks when `--legacy` is supplied', async () => { + const cmd = { legacy: true } as unknown as Command; await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/next-app'), + findOwnPaths(__dirname).resolve('templates/default-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 64db2e52abbe45..68c27d3a1a1b43 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -63,12 +63,12 @@ export default async (opts: OptionValues): Promise => { }, ]); - // Pick the built-in template based on the --next flag + // Pick the built-in template based on the --legacy flag /* eslint-disable-next-line no-restricted-syntax */ const ownPaths = findOwnPaths(__dirname); - const builtInTemplate = opts.next - ? ownPaths.resolve('templates/next-app') - : ownPaths.resolve('templates/default-app'); + const builtInTemplate = opts.legacy + ? ownPaths.resolve('templates/default-app') + : ownPaths.resolve('templates/next-app'); // Use `--template-path` argument as template when specified. Otherwise, use the default template. const templateDir = opts.templatePath diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index cfa448aa1a4a41..ff3ef29b186ee5 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -31,7 +31,7 @@ const main = (argv: string[]) => { .name('backstage-create-app') .version(version) .description('Creates a new app in a new directory or specified path') - .option('--next', 'Use the next generation of the app template') + .option('--legacy', 'Use the legacy version of the app template') .option( '--path [directory]', 'Location to store the app defaulting to a new folder with the app name', diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 006dd6d7b8c343..9b10660d19e3be 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -39,6 +39,7 @@ import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; import { version as cli } from '../../../cli/package.json'; import { version as cliDefaults } from '../../../cli-defaults/package.json'; +import { version as cliModuleNew } from '../../../cli-module-new/package.json'; import { version as config } from '../../../config/package.json'; import { version as coreAppApi } from '../../../core-app-api/package.json'; import { version as coreCompatApi } from '../../../core-compat-api/package.json'; @@ -109,6 +110,7 @@ export const packageVersions = { '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, '@backstage/cli-defaults': cliDefaults, + '@backstage/cli-module-new': cliModuleNew, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 4262e0fc828d35..836ecccac5941d 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,6 +22,28 @@ "prettier:check": "prettier --check .", "new": "backstage-cli new" }, + "backstage": { + "cli": { + "new": { + "globals": { + "license": "UNLICENSED" + }, + "templates": [ + "@backstage/cli-module-new/templates/legacy-frontend-plugin", + "@backstage/cli-module-new/templates/backend-plugin", + "@backstage/cli-module-new/templates/backend-plugin-module", + "@backstage/cli-module-new/templates/plugin-web-library", + "@backstage/cli-module-new/templates/plugin-node-library", + "@backstage/cli-module-new/templates/plugin-common-library", + "@backstage/cli-module-new/templates/web-library", + "@backstage/cli-module-new/templates/node-library", + "@backstage/cli-module-new/templates/cli-module", + "@backstage/cli-module-new/templates/catalog-provider-module", + "@backstage/cli-module-new/templates/scaffolder-backend-module" + ] + } + } + }, "workspaces": [ "packages/*", "plugins/*" diff --git a/packages/create-app/templates/next-app/app-config.yaml.hbs b/packages/create-app/templates/next-app/app-config.yaml.hbs index 67917fd193dd18..c8814a9e96cf4d 100644 --- a/packages/create-app/templates/next-app/app-config.yaml.hbs +++ b/packages/create-app/templates/next-app/app-config.yaml.hbs @@ -17,7 +17,6 @@ app: config: path: / - organization: name: My Company diff --git a/packages/create-app/templates/next-app/examples/template/template.yaml b/packages/create-app/templates/next-app/examples/template/template.yaml index 2a20bd45ed398f..efb33be86601d7 100644 --- a/packages/create-app/templates/next-app/examples/template/template.yaml +++ b/packages/create-app/templates/next-app/examples/template/template.yaml @@ -65,7 +65,7 @@ spec: input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' - + # Let's notify the user that the template has completed using the Notification action - id: notify name: Notify diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 2ace0f549b58ac..b2c7f5cbb0ceaa 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -27,20 +27,7 @@ "new": { "globals": { "license": "UNLICENSED" - }, - "templates": [ - "@backstage/cli-module-new/templates/new-frontend-plugin", - "@backstage/cli-module-new/templates/new-frontend-plugin-module", - "@backstage/cli-module-new/templates/backend-plugin", - "@backstage/cli-module-new/templates/backend-plugin-module", - "@backstage/cli-module-new/templates/plugin-web-library", - "@backstage/cli-module-new/templates/plugin-node-library", - "@backstage/cli-module-new/templates/plugin-common-library", - "@backstage/cli-module-new/templates/web-library", - "@backstage/cli-module-new/templates/node-library", - "@backstage/cli-module-new/templates/catalog-provider-module", - "@backstage/cli-module-new/templates/scaffolder-backend-module" - ] + } } } }, diff --git a/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts b/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts index 839ff883de1c5c..3347034856aa14 100644 --- a/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts +++ b/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts @@ -23,5 +23,6 @@ test('App should render the welcome page', async ({ page }) => { await expect(enterButton).toBeVisible(); await enterButton.click(); - await expect(page.getByText('My Company Catalog')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'APIs' })).toBeVisible(); }); diff --git a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx index d436252edf248c..52d96151cb1b0d 100644 --- a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx @@ -12,19 +12,20 @@ import { SidebarLogo } from './SidebarLogo'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarSearchModal } from '@backstage/plugin-search'; -import { UserSettingsSignInAvatar, Settings as SidebarSettings } from '@backstage/plugin-user-settings'; +import { + UserSettingsSignInAvatar, + Settings as SidebarSettings, +} from '@backstage/plugin-user-settings'; import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; export const SidebarContent = NavContentBlueprint.make({ params: { component: ({ navItems }) => { const nav = navItems.withComponent(item => ( - item.icon} - to={item.href} - text={item.title} - /> + item.icon} to={item.href} text={item.title} /> )); + // Skipped items + nav.take('page:search'); return compatWrapper( diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index 896ac5ed72e2f3..170d1252bfd8fc 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -35,9 +35,9 @@ const ownPaths = findOwnPaths(__dirname); const templatePackagePaths = [ 'packages/cli-module-new/templates/frontend-plugin/package.json.hbs', - 'packages/create-app/templates/default-app/package.json.hbs', - 'packages/create-app/templates/default-app/packages/app/package.json.hbs', - 'packages/create-app/templates/default-app/packages/backend/package.json.hbs', + 'packages/create-app/templates/next-app/package.json.hbs', + 'packages/create-app/templates/next-app/packages/app/package.json.hbs', + 'packages/create-app/templates/next-app/packages/backend/package.json.hbs', ]; export async function runCommand(opts: OptionValues) { @@ -66,20 +66,6 @@ export async function runCommand(opts: OptionValues) { env: { ...process.env, CI: undefined }, }); - await switchToReact17(appDir); - - print(`Running 'yarn install' to install React 17`); - await runOutput(['yarn', 'install'], { cwd: appDir }); - - print(`Running 'yarn tsc' with React 17`); - await runOutput(['yarn', 'tsc'], { cwd: appDir }); - - print(`Running 'yarn test:e2e' with React 17`); - await runOutput(['yarn', 'test:e2e'], { - cwd: appDir, - env: { ...process.env, CI: undefined }, - }); - if ( Boolean(process.env.POSTGRES_USER) || Boolean(process.env.MYSQL_CONNECTION) @@ -412,37 +398,6 @@ async function createPlugin(options: { } } -/** - * Switch the entire project to use React 17 - */ -async function switchToReact17(appDir: string) { - const rootPkg = await fs.readJson(resolvePath(appDir, 'package.json')); - rootPkg.resolutions = { - ...(rootPkg.resolutions || {}), - react: '^17.0.0', - 'react-dom': '^17.0.0', - '@types/react': '^17.0.0', - '@types/react-dom': '^17.0.0', - 'swagger-ui-react/react': '17.0.2', - 'swagger-ui-react/react-dom': '17.0.2', - 'swagger-ui-react/react-redux': '^8', - }; - await fs.writeJson(resolvePath(appDir, 'package.json'), rootPkg, { - spaces: 2, - }); - - await fs.writeFile( - resolvePath(appDir, 'packages/app/src/index.tsx'), - `import '@backstage/cli/asset-types'; -import ReactDOM from 'react-dom'; -import App from './App'; - -ReactDOM.render(, document.getElementById('root')); -`, - 'utf8', - ); -} - /** Drops PG databases */ async function dropDB(database: string, client: string) { try { diff --git a/packages/filter-predicates/package.json b/packages/filter-predicates/package.json index 7c2a6bbf80b0de..678707c833427e 100644 --- a/packages/filter-predicates/package.json +++ b/packages/filter-predicates/package.json @@ -39,7 +39,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^4.0.2" }, "devDependencies": { diff --git a/packages/filter-predicates/src/predicates/schema.test.ts b/packages/filter-predicates/src/predicates/schema.test.ts index 08c92c20e882dc..ff38dabfce3b3a 100644 --- a/packages/filter-predicates/src/predicates/schema.test.ts +++ b/packages/filter-predicates/src/predicates/schema.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createZodV3FilterPredicateSchema, parseFilterPredicate, diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index f5dea017a942ca..e807188f6c170a 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -42,7 +42,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "lodash": "^4.17.21", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/frontend-dynamic-feature-loader/README.md b/packages/frontend-dynamic-feature-loader/README.md index a1ab46d7b04227..a3802c39017a21 100644 --- a/packages/frontend-dynamic-feature-loader/README.md +++ b/packages/frontend-dynamic-feature-loader/README.md @@ -22,11 +22,12 @@ The frontend feature loader provided in this package works hand-in-hand with the Adding a frontend plugin (with new frontend system support, possibly in alpha support), is straightforward and consists in: -- building the frontend plugin with the `frontend-dynamic-container` role, which enables the module federation support, and packages the plugin as a module remote -- copying the frontend package folder, with the `dist` folder generated during the build, to the dynamic plugins root folder of the Backstage installation (defined by the `dynamicPlugins.rootDirectory` configuration value, which is usually set as `dynamic-plugins-root`). +- bundling the frontend plugin with the [`backstage-cli package bundle`](../../docs/tooling/cli/03-commands.md#package-bundle) command, thus producing a self-contained bundle based on Module Federation. +- copying the bundle folder into the Backstage installation dynamic plugins root folder for dynamic loading. -So from a frontend plugin package folder, you would use the following command: +So from a `my-backstage-plugin` frontend plugin package folder, you would use the following command: ```bash -yarn build --role frontend-dynamic-container && cp -R $(pwd) /dynamic-plugins-root/ +yarn backstage-cli package bundle --output-destination /path/to/dynamic-plugins-root +# Creates a self-contained bundle in the /path/to/dynamic-plugins-root/my-backstage-plugin/ sub-folder ``` diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 66973d44068d01..04e11771449d17 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -48,7 +48,7 @@ "@backstage/filter-predicates": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/packages/frontend-plugin-api/report-alpha.api.md b/packages/frontend-plugin-api/report-alpha.api.md index db4570ee2f7e80..c27ba1b814d241 100644 --- a/packages/frontend-plugin-api/report-alpha.api.md +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -14,7 +14,7 @@ import { FilterPredicate } from '@backstage/filter-predicates'; import { JsonObject } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; import { ReactNode } from 'react'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; // @public export type AnyRouteRefParams = diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index b9c5bf269aa2c3..c3936ba0da65f4 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -23,7 +23,7 @@ import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; // @public @deprecated export type AlertApi = { diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index b15ff980ec0971..6dbac61c62d255 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { z, type ZodSchema, type ZodTypeDef } from 'zod'; +import { z, type ZodSchema, type ZodTypeDef } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { PortableSchema } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 8632b3caebdfd8..1b5869460a8e9e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -26,7 +26,7 @@ import { } from '@internal/frontend'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { ExtensionInput } from './createExtensionInput'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; import { createSchemaFromZod } from '../schema/createSchemaFromZod'; import { OpaqueExtensionDefinition } from '@internal/frontend'; import { ExtensionDataContainer } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 2d2288b1204a09..cd593f3c93b4c4 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -26,7 +26,7 @@ import { ctxParamsSymbol, VerifyExtensionAttachTo, } from './createExtension'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; import { ExtensionInput } from './createExtensionInput'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { createExtensionDataContainer } from '@internal/frontend'; diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 59c5545a811bc3..63f54171534102 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -46,7 +46,7 @@ "@backstage/version-bridge": "workspace:^", "i18next": "^22.4.15", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index c2156beb3e6af2..f27e77862b5d59 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -84,7 +84,7 @@ "tar": "^7.5.6", "ts-morph": "^24.0.0", "yaml-diff-patch": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/repo-tools/src/commands/package-docs/Cache.ts b/packages/repo-tools/src/commands/package-docs/Cache.ts index be005ac2807947..0ca38c1c39188b 100644 --- a/packages/repo-tools/src/commands/package-docs/Cache.ts +++ b/packages/repo-tools/src/commands/package-docs/Cache.ts @@ -19,7 +19,7 @@ import { dirname, join as joinPath, relative } from 'node:path'; import crypto from 'node:crypto'; import { Lockfile } from '@backstage/cli-node'; import { exists, rm, mkdirp } from 'fs-extra'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CACHE_DIR, CACHE_FILE } from './constants'; const version = '1'; diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 1bbd38b5786d7c..0ee520153ee6c6 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -25,7 +25,7 @@ "dependencies": { "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts index 7942544acb5b11..39de69a5bf1e8b 100644 --- a/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts +++ b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { OpaqueType } from '@internal/opaque'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ScaffolderFormDecorator, diff --git a/packages/scaffolder-internal/src/wiring/InternalFormField.ts b/packages/scaffolder-internal/src/wiring/InternalFormField.ts index b711341ea5ef04..24e279f47f920e 100644 --- a/packages/scaffolder-internal/src/wiring/InternalFormField.ts +++ b/packages/scaffolder-internal/src/wiring/InternalFormField.ts @@ -15,7 +15,7 @@ */ import { OpaqueType } from '@internal/opaque'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { FormFieldExtensionData, diff --git a/plugins/api-docs/dev/trpc-example-api.yaml b/plugins/api-docs/dev/trpc-example-api.yaml index f5807537b3e6b4..f1847154ced74c 100644 --- a/plugins/api-docs/dev/trpc-example-api.yaml +++ b/plugins/api-docs/dev/trpc-example-api.yaml @@ -8,7 +8,7 @@ spec: lifecycle: experimental owner: team-c definition: | - import { z } from 'zod'; + import { z } from 'zod/v3'; import { publicProcedure, router } from '../trpc'; export const apiRouter = router({ diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 4afa55ce773fb3..3a96dd015f0774 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -26,7 +26,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const apiDocsTranslationRef: TranslationRef< 'api-docs', { diff --git a/plugins/api-docs/report.api.md b/plugins/api-docs/report.api.md index 370cbb3e0e0964..5e7dd8f056033d 100644 --- a/plugins/api-docs/report.api.md +++ b/plugins/api-docs/report.api.md @@ -19,6 +19,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) @@ -62,6 +63,42 @@ const apiDocsPlugin: BackstagePlugin< export { apiDocsPlugin }; export { apiDocsPlugin as plugin }; +// @public (undocumented) +export const apiDocsTranslationRef: TranslationRef< + 'api-docs', + { + readonly 'apiDefinitionCard.error.title': 'Could not fetch the API'; + readonly 'apiDefinitionCard.rawButtonTitle': 'Raw'; + readonly 'apiDefinitionDialog.closeButtonTitle': 'Close'; + readonly 'apiDefinitionDialog.tabsAriaLabel': 'API definition options'; + readonly 'apiDefinitionDialog.rawButtonTitle': 'Raw'; + readonly 'apiDefinitionDialog.toggleButtonAriaLabel': 'Toggle API Definition Dialog'; + readonly 'defaultApiExplorerPage.title': 'APIs'; + readonly 'defaultApiExplorerPage.subtitle': '{{orgName}} API Explorer'; + readonly 'defaultApiExplorerPage.pageTitleOverride': 'APIs'; + readonly 'defaultApiExplorerPage.createButtonTitle': 'Register Existing API'; + readonly 'defaultApiExplorerPage.supportButtonTitle': 'All your APIs'; + readonly 'consumedApisCard.error.title': 'Could not load APIs'; + readonly 'consumedApisCard.title': 'Consumed APIs'; + readonly 'consumedApisCard.emptyContent.title': 'This {{entity}} does not consume any APIs.'; + readonly 'hasApisCard.error.title': 'Could not load APIs'; + readonly 'hasApisCard.title': 'APIs'; + readonly 'hasApisCard.emptyContent.title': 'This {{entity}} does not contain any APIs.'; + readonly 'providedApisCard.error.title': 'Could not load APIs'; + readonly 'providedApisCard.title': 'Provided APIs'; + readonly 'providedApisCard.emptyContent.title': 'This {{entity}} does not provide any APIs.'; + readonly 'apiEntityColumns.typeTitle': 'Type'; + readonly 'apiEntityColumns.apiDefinitionTitle': 'API Definition'; + readonly 'consumingComponentsCard.error.title': 'Could not load components'; + readonly 'consumingComponentsCard.title': 'Consumers'; + readonly 'consumingComponentsCard.emptyContent.title': 'No component consumes this API.'; + readonly 'providingComponentsCard.error.title': 'Could not load components'; + readonly 'providingComponentsCard.title': 'Providers'; + readonly 'providingComponentsCard.emptyContent.title': 'No component provides this API.'; + readonly apisCardHelpLinkTitle: 'Learn how to change this.'; + } +>; + // @public export const ApiExplorerIndexPage: ( props: DefaultApiExplorerPageProps, diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 2724619a685e46..94ae1ac27af09e 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -234,4 +234,10 @@ export default createFrontendPlugin({ ], }); -export { apiDocsTranslationRef } from './translation'; +import { apiDocsTranslationRef as _apiDocsTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-api-docs` instead. + */ +export const apiDocsTranslationRef = _apiDocsTranslationRef; diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts index 49b2805aadd754..ae344890b0ffdf 100644 --- a/plugins/api-docs/src/index.ts +++ b/plugins/api-docs/src/index.ts @@ -34,3 +34,4 @@ export { EntityProvidedApisCard, EntityProvidingComponentsCard, } from './plugin'; +export { apiDocsTranslationRef } from './translation'; diff --git a/plugins/api-docs/src/translation.ts b/plugins/api-docs/src/translation.ts index a6a43952bccdf6..289115fd3a200b 100644 --- a/plugins/api-docs/src/translation.ts +++ b/plugins/api-docs/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** - * @alpha + * @public */ export const apiDocsTranslationRef = createTranslationRef({ id: 'api-docs', diff --git a/plugins/app/package.json b/plugins/app/package.json index 974a39853b1c7c..25ec33883f234b 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -73,7 +73,7 @@ "motion": "^12.0.0", "react-use": "^17.2.4", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index e96be47580e27e..604f8b1cb294dd 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-atlassian-oauth2": "^2.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts index 949f25db9c5d68..21d07b772c13af 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Atlassian auth provider. diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 7145eea19f0638..a206da0824eb27 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -43,7 +43,7 @@ "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", "node-cache": "^5.1.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts index 38b4b60f270b12..f3a65c91083db5 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AwsAlbResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the AWS ALB auth provider. diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index fcb2c85d97a8d2..e2d6b3f6f2aa33 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -41,7 +41,7 @@ "express": "^4.22.0", "jose": "^5.0.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts index 94afa90c5fee2f..b40b137a11bc0f 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AzureEasyAuthResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @public */ export namespace azureEasyAuthSignInResolvers { diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 4835828f1a247b..694fadcbb19c89 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-bitbucket-oauth2": "^0.1.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts index 6691806e639ff3..bef3d2b34d8687 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Bitbucket auth provider. diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index 4ae6ff37eb93bd..220691062ff06a 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", "passport-oauth2": "^1.6.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts index 2e92d8c6add334..96821669bd4571 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Bitbucket Server auth provider. diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 60f6cbc413bacf..70f7e27bdef71b 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -40,7 +40,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.22.0", "jose": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts index 21cb124be4a5b1..aadd80b2a21ec3 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { CloudflareAccessResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Cloudflare Access auth provider. diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 2c29c29e3cffe3..1533f796f8cc96 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -43,7 +43,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "google-auth-library": "^9.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts index 77dc7b106227ff..9d57dc198e31f3 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { GcpIapResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Google auth provider. diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index bcebf762fd0f57..ca3262f3b6d370 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -37,7 +37,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "passport-github2": "^0.1.12", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts index e0654328269525..b66478d7a62384 100644 --- a/plugins/auth-backend-module-github-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { OAuthAuthenticatorResult, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { GithubProfile } from './authenticator'; diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 8a12e491d77a2b..f836a87396dc06 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-gitlab2": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts index 0b6abd0f2860a0..42e30709af5289 100644 --- a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { OAuthAuthenticatorResult, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { GitlabProfile } from './authenticator'; diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index fa0ae5e42dcaec..409f96ac454b84 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -42,7 +42,7 @@ "@backstage/plugin-auth-node": "workspace:^", "google-auth-library": "^9.0.0", "passport-google-oauth20": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts index 297ac0da6e9701..ed9b080b25625a 100644 --- a/plugins/auth-backend-module-google-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Google auth provider. diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index a869d75af20796..fc978ba0232f83 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "jose": "^5.0.0", "passport-microsoft": "^1.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts index 0ce276522efffa..e27c40684457f9 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Microsoft auth provider. diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 6eb517fbc6094c..9f705004ee0ce6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", "passport-oauth2": "^1.6.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts index bad3f015c83df1..f27f3c70a4445c 100644 --- a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the oauth2 auth provider. diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index caece3cfb2438f..380bf340bc54b4 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -37,7 +37,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts index 8ac639b3c06586..6453b2ab6e6129 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { OAuth2ProxyResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * @public diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 85f9b81cdd0e67..94803d49d7aae1 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -42,7 +42,7 @@ "express": "^4.22.0", "openid-client": "^5.5.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index 16b05d8f60bcbf..ee43e869ed4a24 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -39,7 +39,7 @@ "@davidzemon/passport-okta-oauth": "^0.0.7", "express": "^4.22.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-okta-provider/src/resolvers.ts b/plugins/auth-backend-module-okta-provider/src/resolvers.ts index cdb37dbaae2dac..35839b45813c41 100644 --- a/plugins/auth-backend-module-okta-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-okta-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Okta auth provider. diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 4d9258a5df2109..1bef49600fe97c 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-onelogin-oauth": "^0.0.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts index 56710472beb37b..288e070530b81d 100644 --- a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the OneLogin auth provider. diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json index 4191cf7bc77038..24d2e59c165dd4 100644 --- a/plugins/auth-backend-module-openshift-provider/package.json +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -39,7 +39,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "passport-oauth2": "^1.8.0", - "zod": "^3.24.2" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts index 0c9acb6287d5ee..9972c62adfdad5 100644 --- a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts @@ -22,7 +22,7 @@ import { } from '@backstage/plugin-auth-node'; import { createHash } from 'node:crypto'; import OAuth2Strategy from 'passport-oauth2'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @public */ export interface OpenShiftAuthenticatorContext { diff --git a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts index dee55ec4caac93..f342b69133ae43 100644 --- a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts @@ -25,7 +25,7 @@ import { DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export namespace openshiftSignInResolvers { export const displayNameMatchingUserEntityName = createSignInResolverFactory({ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c4fdee3338ba48..6f7cce3511168f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -66,7 +66,7 @@ "minimatch": "^10.2.1", "passport": "^0.7.0", "uuid": "^11.0.0", - "zod": "^4.3.5", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^5.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index dc921a9489b81a..e7ed82b6bd262a 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -27,8 +27,8 @@ import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; import { OfflineAccessService } from './OfflineAccessService'; import { json } from 'express'; -import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error'; +import { z } from 'zod/v4'; +import { fromZodError } from 'zod-validation-error/v4'; import { OidcError } from './OidcError'; function ensureTrailingSlash(url: string): string { diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index c89095dd9534fd..a1d06111b87339 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -50,7 +50,7 @@ "jose": "^5.0.0", "lodash": "^4.17.21", "passport": "^0.7.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1", "zod-validation-error": "^4.0.2" }, diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index a5939e6e24cf71..23565164a4f7d4 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -16,8 +16,8 @@ import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; import { Response as Response_2 } from 'express'; import { Strategy } from 'passport'; -import { ZodSchema } from 'zod'; -import { ZodTypeDef } from 'zod'; +import type { ZodSchema } from 'zod/v3'; +import type { ZodTypeDef } from 'zod/v3'; // @public (undocumented) export interface AuthOwnershipResolutionExtensionPoint { diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts index 2b7442f7469361..2d593ec44a4ae3 100644 --- a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createSignInResolverFactory } from './createSignInResolverFactory'; import { NotAllowedError } from '@backstage/errors'; diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts index 2bcdbefbb26f63..a74a1d4bfdf427 100644 --- a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema, ZodTypeDef } from 'zod'; +import type { ZodSchema, ZodTypeDef } from 'zod/v3'; import { SignInResolver } from '../types'; import zodToJsonSchema from 'zod-to-json-schema'; import { JsonObject } from '@backstage/types'; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 5f034ed3e5097f..ce2a7a30cf26e7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -95,7 +95,7 @@ "uuid": "^11.0.0", "yaml": "^2.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^4.0.2" }, "devDependencies": { diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index cfac644e34c347..3a5fec589195b6 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import path from 'node:path'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { minimatch } from 'minimatch'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index d0a37802e06ce9..a7f348763b66fc 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -17,7 +17,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; import { get } from 'lodash'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const createPropertyRule = (propertyType: 'metadata' | 'spec') => createPermissionRule({ diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 32e0019715be9a..825eb75cb83fea 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 40a96dbf49b164..f4bc8f3feed323 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 62916925560c79..c0d045bea942e4 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index de336ba9664144..74b22842e63cd9 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -16,7 +16,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; /** diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 88cd5abde31025..9d5944f664d176 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -41,7 +41,7 @@ import { import express from 'express'; import { Server } from 'node:http'; import request from 'supertest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, EntitiesCatalog } from '../catalog/types'; import { applyDatabaseMigrations } from '../database/migrations'; import { DbLocationsRow } from '../database/tables'; diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 89377c9e12ca14..c2711a00ed5076 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -33,7 +33,7 @@ import { InputError, serializeError } from '@backstage/errors'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import express from 'express'; import yn from 'yn'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, EntitiesCatalog } from '../catalog/types'; import { CatalogProcessingOrchestrator } from '../processing/types'; import { validateEntityEnvelope } from '../processing/util'; diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 708eec2de3a3aa..a83782a31b4641 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -18,7 +18,7 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates'; import { Request } from 'express'; import lodash from 'lodash'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, QueryEntitiesCursorRequest, diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index f9d4542625dc3f..8bfbe17e9b6010 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -22,7 +22,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogGraphTranslationRef: TranslationRef< 'catalog-graph', { diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 74b9713cf2785b..4a948ce26228d4 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -15,6 +15,7 @@ import { MouseEvent as MouseEvent_2 } from 'react'; import { MouseEventHandler } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public export const ALL_RELATION_PAIRS: RelationPairs; @@ -69,6 +70,34 @@ export const catalogGraphPlugin: BackstagePlugin< // @public export const catalogGraphRouteRef: RouteRef; +// @public (undocumented) +export const catalogGraphTranslationRef: TranslationRef< + 'catalog-graph', + { + readonly 'catalogGraphCard.title': 'Relations'; + readonly 'catalogGraphCard.deepLinkTitle': 'View graph'; + readonly 'catalogGraphPage.title': 'Catalog Graph'; + readonly 'catalogGraphPage.filterToggleButtonTitle': 'Filters'; + readonly 'catalogGraphPage.supportButtonDescription': 'Start tracking your component in by adding it to the software catalog.'; + readonly 'catalogGraphPage.simplifiedSwitchLabel': 'Simplified'; + readonly 'catalogGraphPage.mergeRelationsSwitchLabel': 'Merge relations'; + readonly 'catalogGraphPage.zoomOutDescription': 'Use pinch & zoom to move around the diagram. Click to change active node, shift click to navigate to entity.'; + readonly 'catalogGraphPage.curveFilter.title': 'Curve'; + readonly 'catalogGraphPage.curveFilter.curveStepBefore': 'Step Before'; + readonly 'catalogGraphPage.curveFilter.curveMonotoneX': 'Monotone X'; + readonly 'catalogGraphPage.directionFilter.title': 'Direction'; + readonly 'catalogGraphPage.directionFilter.leftToRight': 'Left to right'; + readonly 'catalogGraphPage.directionFilter.rightToLeft': 'Right to left'; + readonly 'catalogGraphPage.directionFilter.topToBottom': 'Top to bottom'; + readonly 'catalogGraphPage.directionFilter.bottomToTop': 'Bottom to top'; + readonly 'catalogGraphPage.maxDepthFilter.title': 'Max depth'; + readonly 'catalogGraphPage.maxDepthFilter.inputPlaceholder': '∞ Infinite'; + readonly 'catalogGraphPage.maxDepthFilter.clearButtonAriaLabel': 'clear max depth'; + readonly 'catalogGraphPage.selectedKindsFilter.title': 'Kinds'; + readonly 'catalogGraphPage.selectedRelationsFilter.title': 'Relations'; + } +>; + // @public (undocumented) export type CustomLabelClassKey = 'text' | 'secondary'; diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index a2a8c7eb5e6dce..a0f16cda2a2137 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -107,4 +107,10 @@ export default createFrontendPlugin({ extensions: [CatalogGraphPage, CatalogGraphEntityCard, CatalogGraphApi], }); -export { catalogGraphTranslationRef } from './translation'; +import { catalogGraphTranslationRef as _catalogGraphTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-catalog-graph` instead. + */ +export const catalogGraphTranslationRef = _catalogGraphTranslationRef; diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index 95ac5048cb5af4..119e4e495235ea 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -36,3 +36,4 @@ export type { } from './lib/types'; export { Direction } from './lib/types'; export type { TransformationContext } from './lib/graph-transformations'; +export { catalogGraphTranslationRef } from './translation'; diff --git a/plugins/catalog-graph/src/translation.ts b/plugins/catalog-graph/src/translation.ts index 84b76f965def4d..846188f5c081ad 100644 --- a/plugins/catalog-graph/src/translation.ts +++ b/plugins/catalog-graph/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** @public */ export const catalogGraphTranslationRef = createTranslationRef({ id: 'catalog-graph', messages: { diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index b58b6410508cd8..d00d4634a2d8cf 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -18,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogImportTranslationRef: TranslationRef< 'catalog-import', { diff --git a/plugins/catalog-import/report.api.md b/plugins/catalog-import/report.api.md index 238231e498bf74..7eff3a7a9c48a6 100644 --- a/plugins/catalog-import/report.api.md +++ b/plugins/catalog-import/report.api.md @@ -6,7 +6,7 @@ import { ApiRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; -import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; +import { catalogImportTranslationRef as catalogImportTranslationRef_2 } from '@backstage/plugin-catalog-import/alpha'; import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi } from '@backstage/core-plugin-api'; @@ -26,6 +26,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubmitHandler } from 'react-hook-form'; import { TextFieldProps } from '@material-ui/core/TextField/TextField'; import { TranslationFunction } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; @@ -141,13 +142,86 @@ const catalogImportPlugin: BackstagePlugin< export { catalogImportPlugin }; export { catalogImportPlugin as plugin }; +// @public (undocumented) +export const catalogImportTranslationRef: TranslationRef< + 'catalog-import', + { + readonly 'buttons.back': 'Back'; + readonly 'defaultImportPage.headerTitle': 'Register an existing component'; + readonly 'defaultImportPage.contentHeaderTitle': 'Start tracking your component in {{appTitle}}'; + readonly 'defaultImportPage.supportTitle': 'Start tracking your component in {{appTitle}} by adding it to the software catalog.'; + readonly 'importInfoCard.title': 'Register an existing component'; + readonly 'importInfoCard.deepLinkTitle': 'Learn more about the Software Catalog'; + readonly 'importInfoCard.linkDescription': 'Enter the URL to your source code repository to add it to {{appTitle}}.'; + readonly 'importInfoCard.fileLinkTitle': 'Link to an existing entity file'; + readonly 'importInfoCard.examplePrefix': 'Example: '; + readonly 'importInfoCard.fileLinkDescription': 'The wizard analyzes the file, previews the entities, and adds them to the {{appTitle}} catalog.'; + readonly 'importInfoCard.exampleDescription': 'The wizard discovers all {{catalogFilename}} files in the repository, previews the entities, and adds them to the {{appTitle}} catalog.'; + readonly 'importInfoCard.preparePullRequestDescription': 'If no entities are found, the wizard will prepare a Pull Request that adds an example {{catalogFilename}} and prepares the {{appTitle}} catalog to load all entities as soon as the Pull Request is merged.'; + readonly 'importInfoCard.githubIntegration.label': 'GitHub only'; + readonly 'importInfoCard.githubIntegration.title': 'Link to a repository'; + readonly 'importStepper.finish.title': 'Finish'; + readonly 'importStepper.singleLocation.title': 'Select Locations'; + readonly 'importStepper.singleLocation.description': 'Discovered Locations: 1'; + readonly 'importStepper.multipleLocations.title': 'Select Locations'; + readonly 'importStepper.multipleLocations.description': 'Discovered Locations: {{length, number}}'; + readonly 'importStepper.noLocation.title': 'Create Pull Request'; + readonly 'importStepper.noLocation.createPr.detailsTitle': 'Pull Request Details'; + readonly 'importStepper.noLocation.createPr.titleLabel': 'Pull Request Title'; + readonly 'importStepper.noLocation.createPr.titlePlaceholder': 'Add Backstage catalog entity descriptor files'; + readonly 'importStepper.noLocation.createPr.bodyLabel': 'Pull Request Body'; + readonly 'importStepper.noLocation.createPr.bodyPlaceholder': 'A describing text with Markdown support'; + readonly 'importStepper.noLocation.createPr.configurationTitle': 'Entity Configuration'; + readonly 'importStepper.noLocation.createPr.componentNameLabel': 'Name of the created component'; + readonly 'importStepper.noLocation.createPr.componentNamePlaceholder': 'my-component'; + readonly 'importStepper.noLocation.createPr.ownerLoadingText': 'Loading groups…'; + readonly 'importStepper.noLocation.createPr.ownerHelperText': 'Select an owner from the list or enter a reference to a Group or a User'; + readonly 'importStepper.noLocation.createPr.ownerErrorHelperText': 'required value'; + readonly 'importStepper.noLocation.createPr.ownerLabel': 'Entity Owner'; + readonly 'importStepper.noLocation.createPr.ownerPlaceholder': 'my-group'; + readonly 'importStepper.noLocation.createPr.codeownersHelperText': 'WARNING: This may fail if no CODEOWNERS file is found at the target location.'; + readonly 'importStepper.analyze.title': 'Select URL'; + readonly 'importStepper.prepare.title': 'Import Actions'; + readonly 'importStepper.prepare.description': 'Optional'; + readonly 'importStepper.review.title': 'Review'; + readonly 'stepFinishImportLocation.repository.title': 'The following Pull Request has been opened: '; + readonly 'stepFinishImportLocation.repository.description': 'Your entities will be imported as soon as the Pull Request is merged.'; + readonly 'stepFinishImportLocation.locations.new': 'The following entities have been added to the catalog:'; + readonly 'stepFinishImportLocation.locations.backButtonText': 'Register another'; + readonly 'stepFinishImportLocation.locations.existing': 'A refresh was triggered for the following locations:'; + readonly 'stepFinishImportLocation.locations.viewButtonText': 'View Component'; + readonly 'stepFinishImportLocation.backButtonText': 'Register another'; + readonly 'stepInitAnalyzeUrl.error.default': 'Received unknown analysis result of type {{type}}. Please contact the support team.'; + readonly 'stepInitAnalyzeUrl.error.url': 'Must start with http:// or https://.'; + readonly 'stepInitAnalyzeUrl.error.repository': "Couldn't generate entities for your repository"; + readonly 'stepInitAnalyzeUrl.error.locations': 'There are no entities at this location'; + readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; + readonly 'stepInitAnalyzeUrl.nextButtonText': 'Analyze'; + readonly 'stepPrepareCreatePullRequest.description': 'You entered a link to a {{integrationType}} repository but a {{catalogFilename}} could not be found. Use this form to open a Pull Request that creates one.'; + readonly 'stepPrepareCreatePullRequest.nextButtonText': 'Create PR'; + readonly 'stepPrepareCreatePullRequest.previewPr.title': 'Preview Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewPr.subheader': 'Create a new Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewCatalogInfo.title': 'Preview Entities'; + readonly 'stepPrepareSelectLocations.locations.description': 'Select one or more locations that are present in your git repository:'; + readonly 'stepPrepareSelectLocations.locations.selectAll': 'Select All'; + readonly 'stepPrepareSelectLocations.nextButtonText': 'Review'; + readonly 'stepPrepareSelectLocations.existingLocations.description': 'These locations already exist in the catalog:'; + readonly 'stepReviewLocation.refresh': 'Refresh'; + readonly 'stepReviewLocation.import': 'Import'; + readonly 'stepReviewLocation.catalog.new': 'The following entities will be added to the catalog:'; + readonly 'stepReviewLocation.catalog.exists': 'The following locations already exist in the catalog:'; + readonly 'stepReviewLocation.prepareResult.title': 'The following Pull Request has been opened: '; + readonly 'stepReviewLocation.prepareResult.description': 'You can already import the location and {{appTitle}} will fetch the entities as soon as the Pull Request is merged.'; + } +>; + // Warning: (ae-forgotten-export) The symbol "StepperProvider" needs to be exported by the entry point index.d.ts // // @public export function defaultGenerateStepper( flow: ImportFlows, defaults: StepperProvider, - t: TranslationFunction, + t: TranslationFunction, ): StepperProvider; // @public @@ -216,7 +290,7 @@ export interface ImportStepperProps { generateStepper?: ( flow: ImportFlows, defaults: StepperProvider, - t: TranslationFunction, + t: TranslationFunction, ) => StepperProvider; // (undocumented) initialUrl?: string; diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index a00eb39988240c..d0c671fe5a4bfa 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -34,7 +34,13 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; -export * from './translation'; +import { catalogImportTranslationRef as _catalogImportTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-catalog-import` instead. + */ +export const catalogImportTranslationRef = _catalogImportTranslationRef; // TODO: It's currently possible to override the import page with a custom one. We need to decide // whether this type of override is typically done with an input or by overriding the entire extension. @@ -91,5 +97,3 @@ export default createFrontendPlugin({ importPage: rootRouteRef, }, }); - -export { catalogImportTranslationRef } from './translation'; diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index 149c3e55c6e270..1174d812f73f36 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -27,3 +27,4 @@ export { } from './plugin'; export * from './components'; export * from './api'; +export { catalogImportTranslationRef } from './translation'; diff --git a/plugins/catalog-import/src/translation.ts b/plugins/catalog-import/src/translation.ts index 6fcb33f11ccb22..ca4d2e18faca11 100644 --- a/plugins/catalog-import/src/translation.ts +++ b/plugins/catalog-import/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogImportTranslationRef = createTranslationRef({ id: 'catalog-import', messages: { diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 35a0abd20b1b21..9cda23e6b023b0 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -105,7 +105,7 @@ "react-dom": "^18.0.2", "react-router-dom": "^6.30.2", "react-test-renderer": "^16.13.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "peerDependencies": { "@backstage/frontend-test-utils": "workspace:^", diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 3cf5bc652c2b16..0135fe7d645152 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -36,7 +36,7 @@ export const CatalogFilterBlueprint: ExtensionBlueprint<{ dataRefs: never; }>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogReactTranslationRef: TranslationRef< 'catalog-react', { diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 3ae36ce941ae51..f7f195c22641d0 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -31,6 +31,7 @@ import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TextFieldProps } from '@material-ui/core/TextField'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { TypographyProps } from '@material-ui/core/Typography'; // @public (undocumented) @@ -158,6 +159,101 @@ export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; // @public (undocumented) export type CatalogReactEntityTagPickerClassKey = 'input'; +// @public (undocumented) +export const catalogReactTranslationRef: TranslationRef< + 'catalog-react', + { + readonly 'catalogFilter.title': 'Filters'; + readonly 'catalogFilter.buttonTitle': 'Filters'; + readonly 'entityKindPicker.title': 'Kind'; + readonly 'entityKindPicker.errorMessage': 'Failed to load entity kinds'; + readonly 'entityLifecyclePicker.title': 'Lifecycle'; + readonly 'entityNamespacePicker.title': 'Namespace'; + readonly 'entityOwnerPicker.title': 'Owner'; + readonly 'entityProcessingStatusPicker.title': 'Processing Status'; + readonly 'entityTagPicker.title': 'Tags'; + readonly 'entityPeekAheadPopover.title': 'Drill into the entity to see all of the tags.'; + readonly 'entityPeekAheadPopover.entityCardActionsAriaLabel': 'Show'; + readonly 'entityPeekAheadPopover.entityCardActionsTitle': 'Show details'; + readonly 'entityPeekAheadPopover.emailCardAction.title': 'Email {{email}}'; + readonly 'entityPeekAheadPopover.emailCardAction.ariaLabel': 'Email'; + readonly 'entityPeekAheadPopover.emailCardAction.subTitle': 'mailto {{email}}'; + readonly 'entitySearchBar.placeholder': 'Search'; + readonly 'entityTypePicker.title': 'Type'; + readonly 'entityTypePicker.errorMessage': 'Failed to load entity types'; + readonly 'entityTypePicker.optionAllTitle': 'all'; + readonly 'favoriteEntity.addToFavorites': 'Add to favorites'; + readonly 'favoriteEntity.removeFromFavorites': 'Remove from favorites'; + readonly 'inspectEntityDialog.title': 'Entity Inspector'; + readonly 'inspectEntityDialog.closeButtonTitle': 'Close'; + readonly 'inspectEntityDialog.tabsAriaLabel': 'Inspector options'; + readonly 'inspectEntityDialog.ancestryPage.title': 'Ancestry'; + readonly 'inspectEntityDialog.ancestryPage.description': 'This is the ancestry of entities above the current one - as in, the chain(s) of entities down to the current one, where {{processorsLink}} child entities that ultimately led to the current one existing. Note that this is a completely different mechanism from relations.'; + readonly 'inspectEntityDialog.ancestryPage.processorsLink': 'processors emitted'; + readonly 'inspectEntityDialog.colocatedPage.title': 'Colocated'; + readonly 'inspectEntityDialog.colocatedPage.description': 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).'; + readonly 'inspectEntityDialog.colocatedPage.alertNoLocation': 'Entity had no location information.'; + readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.'; + readonly 'inspectEntityDialog.colocatedPage.locationHeader': 'At the same location'; + readonly 'inspectEntityDialog.colocatedPage.originHeader': 'At the same origin'; + readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON'; + readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.'; + readonly 'inspectEntityDialog.overviewPage.title': 'Overview'; + readonly 'inspectEntityDialog.overviewPage.metadata.title': 'Metadata'; + readonly 'inspectEntityDialog.overviewPage.labels': 'Labels'; + readonly 'inspectEntityDialog.overviewPage.status.title': 'Status'; + readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; + readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; + readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; + readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations'; + readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; + readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; + readonly 'inspectEntityDialog.tabNames.json': 'Raw JSON'; + readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; + readonly 'inspectEntityDialog.tabNames.overview': 'Overview'; + readonly 'inspectEntityDialog.tabNames.ancestry': 'Ancestry'; + readonly 'inspectEntityDialog.tabNames.colocated': 'Colocated'; + readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; + readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; + readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; + readonly 'unregisterEntityDialog.deleteEntitySuccessMessage': 'Removed entity {{entityName}}'; + readonly 'unregisterEntityDialog.onlyDeleteStateTitle': 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.'; + readonly 'unregisterEntityDialog.errorStateTitle': 'Internal error: Unknown state'; + readonly 'unregisterEntityDialog.bootstrapState.title': 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedDescription': 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.title': 'This action will unregister the following entities:'; + readonly 'unregisterEntityDialog.unregisterState.description': 'To undo, just re-register the entity in {{appTitle}}.'; + readonly 'unregisterEntityDialog.unregisterState.subTitle': 'Located at the following location:'; + readonly 'unregisterEntityDialog.unregisterState.advancedDescription': 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.unregisterState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.unregisterButtonTitle': 'Unregister Location'; + readonly 'userListPicker.defaultOrgName': 'Company'; + readonly 'userListPicker.orgFilterAllLabel': 'All'; + readonly 'userListPicker.personalFilter.title': 'Personal'; + readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; + readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + readonly 'entityTableColumnTitle.name': 'Name'; + readonly 'entityTableColumnTitle.type': 'Type'; + readonly 'entityTableColumnTitle.label': 'Label'; + readonly 'entityTableColumnTitle.title': 'Title'; + readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.namespace': 'Namespace'; + readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.tags': 'Tags'; + readonly 'entityTableColumnTitle.owner': 'Owner'; + readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityRelationCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'missingAnnotationEmptyState.title': 'Missing Annotation'; + readonly 'missingAnnotationEmptyState.readMore': 'Read more'; + readonly 'missingAnnotationEmptyState.annotationYaml': 'Add the annotation to your {{entityKind}} YAML as shown in the highlighted example below:'; + readonly 'missingAnnotationEmptyState.generateDescription_one': 'The annotation {{annotations}} is missing. You need to add the annotation to your {{entityKind}} if you want to enable this tool.'; + readonly 'missingAnnotationEmptyState.generateDescription_other': 'The annotations {{annotations}} are missing. You need to add the annotations to your {{entityKind}} if you want to enable this tool.'; + } +>; + // @public (undocumented) export type CatalogReactUserListPickerClassKey = | 'root' diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index a4c8daa83b0c81..d473b0923fb162 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -16,7 +16,13 @@ export * from './blueprints'; export * from './converters'; -export { catalogReactTranslationRef } from '../translation'; +import { catalogReactTranslationRef as _catalogReactTranslationRef } from '../translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-catalog-react` instead. + */ +export const catalogReactTranslationRef = _catalogReactTranslationRef; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; export * from '../components/EntityTable/TitleColumn'; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 2b1664abe25eff..1f58e5f6f949b5 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -34,3 +34,4 @@ export * from './overridableComponents'; export { getEntityRelations, getEntitySourceLocation } from './utils'; export type { EntitySourceLocation } from './utils'; export * from './deprecated'; +export { catalogReactTranslationRef } from './translation'; diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 77e0162e8b80b2..db0c7964244d55 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogReactTranslationRef = createTranslationRef({ id: 'catalog-react', messages: { diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index a26143a1f62461..d41cff916f99f7 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -32,7 +32,7 @@ import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-rea import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogTranslationRef: TranslationRef< 'catalog', { diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index cebf1fd5032c6a..84d9a8bd910f13 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -39,6 +39,7 @@ import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core/Tab'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) @@ -215,6 +216,104 @@ export interface CatalogTableRow { // @public (undocumented) export type CatalogTableToolbarClassKey = 'root' | 'text'; +// @public (undocumented) +export const catalogTranslationRef: TranslationRef< + 'catalog', + { + readonly 'deleteEntity.description': 'This entity is not referenced by any location and is therefore not receiving updates.'; + readonly 'deleteEntity.cancelButtonTitle': 'Cancel'; + readonly 'deleteEntity.deleteButtonTitle': 'Delete'; + readonly 'deleteEntity.dialogTitle': 'Are you sure you want to delete this entity?'; + readonly 'deleteEntity.actionButtonTitle': 'Delete entity'; + readonly 'indexPage.title': '{{orgName}} Catalog'; + readonly 'indexPage.createButtonTitle': 'Create'; + readonly 'indexPage.supportButtonContent': 'All your software catalog entities'; + readonly 'entityPage.notFoundMessage': 'There is no {{kind}} with the requested {{link}}.'; + readonly 'entityPage.notFoundLinkText': 'kind, namespace, and name'; + readonly 'aboutCard.title': 'About'; + readonly 'aboutCard.unknown': 'unknown'; + readonly 'aboutCard.refreshButtonTitle': 'Schedule entity refresh'; + readonly 'aboutCard.editButtonTitle': 'Edit Metadata'; + readonly 'aboutCard.editButtonAriaLabel': 'Edit'; + readonly 'aboutCard.createSimilarButtonTitle': 'Create something similar'; + readonly 'aboutCard.refreshScheduledMessage': 'Refresh scheduled'; + readonly 'aboutCard.refreshButtonAriaLabel': 'Refresh'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'aboutCard.viewTechdocs': 'View TechDocs'; + readonly 'aboutCard.viewSource': 'View Source'; + readonly 'aboutCard.descriptionField.value': 'No description'; + readonly 'aboutCard.descriptionField.label': 'Description'; + readonly 'aboutCard.ownerField.value': 'No Owner'; + readonly 'aboutCard.ownerField.label': 'Owner'; + readonly 'aboutCard.domainField.value': 'No Domain'; + readonly 'aboutCard.domainField.label': 'Domain'; + readonly 'aboutCard.systemField.value': 'No System'; + readonly 'aboutCard.systemField.label': 'System'; + readonly 'aboutCard.parentComponentField.value': 'No Parent Component'; + readonly 'aboutCard.parentComponentField.label': 'Parent Component'; + readonly 'aboutCard.typeField.label': 'Type'; + readonly 'aboutCard.lifecycleField.label': 'Lifecycle'; + readonly 'aboutCard.tagsField.value': 'No Tags'; + readonly 'aboutCard.tagsField.label': 'Tags'; + readonly 'aboutCard.targetsField.label': 'Targets'; + readonly 'searchResultItem.type': 'Type'; + readonly 'searchResultItem.kind': 'Kind'; + readonly 'searchResultItem.owner': 'Owner'; + readonly 'searchResultItem.lifecycle': 'Lifecycle'; + readonly 'catalogTable.allFilters': 'All'; + readonly 'catalogTable.warningPanelTitle': 'Could not fetch catalog entities.'; + readonly 'catalogTable.viewActionTitle': 'View'; + readonly 'catalogTable.editActionTitle': 'Edit'; + readonly 'catalogTable.starActionTitle': 'Add to favorites'; + readonly 'catalogTable.unStarActionTitle': 'Remove from favorites'; + readonly 'dependencyOfComponentsCard.title': 'Dependency of components'; + readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component.'; + readonly 'dependsOnComponentsCard.title': 'Depends on components'; + readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component.'; + readonly 'dependsOnResourcesCard.title': 'Depends on resources'; + readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component.'; + readonly 'entityContextMenu.copiedMessage': 'Copied!'; + readonly 'entityContextMenu.moreButtonTitle': 'More'; + readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity'; + readonly 'entityContextMenu.copyURLMenuTitle': 'Copy entity URL'; + readonly 'entityContextMenu.unregisterMenuTitle': 'Unregister entity'; + readonly 'entityContextMenu.moreButtonAriaLabel': 'more'; + readonly 'entityLabelsCard.title': 'Labels'; + readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLabelsCard.columnKeyLabel': 'Label'; + readonly 'entityLabelsCard.columnValueLabel': 'Value'; + readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLabels.ownerLabel': 'Owner'; + readonly 'entityLabels.warningPanelTitle': 'Entity not found'; + readonly 'entityLabels.lifecycleLabel': 'Lifecycle'; + readonly 'entityLinksCard.title': 'Links'; + readonly 'entityLinksCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLinksCard.emptyDescription': 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:'; + readonly 'entityNotFound.title': 'Entity was not found'; + readonly 'entityNotFound.description': 'Want to help us build this? Check out our Getting Started documentation.'; + readonly 'entityNotFound.docButtonTitle': 'DOCS'; + readonly 'entityTabs.tabsAriaLabel': 'Tabs'; + readonly entityProcessingErrorsDescription: 'The error below originates from'; + readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: "; + readonly 'hasComponentsCard.title': 'Has components'; + readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system.'; + readonly 'hasResourcesCard.title': 'Has resources'; + readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system.'; + readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; + readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component.'; + readonly 'hasSubdomainsCard.title': 'Has subdomains'; + readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain.'; + readonly 'hasSystemsCard.title': 'Has systems'; + readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain.'; + readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'systemDiagramCard.title': 'System Diagram'; + readonly 'systemDiagramCard.description': 'Use pinch & zoom to move around the diagram.'; + readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on'; + readonly 'systemDiagramCard.edgeLabels.partOf': 'part of'; + readonly 'systemDiagramCard.edgeLabels.provides': 'provides'; + } +>; + // @public (undocumented) export type ColumnBreakpoints = Record; diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts index c9f4c8c4b7ee33..ae9f75fa099ffc 100644 --- a/plugins/catalog/src/alpha/index.ts +++ b/plugins/catalog/src/alpha/index.ts @@ -16,4 +16,10 @@ export { default } from './plugin'; -export * from './translation'; +import { catalogTranslationRef as _catalogTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-catalog` instead. + */ +export const catalogTranslationRef = _catalogTranslationRef; diff --git a/plugins/catalog/src/alpha/translation.ts b/plugins/catalog/src/alpha/translation.ts index 240dea64071475..19738844e4e243 100644 --- a/plugins/catalog/src/alpha/translation.ts +++ b/plugins/catalog/src/alpha/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogTranslationRef = createTranslationRef({ id: 'catalog', messages: { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 303677b68ac87b..2c1e81f803c162 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -100,3 +100,4 @@ export type { } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; +export { catalogTranslationRef } from './alpha/translation'; diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 0de77172a05f4b..4cadb413bdca5e 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -125,7 +125,7 @@ export const homePageWidgetDataRef: ConfigurableExtensionDataRef< {} >; -// @alpha +// @alpha @deprecated (undocumented) export const homeReactTranslationRef: TranslationRef< 'home-react', { @@ -134,4 +134,6 @@ export const homeReactTranslationRef: TranslationRef< readonly 'cardExtension.settingsButtonTitle': 'Settings'; } >; + +// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home-react/report.api.md b/plugins/home-react/report.api.md index 26ef6267aa9790..0444010dea5bb0 100644 --- a/plugins/home-react/report.api.md +++ b/plugins/home-react/report.api.md @@ -9,6 +9,7 @@ import { JSX as JSX_3 } from 'react'; import { Overrides } from '@material-ui/core/styles/overrides'; import { RJSFSchema } from '@rjsf/utils'; import { StyleRules } from '@material-ui/core/styles/withStyles'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UiSchema } from '@rjsf/utils'; // @public (undocumented) @@ -81,6 +82,16 @@ export function createCardExtension(options: { settings?: CardSettings; }): Extension<(props: CardExtensionProps) => JSX_2.Element>; +// @public +export const homeReactTranslationRef: TranslationRef< + 'home-react', + { + readonly 'settingsModal.title': 'Settings'; + readonly 'settingsModal.closeButtonTitle': 'Close'; + readonly 'cardExtension.settingsButtonTitle': 'Settings'; + } +>; + // @public (undocumented) export type PluginHomeComponentsNameToClassKey = { PluginHomeContentModal: PluginHomeContentModalClassKey; diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 00b7a7bed740b6..c89ff12afda9c3 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -23,7 +23,13 @@ * * @packageDocumentation */ -export { homeReactTranslationRef } from './translation'; +import { homeReactTranslationRef as _homeReactTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-home-react` instead. + */ +export const homeReactTranslationRef = _homeReactTranslationRef; export { HomePageWidgetBlueprint, type HomePageWidgetBlueprintParams, diff --git a/plugins/home-react/src/index.ts b/plugins/home-react/src/index.ts index 54602dc8146a51..f24d85b2b813e0 100644 --- a/plugins/home-react/src/index.ts +++ b/plugins/home-react/src/index.ts @@ -31,3 +31,4 @@ export type { CardConfig, } from './extensions'; export * from './overridableComponents'; +export { homeReactTranslationRef } from './translation'; diff --git a/plugins/home-react/src/translation.ts b/plugins/home-react/src/translation.ts index 1a8fd9f1bf3e39..ab67d4b37e6a65 100644 --- a/plugins/home-react/src/translation.ts +++ b/plugins/home-react/src/translation.ts @@ -17,9 +17,9 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** * Translation reference for the home-react plugin. - * Contains localized text strings for home page components and settings modals. + * Contains localized text strings for home page components and widgets. * - * @alpha + * @public */ export const homeReactTranslationRef = createTranslationRef({ id: 'home-react', diff --git a/plugins/home/package.json b/plugins/home/package.json index d35003d92153c3..23156430367389 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -79,7 +79,7 @@ "react-grid-layout": "1.3.4", "react-resizable": "^3.0.4", "react-use": "^17.2.4", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 0269f471fe0de4..8e8177980cc02d 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -206,7 +206,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha +// @alpha @deprecated (undocumented) export const homeTranslationRef: TranslationRef< 'home', { diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index a90d5755acb993..2d5f8ddeccba0c 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -23,6 +23,7 @@ import { ReactNode } from 'react'; import { RendererProps as RendererProps_2 } from '@backstage/plugin-home-react'; import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { Variant } from '@material-ui/core/styles/createTypography'; // @public @@ -176,6 +177,39 @@ export const homePlugin: BackstagePlugin< {} >; +// @public +export const homeTranslationRef: TranslationRef< + 'home', + { + readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; + readonly 'addWidgetDialog.title': 'Add new widget to dashboard'; + readonly 'customHomepageButtons.cancel': 'Cancel'; + readonly 'customHomepageButtons.clearAll': 'Clear all'; + readonly 'customHomepageButtons.edit': 'Edit'; + readonly 'customHomepageButtons.restoreDefaults': 'Restore defaults'; + readonly 'customHomepageButtons.addWidget': 'Add widget'; + readonly 'customHomepageButtons.save': 'Save'; + readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button."; + readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; + readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings'; + readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget'; + readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit'; + readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites'; + readonly 'visitList.empty.title': 'There are no visits to show yet.'; + readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; + readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.'; + readonly 'quickStart.title': 'Onboarding'; + readonly 'quickStart.description': 'Get started with Backstage'; + readonly 'quickStart.learnMoreLinkTitle': 'Learn more'; + readonly 'visitedByType.action.viewMore': 'View more'; + readonly 'visitedByType.action.viewLess': 'View less'; + readonly 'featuredDocsCard.empty.title': 'No documents to show'; + readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information'; + readonly 'featuredDocsCard.empty.learnMoreLinkTitle': 'DOCS'; + readonly 'featuredDocsCard.learnMoreTitle': 'LEARN MORE'; + } +>; + // @public export const isOperator: (s: string) => s is Operators; diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 194f322317c4de..bdce683cee873a 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -225,4 +225,10 @@ export default createFrontendPlugin({ }, }); -export { homeTranslationRef } from './translation'; +import { homeTranslationRef as _homeTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-home` instead. + */ +export const homeTranslationRef = _homeTranslationRef; diff --git a/plugins/home/src/components/CustomHomepage/types.ts b/plugins/home/src/components/CustomHomepage/types.ts index 8d73d6d66d9936..9ca12334d59ffc 100644 --- a/plugins/home/src/components/CustomHomepage/types.ts +++ b/plugins/home/src/components/CustomHomepage/types.ts @@ -16,7 +16,7 @@ import { CSSProperties, ReactElement, ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; const RSJFTypeSchema: z.ZodType = z.any(); diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts index 3a6f152d03efdc..35bd43460deac3 100644 --- a/plugins/home/src/index.ts +++ b/plugins/home/src/index.ts @@ -42,3 +42,4 @@ export * from './assets'; export * from './homePageComponents'; export * from './deprecated'; export * from './api'; +export { homeTranslationRef } from './translation'; diff --git a/plugins/home/src/translation.ts b/plugins/home/src/translation.ts index 480e2850b912ac..89cb0756d44e19 100644 --- a/plugins/home/src/translation.ts +++ b/plugins/home/src/translation.ts @@ -19,7 +19,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; * Translation reference for the home plugin. * Contains localized text strings for home page components and widgets. * - * @alpha + * @public */ export const homeTranslationRef = createTranslationRef({ id: 'home', diff --git a/plugins/kubernetes-cluster/report-alpha.api.md b/plugins/kubernetes-cluster/report-alpha.api.md index 2ce9c33c47ecfc..08551d0050be0c 100644 --- a/plugins/kubernetes-cluster/report-alpha.api.md +++ b/plugins/kubernetes-cluster/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesClusterTranslationRef: TranslationRef< 'kubernetes-cluster', { diff --git a/plugins/kubernetes-cluster/report.api.md b/plugins/kubernetes-cluster/report.api.md index 2c9a4c93ef972f..29669f19a828bb 100644 --- a/plugins/kubernetes-cluster/report.api.md +++ b/plugins/kubernetes-cluster/report.api.md @@ -5,6 +5,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public export const EntityKubernetesClusterContent: ( @@ -17,6 +18,15 @@ export type EntityKubernetesClusterContentProps = {}; // @public (undocumented) export const isKubernetesClusterAvailable: (entity: Entity) => boolean; +// @public (undocumented) +export const kubernetesClusterTranslationRef: TranslationRef< + 'kubernetes-cluster', + { + readonly 'kubernetesClusterContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission."; + readonly 'kubernetesClusterContentPage.permissionAlert.title': 'Permission required'; + } +>; + // @public (undocumented) export const Router: () => JSX_2.Element; ``` diff --git a/plugins/kubernetes-cluster/src/alpha.ts b/plugins/kubernetes-cluster/src/alpha.ts index e49b7ac9c9c4f1..63ab8b692ce709 100644 --- a/plugins/kubernetes-cluster/src/alpha.ts +++ b/plugins/kubernetes-cluster/src/alpha.ts @@ -14,4 +14,10 @@ * limitations under the License. */ -export { kubernetesClusterTranslationRef } from './translation'; +import { kubernetesClusterTranslationRef as _kubernetesClusterTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-kubernetes-cluster` instead. + */ +export const kubernetesClusterTranslationRef = _kubernetesClusterTranslationRef; diff --git a/plugins/kubernetes-cluster/src/index.ts b/plugins/kubernetes-cluster/src/index.ts index 6171b151e49a49..650101880c2ed8 100644 --- a/plugins/kubernetes-cluster/src/index.ts +++ b/plugins/kubernetes-cluster/src/index.ts @@ -25,3 +25,4 @@ export { type EntityKubernetesClusterContentProps, } from './plugin'; export { Router, isKubernetesClusterAvailable } from './Router'; +export { kubernetesClusterTranslationRef } from './translation'; diff --git a/plugins/kubernetes-cluster/src/translation.ts b/plugins/kubernetes-cluster/src/translation.ts index ba99c6744eff31..aac274a6176696 100644 --- a/plugins/kubernetes-cluster/src/translation.ts +++ b/plugins/kubernetes-cluster/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesClusterTranslationRef = createTranslationRef({ id: 'kubernetes-cluster', messages: { diff --git a/plugins/kubernetes-react/report-alpha.api.md b/plugins/kubernetes-react/report-alpha.api.md index f638bf67895252..f9f6f67c1e64b5 100644 --- a/plugins/kubernetes-react/report-alpha.api.md +++ b/plugins/kubernetes-react/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesReactTranslationRef: TranslationRef< 'kubernetes-react', { diff --git a/plugins/kubernetes-react/report.api.md b/plugins/kubernetes-react/report.api.md index bb8057b2e8e737..e9631fac38aa9b 100644 --- a/plugins/kubernetes-react/report.api.md +++ b/plugins/kubernetes-react/report.api.md @@ -34,6 +34,7 @@ import { Pod } from 'kubernetes-models/v1'; import { Pod as Pod_2 } from 'kubernetes-models/v1/Pod'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import type { TypeMeta } from '@kubernetes-models/base'; import type { V1Job } from '@kubernetes/client-node'; import type { V1ObjectMeta } from '@kubernetes/client-node'; @@ -554,6 +555,91 @@ export class KubernetesProxyClient { }>; } +// @public (undocumented) +export const kubernetesReactTranslationRef: TranslationRef< + 'kubernetes-react', + { + readonly 'namespace.label': 'namespace:'; + readonly 'namespace.labelWithValue': 'namespace: {{namespace}}'; + readonly 'events.noEventsFound': 'No events found'; + readonly 'events.eventTooltip': '{{eventType}} event'; + readonly 'events.firstEvent': 'First event {{timeAgo}} (count: {{count}})'; + readonly 'cluster.label': 'Cluster'; + readonly 'cluster.pods': 'pods'; + readonly 'cluster.pods_one': '{{count}} pod'; + readonly 'cluster.pods_other': '{{count}} pods'; + readonly 'cluster.podsWithErrors': 'pods with errors'; + readonly 'cluster.podsWithErrors_one': '{{count}} pod with errors'; + readonly 'cluster.podsWithErrors_other': '{{count}} pods with errors'; + readonly 'cluster.noPodsWithErrors': 'No pods with errors'; + readonly 'pods.pods_one': '{{count}} pod'; + readonly 'pods.pods_other': '{{count}} pods'; + readonly 'podsTable.columns.name': 'name'; + readonly 'podsTable.columns.id': 'ID'; + readonly 'podsTable.columns.status': 'status'; + readonly 'podsTable.columns.phase': 'phase'; + readonly 'podsTable.columns.containersReady': 'containers ready'; + readonly 'podsTable.columns.totalRestarts': 'total restarts'; + readonly 'podsTable.columns.cpuUsage': 'CPU usage %'; + readonly 'podsTable.columns.memoryUsage': 'Memory usage %'; + readonly 'podsTable.unknown': 'unknown'; + readonly 'podsTable.status.running': 'Running'; + readonly 'podsTable.status.ok': 'OK'; + readonly 'errorPanel.message': 'There was a problem retrieving some Kubernetes resources for the entity: {{entityName}}. This could mean that the Error Reporting card is not completely accurate.'; + readonly 'errorPanel.title': 'There was a problem retrieving Kubernetes objects'; + readonly 'errorPanel.errorsLabel': 'Errors'; + readonly 'errorPanel.clusterLabel': 'Cluster'; + readonly 'errorPanel.clusterLabelValue': 'Cluster: {{cluster}}'; + readonly 'errorPanel.fetchError': 'Error communicating with Kubernetes: {{errorType}}, message: {{message}}'; + readonly 'errorPanel.resourceError': "Error fetching Kubernetes resource: '{{resourcePath}}', error: {{errorType}}, status code: {{statusCode}}"; + readonly 'fixDialog.title': '{{podName}} - {{errorType}}'; + readonly 'fixDialog.events': 'Events:'; + readonly 'fixDialog.helpButton': 'Help'; + readonly 'fixDialog.detectedError': 'Detected error:'; + readonly 'fixDialog.causeExplanation': 'Cause explanation:'; + readonly 'fixDialog.fix': 'Fix:'; + readonly 'fixDialog.crashLogs': 'Crash logs:'; + readonly 'fixDialog.openDocs': 'Open docs'; + readonly 'fixDialog.ariaLabels.close': 'close'; + readonly 'fixDialog.ariaLabels.fixIssue': 'fix issue'; + readonly 'podDrawer.buttons.delete': 'Delete Pod'; + readonly 'podDrawer.cpuRequests': 'CPU requests'; + readonly 'podDrawer.cpuLimits': 'CPU limits'; + readonly 'podDrawer.memoryRequests': 'Memory requests'; + readonly 'podDrawer.memoryLimits': 'Memory limits'; + readonly 'podDrawer.resourceUtilization': 'Resource utilization'; + readonly 'hpa.minReplicas': 'min replicas'; + readonly 'hpa.maxReplicas': 'max replicas'; + readonly 'hpa.replicasSummary': 'min replicas {{min}} / max replicas {{max}}'; + readonly 'hpa.currentCpuUsage': 'current CPU usage:'; + readonly 'hpa.currentCpuUsageLabel': 'current CPU usage: {{value}}%'; + readonly 'hpa.targetCpuUsage': 'target CPU usage:'; + readonly 'hpa.targetCpuUsageLabel': 'target CPU usage: {{value}}%'; + readonly 'errorReporting.columns.name': 'name'; + readonly 'errorReporting.columns.kind': 'kind'; + readonly 'errorReporting.columns.namespace': 'namespace'; + readonly 'errorReporting.columns.messages': 'messages'; + readonly 'errorReporting.columns.cluster': 'cluster'; + readonly 'errorReporting.title': 'Error Reporting'; + readonly 'podLogs.title': 'No logs emitted'; + readonly 'podLogs.description': 'No logs were emitted by the container'; + readonly 'podLogs.buttonText': 'Logs'; + readonly 'podLogs.titleTemplate': '{{podName}} - {{containerName}} logs on cluster {{clusterName}}'; + readonly 'podLogs.buttonAriaLabel': 'get logs'; + readonly 'podExecTerminal.buttonText': 'Terminal'; + readonly 'podExecTerminal.titleTemplate': '{{podName}} - {{containerName}} terminal shell on cluster {{clusterName}}'; + readonly 'podExecTerminal.buttonAriaLabel': 'open terminal'; + readonly 'kubernetesDrawer.yaml': 'YAML'; + readonly 'kubernetesDrawer.closeDrawer': 'Close the drawer'; + readonly 'kubernetesDrawer.managedFields': 'Managed Fields'; + readonly 'kubernetesDrawer.unknownName': 'unknown name'; + readonly 'linkErrorPanel.message': "Could not format the link to the dashboard of your cluster named '{{clusterName}}'. Its dashboardApp property has been set to '{{dashboardApp}}.'"; + readonly 'linkErrorPanel.title': 'There was a problem formatting the link to the Kubernetes dashboard'; + readonly 'linkErrorPanel.errorsLabel': 'Errors:'; + readonly 'kubernetesDialog.closeAriaLabel': 'close'; + } +>; + // @public (undocumented) export const KubernetesStructuredMetadataTableDrawer: < T extends KubernetesDrawerable, diff --git a/plugins/kubernetes-react/src/alpha.ts b/plugins/kubernetes-react/src/alpha.ts index 411bd788786431..60f80039dcfda9 100644 --- a/plugins/kubernetes-react/src/alpha.ts +++ b/plugins/kubernetes-react/src/alpha.ts @@ -14,6 +14,10 @@ * limitations under the License. */ -import { kubernetesReactTranslationRef } from './translation'; +import { kubernetesReactTranslationRef as _kubernetesReactTranslationRef } from './translation'; -export { kubernetesReactTranslationRef }; +/** + * @alpha + * @deprecated Import from `@backstage/plugin-kubernetes-react` instead. + */ +export const kubernetesReactTranslationRef = _kubernetesReactTranslationRef; diff --git a/plugins/kubernetes-react/src/index.ts b/plugins/kubernetes-react/src/index.ts index 326b363b052eb9..e52ac76d498d47 100644 --- a/plugins/kubernetes-react/src/index.ts +++ b/plugins/kubernetes-react/src/index.ts @@ -28,3 +28,4 @@ export * from './api'; export * from './kubernetes-auth-provider'; export * from './components'; export * from './types'; +export { kubernetesReactTranslationRef } from './translation'; diff --git a/plugins/kubernetes-react/src/translation.ts b/plugins/kubernetes-react/src/translation.ts index 0420cc04bf3d44..bc8f3d99dadc28 100644 --- a/plugins/kubernetes-react/src/translation.ts +++ b/plugins/kubernetes-react/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesReactTranslationRef = createTranslationRef({ id: 'kubernetes-react', messages: { diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 708697672479fa..136385e686eb40 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -240,7 +240,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesTranslationRef: TranslationRef< 'kubernetes', { diff --git a/plugins/kubernetes/report.api.md b/plugins/kubernetes/report.api.md index b9450e650d748b..8a6b02a7aa1342 100644 --- a/plugins/kubernetes/report.api.md +++ b/plugins/kubernetes/report.api.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // Warning: (ae-missing-release-tag) "EntityKubernetesContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -37,6 +38,19 @@ const kubernetesPlugin: BackstagePlugin< export { kubernetesPlugin }; export { kubernetesPlugin as plugin }; +// @public (undocumented) +export const kubernetesTranslationRef: TranslationRef< + 'kubernetes', + { + readonly 'entityContent.title': 'Kubernetes'; + readonly 'kubernetesContentPage.title': 'Your Clusters'; + readonly 'kubernetesContentPage.emptyState.title': 'No Kubernetes resources'; + readonly 'kubernetesContentPage.emptyState.description': 'No resources on any known clusters for {{entityName}}'; + readonly 'kubernetesContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission."; + readonly 'kubernetesContentPage.permissionAlert.title': 'Permission required'; + } +>; + // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/kubernetes/src/alpha/index.ts b/plugins/kubernetes/src/alpha/index.ts index 23888f968f9d33..c3419cfa76dbfa 100644 --- a/plugins/kubernetes/src/alpha/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -13,5 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { kubernetesTranslationRef } from './translation'; +import { kubernetesTranslationRef as _kubernetesTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-kubernetes` instead. + */ +export const kubernetesTranslationRef = _kubernetesTranslationRef; export { default } from './plugin'; diff --git a/plugins/kubernetes/src/alpha/translation.ts b/plugins/kubernetes/src/alpha/translation.ts index 238bd3b0840704..17ebe90de07408 100644 --- a/plugins/kubernetes/src/alpha/translation.ts +++ b/plugins/kubernetes/src/alpha/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesTranslationRef = createTranslationRef({ id: 'kubernetes', messages: { diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index 3651a861fc2e2a..54abb07bc3f126 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -29,3 +29,4 @@ export type { EntityKubernetesContentProps } from './plugin'; export { Router, isKubernetesAvailable } from './Router'; // TODO remove this re-export as a breaking change after a couple of releases export * from '@backstage/plugin-kubernetes-react'; +export { kubernetesTranslationRef } from './alpha/translation'; diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 770e52474c52a6..58b8d8fc5ad402 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -47,7 +47,7 @@ "express": "^4.22.0", "express-promise-router": "^4.1.0", "minimatch": "^10.2.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 7cc7b280caec20..55759c0428bd18 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -42,7 +42,6 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/types": "workspace:^", - "@opentelemetry/api": "^1.9.0", "@slack/bolt": "^3.21.4", "@slack/types": "^2.14.0", "@slack/web-api": "^7.5.0", diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index ce7898566be917..0393070b3f7ea5 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -15,6 +15,7 @@ */ import { mockServices } from '@backstage/backend-test-utils'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; import { SlackNotificationProcessor } from './SlackNotificationProcessor'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { KnownBlock, WebClient } from '@slack/web-api'; @@ -125,6 +126,7 @@ const DEFAULT_ENTITIES_RESPONSE = { describe('SlackNotificationProcessor', () => { const logger = mockServices.logger.mock(); const auth = mockServices.auth(); + const metrics = metricsServiceMock.mock(); const config = mockServices.rootConfig({ data: { app: { @@ -157,6 +159,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -224,6 +227,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, blockKitRenderer: () => customBlocks, })[0]; @@ -256,6 +260,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -331,6 +336,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -365,6 +371,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -410,6 +417,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -465,6 +473,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -529,6 +538,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -584,6 +594,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -639,6 +650,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -694,6 +706,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -750,6 +763,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -809,6 +823,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -863,6 +878,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -921,6 +937,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -959,6 +976,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -982,6 +1000,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: [DEFAULT_ENTITIES_RESPONSE.items[2]], }), + metrics, slack, })[0]; @@ -1021,6 +1040,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1066,6 +1086,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1125,6 +1146,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1204,6 +1226,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1298,6 +1321,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, }, )[0]; @@ -1375,6 +1399,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1441,6 +1466,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1481,6 +1507,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1520,6 +1547,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1568,6 +1596,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, }, )[0]; diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index de084df4648d0a..28846fcbb552cb 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -15,6 +15,10 @@ */ import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { + MetricsService, + MetricsServiceCounter, +} from '@backstage/backend-plugin-api/alpha'; import { Entity, isUserEntity, @@ -30,7 +34,6 @@ import { NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { durationToMilliseconds } from '@backstage/types'; -import { Counter, metrics } from '@opentelemetry/api'; import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; import DataLoader from 'dataloader'; import pThrottle from 'p-throttle'; @@ -48,8 +51,8 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly sendNotifications: ( opts: ChatPostMessageArguments[], ) => Promise; - private readonly messagesSent: Counter; - private readonly messagesFailed: Counter; + private readonly messagesSent: MetricsServiceCounter; + private readonly messagesFailed: MetricsServiceCounter; private readonly broadcastChannels?: string[]; private readonly broadcastRoutes?: BroadcastRoute[]; private readonly entityLoader: DataLoader; @@ -64,6 +67,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth: AuthService; logger: LoggerService; catalog: CatalogService; + metrics: MetricsService; slack?: WebClient; broadcastChannels?: string[]; blockKitRenderer?: SlackBlockKitRenderer; @@ -103,6 +107,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth: AuthService; logger: LoggerService; catalog: CatalogService; + metrics: MetricsService; broadcastChannels?: string[]; broadcastRoutes?: BroadcastRoute[]; username?: string; @@ -114,6 +119,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth, catalog, logger, + metrics, slack, broadcastChannels, broadcastRoutes, @@ -159,17 +165,18 @@ export class SlackNotificationProcessor implements NotificationProcessor { }, ); - const meter = metrics.getMeter('default'); - this.messagesSent = meter.createCounter( + this.messagesSent = metrics.createCounter( 'notifications.processors.slack.sent.count', { description: 'Number of messages sent to Slack successfully', + unit: '{message}', }, ); - this.messagesFailed = meter.createCounter( + this.messagesFailed = metrics.createCounter( 'notifications.processors.slack.error.count', { description: 'Number of messages that failed to send to Slack', + unit: '{message}', }, ); diff --git a/plugins/notifications-backend-module-slack/src/module.ts b/plugins/notifications-backend-module-slack/src/module.ts index e3d6aebee522f9..88f0272eb3ba1d 100644 --- a/plugins/notifications-backend-module-slack/src/module.ts +++ b/plugins/notifications-backend-module-slack/src/module.ts @@ -17,6 +17,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; @@ -52,13 +53,15 @@ export const notificationsModuleSlack = createBackendModule({ logger: coreServices.logger, catalog: catalogServiceRef, notifications: notificationsProcessingExtensionPoint, + metrics: metricsServiceRef, }, - async init({ auth, config, logger, catalog, notifications }) { + async init({ auth, config, logger, catalog, notifications, metrics }) { notifications.addProcessor( SlackNotificationProcessor.fromConfig(config, { auth, logger, catalog, + metrics, blockKitRenderer, }), ); diff --git a/plugins/notifications/report-alpha.api.md b/plugins/notifications/report-alpha.api.md index e1feed0cdef647..cfc88c3d7ac597 100644 --- a/plugins/notifications/report-alpha.api.md +++ b/plugins/notifications/report-alpha.api.md @@ -120,7 +120,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const notificationsTranslationRef: TranslationRef< 'plugin.notifications', { diff --git a/plugins/notifications/report.api.md b/plugins/notifications/report.api.md index 2c056d4e91fe8f..21dc817d91fe73 100644 --- a/plugins/notifications/report.api.md +++ b/plugins/notifications/report.api.md @@ -15,6 +15,7 @@ import { NotificationSeverity } from '@backstage/plugin-notifications-common'; import { NotificationStatus } from '@backstage/plugin-notifications-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableProps } from '@backstage/core-components'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export type GetNotificationsCommonOptions = { @@ -187,6 +188,71 @@ export type NotificationsTableProps = Pick< pageSize: number; }; +// @public (undocumented) +export const notificationsTranslationRef: TranslationRef< + 'plugin.notifications', + { + readonly 'table.errors.markAllReadFailed': 'Failed to mark all notifications as read'; + readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}'; + readonly 'table.pagination.firstTooltip': 'First Page'; + readonly 'table.pagination.labelRowsSelect': 'rows'; + readonly 'table.pagination.lastTooltip': 'Last Page'; + readonly 'table.pagination.nextTooltip': 'Next Page'; + readonly 'table.pagination.previousTooltip': 'Previous Page'; + readonly 'table.emptyMessage': 'No records to display'; + readonly 'table.bulkActions.markAllRead': 'Mark all read'; + readonly 'table.bulkActions.markSelectedAsRead': 'Mark selected as read'; + readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread'; + readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later'; + readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected'; + readonly 'table.confirmDialog.title': 'Are you sure?'; + readonly 'table.confirmDialog.markAllReadDescription': 'Mark all notifications as read.'; + readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All'; + readonly 'filters.view.all': 'All'; + readonly 'filters.view.label': 'View'; + readonly 'filters.view.read': 'Read notifications'; + readonly 'filters.view.saved': 'Saved'; + readonly 'filters.view.unread': 'Unread notifications'; + readonly 'filters.title': 'Filters'; + readonly 'filters.severity.normal': 'Normal'; + readonly 'filters.severity.high': 'High'; + readonly 'filters.severity.low': 'Low'; + readonly 'filters.severity.label': 'Min severity'; + readonly 'filters.severity.critical': 'Critical'; + readonly 'filters.topic.label': 'Topic'; + readonly 'filters.topic.anyTopic': 'Any topic'; + readonly 'filters.createdAfter.label': 'Sent out'; + readonly 'filters.createdAfter.placeholder': 'Notifications since'; + readonly 'filters.createdAfter.last24h': 'Last 24h'; + readonly 'filters.createdAfter.lastWeek': 'Last week'; + readonly 'filters.createdAfter.anyTime': 'Any time'; + readonly 'filters.sortBy.origin': 'Origin'; + readonly 'filters.sortBy.label': 'Sort by'; + readonly 'filters.sortBy.placeholder': 'Field to sort by'; + readonly 'filters.sortBy.newest': 'Newest on top'; + readonly 'filters.sortBy.oldest': 'Oldest on top'; + readonly 'filters.sortBy.topic': 'Topic'; + readonly 'settings.table.origin': 'Origin'; + readonly 'settings.table.topic': 'Topic'; + readonly 'settings.title': 'Notification settings'; + readonly 'settings.errors.useNotificationFormat': 'useNotificationFormat must be used within a NotificationFormatProvider'; + readonly 'settings.errorTitle': 'Failed to load settings'; + readonly 'settings.noSettingsAvailable': 'No notification settings available, check back later'; + readonly 'sidebar.title': 'Notifications'; + readonly 'sidebar.errors.markAsReadFailed': 'Failed to mark notification as read'; + readonly 'sidebar.errors.fetchNotificationFailed': 'Failed to fetch notification'; + readonly 'notificationsPage.title': 'Notifications'; + readonly 'notificationsPage.tableTitle.all_one': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.all_other': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_one': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_other': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_one': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_other': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_one': 'Read notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_other': 'Read notifications ({{count}})'; + } +>; + // @public (undocumented) export type UpdateNotificationsOptions = { ids: string[]; diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index b930373a461024..12ae164aa14fd4 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -56,4 +56,10 @@ export default createFrontendPlugin({ extensions: [page, api], }); -export { notificationsTranslationRef } from './translation'; +import { notificationsTranslationRef as _notificationsTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-notifications` instead. + */ +export const notificationsTranslationRef = _notificationsTranslationRef; diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index 25565f1ae123ad..b67ae9b108a27e 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -17,3 +17,4 @@ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; export { useNotificationsApi } from './hooks'; export * from './components'; +export { notificationsTranslationRef } from './translation'; diff --git a/plugins/notifications/src/translation.ts b/plugins/notifications/src/translation.ts index d810ed9966697b..f8c2749c4205df 100644 --- a/plugins/notifications/src/translation.ts +++ b/plugins/notifications/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** @public */ export const notificationsTranslationRef = createTranslationRef({ id: 'plugin.notifications', messages: { diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index 44fd429bfac644..e3d61c263dc4dd 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -202,7 +202,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const orgTranslationRef: TranslationRef< 'org', { diff --git a/plugins/org/report.api.md b/plugins/org/report.api.md index d51a45e3f7e208..2fa5e7a2851dbe 100644 --- a/plugins/org/report.api.md +++ b/plugins/org/report.api.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export type ComponentsGridClassKey = @@ -85,6 +86,35 @@ const orgPlugin: BackstagePlugin< export { orgPlugin }; export { orgPlugin as plugin }; +// @public (undocumented) +export const orgTranslationRef: TranslationRef< + 'org', + { + readonly 'groupProfileCard.groupNotFound': 'Group not found'; + readonly 'groupProfileCard.editIconButtonTitle': 'Edit Metadata'; + readonly 'groupProfileCard.refreshIconButtonTitle': 'Schedule entity refresh'; + readonly 'groupProfileCard.refreshIconButtonAriaLabel': 'Refresh'; + readonly 'groupProfileCard.listItemTitle.email': 'Email'; + readonly 'groupProfileCard.listItemTitle.entityRef': 'Entity Ref'; + readonly 'groupProfileCard.listItemTitle.parentGroup': 'Parent Group'; + readonly 'groupProfileCard.listItemTitle.childGroups': 'Child Groups'; + readonly 'membersListCard.title': '{{groupName}} members'; + readonly 'membersListCard.cardLabel': 'User page for {{memberName}}'; + readonly 'membersListCard.noMembersDescription': 'This group has no members.'; + readonly 'membersListCard.noSearchResult': 'Found no members matching "{{searchTerm}}".'; + readonly 'membersListCard.aggregateMembersToggle.label': 'Include subgroups'; + readonly 'ownershipCard.title': 'Ownership'; + readonly 'ownershipCard.aggregateRelationsToggle.label': 'Include indirect ownership'; + readonly 'userProfileCard.editIconButtonTitle': 'Edit Metadata'; + readonly 'userProfileCard.listItemTitle.email': 'Email'; + readonly 'userProfileCard.listItemTitle.memberOf': 'Member of'; + readonly 'userProfileCard.userNotFound': 'User not found'; + readonly 'userProfileCard.moreGroupButtonTitle': '...More ({{number}})'; + readonly 'userProfileCard.allGroupDialog.title': "All {{name}}'s groups:"; + readonly 'userProfileCard.allGroupDialog.closeButtonTitle': 'Close'; + } +>; + // @public (undocumented) export const OwnershipCard: (props: { entityFilterKind?: string[]; diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 4e1146378112aa..80155c8467e5ce 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -130,4 +130,10 @@ export default createFrontendPlugin({ }, }); -export { orgTranslationRef } from './translation'; +import { orgTranslationRef as _orgTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-org` instead. + */ +export const orgTranslationRef = _orgTranslationRef; diff --git a/plugins/org/src/index.ts b/plugins/org/src/index.ts index 33671697d6b6c2..90f475bb6033c0 100644 --- a/plugins/org/src/index.ts +++ b/plugins/org/src/index.ts @@ -29,3 +29,4 @@ export { EntityUserProfileCard, } from './plugin'; export * from './components'; +export { orgTranslationRef } from './translation'; diff --git a/plugins/org/src/translation.ts b/plugins/org/src/translation.ts index b5309d14e7a56f..aa1c248c17d536 100644 --- a/plugins/org/src/translation.ts +++ b/plugins/org/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** - * @alpha + * @public */ export const orgTranslationRef = createTranslationRef({ id: 'org', diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index ca97759c1928b9..40ef10dcf66da9 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -62,7 +62,7 @@ "express-promise-router": "^4.1.0", "lodash": "^4.17.21", "yn": "^4.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index 7d2a8a34c2baad..7f0caa9670cc41 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -30,7 +30,7 @@ import { createPermissionRule, } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { DiscoveryService } from '@backstage/backend-plugin-api'; describe('PermissionIntegrationClient', () => { diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index 610eb8b34bb861..83dc7780f3c500 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthorizeResult, ConditionalPolicyDecision, diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b6f570562b0648..92a2e4025928f6 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import express, { Request, Response } from 'express'; import Router from 'express-promise-router'; import { InputError } from '@backstage/errors'; diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 85607676cf39a3..e065226c856e85 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -53,7 +53,7 @@ "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", "uuid": "^11.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index f801ba2a4bf252..d178dcf2afde1a 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import { ResponseError } from '@backstage/errors'; import fetch from 'cross-fetch'; import * as uuid from 'uuid'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthorizeResult, PermissionMessageBatch, diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index bfcc5d5ad0aaeb..b8258a42294593 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -64,7 +64,7 @@ "@types/express": "^4.17.6", "express": "^4.22.0", "express-promise-router": "^4.1.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 6345861c939171..09452b969b632a 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -30,7 +30,7 @@ import { PermissionsServiceRequestOptions } from '@backstage/backend-plugin-api' import { PolicyDecision } from '@backstage/plugin-permission-common'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { ResourcePermission } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public export type ApplyConditionsRequest = { diff --git a/plugins/permission-node/src/integration/createConditionExports.test.ts b/plugins/permission-node/src/integration/createConditionExports.test.ts index 926e607baadda2..341b4e73e1503b 100644 --- a/plugins/permission-node/src/integration/createConditionExports.test.ts +++ b/plugins/permission-node/src/integration/createConditionExports.test.ts @@ -18,7 +18,7 @@ import { AuthorizeResult, createPermission, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionExports } from './createConditionExports'; import { createPermissionRule } from './createPermissionRule'; import { createPermissionResourceRef } from './createPermissionResourceRef'; diff --git a/plugins/permission-node/src/integration/createConditionFactory.test.ts b/plugins/permission-node/src/integration/createConditionFactory.test.ts index 520fcc01f95719..02d01e7d14d215 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.test.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionFactory } from './createConditionFactory'; import { createPermissionRule } from './createPermissionRule'; diff --git a/plugins/permission-node/src/integration/createConditionTransformer.test.ts b/plugins/permission-node/src/integration/createConditionTransformer.test.ts index a5507f0b44920a..ce725679028e3a 100644 --- a/plugins/permission-node/src/integration/createConditionTransformer.test.ts +++ b/plugins/permission-node/src/integration/createConditionTransformer.test.ts @@ -18,7 +18,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionTransformer } from './createConditionTransformer'; import { createPermissionRule } from './createPermissionRule'; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 0d28ef710f6729..52352dc454bd72 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-permission-common'; import express from 'express'; import request, { Response } from 'supertest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createPermissionIntegrationRouter, CreatePermissionIntegrationRouterResourceOptions, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 522e60bac5dd41..6d96e45659f8a2 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -16,7 +16,7 @@ import express, { Response } from 'express'; import Router from 'express-promise-router'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { InputError } from '@backstage/errors'; import { diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts index 36f8205cf8573a..f1970a025421dd 100644 --- a/plugins/permission-node/src/integration/createPermissionRule.ts +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -19,7 +19,7 @@ import { PermissionRuleParams, } from '@backstage/plugin-permission-common'; import { NoInfer, PermissionRule } from '../types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { PermissionResourceRef } from './createPermissionResourceRef'; /** diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 578117a4f1844b..a8a337ca88b164 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -18,7 +18,7 @@ import type { PermissionCriteria, PermissionRuleParams, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Prevent use of type parameter from contributing to type inference. diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 10ff9bc6796a3b..46be5ea69d9605 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -51,7 +51,7 @@ "bitbucket": "^2.12.0", "fs-extra": "^11.2.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts index fadc8bcefba422..ed35bfeab81b53 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const repoUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-cookiecutter/report.api.md b/plugins/scaffolder-backend-module-cookiecutter/report.api.md index 49888ad784d6a6..43303508e7eb36 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/report.api.md +++ b/plugins/scaffolder-backend-module-cookiecutter/report.api.md @@ -4,12 +4,12 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { objectOutputType } from 'zod'; +import { objectOutputType } from 'zod/v3'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Writable } from 'node:stream'; -import { ZodTypeAny } from 'zod'; +import { ZodTypeAny } from 'zod/v3'; // @public export interface ContainerRunner { diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 27f4241a8df449..cad8221cd26980 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -55,7 +55,7 @@ "octokit": "^3.0.0", "octokit-plugin-create-pull-request": "^5.0.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index a474272ef7cb6a..a2e0ae0bf3ff0a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const repoUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts index b35047083d2358..50a727be8815fd 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const remoteUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 28517d25f60f27..f803e90518c3a3 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -55,7 +55,7 @@ "@gitbeaker/rest": "^43.8.0", "luxon": "^3.0.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts index 5e8d92338ec535..4208cdb805be40 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts @@ -16,7 +16,7 @@ /* We want to maintain the same information as an enum, so we disable the redeclaration warning */ /* eslint-disable @typescript-eslint/no-redeclare */ -import { z } from 'zod'; +import { z } from 'zod/v3'; const commonGitlabConfig = z.object({ repoUrl: z.string({ description: 'Repository Location' }), diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index e5e27231e522e5..328f9d237edcad 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -21,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Gitlab, GroupSchema, RepositoryTreeSchema } from '@gitbeaker/rest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import commonGitlabConfig from './commonGitlabConfig'; import { SerializedFile } from '@backstage/plugin-scaffolder-node'; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index fc5396377e7127..b8f7b649db9b1c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -99,7 +99,7 @@ "winston-transport": "^4.7.0", "yaml": "^2.0.0", "zen-observable": "^0.10.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts index 0e82092ab3d8d3..84fa7a33e54019 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts @@ -17,7 +17,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { examples } from './read.examples'; const contentSchema = (z: typeof zod) => diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 0c9daf53c27224..9e84b38e39c9d5 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -85,7 +85,7 @@ import express from 'express'; import { Duration } from 'luxon'; import { pathToFileURL } from 'node:url'; import { v4 as uuid } from 'uuid'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { DatabaseTaskStore, DefaultTemplateActionRegistry, diff --git a/plugins/scaffolder-backend/src/service/rules.ts b/plugins/scaffolder-backend/src/service/rules.ts index 5753668089d1ff..7dea3e1f95bd9e 100644 --- a/plugins/scaffolder-backend/src/service/rules.ts +++ b/plugins/scaffolder-backend/src/service/rules.ts @@ -29,7 +29,7 @@ import { import { SerializedTask, TaskFilter } from '@backstage/plugin-scaffolder-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { JsonObject, JsonPrimitive } from '@backstage/types'; import { get } from 'lodash'; diff --git a/plugins/scaffolder-backend/src/util/templating.ts b/plugins/scaffolder-backend/src/util/templating.ts index 3318b64fdfac90..a088c955255ee7 100644 --- a/plugins/scaffolder-backend/src/util/templating.ts +++ b/plugins/scaffolder-backend/src/util/templating.ts @@ -26,7 +26,7 @@ import { } from '@backstage/plugin-scaffolder-node/alpha'; import { JsonValue } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { ZodType, z } from 'zod'; +import { ZodType, z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; /** diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index a79c47d3675b5e..c9859ffa46da93 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -76,7 +76,7 @@ "tar": "^7.5.6", "winston": "^3.2.1", "winston-transport": "^4.7.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index 3a01b7f78828a3..62240f210af3e9 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -8,7 +8,7 @@ import { JsonValue } from '@backstage/types'; import { TaskBroker } from '@backstage/plugin-scaffolder-node'; import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @alpha export type AutocompleteHandler = (input: { diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index 24b51ed98976eb..538f19cd1e2cb5 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -33,7 +33,7 @@ import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/a import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'node:stream'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public export type ActionContext< diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index edf3e203b96fe3..a3dfe3f4693d43 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -15,7 +15,7 @@ */ import { ActionContext, TemplateAction } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Expand, JsonObject } from '@backstage/types'; import { parseSchemas } from './util'; diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index e698306a450c8d..bddf3ba0f3fdec 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -20,7 +20,7 @@ import { join as joinPath, normalize as normalizePath } from 'node:path'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateActionOptions } from './createTemplateAction'; import zodToJsonSchema from 'zod-to-json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Schema } from 'jsonschema'; import { trim } from 'lodash'; diff --git a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts index f6486585889efd..ce8bd517eee4c0 100644 --- a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts +++ b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts @@ -16,7 +16,7 @@ import { ZodFunctionSchema } from '../types'; import { CreatedTemplateFilter, TemplateFilterExample } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * This function is used to create new template filters in type-safe manner. diff --git a/plugins/scaffolder-node/src/alpha/filters/types.ts b/plugins/scaffolder-node/src/alpha/filters/types.ts index 1906ba1c9c4426..e721618f7aa1f0 100644 --- a/plugins/scaffolder-node/src/alpha/filters/types.ts +++ b/plugins/scaffolder-node/src/alpha/filters/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ZodFunctionSchema } from '../types'; export type { TemplateFilter } from '../../types'; diff --git a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts index 2687b83bb48832..0d5a01650cbc87 100644 --- a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts +++ b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CreatedTemplateGlobalFunction, CreatedTemplateGlobalValue, diff --git a/plugins/scaffolder-node/src/alpha/globals/types.ts b/plugins/scaffolder-node/src/alpha/globals/types.ts index 27a3a700e57472..977f733673edea 100644 --- a/plugins/scaffolder-node/src/alpha/globals/types.ts +++ b/plugins/scaffolder-node/src/alpha/globals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JsonValue } from '@backstage/types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ZodFunctionSchema } from '../types'; export type { TemplateGlobal } from '../../types'; diff --git a/plugins/scaffolder-node/src/alpha/types.ts b/plugins/scaffolder-node/src/alpha/types.ts index ca529737754329..171c8e85f9ff94 100644 --- a/plugins/scaffolder-node/src/alpha/types.ts +++ b/plugins/scaffolder-node/src/alpha/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * @alpha diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index f48b242f7bdd66..e9bf8020356536 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -96,7 +96,7 @@ "react-use": "^17.2.4", "use-immer": "^0.11.0", "zen-observable": "^0.10.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 9e01257c5c949b..80b9d9bde3660d 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -40,7 +40,7 @@ import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UiSchema } from '@rjsf/utils'; import { WidgetProps } from '@rjsf/utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @alpha (undocumented) export type BackstageOverrides = Overrides & { @@ -307,16 +307,11 @@ export type ScaffolderReactComponentsNameToClassKey = { // @alpha (undocumented) export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const scaffolderReactTranslationRef: TranslationRef< 'scaffolder-react', { readonly 'workflow.noDescription': 'No description'; - readonly 'stepper.backButtonText': 'Back'; - readonly 'stepper.nextButtonText': 'Next'; - readonly 'stepper.createButtonText': 'Create'; - readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`'; readonly 'scaffolderPageContextMenu.createLabel': 'Create'; readonly 'scaffolderPageContextMenu.moreLabel': 'more'; @@ -324,6 +319,11 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions'; readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List'; readonly 'scaffolderPageContextMenu.templatingExtensionsLabel': 'Templating Extensions'; + readonly 'stepper.backButtonText': 'Back'; + readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.createButtonText': 'Create'; + readonly 'stepper.reviewButtonText': 'Review'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose'; diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index d40b0278bf8b54..9b9b00ad02cfba 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -56,10 +56,11 @@ import { TemplateGlobalFunction as TemplateGlobalFunction_2 } from '@backstage/p import { TemplateGlobalValue as TemplateGlobalValue_2 } from '@backstage/plugin-scaffolder-common'; import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common'; import { TemplatesType } from '@rjsf/utils'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; import { ValidatorType } from '@rjsf/utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public @deprecated export type Action = Action_2; @@ -240,6 +241,31 @@ export type ScaffolderOutputLink = ScaffolderOutputLink_2; // @public @deprecated (undocumented) export type ScaffolderOutputText = ScaffolderOutputText_2; +// @public (undocumented) +export const scaffolderReactTranslationRef: TranslationRef< + 'scaffolder-react', + { + readonly 'workflow.noDescription': 'No description'; + readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`'; + readonly 'scaffolderPageContextMenu.createLabel': 'Create'; + readonly 'scaffolderPageContextMenu.moreLabel': 'more'; + readonly 'scaffolderPageContextMenu.editorLabel': 'Manage Templates'; + readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions'; + readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List'; + readonly 'scaffolderPageContextMenu.templatingExtensionsLabel': 'Templating Extensions'; + readonly 'stepper.backButtonText': 'Back'; + readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.createButtonText': 'Create'; + readonly 'stepper.reviewButtonText': 'Review'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; + readonly 'templateCategoryPicker.title': 'Categories'; + readonly 'templateCard.noDescription': 'No description'; + readonly 'templateCard.chooseButtonText': 'Choose'; + readonly 'cardHeader.detailBtnTitle': 'Show template entity details'; + readonly 'templateOutputs.title': 'Text Output'; + } +>; + // @public export type ScaffolderRJSFField< T = any, diff --git a/plugins/scaffolder-react/src/alpha.ts b/plugins/scaffolder-react/src/alpha.ts index e18c758b3d1d29..dc51183658a27b 100644 --- a/plugins/scaffolder-react/src/alpha.ts +++ b/plugins/scaffolder-react/src/alpha.ts @@ -16,4 +16,10 @@ export * from './next'; -export { scaffolderReactTranslationRef } from './translation'; +import { scaffolderReactTranslationRef as _scaffolderReactTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-scaffolder-react` instead. + */ +export const scaffolderReactTranslationRef = _scaffolderReactTranslationRef; diff --git a/plugins/scaffolder-react/src/index.ts b/plugins/scaffolder-react/src/index.ts index e73df50fce5505..bfffa342a16425 100644 --- a/plugins/scaffolder-react/src/index.ts +++ b/plugins/scaffolder-react/src/index.ts @@ -22,3 +22,4 @@ export * from './api'; export * from './hooks'; export * from './layouts'; export * from './utils'; +export { scaffolderReactTranslationRef } from './translation'; diff --git a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx index 8b5a88f7dfe12d..35eeb28c17f7e2 100644 --- a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx +++ b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx @@ -17,7 +17,7 @@ import { createExtensionBlueprint, createExtensionDataRef, } from '@backstage/frontend-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { OpaqueFormField } from '@internal/scaffolder'; import { FormFieldExtensionData } from './types'; diff --git a/plugins/scaffolder-react/src/next/blueprints/types.ts b/plugins/scaffolder-react/src/next/blueprints/types.ts index eccf7e4606fae2..4f3de47e5ea735 100644 --- a/plugins/scaffolder-react/src/next/blueprints/types.ts +++ b/plugins/scaffolder-react/src/next/blueprints/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CustomFieldValidator, FieldExtensionComponentProps, diff --git a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts index 0064ae039729b6..51963072b4288d 100644 --- a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts +++ b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts @@ -16,7 +16,7 @@ import { AnyApiRef } from '@backstage/core-plugin-api'; import { JsonObject, JsonValue } from '@backstage/types'; import { OpaqueFormDecorator } from '@internal/scaffolder'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @alpha */ export type ScaffolderFormDecoratorContext< diff --git a/plugins/scaffolder-react/src/translation.ts b/plugins/scaffolder-react/src/translation.ts index 18da2f58e73cae..367710f214a9aa 100644 --- a/plugins/scaffolder-react/src/translation.ts +++ b/plugins/scaffolder-react/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const scaffolderReactTranslationRef = createTranslationRef({ id: 'scaffolder-react', messages: { diff --git a/plugins/scaffolder-react/src/utils.ts b/plugins/scaffolder-react/src/utils.ts index dc24df759d57f6..c3ca89855671f4 100644 --- a/plugins/scaffolder-react/src/utils.ts +++ b/plugins/scaffolder-react/src/utils.ts @@ -16,7 +16,7 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { JSONSchema7 } from 'json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CustomFieldExtensionSchema, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b2ee8a47d4b543..400e3f89faff22 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -101,7 +101,7 @@ "react-use": "^17.2.4", "react-window": "^1.8.10", "yaml": "^2.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index b2cd1bbcbe1aa7..60c455ece91a62 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -523,7 +523,7 @@ export type ScaffolderTemplateFormPreviewerClassKey = | 'textArea' | 'preview'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const scaffolderTranslationRef: TranslationRef< 'scaffolder', { diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 17fd25e646a7a0..a57f21f13cd257 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -49,7 +49,8 @@ import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; import { TemplateListPageProps } from '@backstage/plugin-scaffolder/alpha'; import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common'; import { TemplateWizardPageProps } from '@backstage/plugin-scaffolder/alpha'; -import { z } from 'zod'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { z } from 'zod/v3'; // @public @deprecated (undocumented) export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2; @@ -614,6 +615,192 @@ export type ScaffolderTaskOutput = ScaffolderTaskOutput_3; // @public @deprecated (undocumented) export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; +// @public (undocumented) +export const scaffolderTranslationRef: TranslationRef< + 'scaffolder', + { + readonly 'fields.entityNamePicker.title': 'Name'; + readonly 'fields.entityNamePicker.description': 'Unique name of the component'; + readonly 'fields.entityPicker.title': 'Entity'; + readonly 'fields.entityPicker.description': 'An entity from the catalog'; + readonly 'fields.entityTagsPicker.title': 'Tags'; + readonly 'fields.entityTagsPicker.description': "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"; + readonly 'fields.multiEntityPicker.title': 'Entity'; + readonly 'fields.multiEntityPicker.description': 'An entity from the catalog'; + readonly 'fields.myGroupsPicker.title': 'Entity'; + readonly 'fields.myGroupsPicker.description': 'An entity from the catalog'; + readonly 'fields.ownedEntityPicker.title': 'Entity'; + readonly 'fields.ownedEntityPicker.description': 'An entity from the catalog'; + readonly 'fields.ownerPicker.title': 'Owner'; + readonly 'fields.ownerPicker.description': 'The owner of the component'; + readonly 'fields.azureRepoPicker.organization.title': 'Organization'; + readonly 'fields.azureRepoPicker.organization.description': 'The Organization that this repo will belong to'; + readonly 'fields.azureRepoPicker.project.title': 'Project'; + readonly 'fields.azureRepoPicker.project.description': 'The Project that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.project.title': 'Allowed Projects'; + readonly 'fields.bitbucketRepoPicker.project.description': 'The Project that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.project.inputTitle': 'Projects'; + readonly 'fields.bitbucketRepoPicker.workspaces.title': 'Allowed Workspaces'; + readonly 'fields.bitbucketRepoPicker.workspaces.description': 'The Workspace that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.workspaces.inputTitle': 'Workspaces'; + readonly 'fields.gerritRepoPicker.parent.title': 'Parent'; + readonly 'fields.gerritRepoPicker.parent.description': 'The project parent that the repo will belong to'; + readonly 'fields.gerritRepoPicker.owner.title': 'Owner'; + readonly 'fields.gerritRepoPicker.owner.description': 'The owner of the project (optional)'; + readonly 'fields.giteaRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.giteaRepoPicker.owner.description': 'Gitea namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.'; + readonly 'fields.giteaRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.githubRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.githubRepoPicker.owner.description': 'The organization, user or project that this repo will belong to'; + readonly 'fields.githubRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.gitlabRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.gitlabRepoPicker.owner.description': 'GitLab namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.'; + readonly 'fields.gitlabRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.repoUrlPicker.host.title': 'Host'; + readonly 'fields.repoUrlPicker.host.description': 'The host where the repository will be created'; + readonly 'fields.repoUrlPicker.repository.title': 'Repositories Available'; + readonly 'fields.repoUrlPicker.repository.description': 'The name of the repository'; + readonly 'fields.repoUrlPicker.repository.inputTitle': 'Repository'; + readonly 'fields.repoOwnerPicker.title': 'Owner'; + readonly 'fields.repoOwnerPicker.description': 'The owner of the repository'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'actionsPage.content.emptyState.title': 'No information to display'; + readonly 'actionsPage.content.emptyState.description': 'There are no actions installed or there was an issue communicating with backend.'; + readonly 'actionsPage.content.searchFieldPlaceholder': 'Search for an action'; + readonly 'actionsPage.title': 'Installed actions'; + readonly 'actionsPage.action.input': 'Input'; + readonly 'actionsPage.action.output': 'Output'; + readonly 'actionsPage.action.examples': 'Examples'; + readonly 'actionsPage.subtitle': 'This is the collection of all installed actions'; + readonly 'actionsPage.pageTitle': 'Create a New Component'; + readonly 'listTaskPage.content.emptyState.title': 'No information to display'; + readonly 'listTaskPage.content.emptyState.description': 'There are no tasks or there was an issue communicating with backend.'; + readonly 'listTaskPage.content.tableCell.template': 'Template'; + readonly 'listTaskPage.content.tableCell.status': 'Status'; + readonly 'listTaskPage.content.tableCell.owner': 'Owner'; + readonly 'listTaskPage.content.tableCell.created': 'Created'; + readonly 'listTaskPage.content.tableCell.taskID': 'Task ID'; + readonly 'listTaskPage.content.tableTitle': 'Tasks'; + readonly 'listTaskPage.title': 'List template tasks'; + readonly 'listTaskPage.subtitle': 'All tasks that have been started'; + readonly 'listTaskPage.pageTitle': 'Templates Tasks'; + readonly 'ownerListPicker.title': 'Task Owner'; + readonly 'ownerListPicker.options.all': 'All'; + readonly 'ownerListPicker.options.owned': 'Owned'; + readonly 'ongoingTask.title': 'Run of'; + readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; + readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; + readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; + readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; + readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; + readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; + readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; + readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; + readonly 'ongoingTask.pageTitle.noTemplateName': 'Scaffolder Run'; + readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; + readonly 'ongoingTask.retryButtonTitle': 'Retry'; + readonly 'ongoingTask.startOverButtonTitle': 'Start Over'; + readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs'; + readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs'; + readonly 'templateEditorForm.stepper.emptyText': 'There are no spec parameters in the template to preview.'; + readonly 'renderSchema.undefined': 'No schema defined'; + readonly 'renderSchema.tableCell.name': 'Name'; + readonly 'renderSchema.tableCell.type': 'Type'; + readonly 'renderSchema.tableCell.title': 'Title'; + readonly 'renderSchema.tableCell.description': 'Description'; + readonly 'templatingExtensions.content.values.title': 'Values'; + readonly 'templatingExtensions.content.values.notAvailable': 'There are no global template values defined.'; + readonly 'templatingExtensions.content.emptyState.title': 'No information to display'; + readonly 'templatingExtensions.content.emptyState.description': 'There are no templating extensions available or there was an issue communicating with the backend.'; + readonly 'templatingExtensions.content.filters.title': 'Filters'; + readonly 'templatingExtensions.content.filters.schema.input': 'Input'; + readonly 'templatingExtensions.content.filters.schema.output': 'Output'; + readonly 'templatingExtensions.content.filters.schema.arguments': 'Arguments'; + readonly 'templatingExtensions.content.filters.examples': 'Examples'; + readonly 'templatingExtensions.content.filters.notAvailable': 'There are no template filters defined.'; + readonly 'templatingExtensions.content.filters.metadataAbsent': 'Filter metadata unavailable'; + readonly 'templatingExtensions.content.functions.title': 'Functions'; + readonly 'templatingExtensions.content.functions.schema.output': 'Output'; + readonly 'templatingExtensions.content.functions.schema.arguments': 'Arguments'; + readonly 'templatingExtensions.content.functions.examples': 'Examples'; + readonly 'templatingExtensions.content.functions.notAvailable': 'There are no global template functions defined.'; + readonly 'templatingExtensions.content.functions.metadataAbsent': 'Function metadata unavailable'; + readonly 'templatingExtensions.content.searchFieldPlaceholder': 'Search for an extension'; + readonly 'templatingExtensions.title': 'Templating Extensions'; + readonly 'templatingExtensions.subtitle': 'This is the collection of available templating extensions'; + readonly 'templatingExtensions.pageTitle': 'Templating Extensions'; + readonly 'templateTypePicker.title': 'Categories'; + readonly 'templateIntroPage.title': 'Manage Templates'; + readonly 'templateIntroPage.subtitle': 'Edit, preview, and try out templates, forms, and custom fields'; + readonly 'templateFormPage.title': 'Template Editor'; + readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates forms'; + readonly 'templateCustomFieldPage.title': 'Custom Field Explorer'; + readonly 'templateCustomFieldPage.subtitle': 'Edit, preview, and try out custom fields'; + readonly 'templateEditorPage.title': 'Template Editor'; + readonly 'templateEditorPage.subtitle': 'Edit, preview, and try out templates and template forms'; + readonly 'templateEditorPage.dryRunResults.title': 'Dry-run results'; + readonly 'templateEditorPage.dryRunResultsList.title': 'Result {{resultId}}'; + readonly 'templateEditorPage.dryRunResultsList.deleteButtonTitle': 'Delete result'; + readonly 'templateEditorPage.dryRunResultsList.downloadButtonTitle': 'Download as .zip'; + readonly 'templateEditorPage.dryRunResultsView.tab.output': 'Output'; + readonly 'templateEditorPage.dryRunResultsView.tab.log': 'Log'; + readonly 'templateEditorPage.dryRunResultsView.tab.files': 'Files'; + readonly 'templateEditorPage.taskStatusStepper.skippedStepTitle': 'Skipped'; + readonly 'templateEditorPage.customFieldExplorer.preview.title': 'Template Spec'; + readonly 'templateEditorPage.customFieldExplorer.fieldForm.title': 'Field Options'; + readonly 'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle': 'Apply'; + readonly 'templateEditorPage.customFieldExplorer.selectFieldLabel': 'Choose Custom Field Extension'; + readonly 'templateEditorPage.customFieldExplorer.fieldPreview.title': 'Field Preview'; + readonly 'templateEditorPage.templateEditorBrowser.closeConfirmMessage': 'Are you sure? Unsaved changes will be lost'; + readonly 'templateEditorPage.templateEditorBrowser.saveIconTooltip': 'Save all files'; + readonly 'templateEditorPage.templateEditorBrowser.reloadIconTooltip': 'Reload directory'; + readonly 'templateEditorPage.templateEditorBrowser.closeIconTooltip': 'Close directory'; + readonly 'templateEditorPage.templateEditorIntro.title': 'Get started by choosing one of the options below'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.title': 'Load Template Directory'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.description': 'Load a local template directory, allowing you to both edit and try executing your own template.'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers with the page loaded over HTTPS'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.title': 'Create New Template'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.description': 'Create a local template directory, allowing you to both edit and try executing your own template.'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers with the page loaded over HTTPS'; + readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template Form Playground'; + readonly 'templateEditorPage.templateEditorIntro.formEditor.description': 'Preview and edit a template form, either using a sample template or by loading a template from the catalog.'; + readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.title': 'Custom Field Explorer'; + readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.description': 'View and play around with available installed custom field extensions.'; + readonly 'templateEditorPage.templateEditorTextArea.saveIconTooltip': 'Save file'; + readonly 'templateEditorPage.templateEditorTextArea.refreshIconTooltip': 'Reload file'; + readonly 'templateEditorPage.templateEditorTextArea.emptyStateParagraph': 'Please select an action on the file menu.'; + readonly 'templateEditorPage.templateFormPreviewer.title': 'Load Existing Template'; + readonly 'templateListPage.title': 'Create a new component'; + readonly 'templateListPage.subtitle': 'Create new software components using standard templates in your organization'; + readonly 'templateListPage.pageTitle': 'Create a new component'; + readonly 'templateListPage.templateGroups.defaultTitle': 'Templates'; + readonly 'templateListPage.templateGroups.otherTitle': 'Other Templates'; + readonly 'templateListPage.contentHeader.supportButtonTitle': 'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).'; + readonly 'templateListPage.contentHeader.registerExistingButtonTitle': 'Register Existing Component'; + readonly 'templateListPage.additionalLinksForEntity.viewTechDocsTitle': 'View TechDocs'; + readonly 'templateWizardPage.title': 'Create a new component'; + readonly 'templateWizardPage.subtitle': 'Create new software components using standard templates in your organization'; + readonly 'templateWizardPage.pageTitle': 'Create a new component'; + readonly 'templateWizardPage.templateWithTitle': 'Create new {{templateTitle}}'; + readonly 'templateWizardPage.pageContextMenu.editConfigurationTitle': 'Edit Configuration'; + readonly 'templateEditorToolbar.customFieldExplorerTooltip': 'Custom Fields Explorer'; + readonly 'templateEditorToolbar.installedActionsDocumentationTooltip': 'Installed Actions Documentation'; + readonly 'templateEditorToolbar.templatingExtensionsDocumentationTooltip': 'Templating Extensions Documentation'; + readonly 'templateEditorToolbar.addToCatalogButton': 'Publish'; + readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; + readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; + readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbarFileMenu.button': 'File'; + readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; + readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; + readonly 'templateEditorToolbarFileMenu.options.closeEditor': 'Close template editor'; + readonly 'templateEditorToolbarTemplatesMenu.button': 'Templates'; + } +>; + // @public @deprecated (undocumented) export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index a0b30fd3df5532..286101f9421e5a 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -22,7 +22,13 @@ export { type ScaffolderTemplateFormPreviewerClassKey, } from './components'; -export { scaffolderTranslationRef } from '../translation'; +import { scaffolderTranslationRef as _scaffolderTranslationRef } from '../translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-scaffolder` instead. + */ +export const scaffolderTranslationRef = _scaffolderTranslationRef; export * from './api'; export { formFieldsApiRef, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 9a77d6b26e2cfc..e69fed9df381cb 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; export const createEntityQueryFilterExpressionSchema = (z: typeof zod) => diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts index 667b4638b856be..d07e2d031c73c0 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; export const entityQueryFilterExpressionSchema = zod.record( diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts index 2fa6aab1b34690..a2b01d91fae5f9 100644 --- a/plugins/scaffolder/src/components/fields/utils.ts +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JSONSchema7 } from 'json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { FieldSchema as FieldSchemaType } from '@backstage/plugin-scaffolder-react'; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index cc89c656ebf75a..84c9d56ebec6d8 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -38,3 +38,4 @@ export { export * from './components'; export * from './deprecated'; +export { scaffolderTranslationRef } from './translation'; diff --git a/plugins/scaffolder/src/translation.ts b/plugins/scaffolder/src/translation.ts index 94a3fb590f4402..d0f332e2fa0e31 100644 --- a/plugins/scaffolder/src/translation.ts +++ b/plugins/scaffolder/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const scaffolderTranslationRef = createTranslationRef({ id: 'scaffolder', messages: { diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index c734c4e5493c40..3777f83c9d6a2d 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -70,7 +70,7 @@ "lodash": "^4.17.21", "qs": "^6.10.1", "yn": "^4.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index eedd84105a72ef..2be6612c8b11c9 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -15,7 +15,7 @@ */ import express from 'express'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; diff --git a/plugins/search-react/report-alpha.api.md b/plugins/search-react/report-alpha.api.md index 2f7b32ab1650b1..c8a51f015544e7 100644 --- a/plugins/search-react/report-alpha.api.md +++ b/plugins/search-react/report-alpha.api.md @@ -96,7 +96,7 @@ export interface SearchFilterResultTypeBlueprintParams { value: string; } -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const searchReactTranslationRef: TranslationRef< 'search-react', { diff --git a/plugins/search-react/report.api.md b/plugins/search-react/report.api.md index d4b7592682eca7..21fc5e2e946428 100644 --- a/plugins/search-react/report.api.md +++ b/plugins/search-react/report.api.md @@ -28,6 +28,7 @@ import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common' import { SearchResultSet } from '@backstage/plugin-search-common'; import { SetStateAction } from 'react'; import { TextFieldProps } from '@material-ui/core/TextField'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { TypographyProps } from '@material-ui/core/Typography'; // @public (undocumented) @@ -296,6 +297,24 @@ export type SearchPaginationProps = Omit< | 'hasNextPage' >; +// @public (undocumented) +export const searchReactTranslationRef: TranslationRef< + 'search-react', + { + readonly 'searchBar.title': 'Search'; + readonly 'searchBar.placeholder': 'Search in {{org}}'; + readonly 'searchBar.clearButtonTitle': 'Clear'; + readonly 'searchFilter.allOptionTitle': 'All'; + readonly 'searchPagination.limitLabel': 'Results per page:'; + readonly 'searchPagination.limitText': 'of {{num}}'; + readonly noResultsDescription: 'Sorry, no results were found'; + readonly 'searchResultGroup.linkTitle': 'See All'; + readonly 'searchResultGroup.addFilterButtonTitle': 'Add filter'; + readonly 'searchResultPager.next': 'Next'; + readonly 'searchResultPager.previous': 'Previous'; + } +>; + // @public export const SearchResult: (props: SearchResultProps) => JSX_2.Element; diff --git a/plugins/search-react/src/alpha/index.ts b/plugins/search-react/src/alpha/index.ts index 7c44f59d56e2bc..528b49d7dd1941 100644 --- a/plugins/search-react/src/alpha/index.ts +++ b/plugins/search-react/src/alpha/index.ts @@ -14,4 +14,10 @@ * limitations under the License. */ export * from './blueprints'; -export { searchReactTranslationRef } from '../translation'; +import { searchReactTranslationRef as _searchReactTranslationRef } from '../translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const searchReactTranslationRef = _searchReactTranslationRef; diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 8b234e3c05edb7..e376c67185b277 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -34,3 +34,4 @@ export type { SearchContextState, SearchContextValue, } from './context'; +export { searchReactTranslationRef } from './translation'; diff --git a/plugins/search-react/src/translation.ts b/plugins/search-react/src/translation.ts index e4c394d9528971..6105f746513817 100644 --- a/plugins/search-react/src/translation.ts +++ b/plugins/search-react/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; /** - * @alpha + * @public */ export const searchReactTranslationRef = createTranslationRef({ id: 'search-react', diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index e3f2d6108a18fc..9892469cc883b8 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -359,7 +359,7 @@ export const searchPage: OverridableExtensionDefinition<{ }; }>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const searchTranslationRef: TranslationRef< 'search', { diff --git a/plugins/search/report.api.md b/plugins/search/report.api.md index c066d0ff4707d3..73d5a948c31f6d 100644 --- a/plugins/search/report.api.md +++ b/plugins/search/report.api.md @@ -10,6 +10,7 @@ import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchBarBaseProps } from '@backstage/plugin-search-react'; import { SearchResultSet } from '@backstage/plugin-search-common'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export const HomePageSearchBar: ( @@ -78,6 +79,20 @@ const searchPlugin: BackstagePlugin< export { searchPlugin as plugin }; export { searchPlugin }; +// @public (undocumented) +export const searchTranslationRef: TranslationRef< + 'search', + { + readonly 'searchModal.viewFullResults': 'View Full Results'; + readonly 'searchType.tabs.allTitle': 'All'; + readonly 'searchType.allResults': 'All Results'; + readonly 'searchType.accordion.collapse': 'Collapse'; + readonly 'searchType.accordion.numberOfResults': '{{number}} results'; + readonly 'searchType.accordion.allTitle': 'All'; + readonly 'sidebarSearchModal.title': 'Search'; + } +>; + // @public (undocumented) export const SearchType: { (props: SearchTypeProps): JSX_2.Element; diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 9625f5e946e71a..0c93fd71cca3c8 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -285,5 +285,10 @@ export default createFrontendPlugin({ }, }); -/** @alpha */ -export { searchTranslationRef } from './translation'; +import { searchTranslationRef as _searchTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-search` instead. + */ +export const searchTranslationRef = _searchTranslationRef; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 66b032c83cdee6..fc579c55cc6531 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -51,3 +51,4 @@ export { searchPlugin as plugin, searchPlugin, } from './plugin'; +export { searchTranslationRef } from './translation'; diff --git a/plugins/search/src/translation.ts b/plugins/search/src/translation.ts index 37f86b9218e9b2..58b5b72c776652 100644 --- a/plugins/search/src/translation.ts +++ b/plugins/search/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; /** - * @alpha + * @public */ export const searchTranslationRef = createTranslationRef({ id: 'search', diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index e512de98c43767..90791ae5951a4a 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -59,7 +59,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "p-limit": "^3.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/user-settings-backend/src/service/router.ts b/plugins/user-settings-backend/src/service/router.ts index c9971bbb4db66a..6d1518eb8e4678 100644 --- a/plugins/user-settings-backend/src/service/router.ts +++ b/plugins/user-settings-backend/src/service/router.ts @@ -17,7 +17,7 @@ import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import Router from 'express-promise-router'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { UserSettingsStore } from '../database/UserSettingsStore'; import { SignalsService } from '@backstage/plugin-signals-node'; import { diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index c99709d777265d..70a9ec869b74c3 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -155,7 +155,7 @@ export const settingsNavItem: OverridableExtensionDefinition<{ }; }>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const userSettingsTranslationRef: TranslationRef< 'user-settings', { diff --git a/plugins/user-settings/report.api.md b/plugins/user-settings/report.api.md index 2994f2c13ccae4..0b2d683e7a7c09 100644 --- a/plugins/user-settings/report.api.md +++ b/plugins/user-settings/report.api.md @@ -26,6 +26,7 @@ import { SignalApi } from '@backstage/plugin-signals-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export const DefaultProviderSettings: (props: { @@ -163,6 +164,63 @@ export type UserSettingsTabProps = PropsWithChildren<{ // @public (undocumented) export const UserSettingsThemeToggle: () => JSX_2.Element; +// @public (undocumented) +export const userSettingsTranslationRef: TranslationRef< + 'user-settings', + { + readonly 'featureFlags.title': 'Feature Flags'; + readonly 'featureFlags.description': 'Please refresh the page when toggling feature flags'; + readonly 'featureFlags.filterTitle': 'Filter'; + readonly 'featureFlags.clearFilter': 'Clear filter'; + readonly 'featureFlags.emptyFlags.title': 'No Feature Flags'; + readonly 'featureFlags.emptyFlags.action.title': 'An example for how to add a feature flag is highlighted below:'; + readonly 'featureFlags.emptyFlags.action.readMoreButtonTitle': 'Read More'; + readonly 'featureFlags.emptyFlags.description': 'Feature Flags make it possible for plugins to register features in Backstage for users to opt into. You can use this to split out logic in your code for manual A/B testing, etc.'; + readonly 'featureFlags.flagItem.title.disable': 'Disable'; + readonly 'featureFlags.flagItem.title.enable': 'Enable'; + readonly 'featureFlags.flagItem.subtitle.registeredInApplication': 'Registered in the application'; + readonly 'featureFlags.flagItem.subtitle.registeredInPlugin': 'Registered in {{pluginId}} plugin'; + readonly 'languageToggle.select': 'Select language {{language}}'; + readonly 'languageToggle.title': 'Language'; + readonly 'languageToggle.description': 'Change the language'; + readonly 'themeToggle.select': 'Select {{theme}}'; + readonly 'themeToggle.title': 'Theme'; + readonly 'themeToggle.description': 'Change the theme mode'; + readonly 'themeToggle.names.auto': 'Auto'; + readonly 'themeToggle.names.dark': 'Dark'; + readonly 'themeToggle.names.light': 'Light'; + readonly 'themeToggle.selectAuto': 'Select Auto Theme'; + readonly 'signOutMenu.title': 'Sign Out'; + readonly 'signOutMenu.moreIconTitle': 'more'; + readonly 'pinToggle.title': 'Pin Sidebar'; + readonly 'pinToggle.description': 'Prevent the sidebar from collapsing'; + readonly 'pinToggle.ariaLabelTitle': 'Pin Sidebar Switch'; + readonly 'pinToggle.switchTitles.unpin': 'Unpin Sidebar'; + readonly 'pinToggle.switchTitles.pin': 'Pin Sidebar'; + readonly 'identityCard.title': 'Backstage Identity'; + readonly 'identityCard.noIdentityTitle': 'No Backstage Identity'; + readonly 'identityCard.userEntity': 'User Entity'; + readonly 'identityCard.ownershipEntities': 'Ownership Entities'; + readonly 'defaultProviderSettings.description': 'Provides authentication towards {{provider}} APIs and identities'; + readonly 'emptyProviders.title': 'No Authentication Providers'; + readonly 'emptyProviders.action.title': 'Open app-config.yaml and make the changes as highlighted below:'; + readonly 'emptyProviders.action.readMoreButtonTitle': 'Read More'; + readonly 'emptyProviders.description': 'You can add Authentication Providers to Backstage which allows you to use these providers to authenticate yourself.'; + readonly 'providerSettingsItem.title.signOut': 'Sign out from {{title}}'; + readonly 'providerSettingsItem.title.signIn': 'Sign in to {{title}}'; + readonly 'providerSettingsItem.buttonTitle.signOut': 'Sign out'; + readonly 'providerSettingsItem.buttonTitle.signIn': 'Sign in'; + readonly 'authProviders.title': 'Available Providers'; + readonly 'defaultSettingsPage.tabsTitle.featureFlags': 'Feature Flags'; + readonly 'defaultSettingsPage.tabsTitle.authProviders': 'Authentication Providers'; + readonly 'defaultSettingsPage.tabsTitle.general': 'General'; + readonly 'settingsLayout.title': 'Settings'; + readonly sidebarTitle: 'Settings'; + readonly 'profileCard.title': 'Profile'; + readonly 'appearanceCard.title': 'Appearance'; + } +>; + // @public (undocumented) export const useUserProfile: () => | { diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index 87b858fcec0c17..0a267c14c62a2e 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -23,7 +23,13 @@ import { import SettingsIcon from '@material-ui/icons/Settings'; import { settingsRouteRef } from './plugin'; -export * from './translation'; +import { userSettingsTranslationRef as _userSettingsTranslationRef } from './translation'; + +/** + * @alpha + * @deprecated Import from `@backstage/plugin-user-settings` instead. + */ +export const userSettingsTranslationRef = _userSettingsTranslationRef; const userSettingsPage = PageBlueprint.makeWithOverrides({ inputs: { diff --git a/plugins/user-settings/src/index.ts b/plugins/user-settings/src/index.ts index e64d3c926c3490..84693ed60210a5 100644 --- a/plugins/user-settings/src/index.ts +++ b/plugins/user-settings/src/index.ts @@ -27,3 +27,4 @@ export { UserSettingsPage, } from './plugin'; export * from './components'; +export { userSettingsTranslationRef } from './translation'; diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 7445a415f2d27c..c57789f1c52efe 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const userSettingsTranslationRef = createTranslationRef({ id: 'user-settings', messages: { diff --git a/scripts/verify-plugin-directory.js b/scripts/verify-plugin-directory.js index 24c068b96ed4f0..70d3fbe9e900b7 100644 --- a/scripts/verify-plugin-directory.js +++ b/scripts/verify-plugin-directory.js @@ -18,7 +18,7 @@ const fs = require('fs-extra'); const { resolve, join } = require('node:path'); const yaml = require('js-yaml'); -const z = require('zod'); +const z = require('zod/v3'); const configSchema = z.object({ title: z.string(), diff --git a/yarn.lock b/yarn.lock index 32925b244624c3..fff18dbcf40979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,7 +2594,7 @@ __metadata: winston-transport: "npm:^4.5.0" yauzl: "npm:^3.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@google-cloud/cloud-sql-connector": ^1.4.0 @@ -2703,7 +2703,7 @@ __metadata: json-schema: "npm:^0.4.0" knex: "npm:^3.0.0" luxon: "npm:^3.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -2746,7 +2746,7 @@ __metadata: text-extensions: "npm:^2.4.0" uuid: "npm:^11.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/jest": "*" @@ -2828,9 +2828,10 @@ __metadata: dependencies: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/cli-module-auth": "workspace:^" "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" cleye: "npm:^2.3.0" + zod: "npm:^3.25.76 || ^4.0.0" bin: cli-module-actions: bin/backstage-cli-module-actions languageName: unknown @@ -3153,18 +3154,24 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" + "@types/proper-lockfile": "npm:^4" "@types/yarnpkg__lockfile": "npm:^1.1.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:^3.0.0" chalk: "npm:^4.0.0" commander: "npm:^12.0.0" fs-extra: "npm:^11.2.0" + keytar: "npm:^7.9.0" pirates: "npm:^4.0.6" + proper-lockfile: "npm:^4.1.2" semver: "npm:^7.5.3" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@swc/core": ^1.15.6 + dependenciesMeta: + keytar: + optional: true peerDependenciesMeta: "@swc/core": optional: true @@ -3339,7 +3346,7 @@ __metadata: react-router-stable: "npm:react-router@^6.3.0" react-use: "npm:^17.2.4" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3376,7 +3383,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3459,7 +3466,7 @@ __metadata: rehype-sanitize: "npm:^5.0.0" remark-gfm: "npm:^3.0.1" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3492,7 +3499,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3615,7 +3622,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^4.0.2" languageName: unknown linkType: soft @@ -3645,7 +3652,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3770,7 +3777,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -3811,7 +3818,7 @@ __metadata: react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@testing-library/react": ^16.0.0 "@types/jest": "*" @@ -4136,7 +4143,7 @@ __metadata: react-router-dom: "npm:^6.30.2" react-use: "npm:^17.2.4" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -4163,7 +4170,7 @@ __metadata: passport: "npm:^0.7.0" passport-atlassian-oauth2: "npm:^2.1.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4206,7 +4213,7 @@ __metadata: jose: "npm:^5.0.0" msw: "npm:^2.0.8" node-cache: "npm:^5.1.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4225,7 +4232,7 @@ __metadata: express: "npm:^4.22.0" jose: "npm:^5.0.0" passport: "npm:^0.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4244,7 +4251,7 @@ __metadata: passport: "npm:^0.7.0" passport-bitbucket-oauth2: "npm:^0.1.2" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4263,7 +4270,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4285,7 +4292,7 @@ __metadata: msw: "npm:^2.0.0" node-mocks-http: "npm:^1.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4301,7 +4308,7 @@ __metadata: "@backstage/types": "workspace:^" express: "npm:^4.22.0" google-auth-library: "npm:^9.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4319,7 +4326,7 @@ __metadata: "@types/passport-github2": "npm:^1.2.4" passport-github2: "npm:^0.1.12" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4338,7 +4345,7 @@ __metadata: passport: "npm:^0.7.0" passport-gitlab2: "npm:^5.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4356,7 +4363,7 @@ __metadata: google-auth-library: "npm:^9.0.0" passport-google-oauth20: "npm:^2.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4394,7 +4401,7 @@ __metadata: msw: "npm:^1.0.0" passport-microsoft: "npm:^1.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4412,7 +4419,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4426,7 +4433,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" jose: "npm:^5.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4451,7 +4458,7 @@ __metadata: openid-client: "npm:^5.5.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4470,7 +4477,7 @@ __metadata: express: "npm:^4.22.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4489,7 +4496,7 @@ __metadata: passport: "npm:^0.7.0" passport-onelogin-oauth: "npm:^0.0.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4510,7 +4517,7 @@ __metadata: msw: "npm:^2.7.3" passport-oauth2: "npm:^1.8.0" supertest: "npm:^7.1.0" - zod: "npm:^3.24.2" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4597,7 +4604,7 @@ __metadata: passport: "npm:^0.7.0" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" - zod: "npm:^4.3.5" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^5.0.0" languageName: unknown linkType: soft @@ -4626,7 +4633,7 @@ __metadata: passport: "npm:^0.7.0" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" zod-validation-error: "npm:^4.0.2" languageName: unknown @@ -5155,7 +5162,7 @@ __metadata: winston: "npm:^3.13.0" yaml: "npm:^2.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^4.0.2" languageName: unknown linkType: soft @@ -5339,7 +5346,7 @@ __metadata: react-use: "npm:^17.2.4" yaml: "npm:^2.0.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@backstage/frontend-test-utils": "workspace:^" "@types/react": ^17.0.0 || ^18.0.0 @@ -5878,7 +5885,7 @@ __metadata: react-resizable: "npm:^3.0.4" react-router-dom: "npm:^6.30.2" react-use: "npm:^17.2.4" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -6108,7 +6115,7 @@ __metadata: express-promise-router: "npm:^4.1.0" minimatch: "npm:^10.2.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6183,7 +6190,6 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@faker-js/faker": "npm:^10.0.0" - "@opentelemetry/api": "npm:^1.9.0" "@slack/bolt": "npm:^3.21.4" "@slack/types": "npm:^2.14.0" "@slack/web-api": "npm:^7.5.0" @@ -6412,7 +6418,7 @@ __metadata: msw: "npm:^1.0.0" supertest: "npm:^7.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6427,7 +6433,7 @@ __metadata: cross-fetch: "npm:^4.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6450,7 +6456,7 @@ __metadata: express-promise-router: "npm:^4.1.0" msw: "npm:^1.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6551,7 +6557,7 @@ __metadata: fs-extra: "npm:^11.2.0" msw: "npm:^1.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6690,7 +6696,7 @@ __metadata: octokit: "npm:^3.0.0" octokit-plugin-create-pull-request: "npm:^5.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6711,7 +6717,7 @@ __metadata: "@gitbeaker/rest": "npm:^43.8.0" luxon: "npm:^3.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6836,7 +6842,7 @@ __metadata: winston-transport: "npm:^4.7.0" yaml: "npm:^2.0.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6915,7 +6921,7 @@ __metadata: tar: "npm:^7.5.6" winston: "npm:^3.2.1" winston-transport: "npm:^4.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@backstage/backend-test-utils": "workspace:^" @@ -6980,7 +6986,7 @@ __metadata: swr: "npm:^2.0.0" use-immer: "npm:^0.11.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@backstage/frontend-test-utils": "workspace:^" @@ -7065,7 +7071,7 @@ __metadata: react-window: "npm:^1.8.10" swr: "npm:^2.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -7234,7 +7240,7 @@ __metadata: qs: "npm:^6.10.1" supertest: "npm:^7.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -7700,7 +7706,7 @@ __metadata: knex: "npm:^3.0.0" p-limit: "npm:^3.1.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -7822,7 +7828,7 @@ __metadata: ts-morph: "npm:^24.0.0" typedoc: "npm:^0.28.0" yaml-diff-patch: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@microsoft/api-extractor-model": "*" "@microsoft/tsdoc": "*" @@ -10042,7 +10048,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -31204,13 +31210,13 @@ __metadata: linkType: hard "express-rate-limit@npm:^8.2.1, express-rate-limit@npm:^8.2.2": - version: 8.3.1 - resolution: "express-rate-limit@npm:8.3.1" + version: 8.3.0 + resolution: "express-rate-limit@npm:8.3.0" dependencies: ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf + checksum: 10/e896a66fecc10639e65873186fdfb71f19d6af650220eb7ea5450725215c3eed8dc6ddcfa1e68a9db8c9facc3326fbc281512ad3ccd8f107f42a2466ce12c18c languageName: node linkType: hard @@ -39012,14 +39018,14 @@ __metadata: linkType: hard "mini-css-extract-plugin@npm:^2.4.2": - version: 2.10.1 - resolution: "mini-css-extract-plugin@npm:2.10.1" + version: 2.10.0 + resolution: "mini-css-extract-plugin@npm:2.10.0" dependencies: schema-utils: "npm:^4.0.0" tapable: "npm:^2.2.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/2d0cecc3bea85cd7f9b1ce0974f1672976d610a9267e2988ff19f5d03b017bff12b32151a412de0f519a70be7d3b050b499b20101445fb21728cc2d35dd4041a + checksum: 10/bae5350ab82171c6c9a22a4397df14aa69280f5ff0e1ff4d2429ea841bc096927b1e27ba7b75a9c3dd77bd44bab449d6197bd748381f1326cbc8befcb10d1a9e languageName: node linkType: hard @@ -45547,7 +45553,7 @@ __metadata: typescript: "npm:~5.7.0" vite: "npm:^7.1.5" yaml: "npm:^2.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -49294,9 +49300,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.24.4 - resolution: "undici@npm:7.24.4" - checksum: 10/747e76e0fd685ae1bb6fc1a2ebce0caca4ee8bd5599a77da36a3f94eac146987a9547bdbec7a74d18c0776df8ad348dccb4209901ca83fc4076f560de0d5dc7a + version: 7.22.0 + resolution: "undici@npm:7.22.0" + checksum: 10/a7a1813ba4b74c0d46cc8dd160386202c05699ffc487c5d882cf40e6d2435c8d6faff3b8f8675d09bd1ef0386e370675c26b59b9a8c8b3f17b9f82a42236a927 languageName: node linkType: hard @@ -51065,12 +51071,12 @@ __metadata: linkType: soft "yauzl@npm:^3.0.0": - version: 3.2.1 - resolution: "yauzl@npm:3.2.1" + version: 3.2.0 + resolution: "yauzl@npm:3.2.0" dependencies: buffer-crc32: "npm:~0.2.3" pend: "npm:~1.2.0" - checksum: 10/15dfae75fbfe59c6a1b7a2cb27a995cda0ee70549d32d6b19937e84897436170f169f6bbefc34b9e9beb9c9114a1b8a8a40e7687a907909a19681ebcbf35a1f3 + checksum: 10/a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b languageName: node linkType: hard @@ -51227,20 +51233,20 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.24.2, zod@npm:^3.25.76": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 - languageName: node - linkType: hard - -"zod@npm:^3.25 || ^4.0, zod@npm:^4.1.11, zod@npm:^4.3.5": +"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.76 || ^4.0.0, zod@npm:^4.1.11": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10/25fc0f62e01b557b4644bf0b393bbaf47542ab30877c37837ea8caf314a8713d220c7d7fe51f68ffa72f0e1018ddfa34d96f1973d23033f5a2a5a9b6b9d9da01 languageName: node linkType: hard +"zod@npm:^3.25.76": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard + "zstd-codec@npm:^0.1.4, zstd-codec@npm:^0.1.5": version: 0.1.5 resolution: "zstd-codec@npm:0.1.5"