@backstage/plugin-scaffolder-backend 1.7.0 → 1.8.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @backstage/plugin-scaffolder-backend
2
2
 
3
+ ## 1.8.0-next.1
4
+
5
+ ### Minor Changes
6
+
7
+ - 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project.
8
+
9
+ ## 1.8.0-next.0
10
+
11
+ ### Minor Changes
12
+
13
+ - ea14eb62a2: Added a set of default Prometheus metrics around scaffolding. See below for a list of metrics and an explanation of their labels:
14
+
15
+ - `scaffolder_task_count`: Tracks successful task runs.
16
+
17
+ Labels:
18
+
19
+ - `template`: The entity ref of the scaffolded template
20
+ - `user`: The entity ref of the user that invoked the template run
21
+ - `result`: A string describing whether the task ran successfully, failed, or was skipped
22
+
23
+ - `scaffolder_task_duration`: a histogram which tracks the duration of a task run
24
+
25
+ Labels:
26
+
27
+ - `template`: The entity ref of the scaffolded template
28
+ - `result`: A boolean describing whether the task ran successfully
29
+
30
+ - `scaffolder_step_count`: a count that tracks each step run
31
+
32
+ Labels:
33
+
34
+ - `template`: The entity ref of the scaffolded template
35
+ - `step`: The name of the step that was run
36
+ - `result`: A string describing whether the task ran successfully, failed, or was skipped
37
+
38
+ - `scaffolder_step_duration`: a histogram which tracks the duration of each step run
39
+
40
+ Labels:
41
+
42
+ - `template`: The entity ref of the scaffolded template
43
+ - `step`: The name of the step that was run
44
+ - `result`: A string describing whether the task ran successfully, failed, or was skipped
45
+
46
+ You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md
47
+
48
+ ### Patch Changes
49
+
50
+ - 7573b65232: Internal refactor of imports to avoid circular dependencies
51
+ - Updated dependencies
52
+ - @backstage/backend-common@0.16.0-next.0
53
+ - @backstage/plugin-catalog-backend@1.5.1-next.0
54
+ - @backstage/integration@1.4.0-next.0
55
+ - @backstage/backend-tasks@0.3.7-next.0
56
+ - @backstage/catalog-model@1.1.3-next.0
57
+ - @backstage/plugin-auth-node@0.2.7-next.0
58
+ - @backstage/types@1.0.1-next.0
59
+ - @backstage/backend-plugin-api@0.1.4-next.0
60
+ - @backstage/plugin-catalog-node@1.2.1-next.0
61
+ - @backstage/catalog-client@1.1.2-next.0
62
+ - @backstage/config@1.0.4-next.0
63
+ - @backstage/errors@1.1.3-next.0
64
+ - @backstage/plugin-scaffolder-common@1.2.2-next.0
65
+
3
66
  ## 1.7.0
4
67
 
5
68
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-scaffolder-backend",
3
- "version": "1.7.0",
3
+ "version": "1.8.0-next.1",
4
4
  "main": "../dist/index.cjs.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
@@ -565,7 +565,7 @@ sourcePath?: string | undefined;
565
565
  targetPath?: string | undefined;
566
566
  token?: string | undefined;
567
567
  commitAction?: "update" | "create" | "delete" | undefined;
568
- /** @deprecated Use projectPath instead */
568
+ /** @deprecated projectID passed as query parameters in the repoUrl */
569
569
  projectid?: string | undefined;
570
570
  removeSourceBranch?: boolean | undefined;
571
571
  assignee?: string | undefined;
@@ -565,7 +565,7 @@ sourcePath?: string | undefined;
565
565
  targetPath?: string | undefined;
566
566
  token?: string | undefined;
567
567
  commitAction?: "update" | "create" | "delete" | undefined;
568
- /** @deprecated Use projectPath instead */
568
+ /** @deprecated projectID passed as query parameters in the repoUrl */
569
569
  projectid?: string | undefined;
570
570
  removeSourceBranch?: boolean | undefined;
571
571
  assignee?: string | undefined;
package/dist/index.cjs.js CHANGED
@@ -30,6 +30,7 @@ var winston = require('winston');
30
30
  var nunjucks = require('nunjucks');
31
31
  var lodash = require('lodash');
32
32
  var jsonschema = require('jsonschema');
33
+ var promClient = require('prom-client');
33
34
  var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
34
35
  var express = require('express');
35
36
  var Router = require('express-promise-router');
@@ -850,34 +851,41 @@ const parseRepoUrl = (repoUrl, integrations) => {
850
851
  `No matching integration configuration for host ${host}, please check your integrations config`
851
852
  );
852
853
  }
853
- if (type === "bitbucket") {
854
- if (host === "bitbucket.org") {
855
- if (!workspace) {
856
- throw new errors.InputError(
857
- `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`
858
- );
854
+ const repo = parsed.searchParams.get("repo");
855
+ switch (type) {
856
+ case "bitbucket": {
857
+ if (host === "www.bitbucket.org") {
858
+ checkRequiredParams(parsed, "workspace");
859
859
  }
860
+ checkRequiredParams(parsed, "project", "repo");
861
+ break;
860
862
  }
861
- if (!project) {
862
- throw new errors.InputError(
863
- `Invalid repo URL passed to publisher: ${repoUrl}, missing project`
864
- );
863
+ case "gitlab": {
864
+ if (!project) {
865
+ checkRequiredParams(parsed, "owner", "repo");
866
+ }
867
+ break;
865
868
  }
866
- } else {
867
- if (!owner && type !== "gerrit") {
868
- throw new errors.InputError(
869
- `Invalid repo URL passed to publisher: ${repoUrl}, missing owner`
870
- );
869
+ case "gerrit": {
870
+ checkRequiredParams(parsed, "repo");
871
+ break;
872
+ }
873
+ default: {
874
+ checkRequiredParams(parsed, "repo", "owner");
875
+ break;
871
876
  }
872
- }
873
- const repo = parsed.searchParams.get("repo");
874
- if (!repo) {
875
- throw new errors.InputError(
876
- `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`
877
- );
878
877
  }
879
878
  return { host, owner, repo, organization, workspace, project };
880
879
  };
880
+ function checkRequiredParams(repoUrl, ...params) {
881
+ for (let i = 0; i < params.length; i++) {
882
+ if (!repoUrl.searchParams.get(params[i])) {
883
+ throw new errors.InputError(
884
+ `Invalid repo URL passed to publisher: ${repoUrl.toString()}, missing ${params[i]}`
885
+ );
886
+ }
887
+ }
888
+ }
881
889
 
882
890
  const executeShellCommand = async (options) => {
883
891
  const {
@@ -3524,13 +3532,11 @@ const createPublishGitlabMergeRequestAction = (options) => {
3524
3532
  title,
3525
3533
  token: providedToken
3526
3534
  } = ctx.input;
3527
- const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
3528
- const projectPath = `${owner}/${repo}`;
3529
- if (ctx.input.projectid) {
3530
- const deprecationWarning = `Property "projectid" is deprecated and no longer to needed to create a MR`;
3531
- ctx.logger.warn(deprecationWarning);
3532
- console.warn(deprecationWarning);
3533
- }
3535
+ const { host, owner, repo, project } = parseRepoUrl(
3536
+ repoUrl,
3537
+ integrations
3538
+ );
3539
+ const repoID = project ? project : `${owner}/${repo}`;
3534
3540
  const integrationConfig = integrations.gitlab.byHost(host);
3535
3541
  if (!integrationConfig) {
3536
3542
  throw new errors.InputError(
@@ -3578,19 +3584,15 @@ const createPublishGitlabMergeRequestAction = (options) => {
3578
3584
  execute_filemode: file.executable
3579
3585
  };
3580
3586
  });
3581
- const projects = await api.Projects.show(projectPath);
3587
+ const projects = await api.Projects.show(repoID);
3582
3588
  const { default_branch: defaultBranch } = projects;
3583
3589
  try {
3584
- await api.Branches.create(
3585
- projectPath,
3586
- branchName,
3587
- String(defaultBranch)
3588
- );
3590
+ await api.Branches.create(repoID, branchName, String(defaultBranch));
3589
3591
  } catch (e) {
3590
3592
  throw new errors.InputError(`The branch creation failed ${e}`);
3591
3593
  }
3592
3594
  try {
3593
- await api.Commits.create(projectPath, branchName, title, actions);
3595
+ await api.Commits.create(repoID, branchName, ctx.input.title, actions);
3594
3596
  } catch (e) {
3595
3597
  throw new errors.InputError(
3596
3598
  `Committing the changes to ${branchName} failed ${e}`
@@ -3598,7 +3600,7 @@ const createPublishGitlabMergeRequestAction = (options) => {
3598
3600
  }
3599
3601
  try {
3600
3602
  const mergeRequestUrl = await api.MergeRequests.create(
3601
- projectPath,
3603
+ repoID,
3602
3604
  branchName,
3603
3605
  String(defaultBranch),
3604
3606
  title,
@@ -3610,8 +3612,8 @@ const createPublishGitlabMergeRequestAction = (options) => {
3610
3612
  ).then((mergeRequest) => {
3611
3613
  return mergeRequest.web_url;
3612
3614
  });
3613
- ctx.output("projectid", projectPath);
3614
- ctx.output("projectPath", projectPath);
3615
+ ctx.output("projectid", repoID);
3616
+ ctx.output("projectPath", repoID);
3615
3617
  ctx.output("mergeRequestUrl", mergeRequestUrl);
3616
3618
  } catch (e) {
3617
3619
  throw new errors.InputError(`Merge request creation failed${e}`);
@@ -4188,6 +4190,23 @@ function generateExampleOutput(schema) {
4188
4190
  return "<unknown>";
4189
4191
  }
4190
4192
 
4193
+ function createCounterMetric(config) {
4194
+ let metric = promClient.register.getSingleMetric(config.name);
4195
+ if (!metric) {
4196
+ metric = new promClient.Counter(config);
4197
+ promClient.register.registerMetric(metric);
4198
+ }
4199
+ return metric;
4200
+ }
4201
+ function createHistogramMetric(config) {
4202
+ let metric = promClient.register.getSingleMetric(config.name);
4203
+ if (!metric) {
4204
+ metric = new promClient.Histogram(config);
4205
+ promClient.register.registerMetric(metric);
4206
+ }
4207
+ return metric;
4208
+ }
4209
+
4191
4210
  const isValidTaskSpec = (taskSpec) => {
4192
4211
  return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
4193
4212
  };
@@ -4217,6 +4236,7 @@ const createStepLogger = ({
4217
4236
  class NunjucksWorkflowRunner {
4218
4237
  constructor(options) {
4219
4238
  this.options = options;
4239
+ this.tracker = scaffoldingTracker();
4220
4240
  }
4221
4241
  isSingleTemplateString(input) {
4222
4242
  var _a, _b;
@@ -4287,16 +4307,15 @@ class NunjucksWorkflowRunner {
4287
4307
  additionalTemplateGlobals: this.options.additionalTemplateGlobals
4288
4308
  });
4289
4309
  try {
4310
+ const taskTrack = await this.tracker.taskStart(task);
4290
4311
  await fs__default["default"].ensureDir(workspacePath);
4291
- await task.emitLog(
4292
- `Starting up task with ${task.spec.steps.length} steps`
4293
- );
4294
4312
  const context = {
4295
4313
  parameters: task.spec.parameters,
4296
4314
  steps: {},
4297
4315
  user: task.spec.user
4298
4316
  };
4299
4317
  for (const step of task.spec.steps) {
4318
+ const stepTrack = await this.tracker.stepStart(task, step);
4300
4319
  try {
4301
4320
  if (step.if) {
4302
4321
  const ifResult = await this.render(
@@ -4305,17 +4324,10 @@ class NunjucksWorkflowRunner {
4305
4324
  renderTemplate
4306
4325
  );
4307
4326
  if (!isTruthy(ifResult)) {
4308
- await task.emitLog(
4309
- `Skipping step ${step.id} because it's if condition was false`,
4310
- { stepId: step.id, status: "skipped" }
4311
- );
4327
+ await stepTrack.skipFalsy();
4312
4328
  continue;
4313
4329
  }
4314
4330
  }
4315
- await task.emitLog(`Beginning step ${step.name}`, {
4316
- stepId: step.id,
4317
- status: "processing"
4318
- });
4319
4331
  const action = this.options.actionRegistry.get(step.action);
4320
4332
  const { taskLogger, streamLogger } = createStepLogger({ task, step });
4321
4333
  if (task.isDryRun) {
@@ -4341,13 +4353,7 @@ class NunjucksWorkflowRunner {
4341
4353
  )}`
4342
4354
  );
4343
4355
  if (!action.supportsDryRun) {
4344
- task.emitLog(
4345
- `Skipping because ${action.id} does not support dry-run`,
4346
- {
4347
- stepId: step.id,
4348
- status: "skipped"
4349
- }
4350
- );
4356
+ await taskTrack.skipDryRun(step, action);
4351
4357
  const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4352
4358
  if (outputSchema) {
4353
4359
  context.steps[step.id] = {
@@ -4402,19 +4408,15 @@ class NunjucksWorkflowRunner {
4402
4408
  await fs__default["default"].remove(tmpDir);
4403
4409
  }
4404
4410
  context.steps[step.id] = { output: stepOutput };
4405
- await task.emitLog(`Finished step ${step.name}`, {
4406
- stepId: step.id,
4407
- status: "completed"
4408
- });
4411
+ await stepTrack.markSuccessful();
4409
4412
  } catch (err) {
4410
- await task.emitLog(String(err.stack), {
4411
- stepId: step.id,
4412
- status: "failed"
4413
- });
4413
+ await taskTrack.markFailed(step, err);
4414
+ await stepTrack.markFailed();
4414
4415
  throw err;
4415
4416
  }
4416
4417
  }
4417
4418
  const output = this.render(task.spec.output, context, renderTemplate);
4419
+ await taskTrack.markSuccessful();
4418
4420
  return { output };
4419
4421
  } finally {
4420
4422
  if (workspacePath) {
@@ -4423,6 +4425,116 @@ class NunjucksWorkflowRunner {
4423
4425
  }
4424
4426
  }
4425
4427
  }
4428
+ function scaffoldingTracker() {
4429
+ const taskCount = createCounterMetric({
4430
+ name: "scaffolder_task_count",
4431
+ help: "Count of task runs",
4432
+ labelNames: ["template", "user", "result"]
4433
+ });
4434
+ const taskDuration = createHistogramMetric({
4435
+ name: "scaffolder_task_duration",
4436
+ help: "Duration of a task run",
4437
+ labelNames: ["template", "result"]
4438
+ });
4439
+ const stepCount = createCounterMetric({
4440
+ name: "scaffolder_step_count",
4441
+ help: "Count of step runs",
4442
+ labelNames: ["template", "step", "result"]
4443
+ });
4444
+ const stepDuration = createHistogramMetric({
4445
+ name: "scaffolder_step_duration",
4446
+ help: "Duration of a step runs",
4447
+ labelNames: ["template", "step", "result"]
4448
+ });
4449
+ async function taskStart(task) {
4450
+ var _a, _b;
4451
+ await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`);
4452
+ const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
4453
+ const user = ((_b = task.spec.user) == null ? void 0 : _b.ref) || "";
4454
+ const taskTimer = taskDuration.startTimer({
4455
+ template
4456
+ });
4457
+ async function skipDryRun(step, action) {
4458
+ task.emitLog(`Skipping because ${action.id} does not support dry-run`, {
4459
+ stepId: step.id,
4460
+ status: "skipped"
4461
+ });
4462
+ }
4463
+ async function markSuccessful() {
4464
+ taskCount.inc({
4465
+ template,
4466
+ user,
4467
+ result: "ok"
4468
+ });
4469
+ taskTimer({ result: "ok" });
4470
+ }
4471
+ async function markFailed(step, err) {
4472
+ await task.emitLog(String(err.stack), {
4473
+ stepId: step.id,
4474
+ status: "failed"
4475
+ });
4476
+ taskCount.inc({
4477
+ template,
4478
+ user,
4479
+ result: "failed"
4480
+ });
4481
+ taskTimer({ result: "failed" });
4482
+ }
4483
+ return {
4484
+ skipDryRun,
4485
+ markSuccessful,
4486
+ markFailed
4487
+ };
4488
+ }
4489
+ async function stepStart(task, step) {
4490
+ var _a;
4491
+ await task.emitLog(`Beginning step ${step.name}`, {
4492
+ stepId: step.id,
4493
+ status: "processing"
4494
+ });
4495
+ const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
4496
+ const stepTimer = stepDuration.startTimer({
4497
+ template,
4498
+ step: step.name
4499
+ });
4500
+ async function markSuccessful() {
4501
+ await task.emitLog(`Finished step ${step.name}`, {
4502
+ stepId: step.id,
4503
+ status: "completed"
4504
+ });
4505
+ stepCount.inc({
4506
+ template,
4507
+ step: step.name,
4508
+ result: "ok"
4509
+ });
4510
+ stepTimer({ result: "ok" });
4511
+ }
4512
+ async function markFailed() {
4513
+ stepCount.inc({
4514
+ template,
4515
+ step: step.name,
4516
+ result: "failed"
4517
+ });
4518
+ stepTimer({ result: "failed" });
4519
+ }
4520
+ async function skipFalsy() {
4521
+ await task.emitLog(
4522
+ `Skipping step ${step.id} because its if condition was false`,
4523
+ { stepId: step.id, status: "skipped" }
4524
+ );
4525
+ stepTimer({ result: "skipped" });
4526
+ }
4527
+ return {
4528
+ markSuccessful,
4529
+ markFailed,
4530
+ skipFalsy
4531
+ };
4532
+ }
4533
+ return {
4534
+ taskStart,
4535
+ stepStart
4536
+ };
4537
+ }
4426
4538
 
4427
4539
  class TaskWorker {
4428
4540
  constructor(options) {