Merge pull request #24513 from aurorasolar/HACKATHON_SB_1/scaffolder_docs

chore: Software Template Documentation Updates
This commit is contained in:
Ben Lambert
2024-06-11 11:09:54 +02:00
committed by GitHub
4 changed files with 180 additions and 50 deletions
@@ -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);
...
```