Merge pull request #24513 from aurorasolar/HACKATHON_SB_1/scaffolder_docs
chore: Software Template Documentation Updates
This commit is contained in:
@@ -181,9 +181,11 @@ parameters:
|
||||
|
||||
## Use parameters as condition in steps
|
||||
|
||||
Conditions use Javascript equality operators.
|
||||
|
||||
```yaml
|
||||
- name: Only development environments
|
||||
if: ${{ parameters.environment === "staging" and parameters.environment === "development" }}
|
||||
if: ${{ parameters.environment === "staging" or parameters.environment === "development" }}
|
||||
action: debug:log
|
||||
input:
|
||||
message: 'development step'
|
||||
@@ -193,6 +195,12 @@ parameters:
|
||||
action: debug:log
|
||||
input:
|
||||
message: 'production step'
|
||||
|
||||
- name: Non-production environments
|
||||
if: ${{ parameters.environment !== "prod" and parameters.environment !== "production" }}
|
||||
action: debug:log
|
||||
input:
|
||||
message: 'non-production step'
|
||||
```
|
||||
|
||||
## Use parameters as conditional for fields
|
||||
@@ -218,10 +226,15 @@ parameters:
|
||||
lastName:
|
||||
title: Last Name
|
||||
type: string
|
||||
# You can use additional fields of parameters within conditional parameters such as required.
|
||||
required:
|
||||
- lastName
|
||||
```
|
||||
|
||||
## Conditionally set parameters
|
||||
|
||||
The `if` keyword within the parameter uses [nunjucks templating](https://mozilla.github.io/nunjucks/templating.html#if). The `not` keyword is unavailable; instead, use javascript equality.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
parameters:
|
||||
@@ -237,4 +250,9 @@ spec:
|
||||
action: fetch:template
|
||||
input:
|
||||
url: ${{ parameters.path if parameters.path else '/root' }}
|
||||
- id: fetch_not_example
|
||||
name: Fetch template not example
|
||||
action: fetch:template
|
||||
input:
|
||||
url: ${{ '/root' if parameters.path !== true else parameters.path }}
|
||||
```
|
||||
|
||||
@@ -36,6 +36,7 @@ import { z } from 'zod';
|
||||
export const createNewFileAction = () => {
|
||||
return createTemplateAction({
|
||||
id: 'acme:file:create',
|
||||
description: 'Create an Acme file.',
|
||||
schema: {
|
||||
input: z.object({
|
||||
contents: z.string().describe('The contents of the file'),
|
||||
@@ -63,9 +64,10 @@ for reference.
|
||||
|
||||
The `createTemplateAction` takes an object which specifies the following:
|
||||
|
||||
- `id` - a unique ID for your custom action. We encourage you to namespace these
|
||||
- `id` - A unique ID for your custom action. We encourage you to namespace these
|
||||
in some way so that they won't collide with future built-in actions that we
|
||||
may ship with the `scaffolder-backend` plugin.
|
||||
- `description` - An optional field to describe the purpose of the action. This will populate in the `/create/actions` endpoint.
|
||||
- `schema.input` - A `zod` or JSON schema object for input values to your function
|
||||
- `schema.output` - A `zod` or JSON schema object for values which are output from the
|
||||
function using `ctx.output`
|
||||
@@ -80,6 +82,7 @@ import { writeFile } from 'fs';
|
||||
export const createNewFileAction = () => {
|
||||
return createTemplateAction<{ contents: string; filename: string }>({
|
||||
id: 'acme:file:create',
|
||||
description: 'Create an Acme file.',
|
||||
schema: {
|
||||
input: {
|
||||
required: ['contents', 'filename'],
|
||||
@@ -118,7 +121,7 @@ We follow `provider:entity:verb` or as close to this as possible for our built i
|
||||
|
||||
Also feel free to use your company name to namespace them if you prefer too, for example `acme:file:create` like above.
|
||||
|
||||
Prefer to use `camelCase` over `snake-case` for these actions if possible, which leads to better reading and writing of template entity definitions.
|
||||
Prefer to use `camelCase` over `snake_case` or `kebab-case` for these actions if possible, which leads to better reading and writing of template entity definitions.
|
||||
|
||||
> We're aware that there are some exceptions to this, but try to follow as close as possible. We'll be working on migrating these in the repository over time too.
|
||||
|
||||
@@ -143,6 +146,65 @@ argument. It looks like the following:
|
||||
|
||||
## Registering Custom Actions
|
||||
|
||||
To register your new custom action in the Backend System you will need to create a backend module. Here is a very simplified example of how to do that:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-add-start */
|
||||
const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
moduleId: 'custom-extensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
async init({ scaffolder /* ..., other dependencies */ }) {
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
scaffolder.addActions(new createNewFileAction()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions());
|
||||
```
|
||||
|
||||
If your custom action requires core services such as `config` or `cache` they can be imported in the dependencies and passed to the custom action function.
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
...
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
cache: coreServices.cache,
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async init({ scaffolder, cache, config }) {
|
||||
scaffolder.addActions(
|
||||
customActionNeedingCacheAndConfig({ cache: cache, config: config }),
|
||||
);
|
||||
})
|
||||
```
|
||||
|
||||
### Register Custom Actions with the Legacy Backend System
|
||||
|
||||
Once you have your Custom Action ready for usage with the scaffolder, you'll
|
||||
need to pass this into the `scaffolder-backend` `createRouter` function. You
|
||||
should have something similar to the below in
|
||||
@@ -193,42 +255,6 @@ export default async function createPlugin(
|
||||
}
|
||||
```
|
||||
|
||||
### Register Action With New Backend System
|
||||
|
||||
To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
/* highlight-add-start */
|
||||
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-add-start */
|
||||
const scaffolderModuleCustomExtensions = createBackendModule({
|
||||
pluginId: 'scaffolder', // name of the plugin that the module is targeting
|
||||
moduleId: 'custom-extensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
// ... and other dependencies as needed
|
||||
},
|
||||
async init({ scaffolder /* ..., other dependencies */ }) {
|
||||
// Here you have the opportunity to interact with the extension
|
||||
// point before the plugin itself gets instantiated
|
||||
scaffolder.addActions(createNewFileAction()); // just an example
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const backend = createBackend();
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
/* highlight-add-next-line */
|
||||
backend.add(scaffolderModuleCustomExtensions());
|
||||
```
|
||||
|
||||
## List of custom action packages
|
||||
|
||||
Here is a list of Open Source custom actions that you can add to your Backstage
|
||||
|
||||
@@ -311,10 +311,37 @@ spec:
|
||||
```
|
||||
|
||||
If you have a feature flag `experimental-feature` active then
|
||||
your first step would be shown. The same goes for the nested properties in the
|
||||
your first set of parameter fields would be shown. The same goes for the nested properties in the
|
||||
spec. Make sure to use the key `backstage:featureFlag` in your templates if
|
||||
you want to use this functionality.
|
||||
|
||||
Feature Flags cannot be used in `spec.steps[].if`(the conditional on whether to execute an step/action). But you can use feature flags to display parameters that allow for skipping steps.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
type: website
|
||||
owner: team-a
|
||||
parameters:
|
||||
- name: Enter some stuff
|
||||
description: Enter some stuff
|
||||
backstage:featureFlag: experimental-feature
|
||||
properties:
|
||||
skipStep:
|
||||
type: boolean
|
||||
title: Whether or not to skip a step.
|
||||
default: false
|
||||
restOfParameters:
|
||||
...
|
||||
steps:
|
||||
- id: skipMe
|
||||
name: A step to skip if the feature flag is turned on and the user selects true
|
||||
action: debug:log
|
||||
if: ${{ parameters.skipStep }}
|
||||
input:
|
||||
message: |
|
||||
...
|
||||
```
|
||||
|
||||
### The Repository Picker
|
||||
|
||||
In order to make working with repository providers easier, we've built a custom
|
||||
@@ -709,7 +736,7 @@ an entity reference, such as the `kind`, `namespace`, and `name`.
|
||||
|
||||
### pick
|
||||
|
||||
This `pick` filter allows you to select specific properties from an object.
|
||||
This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object.
|
||||
|
||||
**Usage Example**
|
||||
|
||||
|
||||
@@ -4,15 +4,13 @@ title: Writing Tests For Actions
|
||||
description: How to write tests for actions
|
||||
---
|
||||
|
||||
Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with
|
||||
Unit tests to be sure that your actions do what they suppose to do.
|
||||
# Unit Testing Custom Actions
|
||||
|
||||
Make sure that you cover the most of scenario's, which could happen with the action.
|
||||
One of indispensable part of the test is to supply the context to a handler of action for the execution.
|
||||
We encourage you to use a utility method for that, so your tests are immune to structural changes of context.
|
||||
What is inevitably going to happen during the time.
|
||||
Unit tests help prevent regressions in custom action functionality. The `createTemplateAction` function that is the core of a custom action can be difficult to mock. There are helper methods that can assist.
|
||||
|
||||
Example how to use it:
|
||||
## Mocking the Context
|
||||
|
||||
The `handler` property of the `createTemplateAction` input object expects a context. You can create a mock context using the code below:
|
||||
|
||||
```typescript
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
@@ -29,10 +27,11 @@ expect(mockContext.output).toHaveBeenCalledWith(
|
||||
);
|
||||
```
|
||||
|
||||
### Mocking a Workspace within the Context object
|
||||
|
||||
One thing to be aware about: if you would like to call `createMockActionContext` inside `it`,
|
||||
you have to provide a `workspacePath`. By default, `createMockActionContext` uses
|
||||
`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you.
|
||||
This implementation contains a hook inside which creates this limitation. So in this case you can do then:
|
||||
`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. You can use the code below to customize the `workspacePath` without using the default workspace of the `createMockActionContext` function.
|
||||
|
||||
```typescript
|
||||
describe('github:autolinks:create', async () => {
|
||||
@@ -55,3 +54,63 @@ describe('github:autolinks:create', async () => {
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking a Config Core Service
|
||||
|
||||
If your custom Action requires the Config Core Service within execution of the `handler(ctx)` such as the custom action below, mocking the context object can be done by building a `mockContext` with the `ConfigReader` function within the `@backstage/config` package.
|
||||
|
||||
```typescript
|
||||
// custom-action.ts
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export const customActionRequiringConfigCoreService = (config: Config) => {
|
||||
const fieldRequiringValueFromConfig = config.getString('app.service.url');
|
||||
return createTemplateAction({
|
||||
...
|
||||
async handler(ctx) {
|
||||
// Some code requiring the config const
|
||||
ctx.logger.info(fieldRequiringValueFromConfig);
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// custom-action.test.ts
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { customActionRequiringConfigCoreService } from './custom-action.ts';
|
||||
...
|
||||
const mockConfig = new ConfigReader({
|
||||
app: {
|
||||
service: {
|
||||
url: 'https://api.service.io/graphql',
|
||||
apiKeyId: '123',
|
||||
apiKeySecret: '123abc',
|
||||
},
|
||||
},
|
||||
});
|
||||
...
|
||||
const action = customActionRequiringConfigCoreService(mockConfig);
|
||||
await action.handler({
|
||||
...mockContext
|
||||
})
|
||||
```
|
||||
|
||||
## Mocking a Cache Core Service
|
||||
|
||||
Similar to the `Mocking a Config Core Service` section above, if your custom action expects a Cache Core Service Object as part of the function input, you can mock it out with the following:
|
||||
|
||||
```typescript
|
||||
import { CacheService } from '@backstage/backend-plugin-api';
|
||||
|
||||
const mockCacheServiceMethods = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCacheService = mockCacheServiceMethods as unknown as CacheService;
|
||||
|
||||
const action = customActionRequiringCacheCoreService(mockCacheService);
|
||||
...
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user