Skip to content

Commit dd2d1bc

Browse files
committed
feat(scaffolder): create custom action for adding timestamp to templates
1 parent e73b7bc commit dd2d1bc

File tree

12 files changed

+316
-0
lines changed

12 files changed

+316
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Timestamp custom action for Scaffolder Backstage
2+
3+
The timestamp module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
4+
5+
## For administrators
6+
7+
### Installation
8+
9+
#### Procedure
10+
11+
1. Install the Timestamp custom action plugin using the following command:
12+
13+
```console
14+
yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-timestamp
15+
```
16+
17+
2. Integrate the custom action in the `scaffolder-backend` `createRouter` function:
18+
19+
- Install the `@backstage/integration` package using the following command:
20+
21+
```console
22+
yarn workspace backend add @backstage/integration
23+
```
24+
25+
- Add the custom action with the other built-in actions:
26+
27+
```ts title="packages/backend/src/plugins/scaffolder.ts"
28+
29+
import { CatalogClient } from '@backstage/catalog-client';
30+
/* highlight-add-start */
31+
import { ScmIntegrations } from '@backstage/integration';
32+
import {
33+
createBuiltinActions,
34+
createRouter,
35+
} from '@backstage/plugin-scaffolder-backend';
36+
import { createTimestampAction } from '@janus-idp/backstage-scaffolder-backend-module-timestamp';
37+
/* highlight-add-end */
38+
...
39+
40+
export default async function createPlugin(
41+
const catalogClient = new CatalogClient({
42+
discoveryApi: env.discovery,
43+
});
44+
45+
/* highlight-add-start */
46+
const integrations = ScmIntegrations.fromConfig(env.config);
47+
48+
const builtInActions = createBuiltinActions({
49+
integrations: integrations as any,
50+
catalogClient,
51+
config: env.config,
52+
reader: env.reader,
53+
});
54+
const actions = [...builtInActions, createTimestampAction()];
55+
/* highlight-add-end */
56+
57+
58+
return await createRouter({
59+
/* highlight-add-next-line */
60+
actions,
61+
logger: env.logger,
62+
...
63+
});
64+
```
65+
66+
3. Add the **catalog:timestamping** custom action in your template yaml after the `Fetch Skeleton + Template` step to annotate the catalog-info.yaml skeleton with the current timestamp:
67+
68+
```tsx title="template.yaml"
69+
70+
- id: template
71+
name: Fetch Skeleton + Template
72+
action: fetch:template
73+
...
74+
75+
/* highlight-add-start */
76+
- id: timestamp
77+
name: Add Timestamp to catalog-info.yaml
78+
action: catalog:timestamping
79+
/* highlight-add-end */
80+
81+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@janus-idp/backstage-scaffolder-backend-module-timestamp",
3+
"description": "The timestamp module for @backstage/plugin-scaffolder-backend",
4+
"version": "0.1.0",
5+
"main": "src/index.ts",
6+
"types": "src/index.ts",
7+
"license": "Apache-2.0",
8+
"private": true,
9+
"publishConfig": {
10+
"access": "public",
11+
"main": "dist/index.cjs.js",
12+
"types": "dist/index.d.ts"
13+
},
14+
"backstage": {
15+
"role": "backend-plugin-module"
16+
},
17+
"scripts": {
18+
"start": "backstage-cli package start",
19+
"build": "backstage-cli package build",
20+
"lint": "backstage-cli package lint",
21+
"test": "backstage-cli package test --passWithNoTests --coverage",
22+
"clean": "backstage-cli package clean",
23+
"prepack": "backstage-cli package prepack",
24+
"postpack": "backstage-cli package postpack",
25+
"export-dynamic": "janus-cli package export-dynamic-plugin",
26+
"tsc": "tsc"
27+
},
28+
"dependencies": {
29+
"@backstage/plugin-scaffolder-node": "^0.4.2",
30+
"@backstage/backend-common": "^0.21.6",
31+
"@backstage/catalog-model": "^1.4.5",
32+
"fs-extra": "^11.2.0",
33+
"yaml": "^2.0.0"
34+
},
35+
"devDependencies": {
36+
"@backstage/backend-common": "^0.21.6",
37+
"@backstage/cli": "0.26.2"
38+
},
39+
"files": [
40+
"dist",
41+
"dist-scalprum",
42+
"app-config.janus-idp.yaml"
43+
],
44+
"repository": "github:janus-idp/backstage-plugins",
45+
"keywords": [
46+
"backstage",
47+
"plugin"
48+
],
49+
"homepage": "https://janus-idp.io/",
50+
"bugs": "https://github.com/janus-idp/backstage-plugins/issues"
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './timestamp';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getCurrentTimestamp } from './getCurrentTimestamp';
2+
3+
describe('getCurrentTimestamp', () => {
4+
afterEach(() => {
5+
jest.resetAllMocks();
6+
});
7+
8+
it('should return date and time', async () => {
9+
const dateObj = new Date();
10+
dateObj.setDate(12);
11+
dateObj.setMonth(4);
12+
dateObj.setFullYear(2021);
13+
dateObj.setHours(7);
14+
dateObj.setMinutes(3);
15+
dateObj.setSeconds(18);
16+
let dateAndTime = getCurrentTimestamp(dateObj);
17+
expect(dateAndTime).toBe('5/12/2021, 07:03:18 AM');
18+
19+
dateObj.setHours(14);
20+
dateAndTime = getCurrentTimestamp(dateObj);
21+
expect(dateAndTime).toBe('5/12/2021, 02:03:18 PM');
22+
23+
dateObj.setMinutes(20);
24+
dateAndTime = getCurrentTimestamp(dateObj);
25+
expect(dateAndTime).toBe('5/12/2021, 02:20:18 PM');
26+
});
27+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const zeroPad = (d: number) =>
2+
d < 10 ? '0'.concat(d.toString()) : d.toString();
3+
4+
export const getCurrentTimestamp = (date?: Date) => {
5+
const dateObj = date || new Date(Date.now());
6+
7+
const time =
8+
dateObj.getHours() > 12
9+
? `${zeroPad(dateObj.getHours() - 12)}:${zeroPad(
10+
dateObj.getMinutes(),
11+
)}:${zeroPad(dateObj.getSeconds())} PM`
12+
: `${zeroPad(dateObj.getHours())}:${zeroPad(
13+
dateObj.getMinutes(),
14+
)}:${zeroPad(dateObj.getSeconds())} AM`;
15+
return `${dateObj.toLocaleDateString()}, ${time}`;
16+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './timestamp';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: backstage.io/v1alpha1
2+
kind: Component
3+
metadata:
4+
name: ${{values.component_id | dump}}
5+
description: ${{values.description | dump}}
6+
annotations:
7+
github.com/project-slug: ${{values.destination.owner + "/" + values.destination.repo}}
8+
backstage.io/techdocs-ref: dir:.
9+
spec:
10+
type: website
11+
lifecycle: experimental
12+
owner: ${{values.owner | dump}}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { getVoidLogger, resolveSafeChildPath } from '@backstage/backend-common';
2+
import { Entity } from '@backstage/catalog-model';
3+
4+
import * as fs from 'fs-extra';
5+
import * as yaml from 'yaml';
6+
7+
import { PassThrough } from 'stream';
8+
9+
import { createTimestampAction } from './timestamp';
10+
11+
describe('catalog:timestamping', () => {
12+
afterEach(() => {
13+
jest.resetAllMocks();
14+
});
15+
16+
it('should call action', async () => {
17+
const action = createTimestampAction();
18+
19+
const logger = getVoidLogger();
20+
jest.spyOn(logger, 'info');
21+
22+
await action.handler({
23+
workspacePath: 'src/actions/timestamp/mocks',
24+
logger,
25+
logStream: new PassThrough(),
26+
output: jest.fn(),
27+
createTemporaryDirectory() {
28+
// Usage of createMockDirectory is recommended for testing of filesystem operations
29+
throw new Error('Not implemented');
30+
},
31+
} as any);
32+
33+
const updatedCatalogInfoYaml = await fs.readFile(
34+
resolveSafeChildPath(
35+
'src/actions/timestamp/mocks',
36+
'./catalog-info.yaml',
37+
),
38+
'utf8',
39+
);
40+
41+
const entity: Entity = yaml.parse(updatedCatalogInfoYaml);
42+
43+
expect(logger.info).toHaveBeenCalledWith(
44+
'Annotating catalog-info.yaml with current timestamp',
45+
);
46+
expect(
47+
entity?.metadata?.annotations?.['backstage.io/createdAt'],
48+
).toBeTruthy();
49+
50+
// undo catalog-info.yaml file changes
51+
delete entity?.metadata?.annotations?.['backstage.io/createdAt'];
52+
53+
await fs.writeFile(
54+
resolveSafeChildPath(
55+
'src/actions/timestamp/mocks',
56+
'./catalog-info.yaml',
57+
),
58+
yaml.stringify(entity),
59+
'utf8',
60+
);
61+
});
62+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { resolveSafeChildPath } from '@backstage/backend-common';
2+
import { Entity } from '@backstage/catalog-model';
3+
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
4+
5+
import * as fs from 'fs-extra';
6+
import * as yaml from 'yaml';
7+
8+
import { getCurrentTimestamp } from './getCurrentTimestamp';
9+
10+
/**
11+
* Creates a new `catalog:timestamping` Scaffolder action to annotate catalog-info.yaml with creation timestamp.
12+
*
13+
*/
14+
15+
export const createTimestampAction = () => {
16+
return createTemplateAction({
17+
id: 'catalog:timestamping',
18+
description:
19+
'Creates a new `catalog:timestamping` Scaffolder action to annotate scaffolded entities with creation timestamp.',
20+
21+
async handler(ctx) {
22+
const entitySkeleton = await fs.readFile(
23+
resolveSafeChildPath(ctx.workspacePath, './catalog-info.yaml'),
24+
'utf8',
25+
);
26+
27+
const entity: Entity = yaml.parse(entitySkeleton);
28+
29+
const result = yaml.stringify({
30+
...entity,
31+
metadata: {
32+
...entity.metadata,
33+
annotations: {
34+
...entity.metadata.annotations,
35+
'backstage.io/createdAt': getCurrentTimestamp(),
36+
},
37+
},
38+
});
39+
ctx.logger.info(`Annotating catalog-info.yaml with current timestamp`);
40+
await fs.writeFile(
41+
resolveSafeChildPath(ctx.workspacePath, './catalog-info.yaml'),
42+
result,
43+
'utf8',
44+
);
45+
},
46+
});
47+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/***/
2+
/**
3+
* The timestamp module for @backstage/plugin-scaffolder-backend.
4+
*
5+
* @packageDocumentation
6+
*/
7+
8+
export * from './actions';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "@backstage/cli/config/tsconfig.json",
3+
"include": ["src", "dev", "migrations"],
4+
"exclude": ["node_modules"],
5+
"compilerOptions": {
6+
"outDir": "../../dist-types/plugins/scaffolder-timestamp-action",
7+
"rootDir": "."
8+
}
9+
}

0 commit comments

Comments
 (0)