diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0b2b8ad3..a3ef4a31 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,7 +21,7 @@ on:
required: true
env:
- PACKAGE_VERSION: 6.0.${{ github.run_number }}
+ PACKAGE_VERSION: 7.0.${{ github.run_number }}
OCTOPUS_URL: ${{ secrets.OCTOPUS_URL }}
OCTOPUS_API_KEY: ${{ secrets.INTEGRATIONS_API_KEY }}
@@ -36,7 +36,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
- node-version: "16"
+ node-version: "20"
cache: "npm"
- name: Build
@@ -44,7 +44,7 @@ jobs:
run: |
npm ci
npm run build -- --extensionVersion $PACKAGE_VERSION
- echo "::set-output name=package_version::$PACKAGE_VERSION"
+ echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: dist
@@ -66,7 +66,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
- node-version: "16"
+ node-version: "20"
cache: "npm"
- name: Test
@@ -96,7 +96,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
- node-version: "16"
+ node-version: "20"
cache: "npm"
- name: Install Go
@@ -135,7 +135,7 @@ jobs:
echo "::debug::${{github.event_name}}"
OUTPUT_FILE="release_notes.txt"
jq --raw-output '.release.body' ${{ github.event_path }} | sed 's#\r# #g' > $OUTPUT_FILE
- echo "::set-output name=release-note-file::$OUTPUT_FILE"
+ echo "release-note-file=$OUTPUT_FILE" >> $GITHUB_OUTPUT
- name: Create a release in Octopus Deploy 🐙
uses: OctopusDeploy/create-release-action@v3
diff --git a/README.md b/README.md
index 048b62de..78380198 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,10 @@ Microsoft TFS/ADO web extensions are powered by Node.js under the hood. Simply o
### Prerequisites
-* Node.js 10.15.3 (LTS) (`choco install nodejs` or `brew install node@10` or [web](https://nodejs.org))
+* Node.js (`choco install nodejs --version="20.19.4"` or `nvm install 20` or [web](https://nodejs.org))
+ * Pre-v6 tasks: 10.15.3 (EOL)
+ * v6 tasks: 16.20.2 (EOL)
+ * v7 tasks: 20.19.4 (LTS)
* NPM: 5.6.0+ (`npm install npm@latest -g`)
* TFX (`npm install tfx-cli -g`)
* Install golang (`choco install golang` or `brew install go` or [web](https://golang.org))
diff --git a/package-lock.json b/package-lock.json
index b49d3acc..b5302c84 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,8 +7,8 @@
"dependencies": {
"@octopusdeploy/api-client": "^3.7.0",
"azure-devops-node-api": "11.2.0",
- "azure-pipelines-task-lib": "3.3.1",
- "azure-pipelines-tool-lib": "1.3.2",
+ "azure-pipelines-task-lib": "^4.13.0",
+ "azure-pipelines-tool-lib": "^2.0.7",
"command-line-args": "^5.2.1",
"fp-ts": "1.19.5",
"glob": "7.2.0",
@@ -26,7 +26,7 @@
"@types/express": "^4.17.13",
"@types/glob": "7.2.0",
"@types/jest": "28.1.5",
- "@types/node": "^18.0.4",
+ "@types/node": "^20.3.1",
"@types/ramda": "0.28.15",
"@types/test-console": "^2.0.0",
"@types/uuid": "8.3.4",
@@ -47,7 +47,7 @@
"test-console": "^2.0.0",
"ts-jest": "28.0.5",
"ts-node": "^10.9.0",
- "typescript": "^4.7.4",
+ "typescript": "5.1.6",
"yargs": "^17.5.1"
}
},
@@ -1296,14 +1296,6 @@
"integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==",
"dev": true
},
- "node_modules/@types/concat-stream": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
- "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -1336,14 +1328,6 @@
"@types/range-parser": "*"
}
},
- "node_modules/@types/form-data": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
- "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -1442,9 +1426,13 @@
"integrity": "sha512-a2yhRIADupQfOFM75v7GfcQQLUxU705+i/xcZ3N/3PK3Xdo31SUfuCUByWPGOHB1e38m7MxTx/D8FPVsJXZKJw=="
},
"node_modules/@types/node": {
- "version": "18.0.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.4.tgz",
- "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA=="
+ "version": "20.19.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
+ "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
},
"node_modules/@types/prettier": {
"version": "2.6.3",
@@ -1460,7 +1448,8 @@
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
- "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
},
"node_modules/@types/ramda": {
"version": "0.28.15",
@@ -1766,6 +1755,17 @@
"node": ">=6.0"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1970,11 +1970,6 @@
"node": ">=8"
}
},
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
- },
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
@@ -2007,16 +2002,16 @@
}
},
"node_modules/azure-pipelines-task-lib": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-3.3.1.tgz",
- "integrity": "sha512-56ZAr4MHIoa24VNVuwPL4iUQ5MKaigPoYXkBG8E8fiVmh8yZdatUo25meNoQwg77vDY22F63Q44UzXoMWmy7ag==",
+ "version": "4.17.3",
+ "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.17.3.tgz",
+ "integrity": "sha512-UxfH5pk3uOHTi9TtLtdDyugQVkFES5A836ZEePjcs3jYyxm3EJ6IlFYq6gbfd6mNBhrM9fxG2u/MFYIJ+Z0cxQ==",
"dependencies": {
+ "adm-zip": "^0.5.10",
"minimatch": "3.0.5",
- "mockery": "^1.7.0",
+ "nodejs-file-downloader": "^4.11.1",
"q": "^1.5.1",
- "semver": "^5.1.0",
+ "semver": "^5.7.2",
"shelljs": "^0.8.5",
- "sync-request": "6.1.0",
"uuid": "^3.0.1"
}
},
@@ -2032,9 +2027,9 @@
}
},
"node_modules/azure-pipelines-task-lib/node_modules/semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
@@ -2049,13 +2044,13 @@
}
},
"node_modules/azure-pipelines-tool-lib": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/azure-pipelines-tool-lib/-/azure-pipelines-tool-lib-1.3.2.tgz",
- "integrity": "sha512-PtYcd3E2ouwZhLuaOpWA00FYoLjRuJs1V8mNu3u6lBnqeYd4jh/8VL/of6nchm8f2NM6Div+EEnbOcmWvcptPg==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/azure-pipelines-tool-lib/-/azure-pipelines-tool-lib-2.0.8.tgz",
+ "integrity": "sha512-yCFxJfZeNPUDCi7dbmiqVvq5lFpZdqB9kzr/wB9sZuE0RvUEhBF51gtzdR9cI5+NOsfkAVWwQJVWvdGQR5I3Wg==",
"dependencies": {
"@types/semver": "^5.3.0",
"@types/uuid": "^3.4.5",
- "azure-pipelines-task-lib": "^3.1.10",
+ "azure-pipelines-task-lib": "^4.1.0",
"semver": "^5.7.0",
"semver-compare": "^1.0.0",
"typed-rest-client": "^1.8.6",
@@ -2356,7 +2351,8 @@
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
},
"node_modules/bytes": {
"version": "3.1.2",
@@ -2426,11 +2422,6 @@
}
]
},
- "node_modules/caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
- },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2558,47 +2549,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
- "node_modules/concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "engines": [
- "node >= 0.8"
- ],
- "dependencies": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
- },
- "node_modules/concat-stream/node_modules/readable-stream": {
- "version": "2.3.7",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
- "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "node_modules/concat-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
- "node_modules/concat-stream/node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -2653,7 +2603,8 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
},
"node_modules/crc-32": {
"version": "1.2.2",
@@ -2704,7 +2655,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -3997,14 +3947,6 @@
"node": ">=8.0.0"
}
},
- "node_modules/get-port": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
- "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4179,20 +4121,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
- "node_modules/http-basic": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
- "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
- "dependencies": {
- "caseless": "^0.12.0",
- "concat-stream": "^1.6.2",
- "http-response-object": "^3.0.1",
- "parse-cache-control": "^1.0.1"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -4209,19 +4137,18 @@
"node": ">= 0.8"
}
},
- "node_modules/http-response-object": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
- "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
- "@types/node": "^10.0.3"
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
- "node_modules/http-response-object/node_modules/@types/node": {
- "version": "10.17.60",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
- "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
- },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -4427,7 +4354,8 @@
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
},
"node_modules/isexe": {
"version": "2.0.0",
@@ -5497,16 +5425,10 @@
"node": ">=10"
}
},
- "node_modules/mockery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz",
- "integrity": "sha512-gUQA33ayi0tuAhr/rJNZPr7Q7uvlBt4gyJPbi0CDcAfIzIrDu1YgGMFgmAu3stJqBpK57m7+RxUbcS+pt59fKQ=="
- },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/natural-compare": {
"version": "1.4.0",
@@ -5535,6 +5457,17 @@
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
"dev": true
},
+ "node_modules/nodejs-file-downloader": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz",
+ "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "https-proxy-agent": "^5.0.0",
+ "mime-types": "^2.1.27",
+ "sanitize-filename": "^1.6.3"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5679,11 +5612,6 @@
"node": ">=6"
}
},
- "node_modules/parse-cache-control": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
- "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
- },
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -5862,15 +5790,8 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
- },
- "node_modules/promise": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
- "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
- "dependencies": {
- "asap": "~2.0.6"
- }
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
},
"node_modules/prompts": {
"version": "2.4.2",
@@ -6220,6 +6141,14 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "node_modules/sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -6569,27 +6498,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/sync-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
- "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
- "dependencies": {
- "http-response-object": "^3.0.1",
- "sync-rpc": "^1.2.1",
- "then-request": "^6.0.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/sync-rpc": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
- "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
- "dependencies": {
- "get-port": "^3.1.0"
- }
- },
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -6668,45 +6576,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
- "node_modules/then-request": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
- "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
- "dependencies": {
- "@types/concat-stream": "^1.6.0",
- "@types/form-data": "0.0.33",
- "@types/node": "^8.0.0",
- "@types/qs": "^6.2.31",
- "caseless": "~0.12.0",
- "concat-stream": "^1.6.0",
- "form-data": "^2.2.0",
- "http-basic": "^8.1.1",
- "http-response-object": "^3.0.1",
- "promise": "^8.0.0",
- "qs": "^6.4.0"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/then-request/node_modules/@types/node": {
- "version": "8.10.66",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
- "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
- },
- "node_modules/then-request/node_modules/form-data": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
- "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 0.12"
- }
- },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -6743,6 +6612,14 @@
"node": ">=0.6"
}
},
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
"node_modules/ts-jest": {
"version": "28.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
@@ -6925,22 +6802,17 @@
"underscore": "^1.12.1"
}
},
- "node_modules/typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
- },
"node_modules/typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/typical": {
@@ -6956,6 +6828,12 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz",
"integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ=="
},
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -7005,10 +6883,16 @@
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="
},
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
},
"node_modules/utils-merge": {
"version": "1.0.1",
@@ -8230,14 +8114,6 @@
"integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==",
"dev": true
},
- "@types/concat-stream": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
- "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
- "requires": {
- "@types/node": "*"
- }
- },
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -8270,14 +8146,6 @@
"@types/range-parser": "*"
}
},
- "@types/form-data": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
- "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
- "requires": {
- "@types/node": "*"
- }
- },
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -8376,9 +8244,13 @@
"integrity": "sha512-a2yhRIADupQfOFM75v7GfcQQLUxU705+i/xcZ3N/3PK3Xdo31SUfuCUByWPGOHB1e38m7MxTx/D8FPVsJXZKJw=="
},
"@types/node": {
- "version": "18.0.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.4.tgz",
- "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA=="
+ "version": "20.19.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
+ "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~6.21.0"
+ }
},
"@types/prettier": {
"version": "2.6.3",
@@ -8394,7 +8266,8 @@
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
- "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
},
"@types/ramda": {
"version": "0.28.15",
@@ -8597,6 +8470,14 @@
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ=="
},
+ "agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "requires": {
+ "debug": "4"
+ }
+ },
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -8759,11 +8640,6 @@
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
- "asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
- },
"async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
@@ -8795,16 +8671,16 @@
}
},
"azure-pipelines-task-lib": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-3.3.1.tgz",
- "integrity": "sha512-56ZAr4MHIoa24VNVuwPL4iUQ5MKaigPoYXkBG8E8fiVmh8yZdatUo25meNoQwg77vDY22F63Q44UzXoMWmy7ag==",
+ "version": "4.17.3",
+ "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.17.3.tgz",
+ "integrity": "sha512-UxfH5pk3uOHTi9TtLtdDyugQVkFES5A836ZEePjcs3jYyxm3EJ6IlFYq6gbfd6mNBhrM9fxG2u/MFYIJ+Z0cxQ==",
"requires": {
+ "adm-zip": "^0.5.10",
"minimatch": "3.0.5",
- "mockery": "^1.7.0",
+ "nodejs-file-downloader": "^4.11.1",
"q": "^1.5.1",
- "semver": "^5.1.0",
+ "semver": "^5.7.2",
"shelljs": "^0.8.5",
- "sync-request": "6.1.0",
"uuid": "^3.0.1"
},
"dependencies": {
@@ -8817,9 +8693,9 @@
}
},
"semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
},
"uuid": {
"version": "3.4.0",
@@ -8829,13 +8705,13 @@
}
},
"azure-pipelines-tool-lib": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/azure-pipelines-tool-lib/-/azure-pipelines-tool-lib-1.3.2.tgz",
- "integrity": "sha512-PtYcd3E2ouwZhLuaOpWA00FYoLjRuJs1V8mNu3u6lBnqeYd4jh/8VL/of6nchm8f2NM6Div+EEnbOcmWvcptPg==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/azure-pipelines-tool-lib/-/azure-pipelines-tool-lib-2.0.8.tgz",
+ "integrity": "sha512-yCFxJfZeNPUDCi7dbmiqVvq5lFpZdqB9kzr/wB9sZuE0RvUEhBF51gtzdR9cI5+NOsfkAVWwQJVWvdGQR5I3Wg==",
"requires": {
"@types/semver": "^5.3.0",
"@types/uuid": "^3.4.5",
- "azure-pipelines-task-lib": "^3.1.10",
+ "azure-pipelines-task-lib": "^4.1.0",
"semver": "^5.7.0",
"semver-compare": "^1.0.0",
"typed-rest-client": "^1.8.6",
@@ -9055,7 +8931,8 @@
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
},
"bytes": {
"version": "3.1.2",
@@ -9099,11 +8976,6 @@
"integrity": "sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==",
"dev": true
},
- "caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
- },
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -9206,46 +9078,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
- "concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "requires": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- },
- "dependencies": {
- "readable-stream": {
- "version": "2.3.7",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
- "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- }
- }
- },
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -9293,7 +9125,8 @@
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
},
"crc-32": {
"version": "1.2.2",
@@ -9332,7 +9165,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -10206,11 +10038,6 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
- "get-port": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
- "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg=="
- },
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -10323,17 +10150,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
- "http-basic": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
- "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
- "requires": {
- "caseless": "^0.12.0",
- "concat-stream": "^1.6.2",
- "http-response-object": "^3.0.1",
- "parse-cache-control": "^1.0.1"
- }
- },
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -10347,19 +10163,13 @@
"toidentifier": "1.0.1"
}
},
- "http-response-object": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
- "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
+ "https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"requires": {
- "@types/node": "^10.0.3"
- },
- "dependencies": {
- "@types/node": {
- "version": "10.17.60",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
- "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
- }
+ "agent-base": "6",
+ "debug": "4"
}
},
"human-signals": {
@@ -10496,7 +10306,8 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
},
"isexe": {
"version": "2.0.0",
@@ -11332,16 +11143,10 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
- "mockery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz",
- "integrity": "sha512-gUQA33ayi0tuAhr/rJNZPr7Q7uvlBt4gyJPbi0CDcAfIzIrDu1YgGMFgmAu3stJqBpK57m7+RxUbcS+pt59fKQ=="
- },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"natural-compare": {
"version": "1.4.0",
@@ -11367,6 +11172,17 @@
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
"dev": true
},
+ "nodejs-file-downloader": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz",
+ "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==",
+ "requires": {
+ "follow-redirects": "^1.15.6",
+ "https-proxy-agent": "^5.0.0",
+ "mime-types": "^2.1.27",
+ "sanitize-filename": "^1.6.3"
+ }
+ },
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -11471,11 +11287,6 @@
"callsites": "^3.0.0"
}
},
- "parse-cache-control": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
- "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
- },
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -11599,15 +11410,8 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
- },
- "promise": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
- "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
- "requires": {
- "asap": "~2.0.6"
- }
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
},
"prompts": {
"version": "2.4.2",
@@ -11839,6 +11643,14 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "requires": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -12115,24 +11927,6 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
- "sync-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
- "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
- "requires": {
- "http-response-object": "^3.0.1",
- "sync-rpc": "^1.2.1",
- "then-request": "^6.0.0"
- }
- },
- "sync-rpc": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
- "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
- "requires": {
- "get-port": "^3.1.0"
- }
- },
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -12195,41 +11989,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
- "then-request": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
- "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
- "requires": {
- "@types/concat-stream": "^1.6.0",
- "@types/form-data": "0.0.33",
- "@types/node": "^8.0.0",
- "@types/qs": "^6.2.31",
- "caseless": "~0.12.0",
- "concat-stream": "^1.6.0",
- "form-data": "^2.2.0",
- "http-basic": "^8.1.1",
- "http-response-object": "^3.0.1",
- "promise": "^8.0.0",
- "qs": "^6.4.0"
- },
- "dependencies": {
- "@types/node": {
- "version": "8.10.66",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
- "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
- },
- "form-data": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
- "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
- }
- }
- }
- },
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -12257,6 +12016,14 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
+ "truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "requires": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
"ts-jest": {
"version": "28.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
@@ -12369,15 +12136,10 @@
"underscore": "^1.12.1"
}
},
- "typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
- },
"typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true
},
"typical": {
@@ -12390,6 +12152,12 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz",
"integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ=="
},
+ "undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -12420,10 +12188,16 @@
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="
},
+ "utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
},
"utils-merge": {
"version": "1.0.1",
diff --git a/package.json b/package.json
index d0f0fc40..335dec7b 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"@types/express": "^4.17.13",
"@types/glob": "7.2.0",
"@types/jest": "28.1.5",
- "@types/node": "^18.0.4",
+ "@types/node": "^20.3.1",
"@types/ramda": "0.28.15",
"@types/test-console": "^2.0.0",
"@types/uuid": "8.3.4",
@@ -34,14 +34,14 @@
"test-console": "^2.0.0",
"ts-jest": "28.0.5",
"ts-node": "^10.9.0",
- "typescript": "^4.7.4",
+ "typescript": "5.1.6",
"yargs": "^17.5.1"
},
"dependencies": {
"@octopusdeploy/api-client": "^3.7.0",
"azure-devops-node-api": "11.2.0",
- "azure-pipelines-task-lib": "3.3.1",
- "azure-pipelines-tool-lib": "1.3.2",
+ "azure-pipelines-task-lib": "^4.13.0",
+ "azure-pipelines-tool-lib": "^2.0.7",
"command-line-args": "^5.2.1",
"fp-ts": "1.19.5",
"glob": "7.2.0",
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/icon.png b/source/tasks/AwaitTask/AwaitTaskV7/icon.png
new file mode 100644
index 00000000..e1daf69d
Binary files /dev/null and b/source/tasks/AwaitTask/AwaitTaskV7/icon.png differ
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/icon.svg b/source/tasks/AwaitTask/AwaitTaskV7/icon.svg
new file mode 100644
index 00000000..8b1a306e
--- /dev/null
+++ b/source/tasks/AwaitTask/AwaitTaskV7/icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/index.ts b/source/tasks/AwaitTask/AwaitTaskV7/index.ts
new file mode 100644
index 00000000..50922d3d
--- /dev/null
+++ b/source/tasks/AwaitTask/AwaitTaskV7/index.ts
@@ -0,0 +1,26 @@
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+import { Waiter } from "./waiter";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+
+new Waiter(connection, task, logger).run();
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/input-parameters.ts b/source/tasks/AwaitTask/AwaitTaskV7/input-parameters.ts
new file mode 100644
index 00000000..9bea618d
--- /dev/null
+++ b/source/tasks/AwaitTask/AwaitTaskV7/input-parameters.ts
@@ -0,0 +1,76 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { WaitExecutionResult } from "./waiter";
+
+export interface InputParameters {
+ space: string;
+ step: string;
+ tasks: WaitExecutionResult[];
+ pollingInterval: number;
+ timeout: number;
+ showProgress: boolean;
+ cancelOnTimeout: boolean;
+}
+
+export function getInputParameters(logger: Logger, task: TaskWrapper): InputParameters {
+ const space = task.getInput("Space");
+ if (!space) {
+ throw new Error("Failed to successfully build parameters: space name is required.");
+ }
+
+ const step = task.getInput("Step");
+ if (!step) {
+ throw new Error("Failed to successfully build parameters: step name is required.");
+ }
+
+ const taskJson = task.getOutputVariable(step, "server_tasks");
+ if (taskJson === undefined) {
+ throw new Error(`Failed to successfully build parameters: cannot find '${step}.server_tasks' variable from execution step`);
+ }
+ const tasks = JSON.parse(taskJson);
+ if (!Array.isArray(tasks)) {
+ throw new Error(`Failed to successfully build parameters: '${step}.server_tasks' variable from execution step is not an array`);
+ }
+
+ let pollingInterval = 10;
+ const pollingIntervalField = task.getInput("PollingInterval");
+ if (pollingIntervalField) {
+ pollingInterval = +pollingIntervalField;
+ }
+
+ let timeoutSeconds = 600;
+ const timeoutField = task.getInput("TimeoutAfter");
+ if (timeoutField) {
+ timeoutSeconds = +timeoutField;
+ }
+
+ const showProgress = task.getBoolean("ShowProgress") ?? false;
+ if (showProgress && tasks.length > 1) {
+ throw new Error("Failed to successfully build parameters: ShowProgress can only be enabled when waiting for a single task");
+ }
+
+ const cancelOnTimeout = task.getBoolean("CancelOnTimeout") ?? false;
+
+ const parameters: InputParameters = {
+ space: task.getInput("Space") || "",
+ step: step,
+ tasks: tasks,
+ showProgress: showProgress,
+ pollingInterval: pollingInterval,
+ timeout: timeoutSeconds,
+ cancelOnTimeout: cancelOnTimeout
+ };
+
+ const errors: string[] = [];
+ if (parameters.space === "") {
+ errors.push("The Octopus space name is required.");
+ }
+
+ if (errors.length > 0) {
+ throw new Error("Failed to successfully build parameters.\n" + errors.join("\n"));
+ }
+
+ logger.debug?.(`Tasks: \n${JSON.stringify(parameters, null, 2)}`);
+
+ return parameters;
+}
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/task.json b/source/tasks/AwaitTask/AwaitTaskV7/task.json
new file mode 100644
index 00000000..6e333361
--- /dev/null
+++ b/source/tasks/AwaitTask/AwaitTaskV7/task.json
@@ -0,0 +1,94 @@
+{
+ "id": "38df691d-23eb-48d4-8638-61764f48bacb",
+ "name": "OctopusAwaitTask",
+ "friendlyName": "Await Octopus Task Completion",
+ "description": "Await the completion of a execution task",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Deploy",
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "Step",
+ "type": "string",
+ "label": "Step",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The name of the step that queued the deployment/runbook run. You will need to set a output variable reference name on the source step and use that name here."
+ },
+ {
+ "name": "PollingInterval",
+ "type": "int",
+ "label": "Polling Interval",
+ "defaultValue": "10",
+ "required": false,
+ "helpMarkDown": "How frequently, in seconds, to check the status. (Default: 10s)"
+ },
+ {
+ "name": "TimeoutAfter",
+ "type": "int",
+ "label": "Timeout After",
+ "defaultValue": "600",
+ "required": false,
+ "helpMarkDown": "Duration, in seconds, to allow for completion before timing out. (Default: 600s)"
+ },
+ {
+ "name": "ShowProgress",
+ "type": "boolean",
+ "label": "Show Progress",
+ "defaultValue": "false",
+ "required": false,
+ "helpMarkDown": "Log Octopus task outputs to Azure DevOps output. (Default: false)"
+ },
+ {
+ "name": "CancelOnTimeout",
+ "type": "boolean",
+ "label": "Cancel Task on Timeout",
+ "defaultValue": "false",
+ "required": false,
+ "helpMarkDown": "Cancel the Octopus task and mark this task as failed if the timeout is reached. (Default: false)"
+ }
+ ],
+ "outputVariables": [
+ {
+ "name": "completed_successfully",
+ "description": "Whether the task(s) completed successfully or not. This will only be true if all tasks succeeded, and false if any tasks failed."
+ },
+ {
+ "name": "server_task_results",
+ "description": "JSON representation of all tasks results. { \"serverTaskId\": , \"tenantName\": , \"environmentName\": , \"successful\": }"
+ }
+ ],
+ "instanceNameFormat": "Await Octopus Deploy Task",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/AwaitTask/AwaitTaskV7/waiter.ts b/source/tasks/AwaitTask/AwaitTaskV7/waiter.ts
new file mode 100644
index 00000000..501512a4
--- /dev/null
+++ b/source/tasks/AwaitTask/AwaitTaskV7/waiter.ts
@@ -0,0 +1,207 @@
+import { ActivityElement, ActivityLogEntryCategory, ActivityStatus, Client, Logger, ServerTaskWaiter, SpaceRepository, SpaceServerTaskRepository, TaskState } from "@octopusdeploy/api-client";
+import { getDeepLink, OctoServerConnectionDetails } from "tasks/Utils/connection";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { getInputParameters, InputParameters } from "./input-parameters";
+import { ExecutionResult } from "../../Utils/executionResult";
+import { getClient } from "../../Utils/client";
+
+export interface WaitExecutionResult extends ExecutionResult {
+ successful: boolean;
+}
+
+export class Waiter {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {}
+
+ public async run() {
+ try {
+ const inputParameters = getInputParameters(this.logger, this.task);
+
+ const client = await getClient(this.connection, this.logger, "task", "wait", 6);
+
+ const waitExecutionResults = inputParameters.showProgress
+ ? await this.waitWithProgress(client, inputParameters)
+ : await this.waitWithoutProgress(client, inputParameters);
+
+ const url = this.connection.url;
+ const spaceId = await this.getSpaceId(client, inputParameters.space);
+ let failedDeploymentsCount = 0;
+ waitExecutionResults.map((r) => {
+ const link = getDeepLink(url, `${spaceId}/tasks/${r.serverTaskId}`);
+ const context = this.getContext(r);
+ if (r.successful) {
+ this.logger.info?.(`Succeeded: ${link}`);
+ } else {
+ this.logger.warn?.(`Failed: ${link}`);
+ failedDeploymentsCount++;
+ }
+ this.task.setOutputVariable(`${context}.completed_successfully`, r.successful.toString());
+ });
+
+ if (failedDeploymentsCount > 0) {
+ this.task.setFailure(`${failedDeploymentsCount} ${failedDeploymentsCount == 1 ? "task" : "tasks"} failed.`);
+ this.task.setOutputVariable("completed_successfully", "false");
+ } else {
+ this.task.setSuccess("All tasks completed successfully");
+ this.task.setOutputVariable("completed_successfully", "true");
+ }
+
+ this.task.setOutputVariable("server_task_results", JSON.stringify(waitExecutionResults));
+
+ } catch (error) {
+ if (error instanceof Error && error.message.includes("Timeout reached") && error.message.includes("cancelled")) {
+ this.task.setFailure(error.message);
+ this.task.setOutputVariable("completed_successfully", "false");
+ return;
+ }
+
+ this.task.setFailure(`Failed to wait for tasks: ${error}`);
+ this.task.setOutputVariable("completed_successfully", "false");
+ }
+ }
+
+ async getSpaceId(client: Client, spaceName: string): Promise {
+ const spaceRepository = new SpaceRepository(client);
+ const spaceList = await spaceRepository.list({ partialName: spaceName });
+ const matches = spaceList.Items.filter((s) => s.Name.localeCompare(spaceName) === 0);
+ return matches.length > 0 ? matches[0].Id : undefined;
+ }
+
+ getContext(result: WaitExecutionResult): string {
+ return result.tenantName ? result.tenantName.replace(" ", "_") : result.environmentName.replace(" ", "_");
+ }
+
+ async waitWithoutProgress(client: Client, inputParameters: InputParameters): Promise {
+ const waiter = new ServerTaskWaiter(client, inputParameters.space);
+ const taskIds = inputParameters.tasks.map((t) => t.serverTaskId);
+ const lookup = new Map(inputParameters.tasks.map((t) => [t.serverTaskId, t]));
+
+ const results: WaitExecutionResult[] = [];
+
+ await waiter.waitForServerTasksToComplete(taskIds, inputParameters.pollingInterval * 1000, inputParameters.timeout * 1000, (t) => {
+ const taskResult = lookup.get(t.Id);
+ if (!taskResult) return;
+ const context = this.getProgressContext(taskResult);
+
+ if (!t.IsCompleted) {
+ this.logger.info?.(`${taskResult.type}${context} is '${t.State}'`);
+ return;
+ }
+
+ this.logger.info?.(`${taskResult.type}${context} ${t.State === TaskState.Success ? "completed successfully" : "did not complete successfully"}`);
+ taskResult.successful = t.State == TaskState.Success;
+ results.push(taskResult);
+ },
+ inputParameters.cancelOnTimeout);
+
+ return results;
+ }
+
+ async waitWithProgress(client: Client, inputParameters: InputParameters): Promise {
+ const waiter = new ServerTaskWaiter(client, inputParameters.space);
+ const taskIds = inputParameters.tasks.map((t) => t.serverTaskId);
+ const taskLookup = new Map(inputParameters.tasks.map((t) => [t.serverTaskId, t]));
+
+ const taskRepository = new SpaceServerTaskRepository(client, inputParameters.space);
+ const loggedChildTaskIds: string[] = [];
+ const lastTaskUpdate: { [taskId: string]: string } = {};
+
+ const results: WaitExecutionResult[] = [];
+
+ const promises: Promise[] = [];
+ await waiter.waitForServerTasksToComplete(taskIds, inputParameters.pollingInterval * 1000, inputParameters.timeout * 1000, (t) => {
+ const taskResult = taskLookup.get(t.Id);
+ if (!taskResult) return;
+
+ const taskUpdate = `${taskResult.type}${this.getProgressContext(taskResult)} is '${t.State}'`;
+ if (loggedChildTaskIds.length == 0 && lastTaskUpdate[taskResult.serverTaskId] !== taskUpdate) {
+ // Log top level updates until we have details, don't log them again
+ this.logger.info?.(taskUpdate);
+ lastTaskUpdate[taskResult.serverTaskId] = taskUpdate;
+ }
+
+ // Log details of the task
+ const promise = this.printTaskDetails(taskRepository, taskResult, loggedChildTaskIds, results);
+ promises.push(promise);
+ },
+ inputParameters.cancelOnTimeout);
+
+ await Promise.all(promises);
+ return results;
+ }
+
+ async printTaskDetails(repository: SpaceServerTaskRepository, task: WaitExecutionResult, loggedChildTaskIds: string[], results: WaitExecutionResult[]): Promise {
+ try {
+ this.logger.debug?.(`Fetching details on ${task.serverTaskId}`);
+ const taskDetails = await repository.getDetails(task.serverTaskId);
+ this.logger.debug?.(`Fetched details on ${task.serverTaskId}: ${JSON.stringify(taskDetails)}`);
+
+ const activities = taskDetails.ActivityLogs.flatMap((parentActivity) => parentActivity.Children.filter(isComplete).filter((activity) => !loggedChildTaskIds.includes(activity.Id)));
+
+ for (const activity of activities) {
+ this.logWithStatus(`\t${activity.Status}: ${activity.Name}`, activity.Status);
+
+ if (activity.Started && activity.Ended) {
+ const startTime = new Date(activity.Started);
+ const endTime = new Date(activity.Ended);
+ const duration = (endTime.getTime() - startTime.getTime()) / 1000;
+ this.logger.info?.(`\t\t\t---------------------------------`);
+ this.logger.info?.(`\t\t\tStarted: \t${activity.Started}\n\t\t\tEnded: \t${activity.Ended}\n\t\t\tDuration:\t${duration.toFixed(1)}s`);
+ this.logger.info?.(`\t\t\t---------------------------------`);
+ }
+
+ activity.Children.filter(isComplete)
+ .flatMap((child) => child.LogElements)
+ .forEach((log) => {
+ this.logWithCategory(`\t\t${log.OccurredAt}: ${log.MessageText}`, log.Category);
+ log.Detail && this.logger.debug?.(log.Detail);
+ });
+
+ loggedChildTaskIds.push(activity.Id);
+ }
+ if (!taskDetails.Task.IsCompleted) return;
+
+ const message = taskDetails.Task.State === TaskState.Success ? "completed successfully" : "did not complete successfully";
+ this.logger.info?.(`${task.type}${this.getProgressContext(task)} ${message}`);
+ task.successful = taskDetails.Task.State === TaskState.Success;
+ results.push(task);
+ } catch (e) {
+ const error = e instanceof Error ? e : undefined;
+ this.logger.error?.(`Failed to fetch details on ${task}: ${e}`, error);
+ }
+ }
+
+ getProgressContext(task: WaitExecutionResult): string {
+ return `${task.environmentName ? ` to environment '${task.environmentName}'` : ""}${task.tenantName ? ` for tenant '${task.tenantName}'` : ""}`;
+ }
+
+ logWithCategory(message: string, category?: ActivityLogEntryCategory) {
+ switch (category) {
+ case "Error":
+ case "Fatal":
+ this.logger.error?.(message, undefined);
+ break;
+ case "Warning":
+ this.logger.warn?.(message);
+ break;
+ default:
+ this.logger.info?.(message);
+ }
+ }
+
+ logWithStatus(message: string, status?: ActivityStatus) {
+ switch (status) {
+ case "Failed":
+ this.logger.error?.(message, undefined);
+ break;
+ case "SuccessWithWarning":
+ this.logger.warn?.(message);
+ break;
+ default:
+ this.logger.info?.(message);
+ }
+ }
+}
+
+function isComplete(element: ActivityElement) {
+ return element.Status != "Pending" && element.Status != "Running";
+}
diff --git a/source/tasks/BuildInformation/BuildInformationV7/buildInformation.ts b/source/tasks/BuildInformation/BuildInformationV7/buildInformation.ts
new file mode 100644
index 00000000..640c2dcd
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/buildInformation.ts
@@ -0,0 +1,21 @@
+import { OctoServerConnectionDetails } from "../../Utils/connection";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import { BuildInformationRepository, Logger } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { getOverwriteMode } from "./overwriteMode";
+import { IVstsHelper } from "./vsts";
+import { getClient } from "../../Utils/client";
+
+export class BuildInformation {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly logger: Logger, readonly task: TaskWrapper, readonly vsts: IVstsHelper) {}
+
+ public async run() {
+ const command = await createCommandFromInputs(this.logger, this.task, this.vsts);
+ const client = await getClient(this.connection, this.logger, "build-information", "push", 6)
+
+ const overwriteMode = await getOverwriteMode(this.logger, this.task);
+ this.logger.debug?.(`Build Information:\n${JSON.stringify(command, null, 2)}`);
+ const repository = new BuildInformationRepository(client, command.spaceName);
+ await repository.push(command, overwriteMode);
+ }
+}
diff --git a/source/tasks/BuildInformation/BuildInformationV7/icon.png b/source/tasks/BuildInformation/BuildInformationV7/icon.png
new file mode 100644
index 00000000..55087619
Binary files /dev/null and b/source/tasks/BuildInformation/BuildInformationV7/icon.png differ
diff --git a/source/tasks/BuildInformation/BuildInformationV7/icon.svg b/source/tasks/BuildInformation/BuildInformationV7/icon.svg
new file mode 100644
index 00000000..9b9c1c04
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/source/tasks/BuildInformation/BuildInformationV7/index.ts b/source/tasks/BuildInformation/BuildInformationV7/index.ts
new file mode 100644
index 00000000..8e22177e
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/index.ts
@@ -0,0 +1,28 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { getDefaultOctopusConnectionDetailsOrThrow } from "tasks/Utils/connection";
+import * as tasks from "azure-pipelines-task-lib/task";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { BuildInformation } from "./buildInformation";
+import { IVstsHelper, VstsHelper } from "./vsts";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+const vsts: IVstsHelper = new VstsHelper(logger);
+
+new BuildInformation(connection, logger, task, vsts).run();
diff --git a/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.test.ts b/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.test.ts
new file mode 100644
index 00000000..26dea624
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.test.ts
@@ -0,0 +1,88 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import { VstsParameters, IVstsHelper } from "./vsts";
+
+class MockVsts implements IVstsHelper {
+ getVsts(_logger: Logger): Promise {
+ const vsts: VstsParameters = {
+ branch: "/refs/head/main",
+ environment: {
+ projectId: "projectId",
+ projectName: "projectName",
+ buildNumber: "buildNumber",
+ buildId: 1234,
+ buildName: "buildName",
+ buildRepositoryName: "buildRepositoryName",
+ releaseName: "releaseName",
+ releaseUri: "releaseUri",
+ releaseId: "releaseId",
+ teamCollectionUri: "http://teamcollectionuri/",
+ defaultWorkingDirectory: "defaultWorkingDirectory",
+ buildRepositoryProvider: "buildRepositoryProvider",
+ buildRepositoryUri: "buildRepositoryUri",
+ buildSourceVersion: "buildSourceVersion",
+ agentBuildDirectory: "agentBuildDirectory",
+ },
+ vcsType: "vcsType",
+ commits: [{ Comment: "commit comment", Id: "commitId" }],
+ };
+
+ return new Promise((resolve) => resolve(vsts));
+ }
+}
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ let vsts: MockVsts;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ vsts = new MockVsts();
+ });
+
+ test("all regular fields supplied", async () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("PackageVersion", "1.2.3");
+ task.addVariableString("PackageIds", "Package1\nPackage2");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("Replace", "true");
+
+ const command = await createCommandFromInputs(logger, task, vsts);
+ expect(command.Packages.length).toBe(2);
+ expect(command.Packages[0].Id).toBe("Package1");
+ expect(command.Packages[0].Version).toBe("1.2.3");
+ expect(command.Packages[1].Id).toBe("Package2");
+ expect(command.Packages[1].Version).toBe("1.2.3");
+ expect(command.Branch).toBe("/refs/head/main");
+ expect(command.BuildEnvironment).toBe("Azure DevOps");
+ expect(command.spaceName).toBe("Default");
+ expect(command.BuildNumber).toBe("buildNumber");
+ expect(command.BuildUrl).toBe("http://teamcollectionuri/projectName/_build/results?buildId=1234");
+ expect(command.Commits.length).toBe(1);
+ expect(command.Commits[0].Id).toBe("commitId");
+ expect(command.VcsCommitNumber).toBe("buildSourceVersion");
+ expect(command.VcsRoot).toBe("buildRepositoryUri");
+ expect(command.VcsType).toBe("vcsType");
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+
+ test("missing parameters", async () => {
+ const t = async () => {
+ await createCommandFromInputs(logger, task, vsts);
+ };
+ await expect(t).rejects.toThrow("Failed to successfully build parameters:\nspace name is required\nmust specify at least one package name");
+ });
+
+ test("missing package version", async () => {
+ const t = async () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("PackageIds", "Package1");
+ await createCommandFromInputs(logger, task, vsts);
+ };
+ await expect(t).rejects.toThrow("Failed to successfully build parameters:\nmust specify a package version number, in SemVer format");
+ });
+});
diff --git a/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.ts b/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.ts
new file mode 100644
index 00000000..eabaa2e4
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/inputCommandBuilder.ts
@@ -0,0 +1,49 @@
+import { CreateOctopusBuildInformationCommand, Logger, PackageIdentity } from "@octopusdeploy/api-client";
+import { getLineSeparatedItems } from "../../Utils/inputs";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { IVstsHelper } from "./vsts";
+
+export async function createCommandFromInputs(logger: Logger, task: TaskWrapper, vstsHelper: IVstsHelper): Promise {
+ const vsts = await vstsHelper.getVsts(logger);
+ const inputPackages = getLineSeparatedItems(task.getInput("PackageIds") || "") || [];
+ logger.debug?.(`PackageIds: ${inputPackages}`);
+ const packages: PackageIdentity[] = [];
+ for (const packageId of inputPackages) {
+ packages.push({
+ Id: packageId,
+ Version: task.getInput("PackageVersion") || "",
+ });
+ }
+
+ const command: CreateOctopusBuildInformationCommand = {
+ spaceName: task.getInput("Space") || "",
+ BuildEnvironment: "Azure DevOps",
+ BuildNumber: vsts.environment.buildNumber,
+ BuildUrl: vsts.environment.teamCollectionUri.replace(/\/$/, "") + "/" + vsts.environment.projectName + "/_build/results?buildId=" + vsts.environment.buildId,
+ Branch: vsts.branch || "",
+ VcsType: vsts.vcsType,
+ VcsRoot: vsts.environment.buildRepositoryUri,
+ VcsCommitNumber: vsts.environment.buildSourceVersion,
+ Commits: vsts.commits,
+ Packages: packages,
+ };
+
+ const errors: string[] = [];
+ if (!command.spaceName) {
+ errors.push("space name is required");
+ }
+
+ if (!command.Packages || command.Packages.length === 0) {
+ errors.push("must specify at least one package name");
+ } else {
+ if (!command.Packages[0].Version || command.Packages[0].Version === "") {
+ errors.push("must specify a package version number, in SemVer format");
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(`Failed to successfully build parameters:\n${errors.join("\n")}`);
+ }
+
+ return command;
+}
diff --git a/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.test.ts b/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.test.ts
new file mode 100644
index 00000000..3e051611
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.test.ts
@@ -0,0 +1,24 @@
+import { Logger, OverwriteMode } from "@octopusdeploy/api-client";
+import { getOverwriteMode } from "./overwriteMode";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ });
+
+ test("second run", () => {
+ task.addVariableString("system.jobAttempt", "2");
+ const overwriteMode = getOverwriteMode(logger, task);
+ expect(overwriteMode).toBe(OverwriteMode.IgnoreIfExists);
+ });
+
+ test("user provided", () => {
+ task.addVariableString("Replace", "true");
+ const overwriteMode = getOverwriteMode(logger, task);
+ expect(overwriteMode).toBe(OverwriteMode.OverwriteExisting);
+ });
+});
diff --git a/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.ts b/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.ts
new file mode 100644
index 00000000..6f3b5318
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/overwriteMode.ts
@@ -0,0 +1,28 @@
+import { Logger, OverwriteMode } from "@octopusdeploy/api-client";
+import { ReplaceOverwriteMode } from "../../Utils/inputs";
+import { TaskWrapper } from "../../Utils/taskInput";
+
+export function getOverwriteMode(logger: Logger, task: TaskWrapper): OverwriteMode {
+ const isRetry = parseInt(task.getVariable("system.jobAttempt") || "0") > 1;
+ const overwriteMode: ReplaceOverwriteMode =
+ (ReplaceOverwriteMode as any)[task.getInput("Replace", false) || ""] || // eslint-disable-line @typescript-eslint/no-explicit-any
+ (isRetry ? ReplaceOverwriteMode.IgnoreIfExists : ReplaceOverwriteMode.false);
+
+ let apiOverwriteMode: OverwriteMode;
+ switch (overwriteMode) {
+ case ReplaceOverwriteMode.true:
+ apiOverwriteMode = OverwriteMode.OverwriteExisting;
+ break;
+ case ReplaceOverwriteMode.IgnoreIfExists:
+ apiOverwriteMode = OverwriteMode.IgnoreIfExists;
+ break;
+ case ReplaceOverwriteMode.false:
+ apiOverwriteMode = OverwriteMode.FailIfExists;
+ break;
+ default:
+ apiOverwriteMode = OverwriteMode.FailIfExists;
+ break;
+ }
+ logger.debug?.(`Overwrite mode: ${apiOverwriteMode}`);
+ return apiOverwriteMode;
+}
diff --git a/source/tasks/BuildInformation/BuildInformationV7/task.json b/source/tasks/BuildInformation/BuildInformationV7/task.json
new file mode 100644
index 00000000..baa6c22d
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/task.json
@@ -0,0 +1,72 @@
+{
+ "id": "559b81c9-efc1-40f3-9058-71ab1810d837",
+ "name": "OctopusBuildInformation",
+ "friendlyName": "Push Package Build Information to Octopus",
+ "description": "Collect information related to the build, including work items from commit messages, and push to your Octopus Deploy Server.",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Package",
+ "visibility": [
+ "Build"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "PackageIds",
+ "type": "multiLine",
+ "label": "Package IDs",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Newline-separated package IDs; e.g.\nMyCompany.MyApp\nMyCompany.MyApp2"
+ },
+ {
+ "name": "PackageVersion",
+ "type": "string",
+ "label": "Package Version",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The version of the package; must be a valid [SemVer](http://semver.org/) version."
+ },
+ {
+ "name": "Replace",
+ "type": "pickList",
+ "label": "Overwrite Mode",
+ "defaultValue": "false",
+ "required": true,
+ "helpMarkDown": "Normally, if the same package build information already exists on the server, the server will reject the package build information push. This is a good practice as it ensures build information isn't accidentally overwritten or ignored. Use this setting to override this behavior.",
+ "options": {
+ "false": "Fail if exists",
+ "true": "Overwrite existing",
+ "IgnoreIfExists": "Ignore if exists"
+ }
+ }
+ ],
+ "instanceNameFormat": "Push Package Build Information to Octopus",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/BuildInformation/BuildInformationV7/vsts.ts b/source/tasks/BuildInformation/BuildInformationV7/vsts.ts
new file mode 100644
index 00000000..20c8fd6c
--- /dev/null
+++ b/source/tasks/BuildInformation/BuildInformationV7/vsts.ts
@@ -0,0 +1,141 @@
+import { IOctopusBuildInformationCommit, Logger } from "@octopusdeploy/api-client";
+import * as vsts from "azure-devops-node-api";
+import * as tasks from "azure-pipelines-task-lib/task";
+import os from "os";
+
+export interface VstsParameters {
+ branch: string;
+ environment: VstsEnvironmentVariables;
+ vcsType: string;
+ commits: IOctopusBuildInformationCommit[];
+}
+
+export interface IVstsHelper {
+ getVsts(logger: Logger): Promise;
+}
+
+export interface ReleaseEnvironmentVariables {
+ releaseName: string;
+ releaseId: string;
+ releaseUri: string;
+}
+
+export interface BuildEnvironmentVariables {
+ buildNumber: string;
+ buildId: number;
+ buildName: string;
+ buildRepositoryName: string;
+ buildRepositoryProvider: string;
+ buildRepositoryUri: string;
+ buildSourceVersion: string;
+}
+
+export interface AgentEnvironmentVariables {
+ agentBuildDirectory: string;
+}
+
+export interface SystemEnvironmentVariables {
+ projectName: string;
+ projectId: string;
+ teamCollectionUri: string;
+ defaultWorkingDirectory: string;
+}
+
+export type VstsEnvironmentVariables = ReleaseEnvironmentVariables & BuildEnvironmentVariables & AgentEnvironmentVariables & SystemEnvironmentVariables;
+
+export class VstsHelper implements IVstsHelper {
+ constructor(readonly logger: Logger) {}
+ async getVsts(): Promise {
+ const environment = this.getVstsEnvironmentVariables();
+ const vstsConnection = this.createVstsConnection(environment);
+ const branch = await this.getBuildBranch(vstsConnection, environment);
+ const commits = await this.getBuildChanges(vstsConnection, environment, this.logger);
+
+ const vsts: VstsParameters = {
+ branch: branch || "",
+ environment: environment,
+ vcsType: await this.getVcsTypeFromProvider(environment.buildRepositoryProvider),
+ commits: commits,
+ };
+
+ return vsts;
+ }
+
+ private getVstsEnvironmentVariables(): VstsEnvironmentVariables {
+ return {
+ projectId: process.env["SYSTEM_TEAMPROJECTID"] || "",
+ projectName: process.env["SYSTEM_TEAMPROJECT"] || "",
+ buildNumber: process.env["BUILD_BUILDNUMBER"] || "",
+ buildId: Number(process.env["BUILD_BUILDID"]),
+ buildName: process.env["BUILD_DEFINITIONNAME"] || "",
+ buildRepositoryName: process.env["BUILD_REPOSITORY_NAME"] || "",
+ releaseName: process.env["RELEASE_RELEASENAME"] || "",
+ releaseUri: process.env["RELEASE_RELEASEWEBURL"] || "",
+ releaseId: process.env["RELEASE_RELEASEID"] || "",
+ teamCollectionUri: process.env["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"] || "",
+ defaultWorkingDirectory: process.env["SYSTEM_DEFAULTWORKINGDIRECTORY"] || "",
+ buildRepositoryProvider: process.env["BUILD_REPOSITORY_PROVIDER"] || "",
+ buildRepositoryUri: process.env["BUILD_REPOSITORY_URI"] || "",
+ buildSourceVersion: process.env["BUILD_SOURCEVERSION"] || "",
+ agentBuildDirectory: process.env["AGENT_BUILDDIRECTORY"] || "",
+ };
+ }
+
+ private getVcsTypeFromProvider(buildRepositoryProvider: string): string {
+ switch (buildRepositoryProvider) {
+ case "TfsGit":
+ case "GitHub":
+ return "Git";
+ case "TfsVersionControl":
+ return "TFVC";
+ default:
+ return buildRepositoryProvider;
+ }
+ }
+
+ private createVstsConnection(environment: SystemEnvironmentVariables): vsts.WebApi {
+ const vstsAuthorization = tasks.getEndpointAuthorization("SystemVssConnection", true);
+ const token = vstsAuthorization?.parameters["AccessToken"] || "";
+ const authHandler = vsts.getPersonalAccessTokenHandler(token);
+ return new vsts.WebApi(environment.teamCollectionUri, authHandler);
+ }
+
+ private async getBuildBranch(client: vsts.WebApi, environment: VstsEnvironmentVariables): Promise {
+ const api = await client.getBuildApi();
+ const build = await api.getBuild(environment.projectName, environment.buildId);
+ return build.sourceBranch;
+ }
+
+ private async getBuildChanges(client: vsts.WebApi, environment: VstsEnvironmentVariables, logger: Logger): Promise {
+ const api = await client.getBuildApi();
+ const gitApi = await client.getGitApi();
+
+ const changes = await api.getBuildChanges(environment.projectName, environment.buildId, undefined, 100000);
+
+ if (environment.buildRepositoryProvider === "TfsGit") {
+ const promises = changes.map(async (x) => {
+ if (x.messageTruncated && x.id) {
+ const segments = x.location?.split("/");
+ if (segments && segments.length >= 3) {
+ const repositoryId = segments[segments.length - 3];
+
+ try {
+ const commit = await gitApi.getCommit(x.id, repositoryId);
+ x.message = commit.comment;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ logger.warn?.(`Using a truncated commit message for commit ${x.id}, because an error occurred while fetching the full message.${os.EOL}${error.message}`);
+ }
+ }
+ }
+ }
+
+ return x;
+ });
+
+ await Promise.all(promises);
+ }
+
+ return changes.map((change) => ({ Id: change.id || "", Comment: change.message || "" }));
+ }
+}
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/createRelease.ts b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/createRelease.ts
new file mode 100644
index 00000000..c912ecd2
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/createRelease.ts
@@ -0,0 +1,30 @@
+import os from "os";
+import { Client, CreateReleaseCommandV1, Logger, ReleaseRepository } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+
+// Returns the release number that was actually created in Octopus
+export async function createReleaseFromInputs(client: Client, command: CreateReleaseCommandV1, task: TaskWrapper, logger: Logger): Promise {
+ logger.info?.("🐙 Creating a release in Octopus Deploy...");
+
+ try {
+ const repository = new ReleaseRepository(client, command.spaceName);
+ const response = await repository.create(command);
+
+ if (command.IgnoreIfAlreadyExists) {
+ client.info(`🎉 Release ${response.ReleaseVersion} is ready for deployment!`);
+ } else {
+ client.info(`🎉 Release ${response.ReleaseVersion} created successfully!`);
+ }
+
+ task.setOutputVariable("release_number", response.ReleaseVersion);
+
+ return response.ReleaseVersion;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ task.setFailure(`"Failed to execute command. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ task.setFailure(`"Failed to execute command. ${error}`, true);
+ }
+ throw error;
+ }
+}
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.png b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.png
new file mode 100644
index 00000000..9a019843
Binary files /dev/null and b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.png differ
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.svg b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.svg
new file mode 100644
index 00000000..8fcdc79e
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/index.ts b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/index.ts
new file mode 100644
index 00000000..8ee1440d
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/index.ts
@@ -0,0 +1,26 @@
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+import { Release } from "./release";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+
+new Release(connection, task, logger).run();
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.test.ts b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.test.ts
new file mode 100644
index 00000000..f1f97814
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.test.ts
@@ -0,0 +1,86 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+import * as path from "path";
+import fs from "fs";
+import os from "os";
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ });
+
+ test("all regular fields supplied", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("Channel", "Beta");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DefaultPackageVersion", "1.0.1");
+ task.addVariableString("Packages", "Step1:Foo:1.0.0\nBar:2.0.0");
+ task.addVariableString("GitRef", "main");
+ task.addVariableBoolean("IgnoreIfAlreadyExists", true);
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.spaceName).toBe("Default");
+ expect(command.ProjectName).toBe("Awesome project");
+ expect(command.ChannelName).toBe("Beta");
+ expect(command.ReleaseVersion).toBe("1.0.0");
+ expect(command.PackageVersion).toBe("1.0.1");
+ expect(command.Packages).toStrictEqual(["Step1:Foo:1.0.0", "Bar:2.0.0"]);
+ expect(command.GitRef).toBe("main");
+ expect(command.IgnoreIfAlreadyExists).toBe(true);
+
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+
+ test("packages in additional fields", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("Packages", "Step1:Foo:1.0.0\nBar:2.0.0");
+ task.addVariableString("AdditionalArguments", "--package Baz:2.5.0");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Packages).toStrictEqual(["Baz:2.5.0", "Step1:Foo:1.0.0", "Bar:2.0.0"]);
+ });
+
+ test("release notes file", async () => {
+ const tempOutDir = await fs.mkdtempSync(path.join(os.tmpdir(), "octopus_"));
+ const notesPath = path.join(tempOutDir, "notes.txt");
+
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("ReleaseNotesFile", notesPath);
+
+ fs.writeFileSync(notesPath, "this is a release note");
+ const command = createCommandFromInputs(logger, task);
+ expect(command.ReleaseNotes).toBe("this is a release note");
+ });
+
+ test("specifying both release notes and release notes file causes error", async () => {
+ const tempOutDir = await fs.mkdtempSync(path.join(os.tmpdir(), "octopus_"));
+ const notesPath = path.join(tempOutDir, "notes.txt");
+
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("ReleaseNotes", "inline release notes");
+ task.addVariableString("ReleaseNotesFile", notesPath);
+
+ fs.writeFileSync(notesPath, "this is a release note");
+ expect(() => createCommandFromInputs(logger, task)).toThrowError("cannot specify ReleaseNotes and ReleaseNotesFile");
+ });
+
+ test("duplicate variable name, variables field takes precedence", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("Packages", "Step1:Foo:1.0.0\nBar:2.0.0");
+ task.addVariableString("AdditionalArguments", "--package Bar:2.0.0");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Packages).toStrictEqual(["Bar:2.0.0", "Step1:Foo:1.0.0"]);
+ });
+});
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.ts b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.ts
new file mode 100644
index 00000000..a6c82c3e
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/inputCommandBuilder.ts
@@ -0,0 +1,88 @@
+import commandLineArgs from "command-line-args";
+import shlex from "shlex";
+import { getLineSeparatedItems } from "../../Utils/inputs";
+import { CreateReleaseCommandV1, Logger } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { isNullOrWhitespace } from "../../../tasksLegacy/Utils/inputs";
+import fs from "fs";
+
+export function createCommandFromInputs(logger: Logger, task: TaskWrapper): CreateReleaseCommandV1 {
+ const packages: string[] = [];
+ let defaultPackageVersion: string | undefined = undefined;
+
+ const additionalArguments = task.getInput("AdditionalArguments");
+ logger.debug?.("AdditionalArguments:" + additionalArguments);
+ if (additionalArguments) {
+ logger.warn?.("Additional arguments are no longer supported and will be removed in future versions. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.");
+ const optionDefs = [
+ { name: "package", type: String, multiple: true },
+ { name: "defaultPackageVersion", type: String },
+ { name: "packageVersion", type: String },
+ ];
+ const splitArgs = shlex.split(additionalArguments);
+ const options = commandLineArgs(optionDefs, { argv: splitArgs });
+ logger.debug?.(JSON.stringify(options));
+ for (const pkg of options.package) {
+ packages.push(pkg.trim());
+ }
+
+ // defaultPackageVersion and packageVersion both represent the default package version
+ if (options.defaultPackageVersion) {
+ defaultPackageVersion = options.defaultPackageVersion;
+ }
+ if (options.packageVersion) {
+ defaultPackageVersion = options.packageVersion;
+ }
+ }
+
+ const packagesField = task.getInput("Packages");
+ logger.debug?.("Packages:" + packagesField);
+ if (packagesField) {
+ const packagesFieldData = getLineSeparatedItems(packagesField).map((p) => p.trim()) || undefined;
+ if (packagesFieldData) {
+ for (const packageLine of packagesFieldData) {
+ const trimmedPackageLine = packageLine.trim();
+ if (packages.indexOf(trimmedPackageLine) < 0) {
+ packages.push(trimmedPackageLine);
+ }
+ }
+ }
+ }
+
+ const defaultPackageVersionField = task.getInput("DefaultPackageVersion");
+ if (defaultPackageVersionField) {
+ defaultPackageVersion = defaultPackageVersionField;
+ }
+
+ const command: CreateReleaseCommandV1 = {
+ spaceName: task.getInput("Space", true) || "",
+ ProjectName: task.getInput("Project", true) || "",
+ ReleaseVersion: task.getInput("ReleaseNumber"),
+ ChannelName: task.getInput("Channel"),
+ PackageVersion: defaultPackageVersion,
+ Packages: packages.length > 0 ? packages : undefined,
+ ReleaseNotes: task.getInput("ReleaseNotes"),
+ GitRef: task.getInput("GitRef"),
+ GitCommit: task.getInput("GitCommit"),
+ IgnoreIfAlreadyExists: task.getBoolean("IgnoreIfAlreadyExists") || undefined,
+ };
+
+ const releaseNotesFilePath = task.getInput("ReleaseNotesFile");
+
+ if (command.ReleaseNotes && releaseNotesFilePath) {
+ const message = "cannot specify ReleaseNotes and ReleaseNotesFile";
+ task.setFailure(message);
+ throw new Error(message);
+ }
+
+ if (releaseNotesFilePath) {
+ const releaseNotesFile = releaseNotesFilePath;
+ if (!isNullOrWhitespace(releaseNotesFile) && fs.existsSync(releaseNotesFile) && fs.lstatSync(releaseNotesFile).isFile()) {
+ command.ReleaseNotes = fs.readFileSync(releaseNotesFile).toString();
+ }
+ }
+
+ logger.debug?.(JSON.stringify(command));
+
+ return command;
+}
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/release.ts b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/release.ts
new file mode 100644
index 00000000..6b5bd80a
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/release.ts
@@ -0,0 +1,48 @@
+import { Client, CreateReleaseCommandV1, Logger, Project, ProjectRepository, resolveSpaceId } from "@octopusdeploy/api-client";
+import { getDeepLink, OctoServerConnectionDetails } from "../../Utils/connection";
+import { createReleaseFromInputs } from "./createRelease";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import path from "path";
+import { getVstsEnvironmentVariables } from "../../../tasksLegacy/Utils/environment";
+import { v4 as uuidv4 } from "uuid";
+import * as tasks from "azure-pipelines-task-lib";
+import { getClient } from "../../Utils/client";
+
+export class Release {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {}
+
+ public async run() {
+ try {
+ const command = createCommandFromInputs(this.logger, this.task);
+ const client = await getClient(this.connection, this.logger, "release", "create", 6);
+ const version = await createReleaseFromInputs(client, command, this.task, this.logger);
+
+ await this.tryCreateSummary(client, command, version);
+
+ this.task.setSuccess("Release creation succeeded.");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ this.task.setFailure(`"Failed to successfully create release. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ this.task.setFailure(`"Failed to successfully create release. ${error}`, true);
+ }
+ throw error;
+ }
+ }
+
+ private async tryCreateSummary(client: Client, command: CreateReleaseCommandV1, version: string) {
+ const spaceId = await resolveSpaceId(client, command.spaceName);
+ const projectRepo = new ProjectRepository(client, command.spaceName);
+ const projects = await projectRepo.list({ partialName: command.ProjectName });
+ const matchedProjects = projects.Items.filter((p: Project) => p.Name.localeCompare(command.ProjectName) === 0);
+ if (matchedProjects.length === 1) {
+ const link = getDeepLink(this.connection.url, `${spaceId}/projects/${matchedProjects[0].Id}/deployments/releases/${version}`);
+ const markdown = `[Release ${version} created for '${matchedProjects[0].Name}'](${link})`;
+ const markdownFile = path.join(getVstsEnvironmentVariables().defaultWorkingDirectory, `${uuidv4()}.md`);
+ tasks.writeFile(markdownFile, markdown);
+ tasks.addAttachment("Distributedtask.Core.Summary", "Octopus Create Release", markdownFile);
+ }
+ }
+}
diff --git a/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/task.json b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/task.json
new file mode 100644
index 00000000..f92a116b
--- /dev/null
+++ b/source/tasks/CreateOctopusRelease/CreateOctopusReleaseV7/task.json
@@ -0,0 +1,154 @@
+{
+ "id": "4E131B60-5532-4362-95B6-7C67D9841B4F",
+ "name": "OctopusCreateRelease",
+ "friendlyName": "Create Octopus Release",
+ "description": "Create a Release in Octopus Deploy",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Deploy",
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [ ],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "versionControl",
+ "displayName": "Version Control",
+ "isExpanded": false
+ },
+ {
+ "name": "additional",
+ "displayName": "Additional Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "Project",
+ "type": "string",
+ "label": "Project",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The project within Octopus. This must be the name of the project, not the id."
+ },
+ {
+ "name": "ReleaseNumber",
+ "type": "string",
+ "label": "Release Number",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The number to use for this release. You can leave this blank if the release number is calculated by Octopus."
+ },
+ {
+ "name": "Channel",
+ "type": "string",
+ "label": "Channel",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The [channel](https://g.octopushq.com/Channels) to use for the release. This must be the name of the channel, not the id."
+ },
+ {
+ "name": "DefaultPackageVersion",
+ "type": "string",
+ "label": "Default Package Version",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Set this to provide a default package version to use for all packages on all steps. Can be used in conjunction with the Packages field, which can be used to override versions for specific packages."
+ },
+ {
+ "name": "Packages",
+ "type": "multiLine",
+ "label": "Packages",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "A multi-line list of version numbers to use for a package in the release. Format: StepName:Version or PackageID:Version or StepName:PackageName:Version. StepName, PackageID, and PackageName can be replaced with an asterisk ('*'). An asterisk will be assumed for StepName, PackageID, or PackageName if they are omitted."
+ },
+ {
+ "name": "ReleaseNotes",
+ "type": "string",
+ "label": "Release Notes",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Octopus Release notes. This field supports markdown. To include newlines, you can use HTML linebreaks. Can only specify this if 'ReleaseNotesFile' is not supplied."
+ },
+ {
+ "name": "ReleaseNotesFile",
+ "type": "string",
+ "label": "Release Notes File",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Octopus Release notes file. Path to a file that contains the release notes. Supports markdown. Can only specify this if 'ReleaseNotes' is not supplied."
+ },
+ {
+ "name": "GitRef",
+ "type": "string",
+ "label": "Git Reference",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Git branch reference to use when creating the release for version controlled Projects.",
+ "groupName": "versionControl"
+ },
+ {
+ "name": "GitCommit",
+ "type": "string",
+ "label": "Git Commit",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Git commit to use when creating the release for version controlled Projects. Use in conjunction with the gitRef parameter to select any previous commit.",
+ "groupName": "versionControl"
+ },
+ {
+ "name": "IgnoreIfAlreadyExists",
+ "type": "boolean",
+ "label": "Ignore Existing Release",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "If enabled will not attempt to create a new release if there is already one with the same version number",
+ "groupName": "additional"
+ },
+ {
+ "name": "AdditionalArguments",
+ "type": "string",
+ "label": "Additional Arguments",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Additional arguments are no longer supported. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.",
+ "groupName": "additional"
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "release_number",
+ "description": "The Octopus Deploy release number assigned to the Release."
+ }
+ ],
+ "instanceNameFormat": "Create Octopus Release",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/Deploy/DeployV7/createDeployment.ts b/source/tasks/Deploy/DeployV7/createDeployment.ts
new file mode 100644
index 00000000..0ab07801
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/createDeployment.ts
@@ -0,0 +1,49 @@
+import { Client, CreateDeploymentUntenantedCommandV1, DeploymentRepository, EnvironmentRepository, Logger } from "@octopusdeploy/api-client";
+import os from "os";
+import { TaskWrapper } from "../../Utils/taskInput";
+import { ExecutionResult } from "../../Utils/executionResult";
+
+export async function createDeploymentFromInputs(client: Client, command: CreateDeploymentUntenantedCommandV1, task: TaskWrapper, logger: Logger): Promise {
+ logger.info?.("🐙 Deploying a release in Octopus Deploy...");
+
+ try {
+ const deploymentRepository = new DeploymentRepository(client, command.spaceName);
+ const response = await deploymentRepository.create(command);
+
+ client.info(`🎉 ${response.DeploymentServerTasks.length} Deployment${response.DeploymentServerTasks.length > 1 ? "s" : ""} queued successfully!`);
+
+ if (response.DeploymentServerTasks.length === 0) {
+ throw new Error("Expected at least one deployment to be queued.");
+ }
+ if (response.DeploymentServerTasks[0].ServerTaskId === null || response.DeploymentServerTasks[0].ServerTaskId === undefined) {
+ throw new Error("Server task id was not deserialized correctly.");
+ }
+
+ const deploymentIds = response.DeploymentServerTasks.map((x) => x.DeploymentId);
+
+ const deployments = await deploymentRepository.list({ ids: deploymentIds, take: deploymentIds.length });
+
+ const envIds = deployments.Items.map((d) => d.EnvironmentId);
+ const envRepository = new EnvironmentRepository(client, command.spaceName);
+ const environments = await envRepository.list({ ids: envIds, take: envIds.length });
+
+ const results = response.DeploymentServerTasks.map((x) => {
+ return {
+ serverTaskId: x.ServerTaskId,
+ environmentName: environments.Items.filter((e) => e.Id === deployments.Items.filter((d) => d.TaskId === x.ServerTaskId)[0].EnvironmentId)[0].Name,
+ type: "Deployment",
+ } as ExecutionResult;
+ });
+
+ task.setOutputVariable("server_tasks", JSON.stringify(results));
+
+ return results;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ task.setFailure(`"Failed to execute command. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ task.setFailure(`"Failed to execute command. ${error}`, true);
+ }
+ throw error;
+ }
+}
diff --git a/source/tasks/Deploy/DeployV7/deploy.ts b/source/tasks/Deploy/DeployV7/deploy.ts
new file mode 100644
index 00000000..314c267d
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/deploy.ts
@@ -0,0 +1,63 @@
+import { Client, CreateDeploymentUntenantedCommandV1, Logger, resolveSpaceId, ServerTask, SpaceServerTaskRepository } from "@octopusdeploy/api-client";
+import { getDeepLink, OctoServerConnectionDetails } from "../../Utils/connection";
+import { createDeploymentFromInputs } from "./createDeployment";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { getClient } from "../../Utils/client";
+import path from "path";
+import { getVstsEnvironmentVariables } from "../../../tasksLegacy/Utils/environment";
+import { v4 as uuidv4 } from "uuid";
+import { ExecutionResult } from "../../Utils/executionResult";
+import * as tasks from "azure-pipelines-task-lib";
+
+export class Deploy {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {}
+
+ public async run() {
+ try {
+ const command = createCommandFromInputs(this.logger, this.task);
+ const client = await getClient(this.connection, this.logger, "release", "deploy", 6);
+
+ const results = await createDeploymentFromInputs(client, command, this.task, this.logger);
+ await this.tryCreateSummary(client, command, results);
+
+ this.task.setSuccess("Deployment succeeded.");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ this.task.setFailure(`"Failed to successfully deploy release. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ this.task.setFailure(`"Failed to successfully deploy release. ${error}`, true);
+ }
+ throw error;
+ }
+ }
+
+ private async tryCreateSummary(client: Client, command: CreateDeploymentUntenantedCommandV1, results: ExecutionResult[]) {
+ if (results.length === 0) {
+ return;
+ }
+
+ const spaceId = await resolveSpaceId(client, command.spaceName);
+ const taskRepo = new SpaceServerTaskRepository(client, command.spaceName);
+ const allTasks = await taskRepo.getByIds<{ DeploymentId: string }>(results.map((t) => t.serverTaskId));
+ const taskLookup = new Map>();
+ allTasks.forEach(function (t) {
+ taskLookup.set(t.Id, t);
+ });
+
+ const url = this.connection.url;
+ let markdown = `${results[0].type} tasks\n\n`;
+ results.forEach(function (result) {
+ const task = taskLookup.get(result.serverTaskId);
+ if (task != null) {
+ const link = getDeepLink(url, `${spaceId}/deployments/${task.Arguments.DeploymentId}`);
+ markdown += `[${result.environmentName}](${link})\n`;
+ }
+ });
+
+ const markdownFile = path.join(getVstsEnvironmentVariables().defaultWorkingDirectory, `${uuidv4()}.md`);
+ tasks.writeFile(markdownFile, markdown);
+ tasks.addAttachment("Distributedtask.Core.Summary", "Octopus Deploy", markdownFile);
+ }
+}
diff --git a/source/tasks/Deploy/DeployV7/icon.png b/source/tasks/Deploy/DeployV7/icon.png
new file mode 100644
index 00000000..1da59821
Binary files /dev/null and b/source/tasks/Deploy/DeployV7/icon.png differ
diff --git a/source/tasks/Deploy/DeployV7/icon.svg b/source/tasks/Deploy/DeployV7/icon.svg
new file mode 100644
index 00000000..f750c8e8
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/source/tasks/Deploy/DeployV7/index.ts b/source/tasks/Deploy/DeployV7/index.ts
new file mode 100644
index 00000000..b683ec69
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/index.ts
@@ -0,0 +1,26 @@
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { Deploy } from "./deploy";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+
+new Deploy(connection, task, logger).run();
diff --git a/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts b/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts
new file mode 100644
index 00000000..240ea628
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/inputCommandBuilder.test.ts
@@ -0,0 +1,98 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ });
+
+ test("all regular fields supplied", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("Environments", "dev, test");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.EnvironmentNames).toStrictEqual(["dev", "test"]);
+ expect(command.ProjectName).toBe("Awesome project");
+ expect(command.ReleaseVersion).toBe("1.0.0");
+ expect(command.spaceName).toBe("Default");
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+
+ test("variables in additional fields", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("AdditionalArguments", "-v var3=value3 --variable var4=value4");
+ task.addVariableString("Environments", "test");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2", var3: "value3", var4: "value4" });
+ });
+
+ test("missing space", () => {
+ const t = () => {
+ task.addVariableString("Environments", "test");
+ createCommandFromInputs(logger, task);
+ };
+ expect(t).toThrowError("Input required: Space");
+ });
+
+ test("duplicate variable name, variables field takes precedence", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("AdditionalArguments", "-v var1=value3");
+ task.addVariableString("Environments", "test");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+ });
+
+ test("handles escaped colons in variable names", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "Test\\:Variable: Testing3");
+ task.addVariableString("Environments", "test");
+ task.addVariableString("Project", "Test project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ "Test:Variable": "Testing3" });
+ });
+
+ test("handles multiple variables with escaped and unescaped colons", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "Long\\:Variable\\:Name: Value123\nTest\\:Variable: Value: With: Colons");
+ task.addVariableString("Environments", "test");
+ task.addVariableString("Project", "Test project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({
+ "Long:Variable:Name": "Value123",
+ "Test:Variable": "Value: With: Colons"
+ });
+ });
+
+ test("multiline environments", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Environments", "dev, test\nprod");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+ const command = createCommandFromInputs(logger, task);
+ expect(command.EnvironmentNames).toStrictEqual(["dev", "test", "prod"]);
+ });
+});
diff --git a/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts b/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts
new file mode 100644
index 00000000..1c27c6c6
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/inputCommandBuilder.ts
@@ -0,0 +1,68 @@
+import commandLineArgs from "command-line-args";
+import shlex from "shlex";
+import { getLineSeparatedItems, parseVariableString } from "../../Utils/inputs";
+import { CreateDeploymentUntenantedCommandV1, Logger, PromptedVariableValues } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+
+export function createCommandFromInputs(logger: Logger, task: TaskWrapper): CreateDeploymentUntenantedCommandV1 {
+ const variablesMap: PromptedVariableValues | undefined = {};
+
+ const additionalArguments = task.getInput("AdditionalArguments");
+ logger.debug?.("AdditionalArguments:" + additionalArguments);
+ if (additionalArguments) {
+ logger.warn?.("Additional arguments are no longer supported and will be removed in future versions. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.");
+ const optionDefs = [{ name: "variable", alias: "v", type: String, multiple: true }];
+ const splitArgs = shlex.split(additionalArguments);
+ const options = commandLineArgs(optionDefs, { argv: splitArgs });
+ logger.debug?.(JSON.stringify(options));
+ for (const variable of options.variable) {
+ const variableMap = variable.split("=").map((x: string) => x.trim());
+ variablesMap[variableMap[0]] = variableMap[1];
+ }
+ }
+
+ const variablesField = task.getInput("Variables");
+ logger.debug?.("Variables: " + variablesField);
+ if (variablesField) {
+ const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined;
+ if (variables) {
+ for (const variable of variables) {
+ const [name, value] = parseVariableString(variable);
+ variablesMap[name] = value;
+ }
+ }
+ }
+
+ const environmentsField = task.getInput("Environments", true);
+ let environments: string[] = [];
+
+ if (environmentsField) {
+ const lines = getLineSeparatedItems(environmentsField);
+ lines.forEach((l) => {
+ environments = environments.concat(l.split(",").map((e: string) => e.trim()));
+ });
+ }
+ logger.debug?.("Environments:" + environmentsField);
+
+ const command: CreateDeploymentUntenantedCommandV1 = {
+ spaceName: task.getInput("Space", true) || "",
+ ProjectName: task.getInput("Project", true) || "",
+ ReleaseVersion: task.getInput("ReleaseNumber", true) || "",
+ EnvironmentNames: environments,
+ UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined,
+ Variables: variablesMap || undefined,
+ };
+
+ const errors: string[] = [];
+ if (command.spaceName === "") {
+ errors.push("The Octopus space name is required.");
+ }
+
+ if (errors.length > 0) {
+ throw new Error("Failed to successfully build parameters.\n" + errors.join("\n"));
+ }
+
+ logger.debug?.(JSON.stringify(command));
+
+ return command;
+}
diff --git a/source/tasks/Deploy/DeployV7/task.json b/source/tasks/Deploy/DeployV7/task.json
new file mode 100644
index 00000000..0be3eab1
--- /dev/null
+++ b/source/tasks/Deploy/DeployV7/task.json
@@ -0,0 +1,106 @@
+{
+ "id": "8ca1d96a-151d-44b7-bc4f-9251e2ea6971",
+ "name": "OctopusDeployRelease",
+ "friendlyName": "Deploy Octopus Release",
+ "description": "Deploy an Octopus Deploy Release",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Deploy",
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "advanced",
+ "displayName": "Advanced Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "Project",
+ "type": "string",
+ "label": "Project",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The project within Octopus. This must be the name of the project, not the id."
+ },
+ {
+ "name": "ReleaseNumber",
+ "type": "string",
+ "label": "Release Number",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The number of the release to deploy."
+ },
+ {
+ "name": "Environments",
+ "type": "multiLine",
+ "label": "Deploy to Environments",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "List of environments to deploy to, one environment per line. (Note: multiple on a line, separated by commas, is supported to ease migration from earlier versions of the step but this format should be considered deprecated)"
+ },
+ {
+ "name": "Variables",
+ "type": "multiLine",
+ "label": "Values for prompted variables",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Variable values to pass to the the deployment, use syntax `variable: value`"
+ },
+ {
+ "name": "UseGuidedFailure",
+ "type": "boolean",
+ "label": "Use guided failure",
+ "defaultValue": "False",
+ "required": false,
+ "helpMarkDown": "Whether to use guided failure mode if errors occur during the deployment."
+ },
+ {
+ "name": "AdditionalArguments",
+ "type": "string",
+ "label": "Additional Arguments (deprecated)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Additional arguments are no longer supported. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.",
+ "groupName": "advanced"
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "server_tasks",
+ "description": "List of server tasks representing the deployment server tasks."
+ }
+ ],
+ "instanceNameFormat": "Deploy Octopus Release",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/createDeployment.ts b/source/tasks/DeployTenant/TenantedDeployV7/createDeployment.ts
new file mode 100644
index 00000000..cd7d99c3
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/createDeployment.ts
@@ -0,0 +1,50 @@
+import { Client, CreateDeploymentTenantedCommandV1, DeploymentRepository, Logger, TenantRepository } from "@octopusdeploy/api-client";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { ExecutionResult } from "../../Utils/executionResult";
+
+export async function createDeploymentFromInputs(client: Client, command: CreateDeploymentTenantedCommandV1, task: TaskWrapper, logger: Logger): Promise {
+ logger.info?.("🐙 Deploying a release in Octopus Deploy...");
+
+ try {
+ const deploymentRepository = new DeploymentRepository(client, command.spaceName);
+ const response = await deploymentRepository.createTenanted(command);
+
+ client.info(`🎉 ${response.DeploymentServerTasks.length} Deployment${response.DeploymentServerTasks.length > 1 ? "s" : ""} queued successfully!`);
+
+ if (response.DeploymentServerTasks.length === 0) {
+ throw new Error("Expected at least one deployment to be queued.");
+ }
+ if (response.DeploymentServerTasks[0].ServerTaskId === null || response.DeploymentServerTasks[0].ServerTaskId === undefined) {
+ throw new Error("Server task id was not deserialized correctly.");
+ }
+
+ const deploymentIds = response.DeploymentServerTasks.map((x) => x.DeploymentId);
+
+ const deployments = await deploymentRepository.list({ ids: deploymentIds, take: deploymentIds.length });
+
+ const tenantIds = deployments.Items.map((d) => d.TenantId || "");
+ const tenantRepository = new TenantRepository(client, command.spaceName);
+ const tenants = await tenantRepository.list({ ids: tenantIds, take: tenantIds.length });
+
+ const results = response.DeploymentServerTasks.map((x) => {
+ return {
+ serverTaskId: x.ServerTaskId,
+ environmentName: command.EnvironmentName,
+ tenantName: tenants.Items.filter((e) => e.Id === deployments.Items.filter((d) => d.TaskId === x.ServerTaskId)[0].TenantId)[0].Name,
+ type: "Deployment",
+ } as ExecutionResult;
+ });
+
+ task.setOutputVariable("server_tasks", JSON.stringify(results));
+
+ return results;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ task.setFailure(`"Failed to execute command. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ task.setFailure(`"Failed to execute command. ${error}`, true);
+ }
+ throw error;
+ }
+}
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/deploy.ts b/source/tasks/DeployTenant/TenantedDeployV7/deploy.ts
new file mode 100644
index 00000000..ab9fa919
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/deploy.ts
@@ -0,0 +1,64 @@
+import { CreateDeploymentTenantedCommandV1, Logger, Client, resolveSpaceId, SpaceServerTaskRepository, ServerTask } from "@octopusdeploy/api-client";
+import { getDeepLink, OctoServerConnectionDetails } from "../../Utils/connection";
+import { createDeploymentFromInputs } from "./createDeployment";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { getClient } from "../../Utils/client";
+import { ExecutionResult } from "../../Utils/executionResult";
+import path from "path";
+import { getVstsEnvironmentVariables } from "../../../tasksLegacy/Utils/environment";
+import { v4 as uuidv4 } from "uuid";
+import * as tasks from "azure-pipelines-task-lib";
+
+export class Deploy {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {}
+
+ public async run() {
+ try {
+ const command = createCommandFromInputs(this.logger, this.task);
+ const client = await getClient(this.connection, this.logger, "release", "deploy-tenanted", 6);
+
+ const results = await createDeploymentFromInputs(client, command, this.task, this.logger);
+
+ await this.tryCreateSummary(client, command, results);
+
+ this.task.setSuccess("Deployment succeeded.");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ this.task.setFailure(`"Failed to successfully deploy release. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ this.task.setFailure(`"Failed to successfully deploy release. ${error}`, true);
+ }
+ throw error;
+ }
+ }
+
+ private async tryCreateSummary(client: Client, command: CreateDeploymentTenantedCommandV1, results: ExecutionResult[]) {
+ if (results.length === 0) {
+ return;
+ }
+
+ const spaceId = await resolveSpaceId(client, command.spaceName);
+ const taskRepo = new SpaceServerTaskRepository(client, command.spaceName);
+ const allTasks = await taskRepo.getByIds<{ DeploymentId: string }>(results.map((t) => t.serverTaskId));
+ const taskLookup = new Map>();
+ allTasks.forEach(function (t) {
+ taskLookup.set(t.Id, t);
+ });
+
+ const url = this.connection.url;
+ let markdown = `${results[0].type} tasks for '${results[0].environmentName}' environment\n\n`;
+ results.forEach(function (result) {
+ const task = taskLookup.get(result.serverTaskId);
+ if (task != null) {
+ const link = getDeepLink(url, `${spaceId}/deployments/${task.Arguments.DeploymentId}`);
+ markdown += `[${result.tenantName}](${link})\n`;
+ }
+ });
+
+ const markdownFile = path.join(getVstsEnvironmentVariables().defaultWorkingDirectory, `${uuidv4()}.md`);
+ tasks.writeFile(markdownFile, markdown);
+ tasks.addAttachment("Distributedtask.Core.Summary", "Octopus Deploy Tenants", markdownFile);
+ }
+}
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/icon.png b/source/tasks/DeployTenant/TenantedDeployV7/icon.png
new file mode 100644
index 00000000..1da59821
Binary files /dev/null and b/source/tasks/DeployTenant/TenantedDeployV7/icon.png differ
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/icon.svg b/source/tasks/DeployTenant/TenantedDeployV7/icon.svg
new file mode 100644
index 00000000..f750c8e8
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/index.ts b/source/tasks/DeployTenant/TenantedDeployV7/index.ts
new file mode 100644
index 00000000..b683ec69
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/index.ts
@@ -0,0 +1,26 @@
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { Deploy } from "./deploy";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+
+new Deploy(connection, task, logger).run();
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.test.ts b/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.test.ts
new file mode 100644
index 00000000..b69f3573
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.test.ts
@@ -0,0 +1,106 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ });
+
+ test("all regular fields supplied", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("Environment", "dev");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DeployForTenants", "Tenant 1\nTenant 2");
+ task.addVariableString("DeployForTenantTags", "tag set 1/tag 1\ntag set 1/tag 2");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.EnvironmentName).toBe("dev");
+ expect(command.ProjectName).toBe("Awesome project");
+ expect(command.ReleaseVersion).toBe("1.0.0");
+ expect(command.spaceName).toBe("Default");
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+ expect(command.Tenants).toStrictEqual(["Tenant 1", "Tenant 2"]);
+ expect(command.TenantTags).toStrictEqual(["tag set 1/tag 1", "tag set 1/tag 2"]);
+
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+
+ test("variables in additional fields", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("AdditionalArguments", "-v var3=value3 --variable var4=value4");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("Environment", "test");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2", var3: "value3", var4: "value4" });
+ });
+
+ test("missing space", () => {
+ const t = () => {
+ createCommandFromInputs(logger, task);
+ };
+ expect(t).toThrowError("Failed to successfully build parameters: space name is required.");
+ });
+
+ test("duplicate variable name, variables field takes precedence", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("AdditionalArguments", "-v var1=value3");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("Environment", "test");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+ });
+
+ test("handles escaped colons in variable names", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "Test\\:Variable: Testing3");
+ task.addVariableString("Environment", "dev");
+ task.addVariableString("Project", "Test project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({ "Test:Variable": "Testing3" });
+ });
+
+ test("handles multiple variables with escaped and unescaped colons", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Variables", "Long\\:Variable\\:Name: Value123\nTest\\:Variable: Value: With: Colons");
+ task.addVariableString("Environment", "dev");
+ task.addVariableString("Project", "Test project");
+ task.addVariableString("ReleaseNumber", "1.0.0");
+ task.addVariableString("DeployForTenants", "Tenant 1");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(command.Variables).toStrictEqual({
+ "Long:Variable:Name": "Value123",
+ "Test:Variable": "Value: With: Colons"
+ });
+ });
+
+ test("validate tenants and tags", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "project 1");
+ task.addVariableString("ReleaseNumber", "1.2.3");
+ task.addVariableString("Environment", "test");
+ const t = () => {
+ createCommandFromInputs(logger, task);
+ };
+
+ expect(t).toThrowError("Failed to successfully build parameters.\nMust provide at least one tenant or tenant tag.");
+ });
+});
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.ts b/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.ts
new file mode 100644
index 00000000..7a78701c
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/inputCommandBuilder.ts
@@ -0,0 +1,74 @@
+import commandLineArgs from "command-line-args";
+import shlex from "shlex";
+import { getLineSeparatedItems, parseVariableString } from "../../Utils/inputs";
+import { CreateDeploymentTenantedCommandV1, Logger, PromptedVariableValues } from "@octopusdeploy/api-client";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+
+export function createCommandFromInputs(logger: Logger, task: TaskWrapper): CreateDeploymentTenantedCommandV1 {
+ const space = task.getInput("Space");
+ if (!space) {
+ throw new Error("Failed to successfully build parameters: space name is required.");
+ }
+
+ const variablesMap: PromptedVariableValues | undefined = {};
+
+ const additionalArguments = task.getInput("AdditionalArguments");
+ logger.debug?.("AdditionalArguments:" + additionalArguments);
+ if (additionalArguments) {
+ logger.warn?.("Additional arguments are no longer supported and will be removed in future versions. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.");
+ const optionDefs = [{ name: "variable", alias: "v", type: String, multiple: true }];
+ const splitArgs = shlex.split(additionalArguments);
+ const options = commandLineArgs(optionDefs, { argv: splitArgs });
+ logger.debug?.(JSON.stringify(options));
+ for (const variable of options.variable) {
+ const variableMap = variable.split("=").map((x: string) => x.trim());
+ variablesMap[variableMap[0]] = variableMap[1];
+ }
+ }
+
+ const variablesField = task.getInput("Variables");
+ logger.debug?.("Variables: " + variablesField);
+ if (variablesField) {
+ const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined;
+ if (variables) {
+ for (const variable of variables) {
+ const [name, value] = parseVariableString(variable);
+ variablesMap[name] = value;
+ }
+ }
+ }
+
+ const tenantsField = task.getInput("DeployForTenants");
+ logger.debug?.("Tenants: " + tenantsField);
+ const tagsField = task.getInput("DeployForTenantTags");
+ logger.debug?.("Tenant Tags: " + tagsField);
+ const tags = getLineSeparatedItems(tagsField || "")?.map((t: string) => t.trim()) || [];
+
+ const command: CreateDeploymentTenantedCommandV1 = {
+ spaceName: task.getInput("Space") || "",
+ ProjectName: task.getInput("Project", true) || "",
+ ReleaseVersion: task.getInput("ReleaseNumber", true) || "",
+ EnvironmentName: task.getInput("Environment", true) || "",
+ Tenants: getLineSeparatedItems(tenantsField || "")?.map((t: string) => t.trim()) || [],
+ TenantTags: tags,
+ UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined,
+ Variables: variablesMap || undefined,
+ };
+
+ const errors: string[] = [];
+ if (command.spaceName === "") {
+ errors.push("The Octopus space name is required.");
+ }
+
+ if (command.TenantTags.length === 0 && command.Tenants.length === 0) {
+ errors.push("Must provide at least one tenant or tenant tag.");
+ }
+
+ if (errors.length > 0) {
+ throw new Error("Failed to successfully build parameters.\n" + errors.join("\n"));
+ }
+
+ logger.debug?.(JSON.stringify(command));
+
+ return command;
+}
diff --git a/source/tasks/DeployTenant/TenantedDeployV7/task.json b/source/tasks/DeployTenant/TenantedDeployV7/task.json
new file mode 100644
index 00000000..c91c713f
--- /dev/null
+++ b/source/tasks/DeployTenant/TenantedDeployV7/task.json
@@ -0,0 +1,122 @@
+{
+ "id": "a847e2d1-5435-4d52-a774-6d300953e85f",
+ "name": "OctopusDeployReleaseTenanted",
+ "friendlyName": "Deploy Octopus Release to Tenants",
+ "description": "Deploy an Octopus Deploy Release to Tenants",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Deploy",
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "advanced",
+ "displayName": "Advanced Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "Project",
+ "type": "string",
+ "label": "Project",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The project within Octopus. This must be the name of the project, not the id."
+ },
+ {
+ "name": "ReleaseNumber",
+ "type": "string",
+ "label": "Release Number",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The number of the release to deploy."
+ },
+ {
+ "name": "Environment",
+ "type": "string",
+ "label": "Deploy to Environment",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The "
+ },
+ {
+ "name": "DeployForTenants",
+ "type": "multiline",
+ "label": "Tenant(s)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Deploy the release for this list of tenants. Wildcard '*' will deploy to all tenants currently able to deploy to the above provided environment."
+ },
+ {
+ "name": "DeployForTenantTags",
+ "type": "multiLine",
+ "label": "Tenant tag(s)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Deploy the release for tenants who match these tags and are ready to deploy to the provided environment."
+ },
+ {
+ "name": "Variables",
+ "type": "multiLine",
+ "label": "Values for prompted variables",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Variable values to pass to the the deployment, use syntax `variable: value`"
+ },
+ {
+ "name": "UseGuidedFailure",
+ "type": "boolean",
+ "label": "Use guided failure",
+ "defaultValue": "False",
+ "required": false,
+ "helpMarkDown": "Whether to use guided failure mode if errors occur during the deployment."
+ },
+ {
+ "name": "AdditionalArguments",
+ "type": "string",
+ "label": "Additional Arguments (deprecated)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Additional arguments are no longer supported. This field has been retained to ease migration from earlier versions of the step but values should be moved to the appropriate fields.",
+ "groupName": "advanced"
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "server_tasks",
+ "description": "List of server tasks representing the deployment server tasks."
+ }
+ ],
+ "instanceNameFormat": "Deploy Octopus Release Tenants",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.test.ts b/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.test.ts
new file mode 100644
index 00000000..36e92a63
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.test.ts
@@ -0,0 +1,124 @@
+import { DownloadEndpointRetriever } from "./downloadEndpointRetriever";
+import { mkdtemp, rm } from "fs/promises";
+import * as path from "path";
+import os from "os";
+import express from "express";
+import { Server } from "http";
+import { AddressInfo } from "net";
+import { Logger } from "@octopusdeploy/api-client";
+
+describe("OctopusInstaller", () => {
+ let tempOutDir: string;
+ let releasesUrl: string;
+ let server: Server;
+
+ const msgs: string[] = [];
+ const logger: Logger = {
+ debug: (message) => {
+ msgs.push(message);
+ },
+ info: (message) => {
+ msgs.push(message);
+ },
+ warn: (message) => {
+ msgs.push(message);
+ },
+ error: (message, err) => {
+ if (err !== undefined) {
+ msgs.push(err.message);
+ } else {
+ msgs.push(message);
+ }
+ },
+ };
+
+ jest.setTimeout(100000);
+
+ beforeEach(async () => {
+ tempOutDir = await mkdtemp(path.join(os.tmpdir(), "octopus_"));
+ process.env["AGENT_TOOLSDIRECTORY"] = tempOutDir;
+ process.env["AGENT_TEMPDIRECTORY"] = tempOutDir;
+
+ const app = express();
+
+ app.get("/OctopusDeploy/cli/main/releases.json", (_, res) => {
+ const latestToolsPayload = `[
+ {
+ "tag_name": "v7.4.1",
+ "assets": [
+ {
+ "name": "octopus_7.4.1_windows_amd64.zip",
+ "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v7.4.1/octopus_7.4.1_windows_amd64.zip"
+ }
+ ]
+ },
+ {
+ "tag_name": "v8.0.0",
+ "assets": [
+ {
+ "name": "octopus_8.0.0_windows_amd64.zip",
+ "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v8.0.0/octopus_8.0.0_windows_amd64.zip"
+ }
+ ]
+ },
+ {
+ "tag_name": "v8.2.0",
+ "assets": [
+ {
+ "name": "octopus_8.2.0_windows_amd64.zip",
+ "browser_download_url": "http://localhost:${address.port}/OctopusDeploy/cli/releases/download/v8.2.0/octopus_8.2.0_windows_amd64.zip"
+ }
+ ]
+ }
+ ]`;
+
+ res.send(latestToolsPayload);
+ });
+
+ app.get("/OctopusDeploy/cli/releases/download/v7.4.1/octopus_7.4.1_windows_amd64.zip", (_, res) => {
+ res.sendStatus(200);
+ });
+
+ app.get("/OctopusDeploy/cli/releases/download/v8.0.0/octopus_8.0.0_windows_amd64.zip", (_, res) => {
+ res.sendStatus(200);
+ });
+
+ app.get("/OctopusDeploy/cli/releases/download/v8.2.0/octopus_8.2.0_windows_amd64.zip", (_, res) => {
+ res.sendStatus(200);
+ });
+
+ server = await new Promise((resolve) => {
+ const r = app.listen(() => {
+ resolve(r);
+ });
+ });
+
+ const address = server.address() as AddressInfo;
+ releasesUrl = `http://localhost:${address.port}/OctopusDeploy/cli/main/releases.json`;
+ });
+
+ afterEach(async () => {
+ await new Promise((resolve) => {
+ server.close(() => {
+ resolve();
+ });
+ });
+
+ await rm(tempOutDir, { recursive: true });
+ });
+
+ test("Installs specific version", async () => {
+ const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("8.0.0");
+ expect(result.version).toBe("8.0.0");
+ });
+
+ test("Installs wildcard version", async () => {
+ const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("7.*");
+ expect(result.version).toBe("7.4.1");
+ });
+
+ test("Installs latest of latest", async () => {
+ const result = await new DownloadEndpointRetriever(releasesUrl, "win32", "amd64", logger).getEndpoint("*");
+ expect(result.version).toBe("8.2.0");
+ });
+});
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.ts b/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.ts
new file mode 100644
index 00000000..a7b5515e
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/downloadEndpointRetriever.ts
@@ -0,0 +1,152 @@
+import * as tasks from "azure-pipelines-task-lib/task";
+import * as TypedRestClient from "typed-rest-client";
+import { IProxyConfiguration } from "typed-rest-client/Interfaces";
+import { OctopusCLIVersionResolver } from "./octopusCLIVersionResolver";
+import { Logger } from "@octopusdeploy/api-client";
+
+const downloadsRegEx =
+ /^.*_(?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)_(?linux|macOS|windows)_(?arm64|amd64).(?tar.gz|zip)$/i;
+
+type DownloadOption = {
+ version: string;
+ location: string;
+ extension: string;
+ platform?: string;
+ architecture?: string;
+};
+
+export interface Endpoint {
+ downloadUrl: string;
+ version: string;
+ architecture: string;
+}
+
+interface VersionsResponse {
+ versions: string[];
+ downloads: DownloadOption[];
+}
+
+interface GitHubRelease {
+ tag_name: string;
+ assets: GitHubReleaseAsset[];
+}
+
+interface GitHubReleaseAsset {
+ version: string;
+ name: string;
+ browser_download_url: string;
+}
+
+export class DownloadEndpointRetriever {
+ constructor(readonly releasesUrl: string, readonly osPlatform: string, readonly osArch: string, readonly logger: Logger) {}
+
+ public async getEndpoint(versionSpec: string): Promise {
+ this.logger.debug?.(`Attempting to contact ${this.releasesUrl} to find Octopus CLI tool version ${versionSpec}`);
+
+ const versionsResponse: VersionsResponse | null = await this.getVersions();
+ if (versionsResponse === null) {
+ throw Error(`Unable to get versions...`);
+ }
+
+ const version = new OctopusCLIVersionResolver(versionsResponse.versions).getVersion(versionSpec);
+ if (version === null) {
+ throw Error(`The version specified (${version}) is not available to download.`);
+ }
+
+ this.logger.debug?.(`Attempting to find Octopus CLI version ${version}`);
+
+ let platform = "linux";
+ switch (this.osPlatform) {
+ case "darwin":
+ platform = "macOS";
+ break;
+ case "win32":
+ platform = "windows";
+ break;
+ }
+
+ let arch = "amd64";
+ switch (this.osArch) {
+ case "arm":
+ case "arm64":
+ arch = "arm64";
+ break;
+ }
+
+ this.logger.debug?.(`Attempting download for platform '${platform}' and architecture ${this.osArch}`);
+
+ let downloadUrl: string | undefined;
+
+ for (const download of versionsResponse.downloads) {
+ if (download.version === version && download.platform === platform && download.architecture === arch) {
+ downloadUrl = download.location;
+ }
+ }
+
+ if (downloadUrl === undefined || downloadUrl === null) {
+ throw Error(`Failed to resolve endpoint URL to download: ${downloadUrl}`);
+ }
+
+ this.logger.debug?.(`Checking status of download url '${downloadUrl}'`);
+
+ const http = this.restClient();
+ const statusCode = (await http.client.head(downloadUrl)).message.statusCode;
+ if (statusCode !== 200) {
+ throw Error(`Octopus CLI version not found: ${version}`);
+ }
+
+ this.logger.info?.(`✓ Octopus CLI version found: ${version}`);
+ return { downloadUrl, version, architecture: arch };
+ }
+
+ async getVersions(): Promise {
+ const githubReleasesClient = this.restClient();
+
+ const releasesResponse = (await githubReleasesClient.get(this.releasesUrl)).result;
+ if (releasesResponse === null) {
+ return null;
+ }
+
+ const ext: string = this.osPlatform === "win32" ? "zip" : "tar.gz";
+
+ const downloads = releasesResponse.flatMap((v) =>
+ v.assets
+ .filter((a) => downloadsRegEx.test(a.name))
+ .map((a) => {
+ const matches = downloadsRegEx.exec(a.name);
+
+ return {
+ version: matches?.groups?.version || v.tag_name.slice(1),
+ location: a.browser_download_url,
+ extension: matches?.groups?.extension || `.${ext}`,
+ platform: matches?.groups?.platform || undefined,
+ architecture: matches?.groups?.architecture || undefined,
+ };
+ })
+ );
+ const versions = downloads.map((d) => d.version);
+ return {
+ versions,
+ downloads,
+ };
+ }
+
+ private restClient() {
+ const proxyConfiguration = tasks.getHttpProxyConfiguration(this.releasesUrl);
+ let proxySettings: IProxyConfiguration | undefined = undefined;
+
+ if (proxyConfiguration) {
+ this.logger.debug?.(
+ "Using agent configured proxy. If this command should not be sent via the agent's proxy, you might need to add or modify the agent's .proxybypass file. See https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/proxy#specify-proxy-bypass-urls."
+ );
+ proxySettings = {
+ proxyUrl: proxyConfiguration.proxyUrl,
+ proxyUsername: proxyConfiguration.proxyUsername,
+ proxyPassword: proxyConfiguration.proxyPassword,
+ proxyBypassHosts: proxyConfiguration.proxyBypassHosts,
+ };
+ }
+
+ return new TypedRestClient.RestClient("OctoTFS/Tasks", this.releasesUrl, undefined, { proxy: proxySettings });
+ }
+}
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/icon.png b/source/tasks/OctoInstaller/OctoInstallerV7/icon.png
new file mode 100644
index 00000000..295e6e85
Binary files /dev/null and b/source/tasks/OctoInstaller/OctoInstallerV7/icon.png differ
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/icon.svg b/source/tasks/OctoInstaller/OctoInstallerV7/icon.svg
new file mode 100644
index 00000000..f1a574a2
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/index.ts b/source/tasks/OctoInstaller/OctoInstallerV7/index.ts
new file mode 100644
index 00000000..3ef53562
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/index.ts
@@ -0,0 +1,35 @@
+import * as tasks from "azure-pipelines-task-lib/task";
+import { Logger } from "@octopusdeploy/api-client";
+import { Installer } from "./installer";
+import os from "os";
+
+async function run() {
+ try {
+ const version = tasks.getInput("octopusVersion", true) || "";
+
+ const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+ };
+
+ new Installer("https://raw.githubusercontent.com/OctopusDeploy/cli/main/releases.json", os.platform(), os.arch(), logger).run(version);
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error}`, true);
+ }
+ }
+}
+
+run();
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/installer.ts b/source/tasks/OctoInstaller/OctoInstallerV7/installer.ts
new file mode 100644
index 00000000..1ed24bcc
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/installer.ts
@@ -0,0 +1,56 @@
+import * as tools from "azure-pipelines-tool-lib";
+import * as tasks from "azure-pipelines-task-lib";
+import path from "path";
+import { executeWithSetResult } from "../../Utils/octopusTasks";
+import { DownloadEndpointRetriever, Endpoint } from "./downloadEndpointRetriever";
+import { Logger } from "@octopusdeploy/api-client";
+
+const TOOL_NAME = "octopus";
+
+export class Installer {
+ constructor(readonly releasesUrl: string, readonly osPlatform: string, readonly osArch: string, readonly logger: Logger) {}
+
+ public async run(versionSpec: string) {
+ await executeWithSetResult(
+ async () => {
+ const endpoint = await new DownloadEndpointRetriever(this.releasesUrl, this.osPlatform, this.osArch, this.logger).getEndpoint(versionSpec);
+ let toolPath = tools.findLocalTool(TOOL_NAME, endpoint.version);
+
+ if (!toolPath) {
+ toolPath = await this.installTool(endpoint);
+ toolPath = tools.findLocalTool(TOOL_NAME, endpoint.version);
+ }
+
+ tools.prependPath(toolPath);
+ },
+ `Installed octopus v${versionSpec}.`,
+ `Failed to install octopus v${versionSpec}.`
+ );
+ }
+
+ private async installTool(endpoint: Endpoint): Promise {
+ if (!endpoint.downloadUrl) {
+ throw Error(`Failed to download Octopus CLI tool version ${endpoint.version}.`);
+ }
+
+ const downloadPath = await tools.downloadTool(endpoint.downloadUrl);
+
+ //
+ // Extract
+ //
+ let extPath: string;
+ if (this.osPlatform == "win32") {
+ extPath = tasks.getVariable("Agent.TempDirectory") || "";
+ if (!extPath) {
+ throw new Error("Expected Agent.TempDirectory to be set");
+ }
+
+ extPath = path.join(extPath, "n"); // use as short a path as possible due to nested node_modules folders
+ extPath = await tools.extractZip(downloadPath, extPath);
+ } else {
+ extPath = await tools.extractTar(downloadPath);
+ }
+
+ return await tools.cacheDir(extPath, TOOL_NAME, endpoint.version);
+ }
+}
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.test.ts b/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.test.ts
new file mode 100644
index 00000000..eb33e38f
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.test.ts
@@ -0,0 +1,67 @@
+import { OctopusCLIVersionResolver } from "./octopusCLIVersionResolver";
+
+describe("OctopusCLIVersionFetcher tests", () => {
+ test("Gets latest", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]);
+
+ const version = fetcher.getVersion("*");
+
+ expect(version).toBe("2.1.0");
+ });
+
+ test("Fixed returns fixed version", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]);
+
+ const version = fetcher.getVersion("1.0.0");
+
+ expect(version).toBe("1.0.0");
+ });
+
+ test("When version no exists", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]);
+
+ expect(() => fetcher.getVersion("5.0.0")).toThrow();
+ });
+
+ test("Get latest minor", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0", "3.0.0"]);
+
+ const version = fetcher.getVersion("2.*");
+
+ expect(version).toBe("2.1.0");
+ });
+
+ test("Get latest patch", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "1.0.3", "2.1.0", "3.0.0"]);
+
+ const version = fetcher.getVersion("1.0.*");
+
+ expect(version).toBe("1.0.3");
+ });
+
+ test("When version spec if invalid", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0"]);
+
+ expect(() => fetcher.getVersion("*.*")).toThrow();
+
+ expect(() => fetcher.getVersion("*.2")).toThrow();
+
+ expect(() => fetcher.getVersion("sdfs")).toThrow();
+ });
+
+ test("Get latest major", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "2.0.0", "2.1.0", "3.0.0"]);
+
+ const version = fetcher.getVersion("2");
+
+ expect(version).toBe("2.1.0");
+ });
+
+ test("Get latest not pre-release", () => {
+ const fetcher = new OctopusCLIVersionResolver(["1.0.0", "1.0.3", "2.1.0", "3.0.0", "4.0.0-pre"]);
+
+ const version = fetcher.getVersion("*");
+
+ expect(version).toBe("3.0.0");
+ });
+});
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.ts b/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.ts
new file mode 100644
index 00000000..9cca5ca6
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/octopusCLIVersionResolver.ts
@@ -0,0 +1,72 @@
+import { maxSatisfying, valid } from "semver";
+
+export class OctopusCLIVersionResolver {
+ constructor(readonly versions: string[]) {}
+
+ getVersion(versionSpec: string): string {
+ if (versionSpec === "*") {
+ const version = maxSatisfying(this.versions, versionSpec);
+ if (!version) {
+ throw new Error(`A version satisfying '${versionSpec}' could not be found.`);
+ }
+
+ return version;
+ }
+
+ if (valid(versionSpec) === null) {
+ const parts = versionSpec.split(".");
+ if (parts.length > 3) {
+ throw new Error(`The '${versionSpec}' is an invalid version, a version needs to be a maximum of three parts.`);
+ }
+
+ if (parts.length === 1) {
+ const majorVersion = parts[0];
+ if (Number.isNaN(Number.parseInt(majorVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its major part.`);
+ }
+ }
+
+ if (parts.length === 2) {
+ const majorVersion = parts[0];
+ const minorVersion = parts[1];
+
+ // the major version number must be a number
+ if (Number.isNaN(Number.parseInt(majorVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number for its major part.`);
+ }
+
+ if (minorVersion !== "*" && Number.isNaN(Number.parseInt(minorVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its minor part.`);
+ }
+ }
+
+ if (parts.length === 3) {
+ const majorVersion = parts[0];
+ const minorVersion = parts[1];
+ const patchVersion = parts[2];
+
+ // the major version number must be a number
+ if (Number.isNaN(Number.parseInt(majorVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number for its major part.`);
+ }
+
+ // the minor version number must be a number
+ if (Number.isNaN(Number.parseInt(minorVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number for its minor part.`);
+ }
+
+ if (patchVersion !== "*" && Number.isNaN(Number.parseInt(patchVersion))) {
+ throw new Error(`The '${versionSpec}' version needs to specify a number or '*' for its patch part.`);
+ }
+ }
+ }
+
+ const version = maxSatisfying(this.versions, versionSpec);
+
+ if (!version) {
+ throw new Error(`A version satisfying '${versionSpec}' could not be found.`);
+ }
+
+ return version;
+ }
+}
diff --git a/source/tasks/OctoInstaller/OctoInstallerV7/task.json b/source/tasks/OctoInstaller/OctoInstallerV7/task.json
new file mode 100644
index 00000000..eeee4f13
--- /dev/null
+++ b/source/tasks/OctoInstaller/OctoInstallerV7/task.json
@@ -0,0 +1,47 @@
+{
+ "id": "57342b23-3a76-490a-8e78-25d4ade2f2e3",
+ "name": "OctoInstaller",
+ "friendlyName": "Octopus CLI Installer",
+ "description": "Install a specific version of the Octopus CLI (Go)",
+ "helpMarkDown": "Install a specific version of the Octopus CLI (Go)",
+ "category": "Tool",
+ "runsOn": [
+ "Agent",
+ "DeploymentGroup"
+ ],
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "satisfies": ["octopus"],
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "advanced",
+ "displayName": "Advanced Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "octopusVersion",
+ "type": "string",
+ "label": "Octopus CLI Version",
+ "required": true,
+ "helpMarkDown": "Specify version of Octopus CLI to install.
Versions can be given in the following formats`1.*` => Install latest in major version.`7.3.*` => Install latest in major and minor version.`8.0.1` => Install exact version.`*` => Install whatever is latest.
Find the value of `version` for installing Octopus CLI, from the [this link](https://raw.githubusercontent.com/OctopusDeploy/cli/main/releases.json)."
+ }
+ ],
+ "instanceNameFormat": "Install Octopus CLI tool version $(version)",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
diff --git a/source/tasks/PackNuGet/PackNuGetV7/create-package.ts b/source/tasks/PackNuGet/PackNuGetV7/create-package.ts
new file mode 100644
index 00000000..4760a8d1
--- /dev/null
+++ b/source/tasks/PackNuGet/PackNuGetV7/create-package.ts
@@ -0,0 +1,38 @@
+import { Logger, NuGetPackageBuilder, NuGetPackArgs } from "@octopusdeploy/api-client";
+import path from "path";
+import fs from "fs";
+import { InputParameters } from "./input-parameters";
+import { isNullOrWhitespace } from "../../../tasksLegacy/Utils/inputs";
+
+type createPackageResult = {
+ filePath: string;
+ filename: string;
+};
+
+export async function createPackageFromInputs(parameters: InputParameters, logger: Logger): Promise {
+ const builder = new NuGetPackageBuilder();
+ const inputs: NuGetPackArgs = {
+ packageId: parameters.packageId,
+ version: parameters.packageVersion,
+ outputFolder: parameters.outputPath,
+ basePath: parameters.sourcePath,
+ inputFilePatterns: parameters.include,
+ overwrite: parameters.overwrite,
+ logger,
+ };
+
+ inputs.nuspecArgs = {
+ title: parameters.nuGetTitle,
+ description: parameters.nuGetDescription,
+ authors: parameters.nuGetAuthors,
+ releaseNotes: parameters.nuGetReleaseNotes,
+ };
+
+ if (!isNullOrWhitespace(parameters.nuGetReleaseNotesFile) && fs.existsSync(parameters.nuGetReleaseNotesFile) && fs.lstatSync(parameters.nuGetReleaseNotesFile).isFile()) {
+ inputs.nuspecArgs.releaseNotes = fs.readFileSync(parameters.nuGetReleaseNotesFile).toString();
+ }
+
+ const packageFilename = await builder.pack(inputs);
+
+ return { filePath: path.join(parameters.outputPath, packageFilename), filename: packageFilename };
+}
diff --git a/source/tasks/PackNuGet/PackNuGetV7/icon.png b/source/tasks/PackNuGet/PackNuGetV7/icon.png
new file mode 100644
index 00000000..65e99099
Binary files /dev/null and b/source/tasks/PackNuGet/PackNuGetV7/icon.png differ
diff --git a/source/tasks/PackNuGet/PackNuGetV7/icon.svg b/source/tasks/PackNuGet/PackNuGetV7/icon.svg
new file mode 100644
index 00000000..69e50017
--- /dev/null
+++ b/source/tasks/PackNuGet/PackNuGetV7/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/source/tasks/PackNuGet/PackNuGetV7/index.ts b/source/tasks/PackNuGet/PackNuGetV7/index.ts
new file mode 100644
index 00000000..bb22cd92
--- /dev/null
+++ b/source/tasks/PackNuGet/PackNuGetV7/index.ts
@@ -0,0 +1,42 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import * as tasks from "azure-pipelines-task-lib/task";
+import { Logger } from "@octopusdeploy/api-client";
+import { getInputs } from "./input-parameters";
+import os from "os";
+import { createPackageFromInputs } from "./create-package";
+
+async function run() {
+ try {
+ const parameters = getInputs();
+
+ const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+ };
+
+ const result = await createPackageFromInputs(parameters, logger);
+
+ tasks.setVariable("package_file_path", result.filePath);
+ tasks.setVariable("package_filename", result.filename);
+
+ tasks.setResult(tasks.TaskResult.Succeeded, "Pack succeeded");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error}`, true);
+ }
+ }
+}
+
+run();
diff --git a/source/tasks/PackNuGet/PackNuGetV7/input-parameters.ts b/source/tasks/PackNuGet/PackNuGetV7/input-parameters.ts
new file mode 100644
index 00000000..475182dc
--- /dev/null
+++ b/source/tasks/PackNuGet/PackNuGetV7/input-parameters.ts
@@ -0,0 +1,33 @@
+import * as tasks from "azure-pipelines-task-lib/task";
+import { removeTrailingSlashes, safeTrim } from "tasksLegacy/Utils/inputs";
+import { getLineSeparatedItems } from "../../Utils/inputs";
+
+export interface InputParameters {
+ packageId: string;
+ packageVersion: string;
+ outputPath: string;
+ sourcePath: string;
+ include: string[];
+ nuGetDescription: string;
+ nuGetAuthors: string[];
+ nuGetTitle?: string;
+ nuGetReleaseNotes?: string;
+ nuGetReleaseNotesFile?: string;
+ overwrite?: boolean;
+}
+
+export const getInputs = (): InputParameters => {
+ return {
+ packageId: tasks.getInput("PackageId", true) || "",
+ packageVersion: tasks.getInput("PackageVersion", true) || "",
+ outputPath: removeTrailingSlashes(safeTrim(tasks.getPathInput("OutputPath"))) || ".",
+ sourcePath: removeTrailingSlashes(safeTrim(tasks.getPathInput("SourcePath"))) || ".",
+ include: getLineSeparatedItems(tasks.getInput("Include") || "**"),
+ nuGetDescription: tasks.getInput("NuGetDescription", true) || "",
+ nuGetAuthors: getLineSeparatedItems(tasks.getInput("NuGetAuthors", true) || ""),
+ nuGetTitle: tasks.getInput("NuGetTitle"),
+ nuGetReleaseNotes: tasks.getInput("NuGetReleaseNotes"),
+ nuGetReleaseNotesFile: tasks.getInput("NuGetReleaseNotesFile", false),
+ overwrite: tasks.getBoolInput("Overwrite"),
+ };
+};
diff --git a/source/tasks/PackNuGet/PackNuGetV7/task.json b/source/tasks/PackNuGet/PackNuGetV7/task.json
new file mode 100644
index 00000000..4159e4ee
--- /dev/null
+++ b/source/tasks/PackNuGet/PackNuGetV7/task.json
@@ -0,0 +1,131 @@
+{
+ "id": "72e7a1b6-19bc-48e6-8d20-a81f201d65a3",
+ "name": "OctopusPackNuGet",
+ "friendlyName": "Package Application for Octopus - NuGet",
+ "description": "Package your application into a NuGet file.",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Package",
+ "visibility": ["Build", "Release"],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "advanced",
+ "displayName": "Advanced Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "PackageId",
+ "type": "string",
+ "label": "Package ID",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The ID of the package. e.g. MyCompany.App"
+ },
+ {
+ "name": "PackageVersion",
+ "type": "string",
+ "label": "Package Version",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The version of the package; must be a valid [SemVer](http://semver.org/) version."
+ },
+ {
+ "name": "SourcePath",
+ "type": "filePath",
+ "label": "Source Path",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The folder containing the files and folders to package. Defaults to working directory."
+ },
+ {
+ "name": "OutputPath",
+ "type": "filePath",
+ "label": "Output Path",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The directory into which the generated package will be written. Defaults to working directory."
+ },
+ {
+ "name": "Include",
+ "type": "multiLine",
+ "label": "Include",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "File patterns to include, relative to the root path. e.g. /bin/*.dll. Defaults to '**' if not specified."
+ },
+ {
+ "name": "NuGetDescription",
+ "type": "string",
+ "label": "Description",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "A description to add to the NuGet package metadata."
+ },
+ {
+ "name": "NuGetAuthors",
+ "type": "multiLine",
+ "label": "Authors",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Authors to add to the NuGet package metadata."
+ },
+ {
+ "name": "NuGetTitle",
+ "type": "string",
+ "label": "Title",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "A title to add to the NuGet package metadata."
+ },
+ {
+ "name": "NuGetReleaseNotes",
+ "type": "string",
+ "label": "Release Notes",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Release notes to add to the NuGet package metadata."
+ },
+ {
+ "name": "NuGetReleaseNotesFile",
+ "type": "filePath",
+ "label": "Release Notes File",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "A file containing release notes to add to the NuGet package metadata. Overrides `NuGetReleaseNotes`."
+ },
+ {
+ "name": "Overwrite",
+ "type": "boolean",
+ "label": "Overwrite",
+ "defaultValue": "false",
+ "required": false,
+ "helpMarkDown": "Allow an existing package of the same ID and version to be overwritten.",
+ "groupName": "advanced"
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "package_file_path",
+ "description": "The full path to the package file that was created."
+ },
+ {
+ "name": "package_filename",
+ "description": "The filename of the package that was created."
+ }
+ ],
+ "instanceNameFormat": "Package NuGet $(PackageId)",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
diff --git a/source/tasks/PackZip/PackZipV7/create-package.ts b/source/tasks/PackZip/PackZipV7/create-package.ts
new file mode 100644
index 00000000..ade00cbe
--- /dev/null
+++ b/source/tasks/PackZip/PackZipV7/create-package.ts
@@ -0,0 +1,23 @@
+import { Logger, ZipPackageBuilder } from "@octopusdeploy/api-client";
+import path from "path";
+import { InputParameters } from "./input-parameters";
+
+type createPackageResult = {
+ filePath: string;
+ filename: string;
+};
+
+export async function createPackageFromInputs(parameters: InputParameters, logger: Logger): Promise {
+ const builder = new ZipPackageBuilder();
+ const packageFilename = await builder.pack({
+ packageId: parameters.packageId,
+ version: parameters.packageVersion,
+ outputFolder: parameters.outputPath,
+ basePath: parameters.sourcePath,
+ inputFilePatterns: parameters.include,
+ overwrite: parameters.overwrite,
+ logger,
+ });
+
+ return { filePath: path.join(parameters.outputPath, packageFilename), filename: packageFilename };
+}
diff --git a/source/tasks/PackZip/PackZipV7/icon.png b/source/tasks/PackZip/PackZipV7/icon.png
new file mode 100644
index 00000000..65e99099
Binary files /dev/null and b/source/tasks/PackZip/PackZipV7/icon.png differ
diff --git a/source/tasks/PackZip/PackZipV7/icon.svg b/source/tasks/PackZip/PackZipV7/icon.svg
new file mode 100644
index 00000000..69e50017
--- /dev/null
+++ b/source/tasks/PackZip/PackZipV7/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/source/tasks/PackZip/PackZipV7/index.ts b/source/tasks/PackZip/PackZipV7/index.ts
new file mode 100644
index 00000000..bb22cd92
--- /dev/null
+++ b/source/tasks/PackZip/PackZipV7/index.ts
@@ -0,0 +1,42 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import * as tasks from "azure-pipelines-task-lib/task";
+import { Logger } from "@octopusdeploy/api-client";
+import { getInputs } from "./input-parameters";
+import os from "os";
+import { createPackageFromInputs } from "./create-package";
+
+async function run() {
+ try {
+ const parameters = getInputs();
+
+ const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+ };
+
+ const result = await createPackageFromInputs(parameters, logger);
+
+ tasks.setVariable("package_file_path", result.filePath);
+ tasks.setVariable("package_filename", result.filename);
+
+ tasks.setResult(tasks.TaskResult.Succeeded, "Pack succeeded");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute pack. ${error}`, true);
+ }
+ }
+}
+
+run();
diff --git a/source/tasks/PackZip/PackZipV7/input-parameters.ts b/source/tasks/PackZip/PackZipV7/input-parameters.ts
new file mode 100644
index 00000000..cc11f5a0
--- /dev/null
+++ b/source/tasks/PackZip/PackZipV7/input-parameters.ts
@@ -0,0 +1,23 @@
+import * as tasks from "azure-pipelines-task-lib/task";
+import { removeTrailingSlashes, safeTrim } from "tasksLegacy/Utils/inputs";
+import { getLineSeparatedItems } from "../../Utils/inputs";
+
+export interface InputParameters {
+ packageId: string;
+ packageVersion: string;
+ outputPath: string;
+ sourcePath: string;
+ include: string[];
+ overwrite?: boolean;
+}
+
+export const getInputs = (): InputParameters => {
+ return {
+ packageId: tasks.getInput("PackageId", true) || "",
+ packageVersion: tasks.getInput("PackageVersion", true) || "",
+ outputPath: removeTrailingSlashes(safeTrim(tasks.getPathInput("OutputPath"))) || ".",
+ sourcePath: removeTrailingSlashes(safeTrim(tasks.getPathInput("SourcePath"))) || ".",
+ include: getLineSeparatedItems(tasks.getInput("Include") || "**"),
+ overwrite: tasks.getBoolInput("Overwrite"),
+ };
+};
diff --git a/source/tasks/PackZip/PackZipV7/task.json b/source/tasks/PackZip/PackZipV7/task.json
new file mode 100644
index 00000000..8eaad2c4
--- /dev/null
+++ b/source/tasks/PackZip/PackZipV7/task.json
@@ -0,0 +1,91 @@
+{
+ "id": "3f248d80-a755-498d-863c-f936c5821318",
+ "name": "OctopusPackZip",
+ "friendlyName": "Package Application for Octopus - Zip",
+ "description": "Package your application into a Zip file.",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Package",
+ "visibility": ["Build", "Release"],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "groups": [
+ {
+ "name": "advanced",
+ "displayName": "Advanced Options",
+ "isExpanded": false
+ }
+ ],
+ "inputs": [
+ {
+ "name": "PackageId",
+ "type": "string",
+ "label": "Package ID",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The ID of the package. e.g. MyCompany.App"
+ },
+ {
+ "name": "PackageVersion",
+ "type": "string",
+ "label": "Package Version",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The version of the package; must be a valid [SemVer](http://semver.org/) version."
+ },
+ {
+ "name": "SourcePath",
+ "type": "filePath",
+ "label": "Source Path",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The folder containing the files and folders to package. Defaults to working directory."
+ },
+ {
+ "name": "OutputPath",
+ "type": "filePath",
+ "label": "Output Path",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The directory into which the generated package will be written. Defaults to working directory."
+ },
+ {
+ "name": "Include",
+ "type": "multiLine",
+ "label": "Include",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "File patterns to include, relative to the root path. e.g. /bin/*.dll"
+ },
+ {
+ "name": "Overwrite",
+ "type": "boolean",
+ "label": "Overwrite",
+ "defaultValue": "false",
+ "required": false,
+ "helpMarkDown": "Allow an existing package of the same ID and version to be overwritten.",
+ "groupName": "advanced"
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "package_file_path",
+ "description": "The full path to the package file that was created."
+ },
+ {
+ "name": "package_filename",
+ "description": "The filename of the package that was created."
+ }
+ ],
+ "instanceNameFormat": "Package Zip $(PackageId)",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
diff --git a/source/tasks/Push/PushV7/icon.png b/source/tasks/Push/PushV7/icon.png
new file mode 100644
index 00000000..55087619
Binary files /dev/null and b/source/tasks/Push/PushV7/icon.png differ
diff --git a/source/tasks/Push/PushV7/icon.svg b/source/tasks/Push/PushV7/icon.svg
new file mode 100644
index 00000000..9b9c1c04
--- /dev/null
+++ b/source/tasks/Push/PushV7/icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/source/tasks/Push/PushV7/index.ts b/source/tasks/Push/PushV7/index.ts
new file mode 100644
index 00000000..6aac5bec
--- /dev/null
+++ b/source/tasks/Push/PushV7/index.ts
@@ -0,0 +1,43 @@
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { getLineSeparatedItems, getOverwriteModeFromReplaceInput, getRequiredInput } from "../../Utils/inputs";
+import { Push } from "./push";
+import os from "os";
+import { getClient } from "../../Utils/client";
+
+async function run() {
+ try {
+ const spaceName = getRequiredInput("Space");
+ const packages = getLineSeparatedItems(tasks.getInput("Packages", true) || "");
+ const overwriteMode = getOverwriteModeFromReplaceInput(tasks.getInput("Replace", true) || "");
+
+ const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+ const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+ };
+
+ const client = await getClient(connection, logger, "package", "push", 6);
+ await new Push(client).run(spaceName, packages, overwriteMode);
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute push. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ tasks.setResult(tasks.TaskResult.Failed, `"Failed to execute push. ${error}`, true);
+ }
+ }
+}
+
+run();
diff --git a/source/tasks/Push/PushV7/push.ts b/source/tasks/Push/PushV7/push.ts
new file mode 100644
index 00000000..7e38dfe6
--- /dev/null
+++ b/source/tasks/Push/PushV7/push.ts
@@ -0,0 +1,42 @@
+import { ReplaceOverwriteMode } from "../../Utils/inputs";
+import { Client, OverwriteMode, PackageRepository } from "@octopusdeploy/api-client";
+import glob from "glob";
+
+export class Push {
+ constructor(readonly client: Client) {}
+
+ public async run(spaceName: string, packages: string[], overwriteMode: ReplaceOverwriteMode) {
+ const matchedPackages = await this.resolveGlobs(packages);
+
+ let mappedOverwriteMode = OverwriteMode.FailIfExists;
+ if (overwriteMode === ReplaceOverwriteMode.true) {
+ mappedOverwriteMode = OverwriteMode.OverwriteExisting;
+ } else if (overwriteMode === ReplaceOverwriteMode.IgnoreIfExists) {
+ mappedOverwriteMode = OverwriteMode.IgnoreIfExists;
+ }
+
+ const repository = new PackageRepository(this.client, spaceName);
+ await repository.push(matchedPackages, mappedOverwriteMode);
+
+ return packages;
+ }
+
+ private resolveGlobs = async (globs: string[]): Promise => {
+ const globResults = await Promise.all(globs.map(this.pGlobNoNull));
+ const results = ([] as string[]).concat(...globResults);
+
+ return results;
+ };
+
+ private pGlobNoNull = (pattern: string): Promise => {
+ return new Promise((resolve, reject) => {
+ glob(pattern, { nonull: true }, (err, matches) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+ resolve(matches);
+ });
+ });
+ };
+}
diff --git a/source/tasks/Push/PushV7/task.json b/source/tasks/Push/PushV7/task.json
new file mode 100644
index 00000000..523a8e89
--- /dev/null
+++ b/source/tasks/Push/PushV7/task.json
@@ -0,0 +1,65 @@
+{
+ "id": "d05ad9a2-5d9e-4a1c-a887-14034334d6f2",
+ "name": "OctopusPush",
+ "friendlyName": "Push Package(s) to Octopus",
+ "description": "Push your NuGet or Zip package to your Octopus Deploy Server. **v6 of this step requires Octopus 2022.3+**",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Package",
+ "visibility": [
+ "Build",
+ "Release"
+ ],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space Name",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space name within Octopus."
+ },
+ {
+ "name": "Packages",
+ "type": "multiLine",
+ "label": "Packages",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Package files to push. To push multiple packages, enter on multiple lines."
+ },
+ {
+ "name": "Replace",
+ "type": "pickList",
+ "label": "Overwrite Mode",
+ "defaultValue": "false",
+ "required": true,
+ "helpMarkDown": "Normally, if the same package already exists on the server, the server will reject the package push. This is a good practice as it ensures a package isn't accidentally overwritten or ignored. Use this setting to override this behavior.",
+ "options": {
+ "false": "Fail if exists",
+ "true": "Overwrite existing",
+ "IgnoreIfExists": "Ignore if exists"
+ }
+ }
+ ],
+ "instanceNameFormat": "Push Packages to Octopus",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/tasks/RunRunbook/RunRunbookV7/icon.png b/source/tasks/RunRunbook/RunRunbookV7/icon.png
new file mode 100644
index 00000000..3b34013f
Binary files /dev/null and b/source/tasks/RunRunbook/RunRunbookV7/icon.png differ
diff --git a/source/tasks/RunRunbook/RunRunbookV7/icon.svg b/source/tasks/RunRunbook/RunRunbookV7/icon.svg
new file mode 100644
index 00000000..d75de5a3
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/source/tasks/RunRunbook/RunRunbookV7/index.ts b/source/tasks/RunRunbook/RunRunbookV7/index.ts
new file mode 100644
index 00000000..77852885
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/index.ts
@@ -0,0 +1,26 @@
+import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection";
+import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput";
+import { Logger } from "@octopusdeploy/api-client";
+import * as tasks from "azure-pipelines-task-lib/task";
+import { RunbookRun } from "./runbookRun";
+
+const connection = getDefaultOctopusConnectionDetailsOrThrow();
+
+const logger: Logger = {
+ debug: (message) => {
+ tasks.debug(message);
+ },
+ info: (message) => console.log(message),
+ warn: (message) => tasks.warning(message),
+ error: (message, err) => {
+ if (err !== undefined) {
+ tasks.error(err.message);
+ } else {
+ tasks.error(message);
+ }
+ },
+};
+
+const task: TaskWrapper = new ConcreteTaskWrapper();
+
+new RunbookRun(connection, task, logger).run();
diff --git a/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.test.ts b/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.test.ts
new file mode 100644
index 00000000..aeb6e80f
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.test.ts
@@ -0,0 +1,62 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { createCommandFromInputs, CreateGitRunbookRunCommandV1, isCreateGitRunbookRunCommand } from "./inputCommandBuilder";
+import { MockTaskWrapper } from "../../Utils/MockTaskWrapper";
+
+describe("getInputCommand", () => {
+ let logger: Logger;
+ let task: MockTaskWrapper;
+ beforeEach(() => {
+ logger = {};
+ task = new MockTaskWrapper();
+ });
+
+ test("all regular fields supplied", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("Runbook", "A runbook");
+ task.addVariableString("Environments", "dev\nStaging");
+ task.addVariableString("Tenants", "Tenant 1\nTenant 2");
+ task.addVariableString("TenantTags", "tag set 1/tag 1\ntag set 1/tag 2");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(isCreateGitRunbookRunCommand(command)).toBe(false);
+ expect(command.spaceName).toBe("Default");
+ expect(command.ProjectName).toBe("Awesome project");
+ expect(command.RunbookName).toBe("A runbook");
+ expect(command.EnvironmentNames).toStrictEqual(["dev", "Staging"]);
+ expect(command.Tenants).toStrictEqual(["Tenant 1", "Tenant 2"]);
+ expect(command.TenantTags).toStrictEqual(["tag set 1/tag 1", "tag set 1/tag 2"]);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+
+ test("when gitRef is supplied, the command contains the ref plus all regular fields supplied", () => {
+ task.addVariableString("Space", "Default");
+ task.addVariableString("Project", "Awesome project");
+ task.addVariableString("Runbook", "A runbook");
+ task.addVariableString("Environments", "dev\nStaging");
+ task.addVariableString("Tenants", "Tenant 1\nTenant 2");
+ task.addVariableString("TenantTags", "tag set 1/tag 1\ntag set 1/tag 2");
+ task.addVariableString("Variables", "var1: value1\nvar2: value2");
+ task.addVariableString("GitRef", "some-ref");
+
+ const command = createCommandFromInputs(logger, task);
+ expect(isCreateGitRunbookRunCommand(command)).toBe(true);
+ expect(command.spaceName).toBe("Default");
+ expect(command.ProjectName).toBe("Awesome project");
+ expect(command.RunbookName).toBe("A runbook");
+ expect(command.EnvironmentNames).toStrictEqual(["dev", "Staging"]);
+ expect(command.Tenants).toStrictEqual(["Tenant 1", "Tenant 2"]);
+ expect(command.TenantTags).toStrictEqual(["tag set 1/tag 1", "tag set 1/tag 2"]);
+ expect(command.Variables).toStrictEqual({ var1: "value1", var2: "value2" });
+ expect((command as CreateGitRunbookRunCommandV1).GitRef).toBe("some-ref");
+
+ expect(task.lastResult).toBeUndefined();
+ expect(task.lastResultMessage).toBeUndefined();
+ expect(task.lastResultDone).toBeUndefined();
+ });
+});
diff --git a/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.ts b/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.ts
new file mode 100644
index 00000000..0835d872
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/inputCommandBuilder.ts
@@ -0,0 +1,74 @@
+import { getLineSeparatedItems } from "../../Utils/inputs";
+import { CreateRunbookRunCommandV1, Logger, PromptedVariableValues } from "@octopusdeploy/api-client";
+import { RunGitRunbookCommand } from "@octopusdeploy/api-client/dist/features/projects/runbooks/runs/RunGitRunbookCommand";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+
+// The api-client doesn't have a type for this command that we can differentiate from CreateRunbookRunCommandV1
+// so we'll wrap it to make things easier.
+export type CreateGitRunbookRunCommandV1 = RunGitRunbookCommand & {
+ GitRef: string;
+};
+
+export function isCreateGitRunbookRunCommand(command: CreateRunbookRunCommandV1 | CreateGitRunbookRunCommandV1): command is CreateGitRunbookRunCommandV1 {
+ return (command as CreateGitRunbookRunCommandV1).GitRef !== undefined;
+}
+
+export function createCommandFromInputs(logger: Logger, task: TaskWrapper): CreateRunbookRunCommandV1 | CreateGitRunbookRunCommandV1 {
+ const variablesMap: PromptedVariableValues | undefined = {};
+
+ const variablesField = task.getInput("Variables");
+ logger.debug?.("Variables: " + variablesField);
+ if (variablesField) {
+ const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined;
+ if (variables) {
+ for (const variable of variables) {
+ const variableMap = variable.split(":").map((x) => x.trim());
+ variablesMap[variableMap[0]] = variableMap[1];
+ }
+ }
+ }
+
+ const environmentsField = task.getInput("Environments", true);
+ logger.debug?.("Environments: " + environmentsField);
+ const tenantsField = task.getInput("Tenants");
+ logger.debug?.("Tenants: " + tenantsField);
+ const tagsField = task.getInput("TenantTags");
+ logger.debug?.("Tenant Tags: " + tagsField);
+ const tags = getLineSeparatedItems(tagsField || "")?.map((t: string) => t.trim()) || [];
+
+ const gitRef = task.getInput("GitRef");
+ logger.debug?.("GitRef: " + gitRef);
+
+ if (gitRef) {
+ const command: CreateGitRunbookRunCommandV1 = {
+ spaceName: task.getInput("Space") || "",
+ ProjectName: task.getInput("Project", true) || "",
+ RunbookName: task.getInput("Runbook", true) || "",
+ EnvironmentNames: getLineSeparatedItems(environmentsField || "")?.map((t: string) => t.trim()) || [],
+ Tenants: getLineSeparatedItems(tenantsField || "")?.map((t: string) => t.trim()) || [],
+ TenantTags: tags,
+ UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined,
+ Variables: variablesMap || undefined,
+ GitRef: gitRef,
+ };
+
+ logger.debug?.(JSON.stringify(command));
+
+ return command;
+ }
+
+ const command: CreateRunbookRunCommandV1 = {
+ spaceName: task.getInput("Space") || "",
+ ProjectName: task.getInput("Project", true) || "",
+ RunbookName: task.getInput("Runbook", true) || "",
+ EnvironmentNames: getLineSeparatedItems(environmentsField || "")?.map((t: string) => t.trim()) || [],
+ Tenants: getLineSeparatedItems(tenantsField || "")?.map((t: string) => t.trim()) || [],
+ TenantTags: tags,
+ UseGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined,
+ Variables: variablesMap || undefined,
+ };
+
+ logger.debug?.(JSON.stringify(command));
+
+ return command;
+}
diff --git a/source/tasks/RunRunbook/RunRunbookV7/runRunbook.ts b/source/tasks/RunRunbook/RunRunbookV7/runRunbook.ts
new file mode 100644
index 00000000..564e4082
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/runRunbook.ts
@@ -0,0 +1,70 @@
+import { Client, CreateRunbookRunCommandV1, RunbookRunRepository, Logger, TenantRepository, EnvironmentRepository, CreateRunbookRunResponseV1 } from "@octopusdeploy/api-client";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { ExecutionResult } from "../../Utils/executionResult";
+import { CreateGitRunbookRunCommandV1, isCreateGitRunbookRunCommand } from "./inputCommandBuilder";
+import { RunGitRunbookResponse } from "@octopusdeploy/api-client/dist/features/projects/runbooks/runs/RunGitRunbookResponse";
+
+export async function createRunbookRunFromInputs(client: Client, command: CreateRunbookRunCommandV1 | CreateGitRunbookRunCommandV1, task: TaskWrapper, logger: Logger): Promise {
+ logger.info?.("🐙 Running a Runbook in Octopus Deploy...");
+
+ try {
+ const repository = new RunbookRunRepository(client, command.spaceName);
+
+ const response = await createRunbookRun(repository, command);
+
+ logger.info?.(`🎉 ${response.RunbookRunServerTasks.length} Run${response.RunbookRunServerTasks.length > 1 ? "s" : ""} queued successfully!`);
+
+ if (response.RunbookRunServerTasks.length === 0) {
+ throw new Error("Expected at least one run to be queued.");
+ }
+ if (response.RunbookRunServerTasks[0].ServerTaskId === null || response.RunbookRunServerTasks[0].ServerTaskId === undefined) {
+ throw new Error("Server task id was not deserialized correctly.");
+ }
+
+ const runbookRunIds = response.RunbookRunServerTasks.map((x) => x.RunbookRunId);
+
+ const runs = await repository.list({ ids: runbookRunIds, take: runbookRunIds.length });
+
+ const envIds = runs.Items.map((d) => d.EnvironmentId || "");
+ logger.debug?.(`Environment Ids: ${envIds.join(", ")}`);
+ const envRepository = new EnvironmentRepository(client, command.spaceName);
+ const envs = await envRepository.list({ ids: envIds, take: envIds.length });
+
+ const tenantIds = runs.Items.map((d) => d.TenantId || "");
+ logger.debug?.(`Tenant Ids: ${tenantIds.join(", ")}`);
+ const tenantRepository = new TenantRepository(client, command.spaceName);
+ const tenants = await tenantRepository.list({ ids: tenantIds, take: tenantIds.length });
+
+ const results = response.RunbookRunServerTasks.map((x) => {
+ const filteredTenants = tenants.Items.filter((e) => e.Id === runs.Items.filter((d) => d.TaskId === x.ServerTaskId)[0].TenantId);
+ const tenantName = filteredTenants.length > 0 ? filteredTenants[0].Name : null;
+ return {
+ serverTaskId: x.ServerTaskId,
+ environmentName: envs.Items.filter((e) => e.Id === runs.Items.filter((d) => d.TaskId === x.ServerTaskId)[0].EnvironmentId)[0].Name,
+ tenantName: tenantName,
+ type: "Runbook run",
+ } as ExecutionResult;
+ });
+
+ const tasksJson = JSON.stringify(results);
+ logger.debug?.(`server_tasks: ${tasksJson}`);
+ task.setOutputVariable("server_tasks", tasksJson);
+
+ return results;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ task.setFailure(`"Failed to execute command. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ task.setFailure(`"Failed to execute command. ${error}`, true);
+ }
+ throw error;
+ }
+}
+
+async function createRunbookRun(repository: RunbookRunRepository, command: CreateRunbookRunCommandV1 | CreateGitRunbookRunCommandV1): Promise {
+ if (isCreateGitRunbookRunCommand(command)) {
+ return await repository.createGit(command, command.GitRef);
+ }
+ return await repository.create(command);
+}
diff --git a/source/tasks/RunRunbook/RunRunbookV7/runbookRun.ts b/source/tasks/RunRunbook/RunRunbookV7/runbookRun.ts
new file mode 100644
index 00000000..16dbd379
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/runbookRun.ts
@@ -0,0 +1,29 @@
+import { Logger } from "@octopusdeploy/api-client";
+import { OctoServerConnectionDetails } from "../../Utils/connection";
+import { createRunbookRunFromInputs } from "./runRunbook";
+import { createCommandFromInputs } from "./inputCommandBuilder";
+import os from "os";
+import { TaskWrapper } from "tasks/Utils/taskInput";
+import { getClient } from "../../Utils/client";
+
+export class RunbookRun {
+ constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {}
+
+ public async run() {
+ try {
+ const command = createCommandFromInputs(this.logger, this.task);
+ const client = await getClient(this.connection, this.logger, "runbook", "run", 6);
+
+ createRunbookRunFromInputs(client, command, this.task, this.logger);
+
+ this.task.setSuccess("Runbook run succeeded.");
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ this.task.setFailure(`"Failed to successfully run runbook. ${error.message}${os.EOL}${error.stack}`, true);
+ } else {
+ this.task.setFailure(`"Failed to successfully run runbook. ${error}`, true);
+ }
+ throw error;
+ }
+ }
+}
diff --git a/source/tasks/RunRunbook/RunRunbookV7/task.json b/source/tasks/RunRunbook/RunRunbookV7/task.json
new file mode 100644
index 00000000..b0a9b7a5
--- /dev/null
+++ b/source/tasks/RunRunbook/RunRunbookV7/task.json
@@ -0,0 +1,111 @@
+{
+ "id": "5a2273e0-aa4f-4502-bcba-6817835e2bbd",
+ "name": "OctopusRunRunbook",
+ "friendlyName": "Run Octopus Runbook",
+ "description": "Run an Octopus Deploy Runbook",
+ "helpMarkDown": "set-by-pack.ps1",
+ "category": "Deploy",
+ "visibility": ["Build", "Release"],
+ "author": "Octopus Deploy",
+ "version": {
+ "Major": 7,
+ "Minor": 0,
+ "Patch": 0
+ },
+ "demands": [],
+ "minimumAgentVersion": "3.232.1",
+ "inputs": [
+ {
+ "name": "OctoConnectedServiceName",
+ "type": "connectedService:OctopusEndpoint",
+ "label": "Octopus Deploy Server",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "Octopus Deploy server connection"
+ },
+ {
+ "name": "Space",
+ "type": "string",
+ "label": "Space",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The space within Octopus. This must be the name of the space, not the id."
+ },
+ {
+ "name": "Project",
+ "type": "string",
+ "label": "Project",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The project within Octopus. This must be the name of the project, not the id."
+ },
+ {
+ "name": "Runbook",
+ "type": "string",
+ "label": "Runbook",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The name of the runbook to run."
+ },
+ {
+ "name": "Environments",
+ "type": "multiLine",
+ "label": "Environment(s)",
+ "defaultValue": "",
+ "required": true,
+ "helpMarkDown": "The environment names to run the runbook for."
+ },
+ {
+ "name": "GitRef",
+ "type": "string",
+ "label": "Git Reference",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The Git reference to run the runbook for. Only applies when runbooks are stored in a Git repository for config-as-code enabled projects."
+ },
+ {
+ "name": "Tenants",
+ "type": "multiLine",
+ "label": "Tenant(s)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "The tenant names to run the runbook for."
+ },
+ {
+ "name": "TenantTags",
+ "type": "multiLine",
+ "label": "Tenant tag(s)",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Run for all tenants with the given tag(s)."
+ },
+ {
+ "name": "Variables",
+ "type": "multiLine",
+ "label": "Values for prompted variables",
+ "defaultValue": "",
+ "required": false,
+ "helpMarkDown": "Variable values to pass to the run, use syntax `variable: value`."
+ },
+ {
+ "name": "UseGuidedFailure",
+ "type": "boolean",
+ "label": "Use guided failure",
+ "defaultValue": "False",
+ "required": false,
+ "helpMarkDown": "Whether to use guided failure mode if errors occur during the run."
+ }
+ ],
+ "OutputVariables": [
+ {
+ "name": "server_tasks",
+ "description": "A list of objects, containing `ServerTaskId`, `EnvironmentName` and `TenantName`, for each queued run."
+ }
+ ],
+ "instanceNameFormat": "Run Octopus Runbook",
+ "execution": {
+ "Node20_1": {
+ "target": "index.js"
+ }
+ }
+}
diff --git a/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV3/index.ts b/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV3/index.ts
index ebab652b..53b579b6 100644
--- a/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV3/index.ts
+++ b/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV3/index.ts
@@ -40,12 +40,9 @@ async function run() {
}, environmentVariables.defaultWorkingDirectory);
const configure = [
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
argumentEnquote("project", project),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "releaseNumber", releaseNumber),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "channel", channel),
connectionArguments(octoConnection),
flag("enableServiceMessages", true),
@@ -54,7 +51,6 @@ async function run() {
multiArgument(argumentEnquote, "tenant", deployForTenants),
multiArgument(argumentEnquote, "tenanttag", deployForTenantTags),
argumentEnquote("releaseNotesFile", releaseNotesFile),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(octoConnection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV4/index.ts b/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV4/index.ts
index b9f2fc0a..54e91070 100644
--- a/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV4/index.ts
+++ b/source/tasksLegacy/CreateOctopusRelease/CreateOctopusReleaseV4/index.ts
@@ -32,20 +32,14 @@ async function run() {
const octo = await getOrInstallOctoCommandRunner("create-release");
const configure = [
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
- // @ts-expect-error
argumentEnquote("project", project),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "releaseNumber", releaseNumber),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "channel", channel),
connectionArguments(connection),
flag("enableServiceMessages", true),
multiArgument(argumentEnquote, "deployTo", deployToEnvironments),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "gitRef", gitRef),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "gitCommit", gitCommit),
flag("progress", deployToEnvironments.length > 0 && deploymentProgress),
multiArgument(argumentEnquote, "tenant", deployForTenants),
@@ -63,7 +57,6 @@ async function run() {
configure.push(argumentEnquote("releaseNotesFile", releaseNotesFile));
}
- // @ts-expect-error
configure.push(includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments));
const code: number = await octo
diff --git a/source/tasksLegacy/OctopusMetadata/OctopusMetadataV4/index.ts b/source/tasksLegacy/OctopusMetadata/OctopusMetadataV4/index.ts
index 973ac51a..c5166f17 100644
--- a/source/tasksLegacy/OctopusMetadata/OctopusMetadataV4/index.ts
+++ b/source/tasksLegacy/OctopusMetadata/OctopusMetadataV4/index.ts
@@ -69,14 +69,11 @@ async function run() {
const connection = getDefaultOctopusConnectionDetailsOrThrow();
const configure: Array<(tool: ToolRunner) => ToolRunner> = [
connectionArguments(connection),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
multiArgument(argumentEnquote, "package-id", packageIds),
- // @ts-expect-error
argument("version", packageVersion),
argumentEnquote("file", buildInformationFile),
argument("overwrite-mode", overwriteMode),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/Promote/PromoteV3/index.ts b/source/tasksLegacy/Promote/PromoteV3/index.ts
index 53b5eb44..859ad9dd 100644
--- a/source/tasksLegacy/Promote/PromoteV3/index.ts
+++ b/source/tasksLegacy/Promote/PromoteV3/index.ts
@@ -24,17 +24,14 @@ async function run() {
const octo = await getOrInstallOctoCommandRunner("promote-release");
const configure = [
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
argumentEnquote("project", project),
connectionArguments(connection),
- // @ts-expect-error
argumentEnquote("from", from),
multiArgument(argumentEnquote, "to", to),
multiArgument(argumentEnquote, "tenant", deploymentForTenants),
multiArgument(argumentEnquote, "tenanttag", deployForTenantTags),
flag("progress", showProgress),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/Promote/PromoteV4/index.ts b/source/tasksLegacy/Promote/PromoteV4/index.ts
index 672fc0bd..f14e6a7a 100644
--- a/source/tasksLegacy/Promote/PromoteV4/index.ts
+++ b/source/tasksLegacy/Promote/PromoteV4/index.ts
@@ -23,18 +23,14 @@ async function run() {
const octo = await getOrInstallOctoCommandRunner("promote-release");
const configure = [
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
- // @ts-expect-error
argumentEnquote("project", project),
connectionArguments(connection),
- // @ts-expect-error
argumentEnquote("from", from),
multiArgument(argumentEnquote, "to", to),
multiArgument(argumentEnquote, "tenant", deployForTenants),
multiArgument(argumentEnquote, "tenanttag", deployForTenantTags),
flag("progress", showProgress),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/Push/PushV3/index.ts b/source/tasksLegacy/Push/PushV3/index.ts
index e002e3a0..22e2f561 100644
--- a/source/tasksLegacy/Push/PushV3/index.ts
+++ b/source/tasksLegacy/Push/PushV3/index.ts
@@ -22,11 +22,9 @@ async function run() {
const configure = [
connectionArguments(connection),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
multiArgument(argumentEnquote, "package", matchedPackages),
flag("replace-existing", replace),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/Push/PushV4/index.ts b/source/tasksLegacy/Push/PushV4/index.ts
index bd8d0671..9c718d05 100644
--- a/source/tasksLegacy/Push/PushV4/index.ts
+++ b/source/tasksLegacy/Push/PushV4/index.ts
@@ -24,11 +24,9 @@ async function run() {
const configure = [
connectionArguments(connection),
- // @ts-expect-error
argumentIfSet(argumentEnquote, "space", space),
multiArgument(argumentEnquote, "package", matchedPackages),
argument("overwrite-mode", overwriteMode),
- // @ts-expect-error
includeAdditionalArgumentsAndProxyConfig(connection.url, additionalArguments),
];
diff --git a/source/tasksLegacy/Utils/tool.ts b/source/tasksLegacy/Utils/tool.ts
index e892e3e7..51633390 100644
--- a/source/tasksLegacy/Utils/tool.ts
+++ b/source/tasksLegacy/Utils/tool.ts
@@ -99,9 +99,9 @@ export const assertOctoVersionAcceptsIds = async function (): Promise {
const [, major, minor, patch] = outputLastLine.trim().match(/^(\d+)\.(\d+)\.(\d+)\b/) || [0, 0, 0, 0];
const compatible =
`${major}.${minor}.${patch}` == "1.0.0" || // allow dev versions
- major > 6 ||
- (major == 6 && minor > 10) ||
- (major == 6 && minor == 10 && patch >= 0);
+ Number(major) > 6 ||
+ (major === 6 && Number(minor) > 10) ||
+ (major === 6 && minor === 10 && Number(patch) >= 0);
if (!compatible) {
throw new Error("The Octopus CLI tool is too old to run this task. Please use version 6.10.0 or newer, or downgrade the task to version 3.*.");
}