polyci 0.0.1 → 0.0.2

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 (2) hide show
  1. package/dist/main.js +132 -108
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1,29 +1,25 @@
1
1
  import { Command } from "commander";
2
- import { execSync } from "node:child_process";
3
2
  import * as fs from "node:fs";
4
3
  import * as path from "node:path";
5
4
  import pino from "pino";
6
5
  import pretty from "pino-pretty";
7
6
  const log = pino(pretty());
8
- function exec(command, cwd) {
9
- return execSync(command, {
10
- encoding: "utf8",
11
- cwd: cwd ?? process.cwd(),
12
- }).trim();
13
- }
14
- const BRANCH_RULE = "/^(main)|(develop)|(release(-[a-z0-9]+)*)|(feature(-[a-z0-9]+)*)|(patch(-[a-z0-9]+)*)$/";
15
- const IGNORED_DIRS = new Set(["node_modules", ".git", "dist"]);
7
+ const DEFAULT_BRANCH_RULE = "/^(main)|(develop)|(release(-[a-z0-9]+)*)|(feature(-[a-z0-9]+)*)|(patch(-[a-z0-9]+)*)$/";
16
8
  function parseArgs() {
17
9
  const program = new Command();
18
10
  program
19
11
  .argument("[output]", "Output pipeline file path (or use --output)")
20
12
  .option("-o, --output <path>", "Output pipeline file path")
13
+ .option("--modules-root <path>", "Modules root directory", "./modules")
14
+ .option("--branch-rule <regex>", "Branch rule regex for module jobs", DEFAULT_BRANCH_RULE)
21
15
  .option("--cwd <path>", "Working directory", process.cwd())
22
16
  .parse();
23
17
  const options = program.opts();
24
18
  const output = options.output ?? program.args[0] ?? "";
25
19
  const cwd = path.resolve(options.cwd);
26
- return { cwd, output };
20
+ const modulesRoot = path.resolve(cwd, options.modulesRoot);
21
+ const branchRule = options.branchRule;
22
+ return { cwd, output, modulesRoot, branchRule };
27
23
  }
28
24
  function toPosixPath(p) {
29
25
  return p.split(path.sep).join("/");
@@ -36,6 +32,10 @@ function toJobId(value) {
36
32
  .toLowerCase();
37
33
  }
38
34
  function detectModuleType(directory) {
35
+ const baseName = path.basename(directory);
36
+ if (baseName === "node_modules" || baseName === ".git" || baseName === "dist") {
37
+ return "ignored";
38
+ }
39
39
  const packageJson = path.join(directory, "package.json");
40
40
  const viteConfig = path.join(directory, "vite.config.ts");
41
41
  if (fs.existsSync(packageJson) && fs.existsSync(viteConfig)) {
@@ -56,11 +56,15 @@ function detectModuleType(directory) {
56
56
  }
57
57
  return null;
58
58
  }
59
- function discoverModulesRecursively(scanDir, repoRoot, modules) {
59
+ function discoverModulesRecursively(repoRoot, modulesRoot, scanDir, modules) {
60
60
  const moduleType = detectModuleType(scanDir);
61
61
  if (moduleType !== null) {
62
+ if (moduleType === "ignored") {
63
+ return;
64
+ }
62
65
  const relativeModulePath = toPosixPath(path.relative(repoRoot, scanDir));
63
- const moduleName = path.basename(scanDir);
66
+ const relativeNamePath = toPosixPath(path.relative(modulesRoot, scanDir));
67
+ const moduleName = relativeNamePath.split("/").join("-");
64
68
  modules.push({
65
69
  moduleName,
66
70
  modulePath: relativeModulePath,
@@ -73,19 +77,16 @@ function discoverModulesRecursively(scanDir, repoRoot, modules) {
73
77
  for (const entry of entries) {
74
78
  if (!entry.isDirectory())
75
79
  continue;
76
- if (IGNORED_DIRS.has(entry.name))
77
- continue;
78
- discoverModulesRecursively(path.join(scanDir, entry.name), repoRoot, modules);
80
+ discoverModulesRecursively(repoRoot, modulesRoot, path.join(scanDir, entry.name), modules);
79
81
  }
80
82
  }
81
- function discoverModules(repoRoot) {
82
- const modulesRoot = path.join(repoRoot, "modules");
83
+ function discoverModules(repoRoot, modulesRoot) {
83
84
  if (!fs.existsSync(modulesRoot) || !fs.statSync(modulesRoot).isDirectory()) {
84
85
  return [];
85
86
  }
86
87
  const modules = [];
87
- discoverModulesRecursively(modulesRoot, repoRoot, modules);
88
- modules.sort((a, b) => a.modulePath.localeCompare(b.modulePath));
88
+ discoverModulesRecursively(repoRoot, modulesRoot, modulesRoot, modules);
89
+ modules.sort((a, b) => a.modulePath.localeCompare(b.modulePath)); //TODO: Sort by dependency order or earliest modified module first
89
90
  return modules;
90
91
  }
91
92
  function appendGlobalVariables(lines) {
@@ -106,18 +107,18 @@ function appendGlobalVariables(lines) {
106
107
  lines.push(" - deploy");
107
108
  lines.push("");
108
109
  }
109
- function appendRulesForModule(lines, modulePath) {
110
+ function appendRulesForModule(lines, modulePath, branchRule) {
110
111
  lines.push(" rules:");
111
112
  lines.push(" - if: $CI_COMMIT_MESSAGE =~ /^release:/");
112
113
  lines.push(" when: never");
113
- lines.push(` - if: $CI_COMMIT_BRANCH =~ ${BRANCH_RULE}`);
114
+ lines.push(` - if: $CI_COMMIT_BRANCH =~ ${branchRule}`);
114
115
  lines.push(" changes:");
115
116
  lines.push(` - ${modulePath}/**/*`);
116
117
  }
117
- function appendModuleBuildJob(lines, module) {
118
+ function appendModuleBuildJob(lines, module, branchRule) {
118
119
  lines.push(`${module.jobId}_build:`);
119
120
  lines.push(" stage: build");
120
- appendRulesForModule(lines, module.modulePath);
121
+ appendRulesForModule(lines, module.modulePath, branchRule);
121
122
  lines.push(" image: $IMAGE_NODE");
122
123
  lines.push(" variables:");
123
124
  lines.push(` MODULE_NAME: ${module.moduleName}`);
@@ -133,13 +134,12 @@ function appendModuleBuildJob(lines, module) {
133
134
  lines.push(" expire_in: 1 day");
134
135
  lines.push("");
135
136
  }
136
- function appendModuleTestJob(lines, module) {
137
+ function appendModuleTestJob(lines, module, branchRule) {
137
138
  lines.push(`${module.jobId}_test:`);
138
139
  lines.push(" stage: test");
139
- appendRulesForModule(lines, module.modulePath);
140
+ appendRulesForModule(lines, module.modulePath, branchRule);
140
141
  lines.push(" needs:");
141
142
  lines.push(` - job: ${module.jobId}_build`);
142
- lines.push(" optional: true");
143
143
  lines.push(" image: $IMAGE_LINUX");
144
144
  lines.push(" variables:");
145
145
  lines.push(` MODULE_NAME: ${module.moduleName}`);
@@ -150,9 +150,61 @@ function appendModuleTestJob(lines, module) {
150
150
  lines.push(' - echo "test is tasty"');
151
151
  lines.push("");
152
152
  }
153
+ function appendModuleReleaseJob(lines, module, branchRule) {
154
+ lines.push(`${module.jobId}_release:`);
155
+ lines.push(" stage: release");
156
+ appendRulesForModule(lines, module.modulePath, branchRule);
157
+ lines.push(" needs:");
158
+ lines.push(` - job: ${module.jobId}_build`);
159
+ lines.push(` - job: ${module.jobId}_test`);
160
+ lines.push(" image: $IMAGE_NODE");
161
+ lines.push(" variables:");
162
+ lines.push(` MODULE_NAME: ${module.moduleName}`);
163
+ lines.push(` MODULE_PATH: ${module.modulePath}`);
164
+ lines.push(` MODULE_TYPE: ${module.moduleType}`);
165
+ lines.push(" script:");
166
+ lines.push(" - apk update");
167
+ lines.push(" - apk add $GIT_PACKAGE");
168
+ lines.push(" - cp -r ci $MODULE_PATH/ci");
169
+ lines.push(" - cd $MODULE_PATH");
170
+ lines.push(" - npm ci");
171
+ lines.push(" - npx semalease semalease.env");
172
+ lines.push(" - source semalease.env");
173
+ lines.push(" - |");
174
+ lines.push(` if [ "$CI_COMMIT_BRANCH" = "main" ]; then`);
175
+ lines.push(` PACKAGE_VERSION="$NEXT_VERSION"`);
176
+ lines.push(` else`);
177
+ lines.push(` PACKAGE_VERSION="$NEXT_VERSION-$CI_COMMIT_BRANCH-$NEXT_INCREMENT"`);
178
+ lines.push(` fi`);
179
+ lines.push(` if [ "$MODULE_TYPE" = "node-express" ]; then`);
180
+ lines.push(` node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version=process.env.PACKAGE_VERSION; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\\n');"`);
181
+ lines.push(` node ci/prepare-deploy-variables.js --instance-branch $CI_COMMIT_BRANCH`);
182
+ lines.push(` elif [ "$MODULE_TYPE" = "node-vite" ]; then`);
183
+ lines.push(` node ci/set-release-data.js -i $CI_COMMIT_BRANCH --version=$NEXT_VERSION -t`);
184
+ lines.push(` node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version=process.env.PACKAGE_VERSION; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\\n');"`);
185
+ lines.push(` node ci/prepare-deploy-variables.js --instance-branch $CI_COMMIT_BRANCH`);
186
+ lines.push(` else`);
187
+ lines.push(` echo "Invalid module type: $MODULE_TYPE"`);
188
+ lines.push(` exit 1`);
189
+ lines.push(` fi`);
190
+ lines.push(` artifacts:`);
191
+ lines.push(` paths:`);
192
+ lines.push(` - $MODULE_PATH/dist`);
193
+ lines.push(` - $MODULE_PATH/package.json`);
194
+ lines.push(` - $MODULE_PATH/public/release.json`);
195
+ lines.push(` - $MODULE_PATH/semalease.env`);
196
+ lines.push(` - $MODULE_PATH/deploy.env`);
197
+ lines.push(` expire_in: 1 day`);
198
+ lines.push("");
199
+ }
153
200
  function appendGlobalReleaseJob(lines, modules) {
154
201
  lines.push("release_and_tag:");
155
202
  lines.push(" stage: release");
203
+ lines.push(" needs:");
204
+ for (const module of modules) {
205
+ lines.push(` - job: ${module.jobId}_release`);
206
+ lines.push(` optional: true`);
207
+ }
156
208
  lines.push(" image: $IMAGE_NODE");
157
209
  lines.push(" variables:");
158
210
  lines.push(" GITLAB_TOKEN: $SEMANTIC_RELEASE_TOKEN # Used to push release commits/tags.");
@@ -161,111 +213,83 @@ function appendGlobalReleaseJob(lines, modules) {
161
213
  lines.push(" - apk add $GIT_PACKAGE");
162
214
  lines.push(" - |");
163
215
  lines.push(" set -euo pipefail");
164
- lines.push(" git config user.email \"ci-release@local\"");
165
- lines.push(" git config user.name \"CI Release\"");
216
+ lines.push(" git config user.email \"polyci@anarun.net\"");
217
+ lines.push(" git config user.name \"polyci\"");
166
218
  lines.push(" git remote set-url origin \"https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\"");
167
219
  lines.push("");
168
- lines.push(" if [ -n \"${CI_COMMIT_BEFORE_SHA:-}\" ] && [ \"${CI_COMMIT_BEFORE_SHA}\" != \"0000000000000000000000000000000000000000\" ]; then");
169
- lines.push(" CHANGED_FILES=$(git diff --name-only \"${CI_COMMIT_BEFORE_SHA}\" \"${CI_COMMIT_SHA}\")");
170
- lines.push(" else");
171
- lines.push(" CHANGED_FILES=$(git show --pretty='' --name-only \"${CI_COMMIT_SHA}\")");
172
- lines.push(" fi");
173
- lines.push("");
174
- lines.push(" MODULE_MATRIX=$(cat <<'EOF'");
175
220
  for (const module of modules) {
176
- lines.push(` ${module.moduleName}|${module.modulePath}`);
177
- }
178
- lines.push(" EOF");
179
- lines.push(" )");
180
- lines.push("");
181
- lines.push(" AFFECTED_MODULES=\"\"");
182
- lines.push(" while IFS='|' read -r MODULE_NAME MODULE_PATH; do");
183
- lines.push(" [ -n \"${MODULE_NAME}\" ] || continue");
184
- lines.push(" if printf '%s\\n' \"${CHANGED_FILES}\" | grep -q \"^${MODULE_PATH}/\"; then");
185
- lines.push(" AFFECTED_MODULES=\"${AFFECTED_MODULES} ${MODULE_NAME}|${MODULE_PATH}\"");
186
- lines.push(" fi");
187
- lines.push(" done <<'EOF'");
188
- for (const module of modules) {
189
- lines.push(` ${module.moduleName}|${module.modulePath}`);
221
+ lines.push(` echo "Tagging release for ${module.moduleName} (${module.modulePath})"`);
222
+ lines.push(` if [ ! -f "${module.modulePath}/semalease.env" ]; then`);
223
+ lines.push(` echo "Missing ${module.modulePath}/semalease.env, skipping ${module.moduleName}"`);
224
+ lines.push(" continue");
225
+ lines.push(" fi");
226
+ lines.push(` source "${module.modulePath}/semalease.env"`);
227
+ lines.push(" if [ \"$CI_COMMIT_BRANCH\" = \"main\" ]; then");
228
+ lines.push(" if [ \"${NEXT_VERSION:-}\" = \"${LATEST_VERSION:-}\" ]; then");
229
+ lines.push(` echo "Version did not change for ${module.moduleName} on branch $CI_COMMIT_BRANCH, skipping tag"`);
230
+ lines.push(" continue");
231
+ lines.push(" fi");
232
+ lines.push(` TAG_NAME="${module.moduleName}-v\${NEXT_VERSION}"`);
233
+ lines.push(" else");
234
+ lines.push(" if [ \"${NEXT_VERSION:-}\" = \"${LATEST_VERSION:-}\" ] && [ \"${NEXT_INCREMENT:-}\" = \"${LATEST_INCREMENT:-}\" ]; then");
235
+ lines.push(` echo "Version/increment did not change for ${module.moduleName} on branch $CI_COMMIT_BRANCH, skipping tag"`);
236
+ lines.push(" continue");
237
+ lines.push(" fi");
238
+ lines.push(` TAG_NAME="${module.moduleName}-v\${NEXT_VERSION}-\${CI_COMMIT_BRANCH}-\${NEXT_INCREMENT}"`);
239
+ lines.push(" fi");
240
+ if (module.moduleType === "node-express") {
241
+ lines.push(` git add "${module.modulePath}/package.json"`);
242
+ }
243
+ else if (module.moduleType === "node-vite") {
244
+ lines.push(` git add "${module.modulePath}/package.json" "${module.modulePath}/public/release.json"`);
245
+ }
246
+ lines.push(" git tag \"${TAG_NAME}\"");
190
247
  }
191
- lines.push(" EOF");
192
- lines.push("");
193
- lines.push(" if [ -z \"${AFFECTED_MODULES}\" ]; then");
194
- lines.push(" echo \"No affected modules in current commit range\" ");
195
- lines.push(" exit 0");
196
- lines.push(" fi");
197
- lines.push("");
198
- lines.push(" for entry in ${AFFECTED_MODULES}; do");
199
- lines.push(" MODULE_NAME=${entry%%|*}");
200
- lines.push(" MODULE_PATH=${entry#*|}");
201
- lines.push(" echo \"Tagging release for ${MODULE_NAME} (${MODULE_PATH})\"");
202
- lines.push(" cd \"${MODULE_PATH}\"");
203
- lines.push(" npm ci");
204
- lines.push(" npx semalease semalease.env");
205
- lines.push(" source semalease.env");
206
- lines.push(" cd - >/dev/null");
207
- lines.push("");
208
- lines.push(" if [ -z \"${NEXT_VERSION:-}\" ]; then");
209
- lines.push(" echo \"No NEXT_VERSION for ${MODULE_NAME}, skipping tag\"");
210
- lines.push(" continue");
211
- lines.push(" fi");
212
- lines.push("");
213
- lines.push(" TAG_NAME=\"${MODULE_NAME}/v${NEXT_VERSION}\"");
214
- lines.push(" if git rev-parse \"${TAG_NAME}\" >/dev/null 2>&1; then");
215
- lines.push(" echo \"Tag ${TAG_NAME} already exists, skipping\"");
216
- lines.push(" continue");
217
- lines.push(" fi");
218
- lines.push(" git tag \"${TAG_NAME}\"");
219
- lines.push(" done");
220
- lines.push("");
221
- lines.push(" if [ -n \"$(git status --porcelain)\" ]; then");
222
- lines.push(" git add -A");
223
- lines.push(" git commit -m \"release: update affected modules\"");
224
- lines.push(" fi");
225
248
  lines.push("");
249
+ lines.push(" git commit -m \"release: update affected modules\"");
226
250
  lines.push(" git push origin HEAD");
227
251
  lines.push(" git push origin --tags");
252
+ lines.push(` artifacts:`);
253
+ lines.push(` paths:`);
254
+ for (const module of modules) {
255
+ lines.push(` - ${module.modulePath}/dist`);
256
+ lines.push(` - ${module.modulePath}/deploy.env`);
257
+ }
258
+ lines.push(` expire_in: 1 day`);
228
259
  lines.push("");
229
260
  }
230
- function appendModulePublishJob(lines, module) {
261
+ function appendModulePublishJob(lines, module, branchRule) {
231
262
  lines.push(`${module.jobId}_publish:`);
232
263
  lines.push(" stage: publish");
233
- appendRulesForModule(lines, module.modulePath);
264
+ appendRulesForModule(lines, module.modulePath, branchRule);
234
265
  lines.push(" needs:");
235
266
  lines.push(` - job: ${module.jobId}_test`);
236
- lines.push(" optional: true");
237
267
  lines.push(" - job: release_and_tag");
238
- lines.push(" optional: true");
239
268
  lines.push(" image: $IMAGE_DOCKER");
240
269
  lines.push(" variables:");
241
270
  lines.push(` MODULE_NAME: ${module.moduleName}`);
242
271
  lines.push(` MODULE_PATH: ${module.modulePath}`);
243
272
  lines.push(` MODULE_TYPE: ${module.moduleType}`);
244
273
  lines.push(" script:");
274
+ lines.push(" - cp -r ci $MODULE_PATH/ci");
245
275
  lines.push(" - cd $MODULE_PATH");
276
+ lines.push(" - source deploy.env");
246
277
  lines.push(" - npm ci");
247
- lines.push(" - npx semalease semalease.env");
248
- lines.push(" - source semalease.env");
249
- lines.push(" - node ../ci/set-release-data.js -i $CI_COMMIT_BRANCH --version=${NEXT_VERSION} -t");
250
- lines.push(" - npm run build");
251
- lines.push(" - node ci/prepare-deploy-variables.js");
252
- lines.push(" - echo -n $CI_JOB_TOKEN | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY");
253
- lines.push(" - cp ../ci/$MODULE_TYPE/Dockerfile .");
278
+ lines.push(" - cp ci/$MODULE_TYPE/Dockerfile .");
254
279
  lines.push(" - docker build --tag $CI_REGISTRY_IMAGE/$MODULE_NAME:v$INSTANCE_VERSION .");
255
280
  lines.push(" - docker push $CI_REGISTRY_IMAGE/$MODULE_NAME:v$INSTANCE_VERSION");
256
281
  lines.push(" artifacts:");
257
282
  lines.push(" reports:");
258
- lines.push(" dotenv: $MODULE_PATH/.env.deploy");
283
+ lines.push(" dotenv: $MODULE_PATH/deploy.env");
259
284
  lines.push(" expire_in: 1 day");
260
285
  lines.push("");
261
286
  }
262
- function appendModuleDeployJob(lines, module) {
287
+ function appendModuleDeployJob(lines, module, branchRule) {
263
288
  lines.push(`${module.jobId}_deploy:`);
264
289
  lines.push(" stage: deploy");
265
- appendRulesForModule(lines, module.modulePath);
290
+ appendRulesForModule(lines, module.modulePath, branchRule);
266
291
  lines.push(" needs:");
267
292
  lines.push(` - job: ${module.jobId}_publish`);
268
- lines.push(" optional: true");
269
293
  lines.push(" image: $IMAGE_COMPOSE");
270
294
  lines.push(" variables:");
271
295
  lines.push(` MODULE_NAME: ${module.moduleName}`);
@@ -289,34 +313,34 @@ function appendModuleDeployJob(lines, module) {
289
313
  lines.push(" - rm -rf ~/.ssh");
290
314
  lines.push("");
291
315
  }
292
- function buildPipeline(modules) {
316
+ function buildPipeline(modules, branchRule) {
293
317
  const lines = [];
294
318
  appendGlobalVariables(lines);
295
319
  for (const module of modules) {
296
- appendModuleBuildJob(lines, module);
297
- appendModuleTestJob(lines, module);
320
+ appendModuleBuildJob(lines, module, branchRule);
321
+ appendModuleTestJob(lines, module, branchRule);
322
+ appendModuleReleaseJob(lines, module, branchRule);
298
323
  }
299
324
  appendGlobalReleaseJob(lines, modules);
300
325
  for (const module of modules) {
301
- appendModulePublishJob(lines, module);
302
- appendModuleDeployJob(lines, module);
326
+ appendModulePublishJob(lines, module, branchRule);
327
+ appendModuleDeployJob(lines, module, branchRule);
303
328
  }
304
329
  return `${lines.join("\n").trimEnd()}\n`;
305
330
  }
306
331
  function main() {
307
- const { cwd, output } = parseArgs();
332
+ const { cwd, output, modulesRoot, branchRule } = parseArgs();
308
333
  if (!output) {
309
334
  log.error("Output path is required. Use [output] or --output <path>.");
310
335
  process.exit(1);
311
336
  }
312
- const modules = discoverModules(cwd);
337
+ const modules = discoverModules(cwd, modulesRoot);
313
338
  if (modules.length === 0) {
314
- log.error({ cwd }, "No supported modules were discovered under ./modules");
339
+ log.error({ cwd, modulesRoot }, "No supported modules were discovered under modules root");
315
340
  process.exit(1);
316
341
  }
317
- const pipeline = buildPipeline(modules);
342
+ const pipeline = buildPipeline(modules, branchRule);
318
343
  const outputPath = path.resolve(cwd, output);
319
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
320
344
  fs.writeFileSync(outputPath, pipeline, "utf8");
321
345
  log.info({ modules: modules.map((m) => m.modulePath), outputPath }, "Generated GitLab pipeline");
322
346
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "polyci",
3
3
  "description": "Monorepo CI/CD utilities.",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "author": "Alexander Tsarev",