@catladder/pipeline 0.0.0-vars-07a720d0 → 0.0.0-waitfor-deploy-e64a33ba

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.
Files changed (54) hide show
  1. package/dist/build/base/createBuildJob.js +3 -1
  2. package/dist/build/docker.d.ts +1 -0
  3. package/dist/build/docker.js +19 -1
  4. package/dist/build/index.js +1 -1
  5. package/dist/build/node/buildJob.js +2 -2
  6. package/dist/build/node/yarn.d.ts +1 -0
  7. package/dist/build/node/yarn.js +43 -2
  8. package/dist/catladder-gitlab.js +8 -6
  9. package/dist/config/readConfig.d.ts +5 -1
  10. package/dist/config/readConfig.js +20 -5
  11. package/dist/constants.js +1 -1
  12. package/dist/context/index.d.ts +1 -1
  13. package/dist/context/index.js +14 -8
  14. package/dist/context/resolveReferences.d.ts +1 -1
  15. package/dist/context/resolveReferences.js +9 -4
  16. package/dist/deploy/base.js +29 -10
  17. package/dist/deploy/custom/deployJob.d.ts +3 -0
  18. package/dist/deploy/custom/deployJob.js +65 -0
  19. package/dist/deploy/custom/index.d.ts +1 -0
  20. package/dist/deploy/custom/index.js +22 -0
  21. package/dist/deploy/index.js +8 -0
  22. package/dist/deploy/kubernetes/deployJob.js +3 -1
  23. package/dist/deploy/kubernetes/processSecretsAsFiles.d.ts +87 -0
  24. package/dist/deploy/kubernetes/processSecretsAsFiles.js +39 -0
  25. package/dist/deploy/types.d.ts +58 -21
  26. package/dist/deploy/utils.d.ts +4 -3
  27. package/dist/deploy/utils.js +20 -2
  28. package/dist/pipeline/createJobs.js +59 -15
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/dist/types/config.d.ts +12 -0
  31. package/dist/types/context.d.ts +1 -1
  32. package/dist/types/jobs.d.ts +6 -1
  33. package/package.json +1 -1
  34. package/src/build/base/createBuildJob.ts +2 -0
  35. package/src/build/docker.ts +14 -0
  36. package/src/build/index.ts +1 -1
  37. package/src/build/node/buildJob.ts +29 -24
  38. package/src/build/node/yarn.ts +5 -1
  39. package/src/catladder-gitlab.ts +1 -1
  40. package/src/config/readConfig.ts +18 -6
  41. package/src/context/index.ts +14 -8
  42. package/src/context/resolveReferences.ts +16 -12
  43. package/src/deploy/base.ts +36 -11
  44. package/src/deploy/custom/deployJob.ts +23 -0
  45. package/src/deploy/custom/index.ts +1 -0
  46. package/src/deploy/index.ts +5 -0
  47. package/src/deploy/kubernetes/deployJob.ts +4 -3
  48. package/src/deploy/kubernetes/processSecretsAsFiles.ts +34 -0
  49. package/src/deploy/types.ts +53 -4
  50. package/src/deploy/utils.ts +15 -2
  51. package/src/pipeline/createJobs.ts +50 -16
  52. package/src/types/config.ts +12 -0
  53. package/src/types/context.ts +1 -1
  54. package/src/types/jobs.ts +7 -1
@@ -6,32 +6,36 @@ export const resolveReferences = (
6
6
  vars: Record<string, string>,
7
7
  getOtherVariables?: (
8
8
  componentName: string,
9
- variableName: string
10
- ) => string | null
9
+ variableName: string,
10
+ alreadyVisited: Record<string, Record<string, boolean>>
11
+ ) => string | null,
12
+ alreadyVisitedBase: Record<string, Record<string, boolean>> = {}
11
13
  ) => {
12
14
  const replaceSingleValue = (
13
15
  value: string,
14
- alreadyVisited: Record<string, Record<string, boolean>> = {}
16
+ alreadyVisited: Record<string, Record<string, boolean>> = alreadyVisitedBase
15
17
  ): string => {
16
18
  if (REGEX.test(value)) {
17
19
  return value.replace(REGEX, (match, _, componentName, variableName) => {
18
20
  if (alreadyVisited[componentName]?.[variableName]) {
19
21
  return match; // prevent endless loop
20
22
  }
23
+ const newAlreadyVisited = merge({}, alreadyVisited, {
24
+ [componentName]: {
25
+ [variableName]: true,
26
+ },
27
+ });
21
28
  const result = componentName
22
- ? getOtherVariables?.(componentName, variableName) ?? null
29
+ ? getOtherVariables?.(
30
+ componentName,
31
+ variableName,
32
+ newAlreadyVisited
33
+ ) ?? null
23
34
  : vars[variableName]; // is self reference
24
35
 
25
36
  const replaced =
26
37
  result !== null && result !== undefined
27
- ? replaceSingleValue(
28
- result,
29
- merge({}, alreadyVisited, {
30
- [componentName]: {
31
- [variableName]: true,
32
- },
33
- })
34
- )
38
+ ? replaceSingleValue(result, newAlreadyVisited)
35
39
  : match;
36
40
 
37
41
  return replaced;
@@ -1,5 +1,7 @@
1
+ import { requiresDockerBuild } from "../build/docker";
1
2
  import { Context } from "../types";
2
3
  import { CatladderJob } from "../types/jobs";
4
+ import { contextIsStoppable } from "./utils";
3
5
 
4
6
  export const DEPLOY_JOB_NAME = "🚀 Deploy";
5
7
  export const STOP_JOB_NAME = "🛑 Stop ⚠️";
@@ -17,36 +19,53 @@ export const getBaseDeploymentJob = (context: Context): JobWithoutScript => {
17
19
  url: context.environment.url,
18
20
  };
19
21
 
22
+ const hasDocker = requiresDockerBuild(context);
23
+ const isStoppable = contextIsStoppable(context);
24
+
20
25
  const autoStop =
21
26
  context.environment.envType === "review"
22
- ? "2 weeks"
27
+ ? "3 days" // auto stop is not working properly when branch is merged, so we do
23
28
  : context.environment.envType === "dev"
24
29
  ? "3 weeks"
25
30
  : undefined;
26
- // automatically run deploy job, unless its prod and stage is active
27
- const autoDeploy =
31
+ // if auto or manual is configured explicitly, use that
32
+ const whenDeployDefined =
33
+ context.componentConfig.deploy && context.componentConfig.deploy.when
34
+ ? context.componentConfig.deploy.when
35
+ : undefined;
36
+ // otherwise auto deploy if env is not prod. If its prod, deploy automatically if stage is disabled
37
+ const whenDeployDefault =
28
38
  context.environment.envType !== "prod"
29
- ? true // is not stage, auto deploy
39
+ ? "auto" // is not stage, auto deploy
30
40
  : context.componentConfig.env?.stage === false
31
- ? true // is prod, but no staging, auto deploy
32
- : false; // manually deploy
41
+ ? "auto" // is prod, but no staging, auto deploy
42
+ : "manual"; // manually deploy
43
+ const whenDeploy = whenDeployDefined ? whenDeployDefined : whenDeployDefault;
44
+
33
45
  return {
34
46
  name: DEPLOY_JOB_NAME,
35
47
  envMode: "stagePerEnv", // makes it easier to run manual tasks er env
48
+
49
+ needsOtherComponent: context.componentConfig.deploy
50
+ ? context.componentConfig.deploy.waitFor?.map((c) => ({
51
+ componentName: c,
52
+ job: DEPLOY_JOB_NAME,
53
+ artifacts: false,
54
+ })) ?? undefined
55
+ : undefined,
36
56
  // we don't want to deploy when there is a broken test
37
57
  needsStages: [
38
58
  {
39
59
  stage: "build",
40
- artifacts: false,
60
+ artifacts: hasDocker ? false : true, // we asume that no-docker deployments need build artifacts,
41
61
  },
42
62
  {
43
63
  stage: "test",
44
64
  artifacts: false,
45
65
  },
46
66
  ], // workaround for https://gitlab.com/gitlab-org/gitlab/-/issues/220758
47
-
48
67
  rules: [
49
- autoDeploy
68
+ whenDeploy === "auto"
50
69
  ? {
51
70
  when: "on_success",
52
71
  }
@@ -54,14 +73,20 @@ export const getBaseDeploymentJob = (context: Context): JobWithoutScript => {
54
73
  when: "manual",
55
74
  },
56
75
  ],
76
+ allow_failure: whenDeploy === "manual" ? true : false,
57
77
  stage: "deploy",
58
78
  variables: {
59
79
  ...DEPLOY_RUNNER_VARIABLES,
80
+ ...context.environment.envVars,
60
81
  },
61
82
  environment: {
62
83
  ...environment,
63
- on_stop: STOP_JOB_NAME,
64
- auto_stop_in: autoStop,
84
+ ...(isStoppable
85
+ ? {
86
+ on_stop: STOP_JOB_NAME,
87
+ auto_stop_in: autoStop,
88
+ }
89
+ : {}),
65
90
  },
66
91
  };
67
92
  };
@@ -0,0 +1,23 @@
1
+ import { merge } from "lodash";
2
+ import { Context } from "../../types/context";
3
+ import { CatladderJob } from "../../types/jobs";
4
+ import { getBaseDeploymentJob } from "../base";
5
+ import { isOfDeployType } from "../types";
6
+
7
+ export const createCustomDeployJobs = (context: Context): CatladderJob[] => {
8
+ const deployConfig = context.componentConfig.deploy;
9
+ if (deployConfig === false) {
10
+ return [];
11
+ }
12
+ if (!isOfDeployType(deployConfig, "custom")) {
13
+ // should not happen
14
+ throw new Error("deploy config is not custom");
15
+ }
16
+ const baseDeploymentJob = getBaseDeploymentJob(context);
17
+
18
+ return [
19
+ merge({}, baseDeploymentJob, {
20
+ script: [`cd ${context.componentConfig.dir}`, ...deployConfig.script],
21
+ }),
22
+ ];
23
+ };
@@ -0,0 +1 @@
1
+ export * from "./deployJob";
@@ -1,5 +1,6 @@
1
1
  import { Context } from "../types/context";
2
2
  import { CatladderJob } from "../types/jobs";
3
+ import { createCustomDeployJobs } from "./custom";
3
4
  import { createKubernetesDeployJobs } from "./kubernetes";
4
5
  import { DeployConfig } from "./types";
5
6
  export * from "./kubernetes";
@@ -17,4 +18,8 @@ export const DEPLOY_TYPES: DeployTypes = {
17
18
  jobs: createKubernetesDeployJobs,
18
19
  defaults: () => ({}),
19
20
  },
21
+ custom: {
22
+ jobs: createCustomDeployJobs,
23
+ defaults: () => ({}),
24
+ },
20
25
  };
@@ -10,6 +10,7 @@ import { getBaseDeploymentJob, getBaseDeploymentStopJob } from "../base";
10
10
  import { isOfDeployType } from "../types";
11
11
  import { createCloudsqlBaseConfig } from "./cloudsql";
12
12
  import { createMongodbBaseConfig } from "./mongodb";
13
+ import { processSecretsAsFiles } from "./processSecretsAsFiles";
13
14
 
14
15
  export const createKubernetesDeployJobs = (
15
16
  context: Context
@@ -74,6 +75,7 @@ export const createKubernetesDeployJobs = (
74
75
 
75
76
  const defaultKubeValues = {
76
77
  application: defaultAppConfig,
78
+
77
79
  env: env,
78
80
  ...(deployConfig.values?.cloudsql?.enabled
79
81
  ? createCloudsqlBaseConfig(context)
@@ -83,9 +85,8 @@ export const createKubernetesDeployJobs = (
83
85
  : {}),
84
86
  };
85
87
 
86
- const kubeValues = mergeWithMergingArrays(
87
- defaultKubeValues,
88
- deployConfig.values
88
+ const kubeValues = processSecretsAsFiles(
89
+ mergeWithMergingArrays(defaultKubeValues, deployConfig.values)
89
90
  );
90
91
 
91
92
  const kubernetesEnvironment = {
@@ -0,0 +1,34 @@
1
+ import { omit } from "lodash";
2
+ import { DeployConfigKubernetesValues } from "../types";
3
+
4
+ export const processSecretsAsFiles = <
5
+ T extends {
6
+ env: {
7
+ secret: Record<string, string>;
8
+ };
9
+ } & DeployConfigKubernetesValues
10
+ >(
11
+ values: T
12
+ ): T => {
13
+ if (!values.secretsAsFile || values.secretsAsFile.length === 0) {
14
+ return values;
15
+ }
16
+ // the initial idea was that we "shadow" secrets that we mount as files,
17
+ // but there is a weird kubernetes issue with that (https://github.com/kubernetes/kubernetes/issues/46861),
18
+ // so instead we rename the secrets and append _content, so that inside kuberentes, we can use the original name to contain the path and name+_content as the actual content
19
+ return {
20
+ ...values,
21
+ env: {
22
+ ...values.env,
23
+ secret: {
24
+ ...omit(values.env.secret, values.secretsAsFile),
25
+ ...Object.fromEntries(
26
+ values.secretsAsFile.map((key) => [
27
+ key + "_content",
28
+ values.env.secret[key],
29
+ ])
30
+ ),
31
+ },
32
+ },
33
+ };
34
+ };
@@ -1,5 +1,18 @@
1
1
  // eslint-disable-next-line @typescript-eslint/ban-types
2
- export type DeployConfigBase = {};
2
+ export type DeployConfigBase = {
3
+ /**
4
+ * whether to deploy automatically or manual. If not defined, these rules apply:
5
+ * - prod: manual
6
+ * - all other envs: auto
7
+ */
8
+ when?: "manual" | "auto";
9
+
10
+ /**
11
+ * EXPERIMENTAL
12
+ * wait for other components to deploy first, before doing this deployment
13
+ */
14
+ waitFor?: string[];
15
+ };
3
16
  type AllowUnknownProps<T extends Record<string, unknown>> = T &
4
17
  Record<string, unknown>;
5
18
 
@@ -24,6 +37,30 @@ export type DeployConfigKubernetesClusterGCloud = {
24
37
  };
25
38
  export type DeployConfigKubernetesCluster = DeployConfigKubernetesClusterGCloud; // currently only this
26
39
 
40
+ export type KubernetesAutoscaleMetricResourceTarget =
41
+ | {
42
+ type: "Utilization";
43
+ averageUtilization: number;
44
+ }
45
+ | {
46
+ type: "AverageValue";
47
+ averageValue: number | string;
48
+ };
49
+ export type KubernetesAutoscaleMetric = {
50
+ type: "Resource";
51
+ resource: {
52
+ name: "cpu" | "memory";
53
+ target: KubernetesAutoscaleMetricResourceTarget;
54
+ };
55
+ };
56
+ export type KubernetesAutoscale = {
57
+ minReplicas: number;
58
+ maxReplicas: number;
59
+ /**
60
+ * declare a number of metrics (at least one). Usually you would use cpu as a metric
61
+ */
62
+ metrics: KubernetesAutoscaleMetric[];
63
+ };
27
64
  type KubernetesHealthDef = {
28
65
  httpGet?: {
29
66
  path?: string;
@@ -94,6 +131,11 @@ export type DeployConfigKubernetesValues = AllowUnknownProps<{
94
131
  * how many pods will be started
95
132
  */
96
133
  replicas?: number;
134
+
135
+ /**
136
+ * autoscale the pods horizontally
137
+ */
138
+ autoscale?: KubernetesAutoscale;
97
139
  /**
98
140
  * kubernetes resources
99
141
  */
@@ -112,6 +154,11 @@ export type DeployConfigKubernetesValues = AllowUnknownProps<{
112
154
  readinessProbe?: KubernetesHealthDef;
113
155
  livenessProbe?: KubernetesHealthDef;
114
156
  }>;
157
+ /**
158
+ * Mount secrets as files in the filesystem.
159
+ * These secrets are still available as env vars, but will contain the path of the file instead
160
+ */
161
+ secretsAsFile?: string[];
115
162
  }>;
116
163
 
117
164
  export type DeployConfigKubernetes = {
@@ -131,12 +178,14 @@ export type DeployConfigKubernetes = {
131
178
  values?: DeployConfigKubernetesValues;
132
179
  } & DeployConfigBase;
133
180
 
134
- type CustomDeployConfig = {
181
+ type DeployConfigCustom = {
135
182
  type: "custom";
183
+ requiresDocker: boolean;
136
184
  script: string[];
137
- };
185
+ stopScript?: string[];
186
+ } & DeployConfigBase;
138
187
 
139
- export type DeployConfig = DeployConfigKubernetes;
188
+ export type DeployConfig = DeployConfigKubernetes | DeployConfigCustom;
140
189
 
141
190
  export const isOfDeployType = <T extends Array<DeployConfig["type"]>>(
142
191
  t: DeployConfig | false,
@@ -1,5 +1,5 @@
1
- import { Config } from "../types";
2
- import type { DeployConfigKubernetesCluster } from "./types";
1
+ import { Config, Context } from "../types";
2
+ import { DeployConfigKubernetesCluster, isOfDeployType } from "./types";
3
3
 
4
4
  export const getFullKubernetesClusterName = (
5
5
  cluster: DeployConfigKubernetesCluster
@@ -15,3 +15,16 @@ export const getKubernetesNamespace = (
15
15
  ) => {
16
16
  return `${config.customerName}-${config.appName}-${env}`;
17
17
  };
18
+
19
+ export const contextIsStoppable = (context: Context) => {
20
+ const deployConfig = context.componentConfig.deploy;
21
+ if (isOfDeployType(deployConfig, "kubernetes")) {
22
+ return true;
23
+ }
24
+
25
+ if (isOfDeployType(deployConfig, "custom") && deployConfig.stopScript) {
26
+ return true;
27
+ }
28
+
29
+ return false;
30
+ };
@@ -1,5 +1,5 @@
1
1
  import { exec } from "child-process-promise";
2
- import { isObject } from "lodash";
2
+ import { isFunction, isObject } from "lodash";
3
3
  import { BUILD_TYPES } from "../build";
4
4
  import { createContext } from "../context";
5
5
  import { DEPLOY_TYPES } from "../deploy";
@@ -11,6 +11,26 @@ import { notNil } from "../utils";
11
11
  import { makeGitlabJob } from "./gitlab/makeGitlabJob";
12
12
  import { getPackageManagerInfo } from "./packageManager";
13
13
 
14
+ const injectDefaultVarsInCustomJobs = (
15
+ context: Context,
16
+ jobs: CatladderJob[]
17
+ ) =>
18
+ jobs.map(({ variables, ...job }) => ({
19
+ variables: {
20
+ ...(context.environment.envVars ?? {}),
21
+ ...(variables ?? {}),
22
+ },
23
+ ...job,
24
+ }));
25
+ const getCustomJobs = (context: Context) => {
26
+ if (!context.componentConfig.customJobs) {
27
+ return [];
28
+ }
29
+ const rawJobs = isFunction(context.componentConfig.customJobs)
30
+ ? context.componentConfig.customJobs(context)
31
+ : context.componentConfig.customJobs;
32
+ return injectDefaultVarsInCustomJobs(context, rawJobs);
33
+ };
14
34
  const createRawJobs = (context: Context): CatladderJob[] => {
15
35
  if (context.componentConfig.deploy === false) {
16
36
  return [];
@@ -20,7 +40,8 @@ const createRawJobs = (context: Context): CatladderJob[] => {
20
40
  const deployJobs =
21
41
  DEPLOY_TYPES[context.componentConfig.deploy.type].jobs(context);
22
42
 
23
- return [...buildJobs, ...deployJobs];
43
+ const customJobs = getCustomJobs(context);
44
+ return [...buildJobs, ...deployJobs, ...customJobs];
24
45
  };
25
46
  const getFullJobName = (
26
47
  name: string,
@@ -56,22 +77,33 @@ const replaceReferences = (
56
77
  ): CatladderJob<string> => {
57
78
  const stage =
58
79
  job.envMode === "stagePerEnv" ? `${job.stage} ${env}` : job.stage;
80
+
81
+ const needsFromNeeds: CatladderJob["needs"] = job.needs?.map((n) =>
82
+ isObject(n)
83
+ ? {
84
+ job: getFullReferencedJobName(n.job, componentName, env, allRawJobs),
85
+ artifacts: n.artifacts,
86
+ }
87
+ : getFullReferencedJobName(n, componentName, env, allRawJobs)
88
+ );
89
+ const needsFromOtherComponents: CatladderJob["needs"] =
90
+ job.needsOtherComponent?.map((other) => ({
91
+ artifacts: other.artifacts,
92
+ job: getFullReferencedJobName(
93
+ other.job,
94
+ other.componentName,
95
+ env,
96
+ allRawJobs
97
+ ),
98
+ }));
99
+ const needs = [
100
+ ...(needsFromNeeds ?? []),
101
+ ...(needsFromOtherComponents ?? []),
102
+ ];
59
103
  return {
60
104
  ...job,
61
105
  stage,
62
- needs: job.needs?.map((n) =>
63
- isObject(n)
64
- ? {
65
- job: getFullReferencedJobName(
66
- n.job,
67
- componentName,
68
- env,
69
- allRawJobs
70
- ),
71
- artifacts: n.artifacts,
72
- }
73
- : getFullReferencedJobName(n, componentName, env, allRawJobs)
74
- ),
106
+ needs,
75
107
  environment: job.environment?.on_stop
76
108
  ? {
77
109
  ...job.environment,
@@ -122,7 +154,9 @@ export const createJobs = async <T extends PipelineType>(
122
154
  ): Promise<Record<string, PipelineJob<T>>> => {
123
155
  const commitInfo: CommitInfo = {
124
156
  refName: process.env.CI_COMMIT_REF_NAME ?? "unknown",
125
- refSlug: process.env.CI_COMMIT_REF_SLUG ?? "unknown",
157
+ reviewSlug: process.env.CI_MERGE_REQUEST_IID
158
+ ? `mr${process.env.CI_MERGE_REQUEST_IID}`
159
+ : "unknown",
126
160
  buildTime: new Date().toISOString(),
127
161
  buildId: await exec("git describe --tags || git rev-parse HEAD").then((s) =>
128
162
  s.stdout.trim()
@@ -1,6 +1,8 @@
1
1
  import { BuildConfig } from "../build/types";
2
2
  import { DeployConfig } from "../deploy/types";
3
3
  import { PartialDeep } from "type-fest";
4
+ import { CatladderJob } from "./jobs";
5
+ import { Context } from "./context";
4
6
  export type PipelineTrigger = "mainBranch" | "mr" | "taggedRelease";
5
7
 
6
8
  /**
@@ -123,6 +125,16 @@ export type ComponentConfig = {
123
125
  * the directory of the component (e.g. where the package.json or similar is located). You can set "." if you only have one app.
124
126
  */
125
127
  dir: string;
128
+
129
+ /**
130
+ * add custom jobs. Can be an array or a function that receives the context and returns an array of jobs.
131
+ * For convenience, all env vars are injectd as variables
132
+ *
133
+ * Please raise an issue on https://git.panter.ch/catladder/catladder
134
+ * to let us know about why you need to use custom jobs.
135
+ * This feedback will help us to generalize use cases
136
+ */
137
+ customJobs?: CatladderJob[] | ((context: Context) => CatladderJob[]);
126
138
  } & DefaultEnvConfig;
127
139
 
128
140
  export type Config = {
@@ -16,7 +16,7 @@ export type Environment = {
16
16
 
17
17
  export type CommitInfo = {
18
18
  refName: string;
19
- refSlug: string;
19
+ reviewSlug: string;
20
20
  buildTime: string;
21
21
  buildId: string;
22
22
  trigger: PipelineTrigger;
package/src/types/jobs.ts CHANGED
@@ -49,9 +49,15 @@ export type CatladderJob<S = BaseStage> = {
49
49
  }[];
50
50
 
51
51
  /**
52
- * does this require another job?
52
+ * does this require another job (from the same component)?
53
53
  */
54
54
  needs?: Array<string | { job: string; artifacts: boolean }>;
55
+
56
+ needsOtherComponent?: Array<{
57
+ componentName: string;
58
+ job: string;
59
+ artifacts: boolean;
60
+ }>;
55
61
  /**
56
62
  * cache config, we use here the same shape as gitlab itself
57
63
  */