From b3a2b11db925602919c494223332d31d5de08f47 Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Sun, 23 May 2021 17:51:14 +0800 Subject: [PATCH] refactor(pipeline-tasks-runner): rabbitmq --- .vscode/settings.json | 5 + config.yml.example | 2 + package-lock.json | 317 +++++++++++++++++ package.json | 1 + src/app.module.ts | 2 + .../models/pipeline-task-event.ts | 13 + .../pipeline-task.runner.spec.ts | 322 ++++++++++++++++++ src/pipeline-tasks/pipeline-task.runner.ts | 253 ++++++++++++++ src/pipeline-tasks/pipeline-tasks.module.ts | 40 +++ src/pipeline-tasks/pipeline-tasks.resolver.ts | 7 +- src/pipeline-tasks/pipeline-tasks.service.ts | 37 +- src/repos/repos.service.ts | 6 + 12 files changed, 982 insertions(+), 23 deletions(-) create mode 100644 src/pipeline-tasks/models/pipeline-task-event.ts create mode 100644 src/pipeline-tasks/pipeline-task.runner.spec.ts create mode 100644 src/pipeline-tasks/pipeline-task.runner.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c6ad29..ad38e62 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,18 @@ { "cSpell.words": [ "Repos", + "amqp", "boardcat", + "errout", + "fanout", "gitea", + "golevelup", "lpush", "lrange", "metatype", "pmessage", "psubscribe", + "rabbitmq", "rpop", "rpush" ] diff --git a/config.yml.example b/config.yml.example index a7e2586..24e61c6 100644 --- a/config.yml.example +++ b/config.yml.example @@ -14,5 +14,7 @@ db: port: 6379 password: prefix: fennec + rabbitmq: + uri: 'amqp://fennec:fennec@192.168.31.194:5672' workspaces: root: '/Users/ivanli/Projects/fennec/workspaces' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d563b90..484716c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@golevelup/nestjs-rabbitmq": "^1.16.1", "@nestjs/bull": "^0.3.1", "@nestjs/common": "^7.5.1", "@nestjs/config": "^0.6.2", @@ -1411,6 +1412,51 @@ "node": ">=8" } }, + "node_modules/@golevelup/nestjs-common": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.2.tgz", + "integrity": "sha512-mdXppMelEYO+HTilg4W8h8Bt4u1s9qvOip2dygRMxY3O1/JK6GPpPgJ99CiuAT+xxFG9igeN8lfpAnO0+mJXxA==", + "dependencies": { + "shortid": "^2.2.14" + } + }, + "node_modules/@golevelup/nestjs-discovery": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-2.3.1.tgz", + "integrity": "sha512-K3NG34hK1/UM1ardiUSS9c43AAdk6IBJHUMZbBGavqfgNo17N2wjygSDMHFd5Cf+jqTPkt+n5Vr9ua22yCLzlg==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/@golevelup/nestjs-modules": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.4.1.tgz", + "integrity": "sha512-v2GYGtJ8moTfhehavNFBwnHQ+QcgYHssxfqbJZcY4CUqB97E2sWmnAmtgscHDTvHqABOCaq2xvwfw4YTwZU2Ug==", + "dependencies": { + "shortid": "^2.2.14" + } + }, + "node_modules/@golevelup/nestjs-rabbitmq": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-1.16.1.tgz", + "integrity": "sha512-+5rss427yqPbvtIZxQUdjvaXe0Xtq6tE/PVmkHgPETEVDZY+DfqQSYiL/E3QPfFzCytiYxBTSlQZZVjUaNV8Cg==", + "dependencies": { + "@golevelup/nestjs-common": "^1.4.2", + "@golevelup/nestjs-discovery": "^2.3.1", + "@golevelup/nestjs-modules": "^0.4.1", + "amqp-connection-manager": "^3.0.0", + "amqplib": "^0.7.1", + "uuid": "^3.3.2" + } + }, + "node_modules/@golevelup/nestjs-rabbitmq/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/@graphql-tools/batch-delegate": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@graphql-tools/batch-delegate/-/batch-delegate-7.0.2.tgz", @@ -3571,6 +3617,77 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, + "node_modules/amqp-connection-manager": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-3.2.2.tgz", + "integrity": "sha512-o+6Kb4p+xFYwU8MuFxnrPQzhefCE2dcCd8dnevWLTRgQCtOTVC9AQ434hQyjB+Bpq6Vl9cDMWTOZT11ajB6ZSg==", + "dependencies": { + "promise-breaker": "^5.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + }, + "peerDependencies": { + "amqplib": "*" + } + }, + "node_modules/amqplib": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.7.1.tgz", + "integrity": "sha512-KePK3tTOLGU4emTo+PwSDMbc123jrxo13FpRpim1LzJoSlQrIBB2/kMeCC40jK/Zb0olHGaABjLqXDsdK46iLA==", + "dependencies": { + "bitsyntax": "~0.1.0", + "bluebird": "^3.7.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "safe-buffer": "~5.2.1", + "url-parse": "~1.5.1" + }, + "engines": { + "node": ">=0.8 <=15" + } + }, + "node_modules/amqplib/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/amqplib/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/amqplib/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/amqplib/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -4571,6 +4688,32 @@ "node": ">=8" } }, + "node_modules/bitsyntax": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.1.0.tgz", + "integrity": "sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q==", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "~2.6.9", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/bitsyntax/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/bitsyntax/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4596,6 +4739,11 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -4727,6 +4875,11 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, "node_modules/buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -10113,6 +10266,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11185,6 +11343,11 @@ "asap": "~2.0.3" } }, + "node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==" + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -12404,6 +12567,14 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, + "node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "dependencies": { + "nanoid": "^2.1.0" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -16145,6 +16316,50 @@ } } }, + "@golevelup/nestjs-common": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.2.tgz", + "integrity": "sha512-mdXppMelEYO+HTilg4W8h8Bt4u1s9qvOip2dygRMxY3O1/JK6GPpPgJ99CiuAT+xxFG9igeN8lfpAnO0+mJXxA==", + "requires": { + "shortid": "^2.2.14" + } + }, + "@golevelup/nestjs-discovery": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-2.3.1.tgz", + "integrity": "sha512-K3NG34hK1/UM1ardiUSS9c43AAdk6IBJHUMZbBGavqfgNo17N2wjygSDMHFd5Cf+jqTPkt+n5Vr9ua22yCLzlg==", + "requires": { + "lodash": "^4.17.15" + } + }, + "@golevelup/nestjs-modules": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.4.1.tgz", + "integrity": "sha512-v2GYGtJ8moTfhehavNFBwnHQ+QcgYHssxfqbJZcY4CUqB97E2sWmnAmtgscHDTvHqABOCaq2xvwfw4YTwZU2Ug==", + "requires": { + "shortid": "^2.2.14" + } + }, + "@golevelup/nestjs-rabbitmq": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-1.16.1.tgz", + "integrity": "sha512-+5rss427yqPbvtIZxQUdjvaXe0Xtq6tE/PVmkHgPETEVDZY+DfqQSYiL/E3QPfFzCytiYxBTSlQZZVjUaNV8Cg==", + "requires": { + "@golevelup/nestjs-common": "^1.4.2", + "@golevelup/nestjs-discovery": "^2.3.1", + "@golevelup/nestjs-modules": "^0.4.1", + "amqp-connection-manager": "^3.0.0", + "amqplib": "^0.7.1", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, "@graphql-tools/batch-delegate": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@graphql-tools/batch-delegate/-/batch-delegate-7.0.2.tgz", @@ -18146,6 +18361,55 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, + "amqp-connection-manager": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-3.2.2.tgz", + "integrity": "sha512-o+6Kb4p+xFYwU8MuFxnrPQzhefCE2dcCd8dnevWLTRgQCtOTVC9AQ434hQyjB+Bpq6Vl9cDMWTOZT11ajB6ZSg==", + "requires": { + "promise-breaker": "^5.0.0" + } + }, + "amqplib": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.7.1.tgz", + "integrity": "sha512-KePK3tTOLGU4emTo+PwSDMbc123jrxo13FpRpim1LzJoSlQrIBB2/kMeCC40jK/Zb0olHGaABjLqXDsdK46iLA==", + "requires": { + "bitsyntax": "~0.1.0", + "bluebird": "^3.7.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "safe-buffer": "~5.2.1", + "url-parse": "~1.5.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -18983,6 +19247,31 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bitsyntax": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.1.0.tgz", + "integrity": "sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q==", + "requires": { + "buffer-more-ints": "~1.0.0", + "debug": "~2.6.9", + "safe-buffer": "~5.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -19007,6 +19296,11 @@ } } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -19122,6 +19416,11 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, "buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -23479,6 +23778,11 @@ "thenify-all": "^1.0.0" } }, + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -24343,6 +24647,11 @@ "asap": "~2.0.3" } }, + "promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==" + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -25346,6 +25655,14 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, + "shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "requires": { + "nanoid": "^2.1.0" + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", diff --git a/package.json b/package.json index 234eb09..391e7b3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@golevelup/nestjs-rabbitmq": "^1.16.1", "@nestjs/bull": "^0.3.1", "@nestjs/common": "^7.5.1", "@nestjs/config": "^0.6.2", diff --git a/src/app.module.ts b/src/app.module.ts index 8c8745c..47b3448 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,8 @@ import { ParseBodyMiddleware } from './commons/middlewares/parse-body.middleware import { BullModule } from '@nestjs/bull'; import { PubSubModule } from './commons/pub-sub/pub-sub.module'; import { LoggerModule } from 'nestjs-pino'; +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; + import pinoPretty from 'pino-pretty'; @Module({ diff --git a/src/pipeline-tasks/models/pipeline-task-event.ts b/src/pipeline-tasks/models/pipeline-task-event.ts new file mode 100644 index 0000000..8021300 --- /dev/null +++ b/src/pipeline-tasks/models/pipeline-task-event.ts @@ -0,0 +1,13 @@ +import { PipelineUnits } from '../enums/pipeline-units.enum'; +import { TaskStatuses } from '../enums/task-statuses.enum'; + +export class PipelineTaskEvent { + taskId: string; + pipelineId: string; + projectId: string; + unit: PipelineUnits | null; + emittedAt: Date; + message: string; + messageType: 'stdout' | 'stderr' | 'stdin'; + status: TaskStatuses; +} diff --git a/src/pipeline-tasks/pipeline-task.runner.spec.ts b/src/pipeline-tasks/pipeline-task.runner.spec.ts new file mode 100644 index 0000000..4ce37ef --- /dev/null +++ b/src/pipeline-tasks/pipeline-task.runner.spec.ts @@ -0,0 +1,322 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReposService } from '../repos/repos.service'; +import { PipelineUnits } from './enums/pipeline-units.enum'; +import { PipelineTask } from './pipeline-task.entity'; +import { Pipeline } from '../pipelines/pipeline.entity'; +import { Project } from '../projects/project.entity'; +import { TaskStatuses } from './enums/task-statuses.enum'; +import { getLoggerToken, PinoLogger } from 'nestjs-pino'; +import { PipelineTaskRunner } from './pipeline-task.runner'; +import { WorkUnitMetadata } from './models/work-unit-metadata.model'; +import { Code } from 'typeorm'; +describe('PipelineTaskRunner', () => { + let runner: PipelineTaskRunner; + let reposService: ReposService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ReposService, + useValue: { + getWorkspaceRootByTask: () => 'workspace-root', + checkout: async () => undefined, + }, + }, + { + provide: getLoggerToken(PipelineTaskRunner.name), + useValue: new PinoLogger({}), + }, + { + provide: 'spawn', + useValue: () => undefined, + }, + PipelineTaskRunner, + ], + }).compile(); + + reposService = module.get(ReposService); + runner = module.get(PipelineTaskRunner); + }); + + it('should be defined', () => { + expect(runner).toBeDefined(); + }); + + it('onNewTask', async () => { + const task = new PipelineTask(); + let tmpTask; + const doTask = jest + .spyOn(runner, 'doTask') + .mockImplementation(async (task) => { + tmpTask = task; + }); + await runner.onNewTask(task); + expect(tmpTask).toEqual(task); + expect(doTask).toBeCalledTimes(1); + }); + + describe('test biz', () => { + let emitEvent: jest.SpyInstance; + beforeEach(() => { + emitEvent = jest + .spyOn(runner, 'emitEvent') + .mockImplementation((..._) => Promise.resolve()); + }); + + describe('doTask', () => { + let checkout: jest.SpyInstance; + let doTaskUnit: jest.SpyInstance; + + beforeEach(() => { + checkout = jest + .spyOn(runner, 'checkout') + .mockImplementation((..._) => Promise.resolve('/null')); + doTaskUnit = jest + .spyOn(runner, 'doTaskUnit') + .mockImplementation((..._) => Promise.resolve()); + }); + + it('only checkout', async () => { + const task = new PipelineTask(); + (task.id = 'taskId'), (task.pipeline = new Pipeline()); + task.units = [PipelineUnits.checkout]; + task.pipeline.id = 'pipelineId'; + task.pipeline.project = new Project(); + task.pipeline.project.id = 'projectId'; + task.pipeline.workUnitMetadata = new WorkUnitMetadata(); + task.pipeline.workUnitMetadata.version = 1; + task.pipeline.workUnitMetadata.units = [ + { + type: PipelineUnits.checkout, + scripts: [], + }, + ]; + + await runner.doTask(task); + + expect(checkout).toBeCalledTimes(1); + expect(doTaskUnit).toBeCalledTimes(0); + expect(emitEvent).toBeCalledTimes(2); + expect(emitEvent.mock.calls[0][0]).toMatchObject(task); + expect(emitEvent.mock.calls[0][1]).toBeNull(); + expect(emitEvent.mock.calls[0][2]).toEqual(TaskStatuses.working); + expect(emitEvent.mock.calls[1][0]).toMatchObject(task); + expect(emitEvent.mock.calls[1][1]).toBeNull(); + expect(emitEvent.mock.calls[1][2]).toEqual(TaskStatuses.success); + }); + + it('many units', async () => { + const task = new PipelineTask(); + (task.id = 'taskId'), (task.pipeline = new Pipeline()); + task.units = [ + PipelineUnits.checkout, + PipelineUnits.test, + PipelineUnits.deploy, + ]; + task.pipeline.id = 'pipelineId'; + task.pipeline.project = new Project(); + task.pipeline.project.id = 'projectId'; + task.pipeline.workUnitMetadata = new WorkUnitMetadata(); + task.pipeline.workUnitMetadata.version = 1; + task.pipeline.workUnitMetadata.units = [ + { + type: PipelineUnits.checkout, + scripts: [], + }, + { + type: PipelineUnits.installDependencies, + scripts: ['pwd'], + }, + { + type: PipelineUnits.test, + scripts: ['pwd'], + }, + { + type: PipelineUnits.deploy, + scripts: ['pwd', 'uname'], + }, + ]; + + await runner.doTask(task); + + expect(checkout).toBeCalledTimes(1); + expect(doTaskUnit).toBeCalledTimes(2); + expect(emitEvent).toBeCalledTimes(2); + }); + + it('unit work failed', async () => { + const task = new PipelineTask(); + (task.id = 'taskId'), (task.pipeline = new Pipeline()); + task.units = [PipelineUnits.checkout, PipelineUnits.test]; + task.pipeline.id = 'pipelineId'; + task.pipeline.project = new Project(); + task.pipeline.project.id = 'projectId'; + task.pipeline.workUnitMetadata = new WorkUnitMetadata(); + task.pipeline.workUnitMetadata.version = 1; + task.pipeline.workUnitMetadata.units = [ + { + type: PipelineUnits.checkout, + scripts: [], + }, + { + type: PipelineUnits.test, + scripts: ['pwd'], + }, + ]; + + doTaskUnit = jest + .spyOn(runner, 'doTaskUnit') + .mockImplementation((..._) => + Promise.reject(new Error('test error')), + ); + await runner.doTask(task); + + expect(checkout).toBeCalledTimes(1); + expect(doTaskUnit).toBeCalledTimes(1); + expect(emitEvent).toBeCalledTimes(2); + expect(emitEvent.mock.calls[1][0]).toMatchObject(task); + expect(emitEvent.mock.calls[1][1]).toBeNull(); + expect(emitEvent.mock.calls[1][2]).toEqual(TaskStatuses.failed); + }); + }); + + describe('doTaskUnit', () => { + it('success', async () => { + const runScript = jest + .spyOn(runner, 'runScript') + .mockImplementation((..._) => Promise.resolve()); + const task = new PipelineTask(); + + const unit = PipelineUnits.test; + const workspacePath = '/null'; + await runner.doTaskUnit(unit, ['pwd'], task, workspacePath); + + expect(emitEvent.mock.calls[0][0]).toEqual(task); + expect(emitEvent.mock.calls[0][1]).toEqual(unit); + expect(emitEvent.mock.calls[0][2]).toEqual(TaskStatuses.working); + expect(emitEvent.mock.calls[1][0]).toEqual(task); + expect(emitEvent.mock.calls[1][1]).toEqual(unit); + expect(emitEvent.mock.calls[1][2]).toEqual(TaskStatuses.success); + expect(runScript.mock.calls[0][0]).toEqual('pwd'); + expect(runScript.mock.calls[0][1]).toEqual(workspacePath); + expect(runScript.mock.calls[0][2]).toEqual(task); + expect(runScript.mock.calls[0][3]).toEqual(unit); + }); + it('failed', async () => { + const runScript = jest + .spyOn(runner, 'runScript') + .mockImplementation((..._) => + Promise.reject(new Error('test error')), + ); + const task = new PipelineTask(); + + const unit = PipelineUnits.test; + const workspacePath = '/null'; + await expect( + runner.doTaskUnit(unit, ['pwd'], task, workspacePath), + ).rejects.toThrow('test error'); + + expect(emitEvent.mock.calls[1]?.[0]).toEqual(task); + expect(emitEvent.mock.calls[1]?.[1]).toEqual(unit); + expect(emitEvent.mock.calls[1]?.[2]).toEqual(TaskStatuses.failed); + expect(runScript).toBeCalledTimes(1); + }); + }); + + describe('runScript', () => { + it('normal', async () => { + const spawn = jest.fn((..._: any[]) => ({ + stdout: { + on: () => undefined, + }, + stderr: { + on: () => undefined, + }, + addListener: (_: any, fn: (code: number) => void) => { + fn(0); + }, + })); + (runner as any).spawn = spawn; + + const task = new PipelineTask(); + task.id = 'taskId'; + const unit = PipelineUnits.deploy; + + await runner.runScript('script name', 'workspaceRoot', task, unit); + expect(spawn).toHaveBeenCalledTimes(1); + expect(spawn.mock.calls[0][0]).toEqual('script name'); + expect(spawn.mock.calls[0][1]).toMatchObject({ + shell: true, + cwd: 'workspaceRoot', + }); + }); + it('failed', async () => { + const spawn = jest.fn((..._: any[]) => ({ + stdout: { + on: () => undefined, + }, + stderr: { + on: () => undefined, + }, + addListener: (_: any, fn: (code: number) => void) => { + fn(1); + }, + })); + (runner as any).spawn = spawn; + + const task = new PipelineTask(); + task.id = 'taskId'; + const unit = PipelineUnits.deploy; + + expect( + runner.runScript('script name', 'workspaceRoot', task, unit), + ).rejects.toThrowError(); + }); + it('wait emit message done', async () => { + let finishedFn: () => void; + const on = jest.fn((_: any, fn: (buff: Buffer) => void) => { + setTimeout(() => { + fn(Buffer.from('message 1')); + setTimeout(() => { + fn(Buffer.from('message 2')); + setTimeout(() => { + fn(Buffer.from('message 3')); + finishedFn(); + }, 1000); + }, 10); + }, 10); + }); + const spawn = jest.fn((..._: any[]) => ({ + stdout: { + on, + }, + stderr: { + on, + }, + addListener: (_: any, fn: (code: number) => void) => { + finishedFn = () => fn(0); + }, + })); + + let emitSuccessCount = 0; + jest.spyOn(runner, 'emitEvent').mockImplementation((..._: any[]) => { + return new Promise((resolve) => { + setTimeout(() => { + emitSuccessCount++; + resolve(); + }, 1000); + }); + }); + (runner as any).spawn = spawn; + + const task = new PipelineTask(); + task.id = 'taskId'; + const unit = PipelineUnits.deploy; + + await runner.runScript('script name', 'workspaceRoot', task, unit); + expect(emitSuccessCount).toEqual(1 + 6); + }); + }); + }); +}); diff --git a/src/pipeline-tasks/pipeline-task.runner.ts b/src/pipeline-tasks/pipeline-task.runner.ts new file mode 100644 index 0000000..2db8e58 --- /dev/null +++ b/src/pipeline-tasks/pipeline-task.runner.ts @@ -0,0 +1,253 @@ +import { ReposService } from '../repos/repos.service'; +import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; +import { PipelineTask } from './pipeline-task.entity'; +import { ApplicationException } from '../commons/exceptions/application.exception'; +import { PipelineUnits } from './enums/pipeline-units.enum'; +import { TaskStatuses } from './enums/task-statuses.enum'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; +import { PipelineTaskEvent } from './models/pipeline-task-event'; +import { last } from 'ramda'; +import { Inject } from '@nestjs/common'; + +type Spawn = typeof spawn; + +export class PipelineTaskRunner { + readonly processes = new Map(); + + constructor( + private readonly reposService: ReposService, + @InjectPinoLogger(PipelineTaskRunner.name) + private readonly logger: PinoLogger, + @Inject('spawn') + private readonly spawn: Spawn, + ) {} + @RabbitSubscribe({ + exchange: 'new-pipeline-task', + routingKey: 'mac', + queue: 'mac.new-pipeline-task', + }) + async onNewTask(task: PipelineTask) { + this.logger.info({ task }, 'on new task [%s].', task.id); + try { + await this.doTask(task); + } catch (err) { + this.logger.error({ task, err }, err.message); + } + } + @RabbitSubscribe({ + exchange: 'stop-pipeline-task', + routingKey: 'mac', + queue: 'mac.stop-pipeline-task', + }) + async onStopTask(task: PipelineTask) { + this.logger.info({ task }, 'on stop task [%s].', task.id); + const process = this.processes.get(task.id); + if (process) { + this.logger.info({ task }, 'send signal SIGINT to child process.'); + process.kill('SIGINT'); + + setTimeout(() => { + if (process === this.processes.get(task.id)) { + this.logger.info({ task }, 'send signal SIGKILL to child process.'); + process.kill('SIGKILL'); + return; + } + if (this.processes.has(task.id)) { + this.logger.error( + { task }, + 'this pipeline task not stop yet. there is a new process running, maybe is a bug about error capture', + ); + } + }, 10_000); + } else { + this.logger.info({ task }, 'child process is not running.'); + } + } + + async doTask(task: PipelineTask) { + if (task.pipeline.workUnitMetadata.version !== 1) { + throw new ApplicationException( + 'work unit metadata version is not match.', + ); + } + await this.emitEvent( + task, + null, + TaskStatuses.working, + `[start task]`, + 'stdout', + ); + + this.logger.info('running task [%s].', task.id); + try { + const workspaceRoot = await this.checkout(task); + const units = task.units + .filter((unit) => unit !== PipelineUnits.checkout) + .map( + (type) => + task.pipeline.workUnitMetadata.units.find( + (unit) => unit.type === type, + ) ?? { type: type, scripts: [] }, + ); + this.logger.info({ units }, 'begin run units.'); + for (const unit of units) { + await this.doTaskUnit(unit.type, unit.scripts, task, workspaceRoot); + } + await this.emitEvent( + task, + null, + TaskStatuses.success, + `[finished task] success`, + 'stdout', + ); + this.logger.info({ task }, 'task [%s] completed.', task.id); + } catch (err) { + await this.emitEvent( + task, + null, + TaskStatuses.failed, + `[finished unit] ${err.message}`, + 'stderr', + ); + this.logger.error({ task, error: err }, 'task [%s] failed.', task.id); + } finally { + } + } + + async doTaskUnit( + unit: PipelineUnits, + scripts: string[], + task: PipelineTask, + workspaceRoot: string, + ) { + await this.emitEvent( + task, + unit, + TaskStatuses.working, + `[begin unit] ${unit}`, + 'stdin', + ); + this.logger.info({ task }, 'curr unit is %s', unit); + try { + for (const script of scripts) { + this.logger.debug('begin runScript %s', script); + await this.runScript(script, workspaceRoot, task, unit); + this.logger.debug('end runScript %s', script); + } + + await this.emitEvent( + task, + unit, + TaskStatuses.success, + `[finished unit] ${unit}`, + 'stdout', + ); + } catch (err) { + await this.emitEvent( + task, + unit, + TaskStatuses.failed, + `[finished unit] ${err.message}`, + 'stderr', + ); + throw err; + } + } + + async checkout(task: PipelineTask) { + await this.emitEvent( + task, + PipelineUnits.checkout, + TaskStatuses.working, + '[begin unit] checkout', + 'stdin', + ); + try { + const path = await this.reposService.checkout4Task(task); + await this.emitEvent( + task, + PipelineUnits.checkout, + TaskStatuses.success, + 'checkout success.', + 'stdout', + ); + return path; + } catch (err) { + await this.emitEvent( + task, + PipelineUnits.checkout, + TaskStatuses.failed, + 'checkout failed.', + 'stderr', + ); + } + } + + async emitEvent( + task: PipelineTask, + unit: PipelineUnits | null, + status: TaskStatuses, + message: string, + messageType: 'stderr' | 'stdout' | 'stdin', + ) { + const event: PipelineTaskEvent = { + taskId: task.id, + pipelineId: task.pipeline.id, + projectId: task.pipeline.project.id, + unit, + emittedAt: new Date(), + message: last(message) === '\n' ? message : message + '\n', + messageType, + status, + }; + } + + async runScript( + script: string, + workspaceRoot: string, + task: PipelineTask, + unit: PipelineUnits, + ): Promise { + await this.emitEvent(task, unit, TaskStatuses.working, script, 'stdin'); + return new Promise((resolve, reject) => { + const sub = this.spawn(script, { + shell: true, + cwd: workspaceRoot, + }); + this.processes.set(task.id, sub); + let loggingCount = 0; // semaphore + + sub.stderr.on('data', (data: Buffer) => { + const str = data.toString(); + loggingCount++; + + this.emitEvent(task, unit, TaskStatuses.working, str, 'stdout').finally( + () => loggingCount--, + ); + }); + sub.stdout.on('data', (data: Buffer) => { + const str = data.toString(); + loggingCount++; + + this.emitEvent(task, unit, TaskStatuses.working, str, 'stderr').finally( + () => loggingCount--, + ); + }); + sub.addListener('close', async (code) => { + this.processes.delete(task.id); + await new Promise(async (resolve) => { + for (let i = 0; i < 10 && loggingCount > 0; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + this.logger.debug('waiting logging... (%dx500ms)', i); + } + resolve(); + }); + if (code === 0) { + return resolve(); + } + return reject(new ApplicationException('exec script failed')); + }); + }); + } +} diff --git a/src/pipeline-tasks/pipeline-tasks.module.ts b/src/pipeline-tasks/pipeline-tasks.module.ts index 1a4e5bb..114731c 100644 --- a/src/pipeline-tasks/pipeline-tasks.module.ts +++ b/src/pipeline-tasks/pipeline-tasks.module.ts @@ -11,6 +11,10 @@ import { PipelineTaskConsumer } from './pipeline-task.consumer'; import { PIPELINE_TASK_QUEUE } from './pipeline-tasks.constants'; import { PipelineTaskLogsService } from './pipeline-task-logs.service'; import { PubSubModule } from '../commons/pub-sub/pub-sub.module'; +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { PipelineTaskRunner } from './pipeline-task.runner'; +import { spawn } from 'child_process'; @Module({ imports: [ @@ -21,12 +25,48 @@ import { PubSubModule } from '../commons/pub-sub/pub-sub.module'; PubSubModule.forFeature(), RedisModule, ReposModule, + + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + uri: configService.get('db.rabbitmq.uri'), + exchanges: [ + { + name: 'new-pipeline-task', + type: 'fanout', + options: { + durable: true, + }, + }, + { + name: 'stop-pipeline-task', + type: 'fanout', + options: { + durable: true, + }, + }, + { + name: 'update-pipeline-task', + type: 'fanout', + options: { + durable: false, + }, + }, + ], + }), + inject: [ConfigService], + }), ], providers: [ PipelineTasksService, PipelineTasksResolver, PipelineTaskConsumer, PipelineTaskLogsService, + PipelineTaskRunner, + { + provide: 'spawn', + useValue: spawn, + }, ], exports: [PipelineTasksService], }) diff --git a/src/pipeline-tasks/pipeline-tasks.resolver.ts b/src/pipeline-tasks/pipeline-tasks.resolver.ts index 8c80313..3d2757c 100644 --- a/src/pipeline-tasks/pipeline-tasks.resolver.ts +++ b/src/pipeline-tasks/pipeline-tasks.resolver.ts @@ -46,7 +46,12 @@ export class PipelineTasksResolver { } @Query(() => PipelineTask) - async findPipelineTask(@Args('id') id: string) { + async pipelineTask(@Args('id') id: string) { return await this.service.findTaskById(id); } + + @Mutation(() => Boolean) + async stopPipelineTask(@Args('id') id: string) { + const task = await this.service.findTaskById(id); + } } diff --git a/src/pipeline-tasks/pipeline-tasks.service.ts b/src/pipeline-tasks/pipeline-tasks.service.ts index 472f069..8b51296 100644 --- a/src/pipeline-tasks/pipeline-tasks.service.ts +++ b/src/pipeline-tasks/pipeline-tasks.service.ts @@ -15,6 +15,7 @@ import debug from 'debug'; import { InjectPubSub } from '../commons/pub-sub/decorators/inject-pub-sub.decorator'; import { PubSub } from '../commons/pub-sub/pub-sub'; import { observableToAsyncIterable } from '@graphql-tools/utils'; +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; const log = debug('fennec:pipeline-tasks:service'); @@ -30,37 +31,29 @@ export class PipelineTasksService { private readonly redis: RedisService, @InjectPubSub() private readonly pubSub: PubSub, + private readonly amqpConnection: AmqpConnection, ) {} async addTask(dto: CreatePipelineTaskInput) { const pipeline = await this.pipelineRepository.findOneOrFail({ where: { id: dto.pipelineId }, relations: ['project'], }); - const hasUnfinishedTask = await this.repository - .findOne({ - pipelineId: dto.pipelineId, - commit: dto.commit, - status: In([TaskStatuses.pending, TaskStatuses.working]), - }) - .then((val) => !isNil(val)); - if (hasUnfinishedTask) { - throw new ConflictException( - 'There are the same tasks among the unfinished tasks!', - ); - } + // const hasUnfinishedTask = await this.repository + // .findOne({ + // pipelineId: dto.pipelineId, + // commit: dto.commit, + // status: In([TaskStatuses.pending, TaskStatuses.working]), + // }) + // .then((val) => !isNil(val)); + // if (hasUnfinishedTask) { + // throw new ConflictException( + // 'There are the same tasks among the unfinished tasks!', + // ); + // } const task = await this.repository.save(this.repository.create(dto)); task.pipeline = pipeline; - const tasksKey = this.getRedisTokens(pipeline)[1]; - const redis = this.redis.getClient(); - await redis.lpush(tasksKey, JSON.stringify(task)); - log( - 'add task %s:%s-%s', - task.id, - task.pipeline.branch, - task.commit.slice(0, 6), - ); - await this.doNextTask(pipeline); + this.amqpConnection.publish('new-pipeline-task', 'mac', task); return task; } diff --git a/src/repos/repos.service.ts b/src/repos/repos.service.ts index 7567664..1ffad98 100644 --- a/src/repos/repos.service.ts +++ b/src/repos/repos.service.ts @@ -105,4 +105,10 @@ export class ReposService { encodeURIComponent(`${task.pipeline.name}-${task.commit}`), ); } + + async checkout4Task(task: PipelineTask): Promise { + const path = this.getWorkspaceRootByTask(task); + await this.checkout(task, path); + return path; + } }