Merge branch 'master' into canon-radiogroup

This commit is contained in:
Charles de Dreuille
2025-06-23 13:39:50 +01:00
30 changed files with 532 additions and 271 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-defaults': patch
'@backstage/backend-test-utils': minor
---
Add a standard `toString` on credentials objects
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-node-test-utils': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-node': patch
---
An internal refactor which adds additional types to experimental checkpoints
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Replaced deprecated uses of `@backstage/backend-common` with the equivalents in `@backstage/backend-defaults` and `@backstage/backend-plugin-api`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
adding export for ButtonLink so it's importable
@@ -12,7 +12,7 @@ by writing custom actions which can be used alongside our
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
If you want to continue using the builtin actions, include them in the `actions`
array when registering your custom actions, as seen below.
:::
@@ -52,19 +52,20 @@ 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 { z } from 'zod';
import { type 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'),
filename: z
.string()
.describe('The filename of the file that will be created'),
}),
input: {
contents: z => z.string({ description: 'The contents of the file' }),
filename: z =>
z.string({
description: 'The filename of the file that will be created',
}),
},
},
async handler(ctx) {
@@ -95,53 +96,11 @@ The `createTemplateAction` takes an object which specifies the following:
function using `ctx.output`
- `handler` - the actual code which is run as part of the action, with a context
You can also choose to define your custom action using JSON schema instead of `zod`:
```ts title="With JSON Schema"
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
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'],
type: 'object',
properties: {
contents: {
type: 'string',
title: 'Contents',
description: 'The contents of the file',
},
filename: {
type: 'string',
title: 'Filename',
description: 'The filename of the file that will be created',
},
},
},
},
async handler(ctx) {
const { signal } = ctx;
await writeFile(
resolveSafeChildPath(ctx.workspacePath, ctx.input.filename),
ctx.input.contents,
{ signal },
_ => {},
);
},
});
};
```
### Naming Conventions
Try to keep names consistent for both your own custom actions, and any actions contributed to open source. We've found
that a separation of `:` and using a verb as the last part of the name works well.
We follow `provider:entity:verb` or as close to this as possible for our built in actions. For example,
We follow `provider:entity:verb` or as close to this as possible for our built-in actions. For example,
`github:actions:create` or `github:repo:create`.
Also feel free to use your company name to namespace them if you prefer too, for example `acme:file:create` like above.
@@ -151,14 +110,14 @@ and writing of template entity definitions.
### Adding a TemplateExample
A TemplateExample is a way to document different ways that your custom action can be used. Once added it will be visible
A TemplateExample is a way to document different ways that your custom action can be used. Once added, it will be visible
in your Backstage instance under the [/create/actions](https://demo.backstage.io/create/actions) path. You can have multiple
examples for one action that can demonstrate different combinations of inputs and how to use them.
#### Define TemplateExamples
Below is a sample TemplateExample that is used for `publish:github`. The source code is available
on [github](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/src/actions/github.examples.ts)
on [GitHub](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/src/actions/github.examples.ts)
and preview on [demo.backstage.io/create/actions](https://demo.backstage.io/create/actions#publish-github)
```ts title="With JSON Schema"
@@ -222,7 +181,7 @@ return createTemplateAction({
#### Test TemplateAction examples
It is also possible to test your example TemplateActions. You can see a sample test
on [github](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts)
on [GitHub](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts)
### The context object
@@ -234,13 +193,12 @@ argument. It looks like the following:
implement [idempotency of the actions](https://github.com/backstage/backstage/tree/master/beps/0004-scaffolder-task-idempotency)
by not re-running the same function again if it was
executed successfully on the previous run.
- `ctx.logger` - a Winston logger for additional logging inside your action
- `ctx.logStream` - a stream version of the logger if needed
- `ctx.logger` - a [LoggerService](../../backend-system/core-services/logger.md) instance for additional logging inside your action
- `ctx.workspacePath` - a string of the working directory of the template run
- `ctx.input` - an object which should match the `zod` or JSON schema provided in the
`schema.input` part of the action definition
- `ctx.output` - a function which you can call to set outputs that match the
JSON schema or `zod` in `schema.output` for ex. `ctx.output('downloadUrl', myDownloadUrl)`
`zod` schema in `schema.output` for ex. `ctx.output('downloadUrl', myDownloadUrl)`
- `createTemporaryDirectory` a function to call to give you a temporary
directory somewhere on the runner, so you can store some files there rather
than polluting the `workspacePath`
@@ -249,7 +207,7 @@ 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
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"
@@ -327,8 +285,8 @@ const res = await ctx.checkpoint?.({
});
```
You have to define the unique key in scope of the scaffolder task for your checkpoint. During the execution task engine
will check if the checkpoint with such key was already executed or not, if yes, and the run was successful, the callback
You have to define the unique key in the scope of the scaffolder task for your checkpoint. During the execution task engine
will check if the checkpoint with such a key was already executed or not, if yes, and the run was successful, the callback
will be skipped and instead the stored value will be returned.
Whenever you change the return type of the checkpoint, we encourage you to change the ID.
+3 -3
View File
@@ -5942,11 +5942,11 @@ __metadata:
linkType: hard
"docusaurus-pushfeedback@npm:^1.0.0":
version: 1.0.3
resolution: "docusaurus-pushfeedback@npm:1.0.3"
version: 1.0.5
resolution: "docusaurus-pushfeedback@npm:1.0.5"
peerDependencies:
"@docusaurus/core": 3.x
checksum: 10/ee80ae0c1fc079b2c317cab86d83ba50cce18938a6ce0ac647aad25dc68fc0ad659e55c11e93d0e7c23270c503c3cb300111dfbdd04785ef7aaf6fb123b57eae
checksum: 10/5323af7f1c7b4590744ea9099cacb49087d98c48c11e2ed957ed744d6210a3d82fa0cc42ef0f011f25b0d08c2d0040065c8a2b641c7c9160532ceb51ad41475c
languageName: node
linkType: hard
@@ -42,6 +42,23 @@ describe('credentials', () => {
},
});
expect(
createCredentialsWithUserPrincipal(
'user:default/mock',
'my-token',
undefined,
'my-actor',
),
).toEqual({
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'user',
userEntityRef: 'user:default/mock',
actor: { type: 'service', subject: 'my-actor' },
},
});
expect(createCredentialsWithNonePrincipal()).toEqual({
$$type: '@backstage/BackstageCredentials',
version: 'v1',
@@ -64,4 +81,63 @@ describe('credentials', () => {
),
).not.toMatch(/my-token/);
});
it('should have a serializable form both as strings and as JSON', () => {
const simpleService = createCredentialsWithServicePrincipal('my-service');
expect(String(simpleService)).toMatchInlineSnapshot(
`"backstageCredentials{servicePrincipal{my-service}}"`,
);
expect(JSON.stringify(simpleService)).toMatchInlineSnapshot(
`"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"service","subject":"my-service"}}"`,
);
const serviceWithAccessRestrictions = createCredentialsWithServicePrincipal(
'my-service',
undefined,
{
permissionNames: ['perm'],
permissionAttributes: {
action: ['read'],
},
},
);
expect(String(serviceWithAccessRestrictions)).toMatchInlineSnapshot(
`"backstageCredentials{servicePrincipal{my-service,accessRestrictions=cXWOJgUirHkHNZIowUi/YO5nwEwhTicC38iXi2XTYCk}}"`,
);
expect(JSON.stringify(serviceWithAccessRestrictions)).toMatchInlineSnapshot(
`"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"service","subject":"my-service","accessRestrictions":{"permissionNames":["perm"],"permissionAttributes":{"action":["read"]}}}}"`,
);
const simpleUser = createCredentialsWithUserPrincipal(
'user:default/mock',
'my-token',
);
expect(String(simpleUser)).toMatchInlineSnapshot(
`"backstageCredentials{userPrincipal{user:default/mock}}"`,
);
expect(JSON.stringify(simpleUser)).toMatchInlineSnapshot(
`"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"user","userEntityRef":"user:default/mock"}}"`,
);
const userWithActor = createCredentialsWithUserPrincipal(
'user:default/mock',
'my-token',
undefined,
'my-actor',
);
expect(String(userWithActor)).toMatchInlineSnapshot(
`"backstageCredentials{userPrincipal{user:default/mock,actor={servicePrincipal{my-actor}}}}"`,
);
expect(JSON.stringify(userWithActor)).toMatchInlineSnapshot(
`"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"user","userEntityRef":"user:default/mock","actor":{"type":"service","subject":"my-actor"}}}"`,
);
const none = createCredentialsWithNonePrincipal();
expect(String(none)).toMatchInlineSnapshot(
`"backstageCredentials{nonePrincipal}"`,
);
expect(JSON.stringify(none)).toMatchInlineSnapshot(
`"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"none"}}"`,
);
});
});
@@ -22,29 +22,34 @@ import {
BackstageUserPrincipal,
} from '@backstage/backend-plugin-api';
import { InternalBackstageCredentials } from './types';
import { createHash } from 'crypto';
export function createCredentialsWithServicePrincipal(
sub: string,
token?: string,
accessRestrictions?: BackstagePrincipalAccessRestrictions,
): InternalBackstageCredentials<BackstageServicePrincipal> {
return Object.defineProperty(
{
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'service',
subject: sub,
accessRestrictions,
},
},
'token',
{
const principal = createServicePrincipal(sub, accessRestrictions);
const result = {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal,
} as const;
Object.defineProperties(result, {
token: {
enumerable: false,
configurable: true,
writable: true,
value: token,
},
);
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => `backstageCredentials{${principal}}`,
},
});
return result;
}
export function createCredentialsWithUserPrincipal(
@@ -53,36 +58,49 @@ export function createCredentialsWithUserPrincipal(
expiresAt?: Date,
actor?: string,
): InternalBackstageCredentials<BackstageUserPrincipal> {
return Object.defineProperty(
{
$$type: '@backstage/BackstageCredentials',
version: 'v1',
expiresAt,
principal: {
type: 'user',
userEntityRef: sub,
...(actor && {
actor: { type: 'service', subject: actor },
}),
},
},
'token',
{
const principal = createUserPrincipal(
sub,
actor ? createServicePrincipal(actor) : undefined,
);
const result = {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
expiresAt,
principal,
} as const;
Object.defineProperties(result, {
token: {
enumerable: false,
configurable: true,
writable: true,
value: token,
},
);
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => `backstageCredentials{${principal}}`,
},
});
return result;
}
export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials<BackstageNonePrincipal> {
return {
const principal = createNonePrincipal();
const result = {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'none',
principal,
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => `backstageCredentials{${principal}}`,
},
};
});
return result;
}
export function toInternalBackstageCredentials(
@@ -106,3 +124,74 @@ export function toInternalBackstageCredentials(
return internalCredentials;
}
function createServicePrincipal(
sub: string,
accessRestrictions?: BackstagePrincipalAccessRestrictions,
): BackstageServicePrincipal {
const result = {
type: 'service',
subject: sub,
accessRestrictions,
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => {
let parts = sub;
if (accessRestrictions) {
const hash = createHash('sha256')
.update(JSON.stringify(accessRestrictions))
.digest('base64')
.replace(/=+$/, '');
parts += `,accessRestrictions=${hash}`;
}
return `servicePrincipal{${parts}}`;
},
},
});
return result;
}
function createUserPrincipal(
userEntityRef: string,
actor?: BackstageServicePrincipal,
): BackstageUserPrincipal {
const result = {
type: 'user',
userEntityRef,
actor,
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => {
let parts = userEntityRef;
if (actor) {
parts += `,actor={${actor}}`;
}
return `userPrincipal{${parts}}`;
},
},
});
return result;
}
function createNonePrincipal(): BackstageNonePrincipal {
const result = {
type: 'none',
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => 'nonePrincipal',
},
});
return result;
}
@@ -170,4 +170,27 @@ describe('mockCredentials', () => {
"Invalid user entity reference 'wrong', expected <kind>:<namespace>/<name>",
);
});
it('should have a serializable form', () => {
expect(String(mockCredentials.service('my-service'))).toMatchInlineSnapshot(
`"mockCredentials{servicePrincipal{my-service}}"`,
);
expect(
String(mockCredentials.user('user:default/mock')),
).toMatchInlineSnapshot(
`"mockCredentials{userPrincipal{user:default/mock}}"`,
);
expect(
String(
mockCredentials.user('user:default/mock', {
actor: { subject: 'my-actor' },
}),
),
).toMatchInlineSnapshot(
`"mockCredentials{userPrincipal{user:default/mock,actor={my-actor}}}"`,
);
expect(String(mockCredentials.none())).toMatchInlineSnapshot(
`"mockCredentials{nonePrincipal}"`,
);
});
});
@@ -76,10 +76,19 @@ export namespace mockCredentials {
* Creates a mocked credentials object for a unauthenticated principal.
*/
export function none(): BackstageCredentials<BackstageNonePrincipal> {
return {
const result = {
$$type: '@backstage/BackstageCredentials',
principal: { type: 'none' },
};
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
writable: true,
value: () => `mockCredentials{nonePrincipal}`,
},
});
return result;
}
/**
@@ -111,24 +120,32 @@ export namespace mockCredentials {
options?: { actor?: { subject: string } },
): BackstageCredentials<BackstageUserPrincipal> {
validateUserEntityRef(userEntityRef);
return Object.defineProperty(
{
$$type: '@backstage/BackstageCredentials',
principal: {
type: 'user',
userEntityRef,
...(options?.actor && {
actor: { type: 'service', subject: options.actor.subject },
}),
},
const result = {
$$type: '@backstage/BackstageCredentials',
principal: {
type: 'user',
userEntityRef,
...(options?.actor && {
actor: { type: 'service', subject: options.actor.subject } as const,
}),
},
'token',
{
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
value: () =>
`mockCredentials{userPrincipal{${userEntityRef}${
options?.actor ? `,actor={${options.actor.subject}}` : ''
}}}`,
},
token: {
enumerable: false,
configurable: true,
value: user.token(),
},
);
});
return result;
}
/**
@@ -231,14 +248,27 @@ export namespace mockCredentials {
subject: string = DEFAULT_MOCK_SERVICE_SUBJECT,
accessRestrictions?: BackstagePrincipalAccessRestrictions,
): BackstageCredentials<BackstageServicePrincipal> {
return {
const result = {
$$type: '@backstage/BackstageCredentials',
principal: {
type: 'service',
subject,
...(accessRestrictions ? { accessRestrictions } : {}),
},
};
} as const;
Object.defineProperties(result, {
toString: {
enumerable: false,
configurable: true,
value: () =>
`mockCredentials{servicePrincipal{${subject}${
accessRestrictions
? `,accessRestrictions=${JSON.stringify(accessRestrictions)}`
: ''
}}}`,
},
});
return result;
}
/**
+10 -12
View File
@@ -28,30 +28,28 @@ export * from './components/Box';
export * from './components/Grid';
export * from './components/Flex';
export * from './components/Container';
export * from './components/Text';
export * from './components/Heading';
// UI components
export * from './components/Avatar';
export * from './components/Button';
export * from './components/ButtonIcon';
export * from './components/ButtonLink';
export * from './components/Checkbox';
export * from './components/Collapsible';
export * from './components/DataTable';
export * from './components/FieldLabel';
export * from './components/FieldError';
export * from './components/Heading';
export * from './components/Icon';
export * from './components/Link';
export * from './components/Menu';
export * from './components/RadioGroup';
export * from './components/ScrollArea';
export * from './components/Select';
export * from './components/Switch';
export * from './components/ButtonIcon';
export * from './components/ButtonLink';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Tabs';
export * from './components/Text';
export * from './components/TextField';
export * from './components/Tooltip';
export * from './components/Menu';
export * from './components/ScrollArea';
export * from './components/Link';
export * from './components/Select';
export * from './components/Switch';
// Types
export * from './types';
+2 -2
View File
@@ -5,13 +5,13 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
name: 'path',
importNames: ['resolve'],
message:
'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues',
'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-plugin-api` instead as it prevents security issues',
},
],
restrictedSrcSyntax: [
{
message:
'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues',
'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-plugin-api` instead as it prevents security issues',
selector: 'MemberExpression[object.name="path"][property.name="resolve"]',
},
],
-1
View File
@@ -61,7 +61,6 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
+2 -14
View File
@@ -14,7 +14,6 @@ import { Duration } from 'luxon';
import { EventsService } from '@backstage/plugin-events-node';
import { HumanDuration } from '@backstage/types';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
@@ -38,6 +37,7 @@ import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateFilter } from '@backstage/plugin-scaffolder-node';
import { TemplateGlobal } from '@backstage/plugin-scaffolder-node';
import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common';
import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
@@ -442,19 +442,7 @@ export class TaskManager implements TaskContext {
// (undocumented)
get spec(): TaskSpecV1beta3;
// (undocumented)
updateCheckpoint?(
options:
| {
key: string;
status: 'success';
value: JsonValue;
}
| {
key: string;
status: 'failed';
reason: string;
},
): Promise<void>;
updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise<void>;
}
// @public @deprecated
@@ -14,12 +14,15 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import { DatabaseManager } from '@backstage/backend-defaults/database';
import { ConfigReader } from '@backstage/config';
import { DatabaseTaskStore, RawDbTaskEventRow } from './DatabaseTaskStore';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { ConflictError } from '@backstage/errors';
import { createMockDirectory } from '@backstage/backend-test-utils';
import {
mockServices,
createMockDirectory,
} from '@backstage/backend-test-utils';
import fs from 'fs-extra';
import { EventsService } from '@backstage/plugin-events-node';
@@ -33,7 +36,10 @@ const createStore = async (events?: EventsService) => {
},
},
}),
).forPlugin('scaffolder');
).forPlugin('scaffolder', {
logger: mockServices.logger.mock(),
lifecycle: mockServices.lifecycle.mock(),
});
const store = await DatabaseTaskStore.create({
database: manager,
events,
@@ -60,6 +60,10 @@ import { scaffolderActionRules } from '../../service/rules';
import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
import { BackstageLoggerTransport, WinstonLogger } from './logger';
import { convertFiltersToRecord } from '../../util/templating';
import {
CheckpointState,
CheckpointContext,
} from '@backstage/plugin-scaffolder-node/alpha';
type NunjucksWorkflowRunnerOptions = {
workingDirectory: string;
@@ -91,16 +95,6 @@ type TemplateContext = {
};
};
type CheckpointState =
| {
status: 'failed';
reason: string;
}
| {
status: 'success';
value: JsonValue;
};
const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => {
return taskSpec.apiVersion === 'scaffolder.backstage.io/v1beta3';
};
@@ -384,10 +378,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
secrets: task.secrets ?? {},
logger: taskLogger,
workspacePath,
async checkpoint<T extends JsonValue | void>(opts: {
key?: string;
fn: () => Promise<T> | T;
}) {
async checkpoint<T extends JsonValue | void>(
opts: CheckpointContext<T>,
) {
const { key: checkpointKey, fn } = opts;
const key = `v1.task.checkpoint.${step.id}.${checkpointKey}`;
@@ -396,9 +389,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
if (prevTaskState) {
const prevState = (
prevTaskState.state?.checkpoints as {
[key: string]: CheckpointState;
}
prevTaskState.state?.checkpoints as CheckpointState
)?.[key];
if (prevState && prevState.status === 'success') {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import { DatabaseManager } from '@backstage/backend-defaults/database';
import { ConfigReader } from '@backstage/config';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import {
@@ -36,7 +36,10 @@ async function createStore(): Promise<DatabaseTaskStore> {
},
},
}),
).forPlugin('scaffolder');
).forPlugin('scaffolder', {
logger: mockServices.logger.mock(),
lifecycle: mockServices.lifecycle.mock(),
});
return await DatabaseTaskStore.create({
database: manager,
@@ -32,30 +32,19 @@ import {
TaskSecrets,
TaskStatus,
} from '@backstage/plugin-scaffolder-node';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
import {
JsonObject,
JsonValue,
Observable,
createDeferred,
} from '@backstage/types';
CheckpointState,
WorkspaceProvider,
UpdateTaskCheckpointOptions,
} from '@backstage/plugin-scaffolder-node/alpha';
import { JsonObject, Observable, createDeferred } from '@backstage/types';
import ObservableImpl from 'zen-observable';
import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
import { readDuration } from './helper';
import { InternalTaskSecrets, TaskStore } from './types';
type TaskState = {
checkpoints: {
[key: string]:
| {
status: 'failed';
reason: string;
}
| {
status: 'success';
value: JsonValue;
};
};
checkpoints: CheckpointState;
};
/**
* TaskManager
@@ -152,20 +141,9 @@ export class TaskManager implements TaskContext {
return this.storage.getTaskState?.({ taskId: this.task.taskId });
}
async updateCheckpoint?(
options:
| {
key: string;
status: 'success';
value: JsonValue;
}
| {
key: string;
status: 'failed';
reason: string;
},
): Promise<void> {
async updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise<void> {
const { key, ...value } = options;
if (this.task.state) {
(this.task.state as TaskState).checkpoints[key] = value;
} else {
@@ -15,7 +15,7 @@
*/
import os from 'os';
import { DatabaseManager } from '@backstage/backend-common';
import { DatabaseManager } from '@backstage/backend-defaults/database';
import { ConfigReader } from '@backstage/config';
import { DatabaseTaskStore } from './DatabaseTaskStore';
import { StorageTaskBroker } from './StorageTaskBroker';
@@ -49,7 +49,10 @@ async function createStore(): Promise<DatabaseTaskStore> {
},
},
}),
).forPlugin('scaffolder');
).forPlugin('scaffolder', {
logger: mockServices.logger.mock(),
lifecycle: mockServices.lifecycle.mock(),
});
return await DatabaseTaskStore.create({
database: manager,
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import { DatabaseManager } from '@backstage/backend-defaults/database';
import { ConfigReader } from '@backstage/config';
import request from 'supertest';
import ObservableImpl from 'zen-observable';
@@ -77,7 +77,10 @@ function createDatabase(): DatabaseService {
},
},
}),
).forPlugin('scaffolder');
).forPlugin('scaffolder', {
logger: mockServices.logger.mock(),
lifecycle: mockServices.lifecycle.mock(),
});
}
const config = new ConfigReader({});
@@ -22,6 +22,7 @@ import {
} from '@backstage/backend-test-utils';
import { JsonObject, JsonValue } from '@backstage/types';
import { ActionContext } from '@backstage/plugin-scaffolder-node';
import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha';
import { loggerToWinstonLogger } from './loggerToWinstonLogger';
/**
@@ -44,10 +45,9 @@ export function createMockActionContext<
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
input: {} as TActionInput,
async checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T> {
async checkpoint<T extends JsonValue | void>(
opts: CheckpointContext<T>,
): Promise<T> {
return opts.fn();
},
getInitiatorCredentials: () => Promise.resolve(credentials),
@@ -27,6 +27,31 @@ export type AutocompleteHandler = ({
}[];
}>;
// @alpha
export type CheckpointContext<T extends JsonValue | void = JsonValue> = {
key: string;
fn: () => Promise<T> | T;
};
// @alpha
export type CheckpointState = {
[key: string]: CheckpointStateValue;
};
// @alpha
export type CheckpointStateValue<T extends JsonValue = JsonValue> =
| {
status: 'failed';
reason: string;
}
| {
status: 'success';
value: T;
};
// @alpha
export type CheckpointStatus = 'failed' | 'success';
// @alpha (undocumented)
export type CreatedTemplateFilter<
TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]],
@@ -187,6 +212,11 @@ export type TemplateGlobalFunctionExample = {
notes?: string;
};
// @alpha
export type UpdateTaskCheckpointOptions = {
key: string;
} & CheckpointStateValue;
// @alpha
export interface WorkspaceProvider {
// (undocumented)
+6 -17
View File
@@ -4,6 +4,7 @@
```ts
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha';
import { Expand } from '@backstage/types';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
@@ -15,6 +16,7 @@ import { ScmIntegrations } from '@backstage/integration';
import { SpawnOptionsWithoutStdio } from 'child_process';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { TemplateInfo } from '@backstage/plugin-scaffolder-common';
import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { UserEntity } from '@backstage/catalog-model';
import { Writable } from 'stream';
@@ -30,10 +32,9 @@ export type ActionContext<
secrets?: TaskSecrets;
workspacePath: string;
input: TActionInput;
checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T>;
checkpoint<T extends JsonValue | void>(
opts: CheckpointContext<T>,
): Promise<T>;
output(
name: keyof TActionOutput,
value: TActionOutput[keyof TActionOutput],
@@ -446,19 +447,7 @@ export interface TaskContext {
// (undocumented)
taskId?: string;
// (undocumented)
updateCheckpoint?(
options:
| {
key: string;
status: 'success';
value: JsonValue;
}
| {
key: string;
status: 'failed';
reason: string;
},
): Promise<void>;
updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise<void>;
}
// @public
+5 -4
View File
@@ -23,6 +23,8 @@ import {
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha';
/**
* ActionContext is passed into scaffolder actions.
* @public
@@ -36,10 +38,9 @@ export type ActionContext<
secrets?: TaskSecrets;
workspacePath: string;
input: TActionInput;
checkpoint<T extends JsonValue | void>(opts: {
key: string;
fn: () => Promise<T> | T;
}): Promise<T>;
checkpoint<T extends JsonValue | void>(
opts: CheckpointContext<T>,
): Promise<T>;
output(
name: keyof TActionOutput,
value: TActionOutput[keyof TActionOutput],
@@ -0,0 +1,16 @@
/*
* 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 * from './types';
@@ -0,0 +1,57 @@
/*
* 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 { JsonValue } from '@backstage/types';
/**
* The status of a checkpoint, indicating whether it succeeded or failed.
*
* @alpha
*/
export type CheckpointStatus = 'failed' | 'success';
/**
* Represents the union of all possible checkpoint state values.
*
* @alpha
*/
export type CheckpointStateValue<T extends JsonValue = JsonValue> =
| { status: 'failed'; reason: string }
| { status: 'success'; value: T };
/**
* A map of checkpoint keys to their states.
*
* @alpha
*/
export type CheckpointState = {
[key: string]: CheckpointStateValue;
};
/**
* Context for checkpoint function invocation.
*
* @alpha
*/
export type CheckpointContext<T extends JsonValue | void = JsonValue> = {
/**
* Unique key for the checkpoint
*/
key: string;
/**
* Function to execute for the checkpoint
*/
fn: () => Promise<T> | T;
};
@@ -28,6 +28,7 @@ export * from '../tasks/alpha';
export * from './filters';
export * from './globals';
export * from './types';
export * from './checkpoints';
/**
* Extension point for managing scaffolder actions.
@@ -1,3 +1,5 @@
import { CheckpointStateValue } from '../alpha';
/*
* Copyright 2024 The Backstage Authors
*
@@ -14,3 +16,12 @@
* limitations under the License.
*/
export * from './serializer';
/**
* Options for updating a checkpoint in a task.
*
* @alpha
*/
export type UpdateTaskCheckpointOptions = {
key: string;
} & CheckpointStateValue;
+3 -14
View File
@@ -16,7 +16,8 @@
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { JsonObject, JsonValue, Observable } from '@backstage/types';
import { JsonObject, Observable } from '@backstage/types';
import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha';
/**
* TaskSecrets
@@ -129,19 +130,7 @@ export interface TaskContext {
| undefined
>;
updateCheckpoint?(
options:
| {
key: string;
status: 'success';
value: JsonValue;
}
| {
key: string;
status: 'failed';
reason: string;
},
): Promise<void>;
updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise<void>;
serializeWorkspace?(options: { path: string }): Promise<void>;
+33 -34
View File
@@ -7499,7 +7499,6 @@ __metadata:
resolution: "@backstage/plugin-scaffolder-backend@workspace:plugins/scaffolder-backend"
dependencies:
"@backstage/backend-app-api": "workspace:^"
"@backstage/backend-common": "npm:^0.25.0"
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
@@ -8757,9 +8756,9 @@ __metadata:
languageName: node
linkType: hard
"@changesets/assemble-release-plan@npm:^6.0.8":
version: 6.0.8
resolution: "@changesets/assemble-release-plan@npm:6.0.8"
"@changesets/assemble-release-plan@npm:^6.0.9":
version: 6.0.9
resolution: "@changesets/assemble-release-plan@npm:6.0.9"
dependencies:
"@changesets/errors": "npm:^0.2.0"
"@changesets/get-dependents-graph": "npm:^2.1.3"
@@ -8767,7 +8766,7 @@ __metadata:
"@changesets/types": "npm:^6.1.0"
"@manypkg/get-packages": "npm:^1.1.3"
semver: "npm:^7.5.3"
checksum: 10/5d01fc42c67229874cc70b93fbdc971e11909aa7a72f1909c585ecb3fdc69f3ac105d243e1341cd5b07c02dee133be461fa48138125f00d137e71f8b7e8f428e
checksum: 10/f84656eabb700ed77f97751b282e1701636ed45a44b443abd9af0291870495cc046fee301478010f39a1dc455799065ae007b9d7d2bb5ae8b793b65bbb8e052a
languageName: node
linkType: hard
@@ -8781,16 +8780,16 @@ __metadata:
linkType: hard
"@changesets/cli@npm:^2.14.0":
version: 2.29.4
resolution: "@changesets/cli@npm:2.29.4"
version: 2.29.5
resolution: "@changesets/cli@npm:2.29.5"
dependencies:
"@changesets/apply-release-plan": "npm:^7.0.12"
"@changesets/assemble-release-plan": "npm:^6.0.8"
"@changesets/assemble-release-plan": "npm:^6.0.9"
"@changesets/changelog-git": "npm:^0.2.1"
"@changesets/config": "npm:^3.1.1"
"@changesets/errors": "npm:^0.2.0"
"@changesets/get-dependents-graph": "npm:^2.1.3"
"@changesets/get-release-plan": "npm:^4.0.12"
"@changesets/get-release-plan": "npm:^4.0.13"
"@changesets/git": "npm:^3.0.4"
"@changesets/logger": "npm:^0.1.1"
"@changesets/pre": "npm:^2.0.2"
@@ -8814,7 +8813,7 @@ __metadata:
term-size: "npm:^2.1.0"
bin:
changeset: bin.js
checksum: 10/fc325447b81a811464107e72a687f6c0414c5f928e518cb122d1efde1d71c205b1972464795ab97fb26087900ea55b99a551e9a010c9681def3d7561fd1c3f0b
checksum: 10/f401da29025d7bcc07b732bb09a9627f785bfc21c7c2005861d11ffea732bc14d33394fc2fcae50cc5f2b710f6080c5babe2fa90d432de5fdb47ae6afc147936
languageName: node
linkType: hard
@@ -8854,17 +8853,17 @@ __metadata:
languageName: node
linkType: hard
"@changesets/get-release-plan@npm:^4.0.12":
version: 4.0.12
resolution: "@changesets/get-release-plan@npm:4.0.12"
"@changesets/get-release-plan@npm:^4.0.13":
version: 4.0.13
resolution: "@changesets/get-release-plan@npm:4.0.13"
dependencies:
"@changesets/assemble-release-plan": "npm:^6.0.8"
"@changesets/assemble-release-plan": "npm:^6.0.9"
"@changesets/config": "npm:^3.1.1"
"@changesets/pre": "npm:^2.0.2"
"@changesets/read": "npm:^0.6.5"
"@changesets/types": "npm:^6.1.0"
"@manypkg/get-packages": "npm:^1.1.3"
checksum: 10/d6482ecb6f1c2c47266493a36d05b484f0950d0a4472820649e953d073e3fdd612cdd8a4df9e3d7e00756d4e446dae639f9d6e0dab8a25f76bb6df77cd91c21c
checksum: 10/9983fae5a68012c4c418ddd62f2fb3d325363f21160252ff7b868503a1a2effb8fdd32e4a0289b72653afc3605ce19d163ff69205c942a0004efb571a5f78fd0
languageName: node
linkType: hard
@@ -9105,11 +9104,11 @@ __metadata:
linkType: hard
"@dagrejs/dagre@npm:^1.1.4":
version: 1.1.4
resolution: "@dagrejs/dagre@npm:1.1.4"
version: 1.1.5
resolution: "@dagrejs/dagre@npm:1.1.5"
dependencies:
"@dagrejs/graphlib": "npm:2.2.4"
checksum: 10/0b3744b170c68ae0666e03aca19c3100d5131feafeb54b3ea096b749a9f0fe5385b8bd8889c11a49493cfab945b2486b9e30bc41b321755ed718e9f5cb4b74f1
checksum: 10/c00abd1e04d19f90ad8dfa0a4e16365371bc4309affead3827a1b39f6b0b946643b8af0b1e5519011deca3fda4c7471b27e9ebb03423309a94f95ac0b881ac4f
languageName: node
linkType: hard
@@ -14865,13 +14864,13 @@ __metadata:
linkType: hard
"@playwright/test@npm:^1.32.3":
version: 1.53.0
resolution: "@playwright/test@npm:1.53.0"
version: 1.53.1
resolution: "@playwright/test@npm:1.53.1"
dependencies:
playwright: "npm:1.53.0"
playwright: "npm:1.53.1"
bin:
playwright: cli.js
checksum: 10/968df4fba133dd18b8c65504c3cc5a3a6071e49f0706c6524711cdfab321a51debfeb506b9ff0a8f7dd8ce3015921d82fa51429d8f11d392cc68de1938703c33
checksum: 10/98fb9b962710183d465b695daab2006296fd9a703ecb1b763a38cd12a39f7d6066f9539d1758e54313d393353fafb16b90fb31e4add1ca99ffec99b8b1b40fb9
languageName: node
linkType: hard
@@ -21782,9 +21781,9 @@ __metadata:
linkType: hard
"@types/lodash@npm:^4.14.151":
version: 4.17.17
resolution: "@types/lodash@npm:4.17.17"
checksum: 10/496459a3cb1a0733bb60532de3899ad6297717af0b9b26ad6821154b2005fec86f29ccd47a2e6f4da4a8c7c818bb8ae73901144e8057ea86b7b02a3d7bb9d13f
version: 4.17.18
resolution: "@types/lodash@npm:4.17.18"
checksum: 10/54ebb15b29925112dbe9da3abd99fb80d7202bc5ba20fc1b4fc8ea835d0012f00cbd9a3e7f367b70e7c3f2d5ee635964e3920a489625647b558f02994b3dd381
languageName: node
linkType: hard
@@ -42032,27 +42031,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.53.0":
version: 1.53.0
resolution: "playwright-core@npm:1.53.0"
"playwright-core@npm:1.53.1":
version: 1.53.1
resolution: "playwright-core@npm:1.53.1"
bin:
playwright-core: cli.js
checksum: 10/881f27a9b7edd9954700489a5a4212cb91bcada226fd1d79a239b2eab0f333df1e2e41e275e6fa846d7f57c6a92afe14dca33ca7a2ce303dfb687d02511b7c69
checksum: 10/d0ea8674c3abb76069255ca81bc0dfdef3f9548207f1404eec036bb8724135710f25ee791bfd7c043d5b9c2ccfa42288b0308d61dc5efc60a13b18811a15c4cd
languageName: node
linkType: hard
"playwright@npm:1.53.0":
version: 1.53.0
resolution: "playwright@npm:1.53.0"
"playwright@npm:1.53.1":
version: 1.53.1
resolution: "playwright@npm:1.53.1"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.53.0"
playwright-core: "npm:1.53.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10/0b0258630f39b4d6ff1555d008ee4d591fe45cbe1e0f643a612397e3e6b1f7a99a2037a957eaa7351edd907ba10966ba105b2d244eafd1b247378910b660f086
checksum: 10/74b3178d5ae3fde8de08fe6c221578530368f1abb8794fce234d06e0043178201eb3b7410354418517f23c105bd54ac1432da5f46c50353a5e6a80198a95f2cf
languageName: node
linkType: hard