From bcb6367c7f569522c1f97c10feceee668b812498 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 16:57:34 +0100 Subject: [PATCH 01/29] Add `init-package.sh` script --- bin/init-package.sh | 124 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100755 bin/init-package.sh diff --git a/bin/init-package.sh b/bin/init-package.sh new file mode 100755 index 000000000..9a5153cdf --- /dev/null +++ b/bin/init-package.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +set -euo pipefail + +DRY_RUN=false +SCOPE="" + +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + -*) + echo "Unknown option: $1" + echo "Usage: $0 [--dry-run] " + exit 1 + ;; + *) + SCOPE="$1" + shift + ;; + esac +done + +if [ -z "$SCOPE" ]; then + echo "Usage: $0 [--dry-run] " + exit 1 +fi +PLUGIN_PKG="@datadog/datadog-ci-plugin-$SCOPE" +PLUGIN_DIR="packages/plugin-$SCOPE" + +if [ -d "$PLUGIN_DIR" ]; then + echo "Plugin directory $PLUGIN_DIR already exists!" + echo "This script should only be run once per scope, before migrate.sh" + exit 1 +fi + +echo "This script will initialize and publish an empty package for $PLUGIN_PKG" +echo + +read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN +echo +if [ -z "$INIT_NPM_AUTH_TOKEN" ]; then + echo "Error: NPM auth token cannot be empty" + exit 1 +fi + +# Export this for subsequent yarn commands in the script +export INIT_NPM_AUTH_TOKEN + +# Do not hardcode the token in .yarnrc.yml, it will be read from the environment variable +yarn config set npmAuthToken '${INIT_NPM_AUTH_TOKEN}' +echo + +echo "1. Creating plugin directory structure" +mkdir -p "$PLUGIN_DIR" +env cp LICENSE "$PLUGIN_DIR" +echo "Empty package" > "$PLUGIN_DIR/README.md" +cat > "$PLUGIN_DIR/package.json" < Date: Thu, 15 Jan 2026 16:57:56 +0100 Subject: [PATCH 02/29] Adapt `migrate.sh` script --- bin/migrate.sh | 82 ++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 63 deletions(-) diff --git a/bin/migrate.sh b/bin/migrate.sh index 8f1964da2..dc8564bb0 100755 --- a/bin/migrate.sh +++ b/bin/migrate.sh @@ -25,6 +25,20 @@ SRC_DIR="packages/datadog-ci/src/commands/$SCOPE" DST_DIR="$PLUGIN_DIR/src" BASE_DIR="packages/base/src/commands/$SCOPE" +# Check that init-package.sh was run first +if [ ! -d "$PLUGIN_DIR" ]; then + echo "Plugin directory $PLUGIN_DIR does not exist!" + echo "Please run ./bin/init-package.sh $SCOPE first to initialize and publish the empty package." + exit 1 +fi + +# Check that this script wasn't already run (tsconfig.json is created by migrate.sh) +if [ -f "$PLUGIN_DIR/tsconfig.json" ]; then + echo "Plugin directory $PLUGIN_DIR already has a tsconfig.json file!" + echo "This indicates the package was already migrated." + exit 1 +fi + echo 1. Move the folder if [ ! -d "$SRC_DIR" ]; then echo "Source directory $SRC_DIR does not exist!" @@ -32,78 +46,20 @@ if [ ! -d "$SRC_DIR" ]; then fi if [ -d "$DST_DIR" ]; then echo "Destination directory $DST_DIR already exists!" - echo "You can run \`rm -rf packages/plugin-$SCOPE && rm -rf $BASE_DIR\` to clean up a previous run of this script." + echo "You can run \`rm -rf packages/plugin-$SCOPE/src && rm -rf $BASE_DIR\` to clean up a previous run of this script." exit 1 fi -mkdir -p "$PLUGIN_DIR" env mv "$SRC_DIR" "$DST_DIR" env mv "$DST_DIR/README.md" "$PLUGIN_DIR" -env cp LICENSE "$PLUGIN_DIR" mkdir -p "$BASE_DIR" mv "$DST_DIR/cli.ts" "$BASE_DIR/cli.ts" echo "Moved $SRC_DIR to $DST_DIR" -echo 2. Create package.json -cat > "$PLUGIN_DIR/package.json" < "$PLUGIN_DIR/tsconfig.json" < Date: Thu, 15 Jan 2026 16:58:37 +0100 Subject: [PATCH 03/29] Fix lint-packages script --- bin/lint-packages.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bin/lint-packages.ts b/bin/lint-packages.ts index d253afe74..a68d5f14b 100644 --- a/bin/lint-packages.ts +++ b/bin/lint-packages.ts @@ -568,6 +568,7 @@ if (Object.keys(impactedGithubActions).length > 0) { // #endregion if (fix) { + // Both commands always exit with 0, even when they make changes exec('yarn syncpack fix') exec('yarn syncpack format') } else { @@ -582,7 +583,12 @@ if (fix) { } if (fix) { - exec('yarn knip --fix') + try { + // This command exits with 1 when it makes changes + exec('yarn knip --fix') + } catch { + // ignore error + } } else { try { exec('yarn knip') From ef19812e0b151400fa70b732f6bb036bc9c0ff1f Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 16:59:10 +0100 Subject: [PATCH 04/29] Add docs about listing packages --- CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f882c99b..9c3b3d314 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,26 @@ Pull requests for bug fixes are welcome, but before submitting new features or changes to current functionality, [open an issue](https://github.com/DataDog/datadog-ci/issues/new) and discuss your ideas or propose the changes you wish to make. After a resolution is reached, a PR can be submitted for review. +### Listing NPM packages + +This repository is a monorepo containing multiple packages published to NPM. You can list all packages with the following command: + +```sh +npm search 'maintainer:datadog keywords:datadog-ci' +``` + +To only list the plugins: + +```sh +npm search 'maintainer:datadog keywords:datadog-ci,plugin' +``` + +You can also use the following datadog-ci command to get more information: + +```sh +yarn launch plugin list --all +``` + ### Running command in development environment When developing the tool, it is possible to run commands using `yarn launch`. It relies on `tsx`, so it does not require building the project for every new change. From 0f96c7e18433077a1a65fa6e834dc9173d999fd1 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 16:59:39 +0100 Subject: [PATCH 05/29] Check published packages in CI --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88922a3f7..8e620ab54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,8 +159,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: "@testmeasure1:20" - EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure1:40" + EXTRA_TEST_QUERY_FILTER: '@testmeasure1:20' + EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure1:40' DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -173,8 +173,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: "@testmeasure2:60" - EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure2:80" + EXTRA_TEST_QUERY_FILTER: '@testmeasure2:60' + EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure2:80' DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -264,8 +264,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: "@testmeasure1:20" - EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure1:40" + EXTRA_TEST_QUERY_FILTER: '@testmeasure1:20' + EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure1:40' DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -278,8 +278,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: "@testmeasure2:60" - EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure2:80" + EXTRA_TEST_QUERY_FILTER: '@testmeasure2:60' + EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure2:80' DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -634,8 +634,8 @@ jobs: cpu_count: 2 diff_aware: false - check-licenses: - name: Check licenses + additional-checks: + name: Additional checks runs-on: ubuntu-latest steps: @@ -644,7 +644,26 @@ jobs: uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 22.19.0 - # The install step has been added here such that the `.yarn/install-state.gz` file is generated. This file is used - # by the script `check-licenses` below. - run: yarn install --immutable - - run: yarn check-licenses + - name: Check licenses + run: yarn check-licenses + - name: Check published packages + run: | + local=$(yarn workspaces list --json --no-private | jq '.name' | sort) + remote=$(npm search 'maintainer:datadog keywords:datadog-ci' --json | jq '.[].name' | sort) + + DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" <(echo "$remote") <(echo "$local")) || true + + if [ -z "$DIFF_OUTPUT" ]; then + echo "All local packages exist on NPM ✅" + exit 0 + else + echo "$DIFF_OUTPUT" + echo "### Some local packages were not published to NPM yet ❌" >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + echo "$DIFF_OUTPUT" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo >> $GITHUB_STEP_SUMMARY + echo "**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" >> $GITHUB_STEP_SUMMARY + exit 1 + fi From 59955fe96e190bf1549ed13c3119b1ab6e4a304a Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 17:00:09 +0100 Subject: [PATCH 06/29] Init `junit` plugin --- packages/plugin-junit/LICENSE | 201 +++++++++++++++++++++++++++++ packages/plugin-junit/README.md | 1 + packages/plugin-junit/package.json | 40 ++++++ yarn.lock | 8 ++ 4 files changed, 250 insertions(+) create mode 100644 packages/plugin-junit/LICENSE create mode 100644 packages/plugin-junit/README.md create mode 100644 packages/plugin-junit/package.json diff --git a/packages/plugin-junit/LICENSE b/packages/plugin-junit/LICENSE new file mode 100644 index 000000000..f66d768ce --- /dev/null +++ b/packages/plugin-junit/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Datadog, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/plugin-junit/README.md b/packages/plugin-junit/README.md new file mode 100644 index 000000000..6a2bcd747 --- /dev/null +++ b/packages/plugin-junit/README.md @@ -0,0 +1 @@ +Empty package diff --git a/packages/plugin-junit/package.json b/packages/plugin-junit/package.json new file mode 100644 index 000000000..02d505af9 --- /dev/null +++ b/packages/plugin-junit/package.json @@ -0,0 +1,40 @@ +{ + "name": "@datadog/datadog-ci-plugin-junit", + "version": "0.0.2", + "description": "Datadog CI plugin for `junit` commands", + "license": "Apache-2.0", + "keywords": [ + "datadog", + "datadog-ci", + "plugin" + ], + "homepage": "https://github.com/DataDog/datadog-ci/tree/master/packages/plugin-junit", + "repository": { + "type": "git", + "url": "https://github.com/DataDog/datadog-ci.git", + "directory": "packages/plugin-junit" + }, + "exports": { + "./package.json": "./package.json", + "./commands/*": { + "development": "./src/commands/*.ts", + "default": "./dist/commands/*.js" + } + }, + "files": [ + "dist/**/*", + "README", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "yarn package:clean; yarn package:build", + "lint": "yarn package:lint", + "prepack": "yarn package:clean-dist" + }, + "peerDependencies": { + "@datadog/datadog-ci-base": "workspace:*" + } +} diff --git a/yarn.lock b/yarn.lock index 228a9ae94..0ac84e2dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1642,6 +1642,14 @@ __metadata: languageName: unknown linkType: soft +"@datadog/datadog-ci-plugin-junit@workspace:packages/plugin-junit": + version: 0.0.0-use.local + resolution: "@datadog/datadog-ci-plugin-junit@workspace:packages/plugin-junit" + peerDependencies: + "@datadog/datadog-ci-base": "workspace:*" + languageName: unknown + linkType: soft + "@datadog/datadog-ci-plugin-lambda@workspace:packages/plugin-lambda": version: 0.0.0-use.local resolution: "@datadog/datadog-ci-plugin-lambda@workspace:packages/plugin-lambda" From 1b58d6fb03738e744a428cdbf4a912ccca3e767d Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 17:23:41 +0100 Subject: [PATCH 07/29] Colorize scripts --- bin/init-package.sh | 15 +++++++++++---- bin/migrate.sh | 32 ++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/bin/init-package.sh b/bin/init-package.sh index 9a5153cdf..eabe00ada 100755 --- a/bin/init-package.sh +++ b/bin/init-package.sh @@ -36,7 +36,14 @@ if [ -d "$PLUGIN_DIR" ]; then exit 1 fi -echo "This script will initialize and publish an empty package for $PLUGIN_PKG" +BOLD='\033[1m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "This script will initialize and publish an empty package for ${BLUE}${BOLD}$PLUGIN_PKG${NC}" +echo +echo -e "${BOLD}Please follow the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before running this script." echo read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN @@ -115,10 +122,10 @@ yarn config unset npmAuthToken echo if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] Would have published $PLUGIN_PKG@0.0.1" + echo -e "${GREEN}[DRY-RUN] Would have published ${BOLD}$PLUGIN_PKG@0.0.1${NC}${NC}" else - echo "Successfully published $PLUGIN_PKG@0.0.1" + echo -e "${GREEN}Successfully published ${BOLD}$PLUGIN_PKG@0.0.1${NC}${NC}" fi echo -echo "If needed, you can now run: ./bin/migrate.sh $SCOPE" +echo -e "If needed, you can now run: ${BLUE}bin/migrate.sh $SCOPE${NC}" diff --git a/bin/migrate.sh b/bin/migrate.sh index dc8564bb0..713dfa36c 100755 --- a/bin/migrate.sh +++ b/bin/migrate.sh @@ -25,17 +25,25 @@ SRC_DIR="packages/datadog-ci/src/commands/$SCOPE" DST_DIR="$PLUGIN_DIR/src" BASE_DIR="packages/base/src/commands/$SCOPE" +BOLD='\033[1m' +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + # Check that init-package.sh was run first if [ ! -d "$PLUGIN_DIR" ]; then - echo "Plugin directory $PLUGIN_DIR does not exist!" - echo "Please run ./bin/init-package.sh $SCOPE first to initialize and publish the empty package." + echo -e "${RED}Plugin directory ${BOLD}$PLUGIN_DIR${NC} does not exist!${NC}" + echo + echo -e "${BLUE}To initialize and ${BOLD}publish an initial version${NC}${BLUE} of the package, please run ${BOLD}bin/init-package.sh $SCOPE${NC}" exit 1 fi # Check that this script wasn't already run (tsconfig.json is created by migrate.sh) if [ -f "$PLUGIN_DIR/tsconfig.json" ]; then - echo "Plugin directory $PLUGIN_DIR already has a tsconfig.json file!" - echo "This indicates the package was already migrated." + echo -e "${RED}Plugin directory ${BOLD}$PLUGIN_DIR${NC}${RED} already has a tsconfig.json file!${NC}" + echo + echo -e "${BLUE}This indicates the package was already migrated.${NC}" exit 1 fi @@ -105,16 +113,16 @@ echo "7. Run \`yarn lint:packages --fix\`" yarn lint:packages --fix if yarn workspace @datadog/datadog-ci-base build && yarn workspace "$PLUGIN_PKG" build && yarn workspace "$PLUGIN_PKG" lint --fix; then - echo Done + echo -e "${GREEN}Done${NC}" else - echo "Linting failed. Please fix the issues manually." + echo -e "${RED}Linting failed. Please fix the issues manually.${NC}" fi git add -A echo -echo "Manual steps remaining:" -echo "- Commit the changes, then make the following manual changes:" -echo "- Move any shared helpers to @datadog/datadog-ci-base if needed." -echo "- Split FooCommand/PluginCommand classes as described in https://datadoghq.atlassian.net/wiki/spaces/dtdci/pages/5472846600/How+to+Split+a+command+scope+into+a+plugin+package#Refactor" -echo "- Run yarn build and yarn lint as needed to ensure everything works." -echo "- **Important:** Update any outdated links in the Documentation repo. See https://datadoghq.atlassian.net/wiki/spaces/dtdci/pages/5472846600/How+to+Split+a+command+scope+into+a+plugin+package#Update-links-pointing-to-the-package" \ No newline at end of file +echo -e "${BOLD}Manual steps remaining:${NC}" +echo -e "- ${BLUE}Commit${NC} the changes, then make the following manual changes." +echo -e "- Move any shared helpers to ${BLUE}@datadog/datadog-ci-base${NC} if needed." +echo -e "- Split FooCommand/PluginCommand classes as described in ${BLUE}https://datadoghq.atlassian.net/wiki/spaces/dtdci/pages/5472846600/How+to+Split+a+command+scope+into+a+plugin+package#Refactor${NC}" +echo -e "- Run ${BLUE}yarn build${NC} and ${BLUE}yarn lint${NC} as needed to ensure everything works." +echo -e "- ${BOLD}Important:${NC} Update any outdated links in the Documentation repo. See ${BLUE}https://datadoghq.atlassian.net/wiki/spaces/dtdci/pages/5472846600/How+to+Split+a+command+scope+into+a+plugin+package#Update-links-pointing-to-the-package${NC}" \ No newline at end of file From b5b1f58589cf7d44abefd8e86fbefab90fd40735 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 18:25:19 +0100 Subject: [PATCH 08/29] Reintroduce `--provenance` --- .github/workflows/publish-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 5bd42fc5c..250470bfc 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -6,7 +6,7 @@ on: - v* # Any version tag permissions: - id-token: write # To publish on NPM with provenance and to federate tokens + id-token: write # For OIDC publishing with provenance and to federate tokens contents: write # Required for the draft release jobs: @@ -258,11 +258,11 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 24.11.1 # Version supporting OIDC - registry-url: "https://registry.npmjs.org" + node-version: '24' # Needed for OIDC publishing + registry-url: 'https://registry.npmjs.org' - run: yarn install --immutable - run: yarn build - - run: yarn publish:all + - run: yarn publish:all --provenance bump-ci-integrations: name: Bump datadog-ci in integration From 0d100742919668ac7e58d49be3572d2961de8cc9 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 18:28:45 +0100 Subject: [PATCH 09/29] Publish junit `0.0.3` with `--provenance` --- .github/workflows/test-release.yml | 21 +++++++++++++++++++++ packages/plugin-junit/package.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 000000000..157d04714 --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,21 @@ +name: Test publish package on NPM + +on: + push: + branches: [corentin.girard/SYNTH-23939/init-plugin] + +permissions: + id-token: write + +jobs: + npm-publish: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org' + - run: yarn install --immutable + - run: yarn workspace @datadog/datadog-ci-plugin-junit npm publish --provenance diff --git a/packages/plugin-junit/package.json b/packages/plugin-junit/package.json index 02d505af9..b1625bb62 100644 --- a/packages/plugin-junit/package.json +++ b/packages/plugin-junit/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/datadog-ci-plugin-junit", - "version": "0.0.2", + "version": "0.0.3", "description": "Datadog CI plugin for `junit` commands", "license": "Apache-2.0", "keywords": [ From 4880c9c2eb62d83c995044314ad1be94901c2f79 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 18:49:13 +0100 Subject: [PATCH 10/29] Delete temporary workflow --- .github/workflows/test-release.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml deleted file mode 100644 index 157d04714..000000000 --- a/.github/workflows/test-release.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Test publish package on NPM - -on: - push: - branches: [corentin.girard/SYNTH-23939/init-plugin] - -permissions: - id-token: write - -jobs: - npm-publish: - runs-on: ubuntu-latest - environment: npm - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 - with: - node-version: '24' - registry-url: 'https://registry.npmjs.org' - - run: yarn install --immutable - - run: yarn workspace @datadog/datadog-ci-plugin-junit npm publish --provenance From d91da285a2d158571c893fba482bbfa69ee19c68 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 20:02:32 +0100 Subject: [PATCH 11/29] Run migrate script --- .github/CODEOWNERS | 3 +- .github/workflows/ci.yml | 2 + README.md | 351 +++++++++++++++++- packages/base/package.json | 4 + packages/base/src/cli.ts | 2 + packages/base/src/commands/junit/cli.ts | 7 + packages/datadog-ci/package.json | 5 +- .../shims/injected-plugin-submodules.js | 3 + packages/datadog-ci/src/commands/junit/cli.ts | 3 - packages/datadog-ci/tsconfig.json | 1 + packages/plugin-junit/LICENSE | 201 ++++++++++ .../commands/junit => plugin-junit}/README.md | 6 +- packages/plugin-junit/package.json | 50 +++ .../fixtures/autodiscovery/junit-report.xml | 0 .../autodiscovery/nested/TEST-suite.xml | 0 .../fixtures/autodiscovery/regular-file.xml | 0 .../fixtures/autodiscovery/test-results.xml | 0 .../src}/__tests__/fixtures/empty.xml | 0 .../src}/__tests__/fixtures/go-report.xml | 0 .../src}/__tests__/fixtures/invalid.xml | 0 .../src}/__tests__/fixtures/java-report.xml | 0 .../fixtures/junit.xml/valid-report-2.xml | 0 .../fixtures/junit.xml/valid-report.xml | 0 .../fixtures/other/other-non-junit-file.txt | 0 .../fixtures/subfolder/invalid-no-tests.xml | 0 .../fixtures/subfolder/js-report.xml | 0 .../fixtures/subfolder/non-junit-file.txt | 0 .../src}/__tests__/upload.test.ts | 130 +++---- .../src}/__tests__/utils.test.ts | 0 .../junit => plugin-junit/src}/api.ts | 0 .../junit => plugin-junit/src}/interfaces.ts | 0 .../junit => plugin-junit/src}/renderer.ts | 0 .../junit => plugin-junit/src}/upload.ts | 0 .../junit => plugin-junit/src}/utils.ts | 0 packages/plugin-junit/tsconfig.json | 9 + tsconfig.json | 1 + yarn.lock | 23 +- 37 files changed, 723 insertions(+), 78 deletions(-) mode change 120000 => 100644 README.md create mode 100644 packages/base/src/commands/junit/cli.ts delete mode 100644 packages/datadog-ci/src/commands/junit/cli.ts create mode 100644 packages/plugin-junit/LICENSE rename packages/{datadog-ci/src/commands/junit => plugin-junit}/README.md (94%) create mode 100644 packages/plugin-junit/package.json rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/autodiscovery/junit-report.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/autodiscovery/regular-file.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/autodiscovery/test-results.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/empty.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/go-report.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/invalid.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/java-report.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/junit.xml/valid-report-2.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/junit.xml/valid-report.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/other/other-non-junit-file.txt (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/subfolder/invalid-no-tests.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/subfolder/js-report.xml (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/fixtures/subfolder/non-junit-file.txt (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/upload.test.ts (73%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/__tests__/utils.test.ts (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/api.ts (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/interfaces.ts (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/renderer.ts (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/upload.ts (100%) rename packages/{datadog-ci/src/commands/junit => plugin-junit/src}/utils.ts (100%) create mode 100644 packages/plugin-junit/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3ff9b11f..3ef9114a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,7 +9,8 @@ packages/plugin-deployment @DataDog/datadog-ci-admins @DataD packages/base/src/commands/deployment @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend packages/plugin-dora @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend packages/base/src/commands/dora @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend -packages/datadog-ci/src/commands/junit @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend +packages/plugin-junit @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend +packages/base/src/commands/junit @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend packages/datadog-ci/src/commands/measure @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend packages/base/src/commands/tag @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend packages/datadog-ci/src/commands/trace @DataDog/datadog-ci-admins @DataDog/ci-app-libraries @DataDog/ci-app-backend diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88922a3f7..85f89b4de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -366,6 +366,7 @@ jobs: "@datadog/datadog-ci-plugin-deployment": "file:./artifacts/@datadog-datadog-ci-plugin-deployment-20.tgz", "@datadog/datadog-ci-plugin-dora": "file:./artifacts/@datadog-datadog-ci-plugin-dora-20.tgz", "@datadog/datadog-ci-plugin-gate": "file:./artifacts/@datadog-datadog-ci-plugin-gate-20.tgz", + "@datadog/datadog-ci-plugin-junit": "file:./artifacts/@datadog-datadog-ci-plugin-junit-20.tgz", "@datadog/datadog-ci-plugin-sarif": "file:./artifacts/@datadog-datadog-ci-plugin-sarif-20.tgz", "@datadog/datadog-ci-plugin-sbom": "file:./artifacts/@datadog-datadog-ci-plugin-sbom-20.tgz" }, @@ -445,6 +446,7 @@ jobs: "@datadog/datadog-ci-plugin-deployment": "file:./artifacts/@datadog-datadog-ci-plugin-deployment-20.tgz", "@datadog/datadog-ci-plugin-dora": "file:./artifacts/@datadog-datadog-ci-plugin-dora-20.tgz", "@datadog/datadog-ci-plugin-gate": "file:./artifacts/@datadog-datadog-ci-plugin-gate-20.tgz", + "@datadog/datadog-ci-plugin-junit": "file:./artifacts/@datadog-datadog-ci-plugin-junit-20.tgz", "@datadog/datadog-ci-plugin-sarif": "file:./artifacts/@datadog-datadog-ci-plugin-sarif-20.tgz", "@datadog/datadog-ci-plugin-sbom": "file:./artifacts/@datadog-datadog-ci-plugin-sbom-20.tgz" }, diff --git a/README.md b/README.md deleted file mode 120000 index cb2f15689..000000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -packages/datadog-ci/README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..ebbaf3b3b --- /dev/null +++ b/README.md @@ -0,0 +1,350 @@ +# Datadog CI + +[![NPM Version](https://img.shields.io/npm/v/@datadog/datadog-ci)](https://www.npmjs.com/package/@datadog/datadog-ci) [![Continuous Integration](https://github.com/DataDog/datadog-ci/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/DataDog/datadog-ci/actions/workflows/ci.yml?query=branch%3Amaster) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![Node.js Version](https://img.shields.io/badge/Node.js-20+-green) + +Execute commands from your Continuous Integration (CI) and Continuous Delivery (CD) pipelines to integrate with existing Datadog products. + +See the [Usage section](#usage) for a list of available commands. + +## How to install the CLI + +The package is under [@datadog/datadog-ci](https://www.npmjs.com/package/@datadog/datadog-ci) and can be installed through NPM or Yarn: + +```sh +# NPM +npm install --save-dev @datadog/datadog-ci + +# Yarn +yarn add --dev @datadog/datadog-ci +``` + +If you need `datadog-ci` as a CLI tool instead of a package, you can run it with [`npx`](https://www.npmjs.com/package/npx) or install it globally: + +```sh +# npx +npx @datadog/datadog-ci@v4 [scope] + +# NPM install globally +npm install -g @datadog/datadog-ci + +# Yarn v1 add globally +yarn global add @datadog/datadog-ci +``` + +For more ways to install the CLI, see [this section](#more-ways-to-install-the-cli). + +## Installing a plugin + +Plugins are separate packages that were split from the `@datadog/datadog-ci` package to reduce its installation size. + +Use `datadog-ci plugin list` to list the available plugins: + +```sh +datadog-ci plugin list +``` + +Use `datadog-ci plugin install` to install a plugin: + +```sh +datadog-ci plugin install +``` + +By default, running a command that requires a plugin will automatically install the plugin if it is not already installed. You can disable this behavior with `DISABLE_PLUGIN_AUTO_INSTALL=1`. + +## Usage + +```bash +Usage: datadog-ci [options] +``` + +The following `` and `` values are available. + +#### `aas` + +**README:** [📚](/packages/plugin-aas) | **Documentation:** [🔗](https://docs.datadoghq.com/serverless/azure_app_services/) | **Plugin:** `@datadog/datadog-ci-plugin-aas` + +- `instrument`: Apply Datadog instrumentation to the given Azure App Services. +- `uninstrument`: Revert Datadog instrumentation from the given Azure App Services. + +#### `cloud-run` + +**README:** [📚](/packages/plugin-cloud-run) | **Documentation:** [🔗](https://docs.datadoghq.com/serverless/google_cloud_run/) | **Plugin:** `@datadog/datadog-ci-plugin-cloud-run` + +- `flare`: Troubleshoot your issues with Cloud Run service configuration. +- `instrument`: Apply Datadog instrumentation to the given Cloud Run Services. +- `uninstrument`: Revert Datadog instrumentation from the given Cloud Run Services. + +#### `container-app` + +**README:** [📚](/packages/plugin-container-app) | **Documentation:** [🔗](https://docs.datadoghq.com/serverless/azure_container_apps/sidecar/) | **Plugin:** `@datadog/datadog-ci-plugin-container-app` + +- `instrument`: Apply Datadog instrumentation to Azure Container Apps. +- `uninstrument`: Revert Datadog instrumentation from Azure Container Apps. + +#### `coverage` + +**README:** [📚](/packages/plugin-coverage) | **Documentation:** [🔗](https://docs.datadoghq.com/code_coverage/) + +- `upload`: Upload code coverage report files to Datadog. + +#### `dora` + +**README:** [📚](/packages/plugin-dora) | **Documentation:** [🔗](https://docs.datadoghq.com/dora_metrics/) + +- `deployment`: Send a new deployment event for DORA Metrics to Datadog. + +#### `dsyms` + +**README:** [📚](/packages/datadog-ci/src/commands/dsyms) | **Documentation:** [🔗](https://docs.datadoghq.com/real_user_monitoring/error_tracking/ios/) + +- `upload`: Upload iOS dSYM files for Error Tracking (macOS only). + +#### `flutter-symbols` + +**README:** [📚](/packages/datadog-ci/src/commands/flutter-symbols) | **Documentation:** [🔗](https://docs.datadoghq.com/real_user_monitoring/error_tracking/flutter/) + +- `upload`: Upload Flutter symbols for Error Tracking. + +#### `gate` + +**README:** [📚](/packages/plugin-gate) | **Documentation:** [🔗](https://docs.datadoghq.com/quality_gates/) + +> [!WARNING] +> +> **Deprecated:** Datadog Quality Gates is being replaced by the new PR Gates in 2026. To start the migration, please fill out this form: https://forms.gle/qnhANsE1ABtHrjqz9 +> +> Learn more about PR Gates: https://docs.datadoghq.com/pr_gates + +- `evaluate`: Evaluate Quality Gates rules in Datadog. + +#### `git-metadata` + +**README:** [📚](/packages/base/src/commands/git-metadata) | **Documentation:** [🔗](https://docs.datadoghq.com/integrations/guide/source-code-integration/) + +- `upload`: Upload Git metadata for the Source Code Integration. + +#### `junit` + +**README:** [📚](/packages/datadog-ci/src/commands/junit) | **Documentation:** [🔗](https://docs.datadoghq.com/tests/setup/junit_xml/) + +- `upload`: Upload JUnit test reports for Test Visibility. + +#### `lambda` + +**README:** [📚](/packages/plugin-lambda) | **Documentation:** [🔗](https://docs.datadoghq.com/serverless/aws_lambda/) | **Plugin:** `@datadog/datadog-ci-plugin-lambda` + +- `flare`: Troubleshoot your issues with Datadog instrumentation on your AWS Lambda functions. +- `instrument`: Apply Datadog instrumentation to the given AWS Lambda functions. +- `uninstrument`: Revert Datadog instrumentation from the given AWS Lambda functions. + +#### `measure` + +**README:** [📚](/packages/datadog-ci/src/commands/measure) | **Documentation:** [🔗](https://docs.datadoghq.com/continuous_integration/pipelines/custom_tags_and_measures/) + +- Add measures to a CI Visibility pipeline trace or job span in Datadog. + +#### `pe-symbols` + +**README:** [📚](/packages/datadog-ci/src/commands/pe-symbols) | **Documentation:** [🔗](https://docs.datadoghq.com/profiler/enabling/ddprof/) + +- `upload`: Upload Windows PE debug info files for Profiling. + +#### `react-native` + +**README:** [📚](/packages/datadog-ci/src/commands/react-native) | **Documentation:** [🔗](https://docs.datadoghq.com/real_user_monitoring/error_tracking/reactnative/) + +- `codepush`: Upload React Native CodePush sourcemaps for Error Tracking. [🔗](https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/setup/codepush/) +- `upload`: Upload React Native sourcemaps for Error Tracking. +- `xcode`: Upload React Native sourcemaps for Error Tracking from the XCode bundle build phase. + +#### `sarif` + +**README:** [📚](/packages/plugin-sarif) | **Documentation:** [🔗](https://docs.datadoghq.com/code_analysis/static_analysis/) + +- `upload`: Upload Static Analysis Results Interchange Format (SARIF) reports to Datadog. + +#### `sbom` + +**README:** [📚](/packages/plugin-sbom) | **Documentation:** [🔗](https://docs.datadoghq.com/code_analysis/software_composition_analysis/) + +- `upload`: Upload Software Bill of Materials (SBOM) files to Datadog. + +#### `sourcemaps` + +**README:** [📚](/packages/datadog-ci/src/commands/sourcemaps) | **Documentation:** [🔗](https://docs.datadoghq.com/real_user_monitoring/guide/upload-javascript-source-maps/) + +- `upload`: Upload JavaScript sourcemaps for Error Tracking. + +#### `stepfunctions` + +**README:** [📚](/packages/plugin-stepfunctions) | **Documentation:** [🔗](https://docs.datadoghq.com/serverless/step_functions/installation/?tab=datadogcli) | **Plugin:** `@datadog/datadog-ci-plugin-stepfunctions` + +- `instrument`: Instrument AWS Step Function with Datadog to get logs and traces. +- `uninstrument`: Uninstrument AWS Step Function. + +#### `synthetics` + +**README:** [📚](/packages/plugin-synthetics) | **Plugin:** `@datadog/datadog-ci-plugin-synthetics` + +- `run-tests`: Run Continuous Testing tests from the CI. [🔗](https://docs.datadoghq.com/continuous_testing/) +- `upload-application`: Upload a new version to an existing mobile application in Datadog. [🔗](https://docs.datadoghq.com/mobile_app_testing/) + +#### `tag` + +**README:** [📚](/packages/base/src/commands/tag) | **Documentation:** [🔗](https://docs.datadoghq.com/continuous_integration/pipelines/custom_tags_and_measures/) + +- Add custom tags to a CI Visibility pipeline trace or job span in Datadog. + +#### `trace` + +**README:** [📚](/packages/datadog-ci/src/commands/trace) | **Documentation:** [🔗](https://docs.datadoghq.com/continuous_integration/pipelines/custom_commands/) + +- Add custom commands to a CI Visibility pipeline in Datadog. + +#### `unity-symbols` + +**README:** [📚](/packages/datadog-ci/src/commands/unity-symbols) | **Documentation:** [🔗](https://docs.datadoghq.com/real_user_monitoring/error_tracking/unity/) + +- `upload`: Upload Unity symbols for Error Tracking. + +### Beta commands + +The following are **beta** commands, you can enable them with with `DD_BETA_COMMANDS_ENABLED=1`: + +#### `deployment` + +**README:** [📚](/packages/plugin-deployment) | **Documentation:** [🔗](https://docs.datadoghq.com/continuous_delivery/) + +- `mark`: Mark a CI job as a deployment. +- `correlate`: Correlate GitOps CD deployments with application repositories CI pipelines. [🔗](https://docs.datadoghq.com/continuous_delivery/deployments/argocd#correlate-deployments-with-ci-pipelines) +- `correlate-image`: Correlate an image from a CD provider with its source commit. [🔗](https://docs.datadoghq.com/continuous_delivery/deployments/argocd#correlate-images-with-source-code) +- `gate`: Evaluate a Deployment Gate. [🔗](https://docs.datadoghq.com/deployment_gates/) + +#### `elf-symbols` + +**README:** [📚](/packages/datadog-ci/src/commands/elf-symbols) | **Documentation:** [🔗](https://docs.datadoghq.com/profiler/enabling/ddprof/) + +- `upload`: Upload Elf debug info files for Profiling. + +### FIPS support + +The `fips` option allows `datadog-ci` to use a FIPS cryptographic module provider if the OpenSSL library installed on the host system provides it. + +**Note**: `datadog-ci` cannot assert if such a provider is available, and doesn't throw any error if the provider is not FIPS validated. + +Node.js versions below 17 are incompatible with OpenSSL 3, which provides FIPS support. +If you are using a Node.js version below 17, enabling the `fips` option causes the command to throw an error. +The option `fips-ignore-error` ignores this error. + +#### `fips` +Enable `datadog-ci` FIPS support if a FIPS validated provider is installed on the host system. +If you do not have a FIPS provider installed, `datadog-ci` does not raise an error. + +- ENV variable: `DATADOG_FIPS=true` +- CLI param: `--fips` + +#### `fips-ignore-error` +Ignore Node.js errors if FIPS cannot be enabled on the host system. + +**Note**: the absence of an error doesn't indicate that FIPS is enabled successfully. + +- ENV variable: `DATADOG_FIPS_IGNORE_ERROR=true` +- CLI param: `--fips-ignore-error` + + +## More ways to install the CLI + +### Standalone binary + +If installing Node.js in the CI is an issue, standalone binaries are attached to each [GitHub release](https://github.com/DataDog/datadog-ci/releases). + +Supported architectures: +- `linux-x64` +- `linux-arm64` +- `darwin-x64` (MacOS) +- `darwin-arm64` (MacOS) +- `win-x64` (Windows) + +> [!WARNING] +> Using `strip` to remove debugging symbols from the standalone binary as an attempt to make it smaller may cause segmentation faults when running `datadog-ci`. See https://github.com/nodejs/postject/issues/90. + +> [!NOTE] +> To determine the version when unable to run `datadog-ci --version` (for example, because of segmentation faults or an architecture mismatch), run `grep --text STANDALONE_BINARY_VERSION path/to/datadog-ci`. + +To install the standalone binary: + +#### Linux + +```sh +# Linux x64 +curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_linux-x64" --output "/usr/local/bin/datadog-ci" && chmod +x /usr/local/bin/datadog-ci + +# Linux arm64 +curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_linux-arm64" --output "/usr/local/bin/datadog-ci" && chmod +x /usr/local/bin/datadog-ci +``` + +#### MacOS + +```sh +# MacOS x64 +curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_darwin-x64" --output "/usr/local/bin/datadog-ci" && chmod +x /usr/local/bin/datadog-ci + +# MacOS arm64 +curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_darwin-arm64" --output "/usr/local/bin/datadog-ci" && chmod +x /usr/local/bin/datadog-ci +``` + +#### Windows + +```sh +# Windows x64 +Invoke-WebRequest -Uri "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_win-x64" -OutFile "datadog-ci.exe" +``` + +Then, you can run `datadog-ci` commands normally: + +```sh +datadog-ci version +``` + +### Container image + +To run `datadog-ci` from a container, you can use the `datadog/ci` image available in [Docker Hub](https://hub.docker.com/r/datadog/ci) as well as the public [Amazon ECR](https://gallery.ecr.aws/datadog/ci) and [Google GC](https://console.cloud.google.com/gcr/images/datadoghq/global/ci) registries. + +```shell +docker pull datadog/ci +``` + +This example demonstrates how to run a command using the container and passing in the API and app keys: + +```shell +export DD_API_KEY=$(cat /secret/dd_api_key) +export DD_APP_KEY=$(cat /secret/dd_app_key) +docker run --rm -it -v $(pwd):/w -e DD_API_KEY -e DD_APP_KEY datadog/ci [] [options] +``` + +#### Building your own container image + +You can build an image using the provided [Dockerfile](https://github.com/DataDog/datadog-ci/blob/master/container/Dockerfile): + +```sh +cd container +docker build --tag datadog-ci . +``` + +Optionally, you can use the `VERSION` build argument to build an image for a specific version: + +```sh +docker build --build-arg "VERSION=v3.9.0" --tag datadog-ci . +``` + +## Migration guide + +If you are upgrading from a previous major version, read our [MIGRATING.md](MIGRATING.md) document to understand the changes and how to adapt your scripts. + +## Development + +Before contributing to this open source project, read our [CONTRIBUTING.md](CONTRIBUTING.md) document. + +## License + +[Apache License, v2.0](LICENSE) diff --git a/packages/base/package.json b/packages/base/package.json index 9885992ac..6f7834168 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -62,6 +62,7 @@ "@datadog/datadog-ci-plugin-deployment": "workspace:*", "@datadog/datadog-ci-plugin-dora": "workspace:*", "@datadog/datadog-ci-plugin-gate": "workspace:*", + "@datadog/datadog-ci-plugin-junit": "workspace:*", "@datadog/datadog-ci-plugin-lambda": "workspace:*", "@datadog/datadog-ci-plugin-sarif": "workspace:*", "@datadog/datadog-ci-plugin-sbom": "workspace:*", @@ -87,6 +88,9 @@ "@datadog/datadog-ci-plugin-gate": { "optional": true }, + "@datadog/datadog-ci-plugin-junit": { + "optional": true + }, "@datadog/datadog-ci-plugin-lambda": { "optional": true }, diff --git a/packages/base/src/cli.ts b/packages/base/src/cli.ts index 2b3d382f6..2d1b37236 100644 --- a/packages/base/src/cli.ts +++ b/packages/base/src/cli.ts @@ -9,6 +9,7 @@ import {commands as doraCommands} from './commands/dora/cli' import {commands as dsymsCommands} from './commands/dsyms/cli' import {commands as gateCommands} from './commands/gate/cli' import {commands as gitMetadataCommands} from './commands/git-metadata/cli' +import {commands as junitCommands} from './commands/junit/cli' import {commands as lambdaCommands} from './commands/lambda/cli' import {commands as pluginCommands} from './commands/plugin/cli' import {commands as sarifCommands} from './commands/sarif/cli' @@ -27,6 +28,7 @@ export const commands = { 'dsyms': dsymsCommands, 'gate': gateCommands, 'git-metadata': gitMetadataCommands, + 'junit': junitCommands, 'lambda': lambdaCommands, 'plugin': pluginCommands, 'sarif': sarifCommands, diff --git a/packages/base/src/commands/junit/cli.ts b/packages/base/src/commands/junit/cli.ts new file mode 100644 index 000000000..d8f8b6df8 --- /dev/null +++ b/packages/base/src/commands/junit/cli.ts @@ -0,0 +1,7 @@ +/* eslint-disable import-x/order */ +import {JunitUploadCommand} from './upload' + +// prettier-ignore +export const commands = [ + JunitUploadCommand, +] diff --git a/packages/datadog-ci/package.json b/packages/datadog-ci/package.json index 2899c29dd..46fc8b955 100644 --- a/packages/datadog-ci/package.json +++ b/packages/datadog-ci/package.json @@ -45,6 +45,7 @@ "@datadog/datadog-ci-plugin-deployment": "workspace:*", "@datadog/datadog-ci-plugin-dora": "workspace:*", "@datadog/datadog-ci-plugin-gate": "workspace:*", + "@datadog/datadog-ci-plugin-junit": "workspace:*", "@datadog/datadog-ci-plugin-sarif": "workspace:*", "@datadog/datadog-ci-plugin-sbom": "workspace:*", "axios": "^1.12.1", @@ -56,14 +57,12 @@ "semver": "^7.5.3", "simple-git": "3.16.0", "typanion": "^3.14.0", - "upath": "^2.0.1", - "uuid": "^9.0.0" + "upath": "^2.0.1" }, "devDependencies": { "@types/jest": "29.5.3", "@types/js-yaml": "^4.0.5", "@types/semver": "^7.7.1", - "@types/uuid": "^9.0.2", "esbuild": "^0.25.9" } } diff --git a/packages/datadog-ci/shims/injected-plugin-submodules.js b/packages/datadog-ci/shims/injected-plugin-submodules.js index 3bbb6200f..946f39542 100644 --- a/packages/datadog-ci/shims/injected-plugin-submodules.js +++ b/packages/datadog-ci/shims/injected-plugin-submodules.js @@ -28,6 +28,9 @@ const injectedPluginSubmodules = { 'gate': { 'evaluate': require('@datadog/datadog-ci-plugin-gate/commands/evaluate'), }, + 'junit': { + 'upload': require('@datadog/datadog-ci-plugin-junit/commands/upload'), + }, 'lambda': { 'flare': require('@datadog/datadog-ci-plugin-lambda/commands/flare'), 'instrument': require('@datadog/datadog-ci-plugin-lambda/commands/instrument'), diff --git a/packages/datadog-ci/src/commands/junit/cli.ts b/packages/datadog-ci/src/commands/junit/cli.ts deleted file mode 100644 index d35161117..000000000 --- a/packages/datadog-ci/src/commands/junit/cli.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {JunitUploadCommand} from './upload' - -export const commands = [JunitUploadCommand] diff --git a/packages/datadog-ci/tsconfig.json b/packages/datadog-ci/tsconfig.json index d2d669664..91b971d47 100644 --- a/packages/datadog-ci/tsconfig.json +++ b/packages/datadog-ci/tsconfig.json @@ -12,6 +12,7 @@ {"path": "../plugin-deployment"}, {"path": "../plugin-dora"}, {"path": "../plugin-gate"}, + {"path": "../plugin-junit"}, {"path": "../plugin-sarif"}, {"path": "../plugin-sbom"} ] diff --git a/packages/plugin-junit/LICENSE b/packages/plugin-junit/LICENSE new file mode 100644 index 000000000..f66d768ce --- /dev/null +++ b/packages/plugin-junit/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Datadog, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/datadog-ci/src/commands/junit/README.md b/packages/plugin-junit/README.md similarity index 94% rename from packages/datadog-ci/src/commands/junit/README.md rename to packages/plugin-junit/README.md index 0f5abf998..e1296e97d 100644 --- a/packages/datadog-ci/src/commands/junit/README.md +++ b/packages/plugin-junit/README.md @@ -66,7 +66,7 @@ To verify this command works as expected, you can use `--dry-run`: ```bash export DD_API_KEY='' -yarn launch junit upload ./packages/datadog-ci/src/commands/junit/__tests__/fixtures/java-report.xml --service example-upload --dry-run +yarn launch junit upload ./packages/plugin-junit/__tests__/fixtures/java-report.xml --service example-upload --dry-run ``` Successful output should look like this: @@ -74,9 +74,9 @@ Successful output should look like this: ```bash ⚠️ DRY-RUN MODE ENABLED. WILL NOT UPLOAD JUNIT XML Starting upload with concurrency 20. -Will upload jUnit XML file src/commands/junit/__tests__/fixtures/java-report.xml +Will upload jUnit XML file src/__tests__/fixtures/java-report.xml service: example-upload -[DRYRUN] Uploading jUnit XML test report file in src/commands/junit/__tests__/fixtures/java-report.xml +[DRYRUN] Uploading jUnit XML test report file in src/__tests__/fixtures/java-report.xml ✅ Uploaded 1 files in 0 seconds. ``` diff --git a/packages/plugin-junit/package.json b/packages/plugin-junit/package.json new file mode 100644 index 000000000..c6ff5e5eb --- /dev/null +++ b/packages/plugin-junit/package.json @@ -0,0 +1,50 @@ +{ + "name": "@datadog/datadog-ci-plugin-junit", + "version": "5.2.1", + "description": "Datadog CI plugin for `junit` commands", + "license": "Apache-2.0", + "keywords": [ + "datadog", + "datadog-ci", + "plugin" + ], + "homepage": "https://github.com/DataDog/datadog-ci/tree/master/packages/plugin-junit", + "repository": { + "type": "git", + "url": "https://github.com/DataDog/datadog-ci.git", + "directory": "packages/plugin-junit" + }, + "exports": { + "./package.json": "./package.json", + "./commands/*": { + "development": "./src/commands/*.ts", + "default": "./dist/commands/*.js" + } + }, + "files": [ + "dist/**/*", + "README", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "yarn package:clean; yarn package:build", + "lint": "yarn package:lint", + "prepack": "yarn package:clean-dist" + }, + "peerDependencies": { + "@datadog/datadog-ci-base": "workspace:*" + }, + "dependencies": { + "axios": "^1.12.1", + "chalk": "3.0.0", + "clipanion": "^3.2.1", + "fast-xml-parser": "^4.4.1", + "form-data": "^4.0.4", + "typanion": "^3.14.0", + "upath": "^2.0.1", + "uuid": "^9.0.0" + } +} diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml b/packages/plugin-junit/src/__tests__/fixtures/autodiscovery/junit-report.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml rename to packages/plugin-junit/src/__tests__/fixtures/autodiscovery/junit-report.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml b/packages/plugin-junit/src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml rename to packages/plugin-junit/src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/regular-file.xml b/packages/plugin-junit/src/__tests__/fixtures/autodiscovery/regular-file.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/regular-file.xml rename to packages/plugin-junit/src/__tests__/fixtures/autodiscovery/regular-file.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml b/packages/plugin-junit/src/__tests__/fixtures/autodiscovery/test-results.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml rename to packages/plugin-junit/src/__tests__/fixtures/autodiscovery/test-results.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/empty.xml b/packages/plugin-junit/src/__tests__/fixtures/empty.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/empty.xml rename to packages/plugin-junit/src/__tests__/fixtures/empty.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/go-report.xml b/packages/plugin-junit/src/__tests__/fixtures/go-report.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/go-report.xml rename to packages/plugin-junit/src/__tests__/fixtures/go-report.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/invalid.xml b/packages/plugin-junit/src/__tests__/fixtures/invalid.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/invalid.xml rename to packages/plugin-junit/src/__tests__/fixtures/invalid.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/java-report.xml b/packages/plugin-junit/src/__tests__/fixtures/java-report.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/java-report.xml rename to packages/plugin-junit/src/__tests__/fixtures/java-report.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/junit.xml/valid-report-2.xml b/packages/plugin-junit/src/__tests__/fixtures/junit.xml/valid-report-2.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/junit.xml/valid-report-2.xml rename to packages/plugin-junit/src/__tests__/fixtures/junit.xml/valid-report-2.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/junit.xml/valid-report.xml b/packages/plugin-junit/src/__tests__/fixtures/junit.xml/valid-report.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/junit.xml/valid-report.xml rename to packages/plugin-junit/src/__tests__/fixtures/junit.xml/valid-report.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/other/other-non-junit-file.txt b/packages/plugin-junit/src/__tests__/fixtures/other/other-non-junit-file.txt similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/other/other-non-junit-file.txt rename to packages/plugin-junit/src/__tests__/fixtures/other/other-non-junit-file.txt diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/invalid-no-tests.xml b/packages/plugin-junit/src/__tests__/fixtures/subfolder/invalid-no-tests.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/invalid-no-tests.xml rename to packages/plugin-junit/src/__tests__/fixtures/subfolder/invalid-no-tests.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/js-report.xml b/packages/plugin-junit/src/__tests__/fixtures/subfolder/js-report.xml similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/js-report.xml rename to packages/plugin-junit/src/__tests__/fixtures/subfolder/js-report.xml diff --git a/packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/non-junit-file.txt b/packages/plugin-junit/src/__tests__/fixtures/subfolder/non-junit-file.txt similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/fixtures/subfolder/non-junit-file.txt rename to packages/plugin-junit/src/__tests__/fixtures/subfolder/non-junit-file.txt diff --git a/packages/datadog-ci/src/commands/junit/__tests__/upload.test.ts b/packages/plugin-junit/src/__tests__/upload.test.ts similarity index 73% rename from packages/datadog-ci/src/commands/junit/__tests__/upload.test.ts rename to packages/plugin-junit/src/__tests__/upload.test.ts index bff855c17..df110cbd7 100644 --- a/packages/datadog-ci/src/commands/junit/__tests__/upload.test.ts +++ b/packages/plugin-junit/src/__tests__/upload.test.ts @@ -29,7 +29,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], config: {}, context: command.context, service: 'service', @@ -43,16 +43,16 @@ describe('upload', () => { expect(files.length).toBe(2) const filePaths = files.map((file) => file.xmlPath) - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/go-report.xml') - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/java-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/go-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/java-report.xml') const output = command.context.stdout.toString() expect(output).toContain( - renderInvalidFile('src/commands/junit/__tests__/fixtures/empty.xml', 'Start tag expected.') + renderInvalidFile('src/__tests__/fixtures/empty.xml', 'Start tag expected.') ) expect(output).toContain( renderInvalidFile( - 'src/commands/junit/__tests__/fixtures/invalid.xml', + 'src/__tests__/fixtures/invalid.xml', 'Neither nor are the root tag.' ) ) @@ -62,7 +62,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/go-report.xml'], + basePaths: ['src/__tests__/fixtures/go-report.xml'], config: {}, context: command.context, service: 'service', @@ -77,7 +77,7 @@ describe('upload', () => { expect(files.length).toEqual(1) expect(files[0]).toMatchObject({ - xmlPath: 'src/commands/junit/__tests__/fixtures/go-report.xml', + xmlPath: 'src/__tests__/fixtures/go-report.xml', }) }) @@ -85,7 +85,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/does-not-exist.xml'], + basePaths: ['src/__tests__/fixtures/does-not-exist.xml'], config: {}, context: command.context, service: 'service', @@ -105,8 +105,8 @@ describe('upload', () => { const files = await command['getMatchingJUnitXMLFiles'].call( { basePaths: [ - 'src/commands/junit/__tests__/fixtures', - 'src/commands/junit/__tests__/fixtures/subfolder/js-report.xml', + 'src/__tests__/fixtures', + 'src/__tests__/fixtures/subfolder/js-report.xml', ], config: {}, context: command.context, @@ -122,16 +122,16 @@ describe('upload', () => { // Check that all expected files are present, regardless of order const filePaths = files.map((file) => file.xmlPath) expect(filePaths.length).toEqual(3) - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/go-report.xml') - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/java-report.xml') - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/subfolder/js-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/go-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/java-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/subfolder/js-report.xml') }) test('should allow folders with extensions', async () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/junit.xml'], + basePaths: ['src/__tests__/fixtures/junit.xml'], config: {}, context: command.context, service: 'service', @@ -145,15 +145,15 @@ describe('upload', () => { expect(files.length).toBe(2) const filePaths = files.map((file) => file.xmlPath) - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/junit.xml/valid-report-2.xml') - expect(filePaths).toContain('src/commands/junit/__tests__/fixtures/junit.xml/valid-report.xml') + expect(filePaths).toContain('src/__tests__/fixtures/junit.xml/valid-report-2.xml') + expect(filePaths).toContain('src/__tests__/fixtures/junit.xml/valid-report.xml') }) test('should not have repeated files', async () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures', 'src/commands/junit/__tests__/fixtures/go-report.xml'], + basePaths: ['src/__tests__/fixtures', 'src/__tests__/fixtures/go-report.xml'], config: {}, context: command.context, service: 'service', @@ -172,7 +172,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], config: {}, context: command.context, service: 'service', @@ -195,7 +195,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], config: {}, context: command.context, logs: true, @@ -219,7 +219,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/subfolder/invalid-no-tests.xml'], + basePaths: ['src/__tests__/fixtures/subfolder/invalid-no-tests.xml'], config: {}, context: command.context, logs: true, @@ -234,7 +234,7 @@ describe('upload', () => { const output = command.context.stdout.toString() expect(output).toContain( renderInvalidFile( - 'src/commands/junit/__tests__/fixtures/subfolder/invalid-no-tests.xml', + 'src/__tests__/fixtures/subfolder/invalid-no-tests.xml', 'The junit report file is empty, there are no elements.' ) ) @@ -258,14 +258,14 @@ describe('upload', () => { const fileNames = files.map((file) => file.xmlPath) expect(fileNames.length).toBe(9) - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/go-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/java-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/junit.xml/valid-report-2.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/junit.xml/valid-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/subfolder/js-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/go-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/java-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/junit.xml/valid-report-2.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/junit.xml/valid-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/subfolder/js-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/junit-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/test-results.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') }) test('should fetch nested folders and ignore non xml files', async () => { @@ -286,21 +286,21 @@ describe('upload', () => { const fileNames = files.map((file) => file.xmlPath) expect(fileNames.length).toBe(9) - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/go-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/java-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/junit.xml/valid-report-2.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/junit.xml/valid-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/subfolder/js-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml') - expect(fileNames).toContain('./src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/go-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/java-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/junit.xml/valid-report-2.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/junit.xml/valid-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/subfolder/js-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/junit-report.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/test-results.xml') + expect(fileNames).toContain('./src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') }) test('should discover junit XML files automatically with recursive search', async () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/autodiscovery'], + basePaths: ['src/__tests__/fixtures/autodiscovery'], automaticReportsDiscovery: true, config: {}, context: command.context, @@ -315,19 +315,19 @@ describe('upload', () => { const fileNames = files.map((file) => file.xmlPath) expect(fileNames.length).toBe(3) - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml') - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml') - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') - expect(fileNames).not.toContain('src/commands/junit/__tests__/fixtures/autodiscovery/regular-file.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/junit-report.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/test-results.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') + expect(fileNames).not.toContain('src/__tests__/fixtures/autodiscovery/regular-file.xml') }) test('should discover junit XML files automatically excluding ignored paths', async () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures/autodiscovery'], + basePaths: ['src/__tests__/fixtures/autodiscovery'], automaticReportsDiscovery: true, - ignoredPaths: 'src/commands/junit/__tests__/fixtures/autodiscovery/nested', + ignoredPaths: 'src/__tests__/fixtures/autodiscovery/nested', config: {}, context: command.context, service: 'service', @@ -341,8 +341,8 @@ describe('upload', () => { const fileNames = files.map((file) => file.xmlPath) expect(fileNames.length).toBe(2) - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml') - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/test-results.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/junit-report.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/test-results.xml') }) test('should combine explicit file paths with auto-discovered files', async () => { @@ -350,8 +350,8 @@ describe('upload', () => { const files = await command['getMatchingJUnitXMLFiles'].call( { basePaths: [ - 'src/commands/junit/__tests__/fixtures/autodiscovery/nested', - 'src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml', + 'src/__tests__/fixtures/autodiscovery/nested', + 'src/__tests__/fixtures/autodiscovery/junit-report.xml', ], automaticReportsDiscovery: true, config: {}, @@ -367,8 +367,8 @@ describe('upload', () => { const fileNames = files.map((file) => file.xmlPath) expect(fileNames.length).toBe(2) - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/junit-report.xml') - expect(fileNames).toContain('src/commands/junit/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/junit-report.xml') + expect(fileNames).toContain('src/__tests__/fixtures/autodiscovery/nested/TEST-suite.xml') }) }) describe('getSpanTags', () => { @@ -481,7 +481,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const xPathTags = command['parseXPathTags'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], config: {}, context: command.context, service: 'service', @@ -498,7 +498,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) command['parseXPathTags'].call( { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], config: {}, context: command.context, service: 'service', @@ -515,42 +515,42 @@ describe('execute', () => { const runCLI = makeRunCLI(JunitUploadCommand, ['junit', 'upload', '--service', 'test-service', '--dry-run', '--logs']) test('relative path with double dots', async () => { - const {context, code} = await runCLI(['src/commands/junit/__tests__/doesnotexist/../fixtures']) + const {context, code} = await runCLI(['src/__tests__/doesnotexist/../fixtures']) const output = context.stdout.toString().split('\n') expect(code).toBe(0) checkConsoleOutput(output, { - basePaths: ['src/commands/junit/__tests__/fixtures'], + basePaths: ['src/__tests__/fixtures'], concurrency: 20, service: 'test-service', }) }) test('multiple paths', async () => { - const {context, code} = await runCLI(['src/commands/junit/first/', 'src/commands/junit/second/']) + const {context, code} = await runCLI(['src/first/', 'src/second/']) const output = context.stdout.toString().split('\n') expect(code).toBe(0) checkConsoleOutput(output, { - basePaths: ['src/commands/junit/first/', 'src/commands/junit/second/'], + basePaths: ['src/first/', 'src/second/'], concurrency: 20, service: 'test-service', }) }) test('absolute path', async () => { - const {context, code} = await runCLI([CWD + '/src/commands/junit/__tests__/fixtures']) + const {context, code} = await runCLI([CWD + '/src/__tests__/fixtures']) const output = context.stdout.toString().split('\n') expect(code).toBe(0) checkConsoleOutput(output, { - basePaths: [`${CWD}/src/commands/junit/__tests__/fixtures`], + basePaths: [`${CWD}/src/__tests__/fixtures`], concurrency: 20, service: 'test-service', }) }) test('single file', async () => { - const {context, code} = await runCLI([CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml']) + const {context, code} = await runCLI([CWD + '/src/__tests__/fixtures/single_file.xml']) const output = context.stdout.toString().split('\n') - const path = `${CWD}/src/commands/junit/__tests__/fixtures/single_file.xml` + const path = `${CWD}/src/__tests__/fixtures/single_file.xml` expect(code).toBe(0) expect(output[0]).toContain('DRY-RUN MODE ENABLED. WILL NOT UPLOAD JUNIT XML') expect(output[1]).toContain('Starting upload with concurrency 20.') @@ -559,7 +559,7 @@ describe('execute', () => { }) test('with git metadata without argument (default value is true)', async () => { - const {context, code} = await runCLI(['--verbose', CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml']) + const {context, code} = await runCLI(['--verbose', CWD + '/src/__tests__/fixtures/single_file.xml']) const output = context.stdout.toString().split('\n') expect(id).toHaveBeenCalled() expect(code).toBe(0) @@ -570,7 +570,7 @@ describe('execute', () => { const {context, code} = await runCLI([ '--verbose', '--skip-git-metadata-upload', // should tolerate the option as a boolean flag - CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml', + CWD + '/src/__tests__/fixtures/single_file.xml', ]) const output = context.stdout.toString().split('\n') expect(id).not.toHaveBeenCalled() @@ -582,7 +582,7 @@ describe('execute', () => { const {context, code} = await runCLI([ '--verbose', '--skip-git-metadata-upload=1', // should tolerate the option as a boolean flag - CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml', + CWD + '/src/__tests__/fixtures/single_file.xml', ]) const output = context.stdout.toString().split('\n') expect(id).not.toHaveBeenCalled() @@ -593,7 +593,7 @@ describe('execute', () => { test('with git metadata (with argument set to 0)', async () => { const {context, code} = await runCLI([ '--skip-git-metadata-upload=0', - CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml', + CWD + '/src/__tests__/fixtures/single_file.xml', ]) const output = context.stdout.toString().split('\n') expect(code).toBe(0) @@ -601,7 +601,7 @@ describe('execute', () => { }) test('id headers are added when git metadata is uploaded', async () => { - await runCLI(['--skip-git-metadata-upload=0', CWD + '/src/commands/junit/__tests__/fixtures/single_file.xml']) + await runCLI(['--skip-git-metadata-upload=0', CWD + '/src/__tests__/fixtures/single_file.xml']) expect(id).toHaveBeenCalled() }, 10000000) }) diff --git a/packages/datadog-ci/src/commands/junit/__tests__/utils.test.ts b/packages/plugin-junit/src/__tests__/utils.test.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/__tests__/utils.test.ts rename to packages/plugin-junit/src/__tests__/utils.test.ts diff --git a/packages/datadog-ci/src/commands/junit/api.ts b/packages/plugin-junit/src/api.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/api.ts rename to packages/plugin-junit/src/api.ts diff --git a/packages/datadog-ci/src/commands/junit/interfaces.ts b/packages/plugin-junit/src/interfaces.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/interfaces.ts rename to packages/plugin-junit/src/interfaces.ts diff --git a/packages/datadog-ci/src/commands/junit/renderer.ts b/packages/plugin-junit/src/renderer.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/renderer.ts rename to packages/plugin-junit/src/renderer.ts diff --git a/packages/datadog-ci/src/commands/junit/upload.ts b/packages/plugin-junit/src/upload.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/upload.ts rename to packages/plugin-junit/src/upload.ts diff --git a/packages/datadog-ci/src/commands/junit/utils.ts b/packages/plugin-junit/src/utils.ts similarity index 100% rename from packages/datadog-ci/src/commands/junit/utils.ts rename to packages/plugin-junit/src/utils.ts diff --git a/packages/plugin-junit/tsconfig.json b/packages/plugin-junit/tsconfig.json new file mode 100644 index 000000000..bfc7b6bf3 --- /dev/null +++ b/packages/plugin-junit/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index ea171bf1a..429014b5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ {"path": "./packages/plugin-deployment"}, {"path": "./packages/plugin-dora"}, {"path": "./packages/plugin-gate"}, + {"path": "./packages/plugin-junit"}, {"path": "./packages/plugin-lambda"}, {"path": "./packages/plugin-sarif"}, {"path": "./packages/plugin-sbom"}, diff --git a/yarn.lock b/yarn.lock index 228a9ae94..bc0c3e01a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1531,6 +1531,7 @@ __metadata: "@datadog/datadog-ci-plugin-deployment": "workspace:*" "@datadog/datadog-ci-plugin-dora": "workspace:*" "@datadog/datadog-ci-plugin-gate": "workspace:*" + "@datadog/datadog-ci-plugin-junit": "workspace:*" "@datadog/datadog-ci-plugin-lambda": "workspace:*" "@datadog/datadog-ci-plugin-sarif": "workspace:*" "@datadog/datadog-ci-plugin-sbom": "workspace:*" @@ -1549,6 +1550,8 @@ __metadata: optional: true "@datadog/datadog-ci-plugin-gate": optional: true + "@datadog/datadog-ci-plugin-junit": + optional: true "@datadog/datadog-ci-plugin-lambda": optional: true "@datadog/datadog-ci-plugin-sarif": @@ -1642,6 +1645,23 @@ __metadata: languageName: unknown linkType: soft +"@datadog/datadog-ci-plugin-junit@workspace:*, @datadog/datadog-ci-plugin-junit@workspace:packages/plugin-junit": + version: 0.0.0-use.local + resolution: "@datadog/datadog-ci-plugin-junit@workspace:packages/plugin-junit" + dependencies: + axios: "npm:^1.12.1" + chalk: "npm:3.0.0" + clipanion: "npm:^3.2.1" + fast-xml-parser: "npm:^4.4.1" + form-data: "npm:^4.0.4" + typanion: "npm:^3.14.0" + upath: "npm:^2.0.1" + uuid: "npm:^9.0.0" + peerDependencies: + "@datadog/datadog-ci-base": "workspace:*" + languageName: unknown + linkType: soft + "@datadog/datadog-ci-plugin-lambda@workspace:packages/plugin-lambda": version: 0.0.0-use.local resolution: "@datadog/datadog-ci-plugin-lambda@workspace:packages/plugin-lambda" @@ -1755,12 +1775,12 @@ __metadata: "@datadog/datadog-ci-plugin-deployment": "workspace:*" "@datadog/datadog-ci-plugin-dora": "workspace:*" "@datadog/datadog-ci-plugin-gate": "workspace:*" + "@datadog/datadog-ci-plugin-junit": "workspace:*" "@datadog/datadog-ci-plugin-sarif": "workspace:*" "@datadog/datadog-ci-plugin-sbom": "workspace:*" "@types/jest": "npm:29.5.3" "@types/js-yaml": "npm:^4.0.5" "@types/semver": "npm:^7.7.1" - "@types/uuid": "npm:^9.0.2" axios: "npm:^1.12.1" chalk: "npm:3.0.0" clipanion: "npm:^3.2.1" @@ -1772,7 +1792,6 @@ __metadata: simple-git: "npm:3.16.0" typanion: "npm:^3.14.0" upath: "npm:^2.0.1" - uuid: "npm:^9.0.0" bin: datadog-ci: dist/cli.js languageName: unknown From f77411e4c87f63f9bfd6844bd68c74490e6be6f2 Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 20:15:27 +0100 Subject: [PATCH 12/29] Split `JunitUploadCommand` --- packages/base/src/commands/junit/upload.ts | 97 +++++++++++++++++++ packages/datadog-ci/src/commands/cli.ts | 2 - .../datadog-ci/src/commands/coverage/api.ts | 1 + .../src/commands/coverage/upload.ts | 4 +- .../plugin-junit/src/__tests__/upload.test.ts | 11 +-- packages/plugin-junit/src/upload.ts | 92 +----------------- 6 files changed, 104 insertions(+), 103 deletions(-) create mode 100644 packages/base/src/commands/junit/upload.ts diff --git a/packages/base/src/commands/junit/upload.ts b/packages/base/src/commands/junit/upload.ts new file mode 100644 index 000000000..f3bc3ef69 --- /dev/null +++ b/packages/base/src/commands/junit/upload.ts @@ -0,0 +1,97 @@ +import {Command, Option} from 'clipanion' +import * as t from 'typanion' + +import {BaseCommand} from '@datadog/datadog-ci-base' +import * as validation from '@datadog/datadog-ci-base/helpers/validation' + +import {executePluginCommand} from '../../helpers/plugin' + +export class JunitUploadCommand extends BaseCommand { + public static paths = [['junit', 'upload']] + + public static usage = Command.Usage({ + category: 'CI Visibility', + description: 'Upload jUnit XML test reports files to Datadog.', + details: ` + This command will upload to jUnit XML test reports files to Datadog.\n + See README for details. + `, + examples: [ + ['Upload all jUnit XML test report files in current directory', 'datadog-ci junit upload --service my-service .'], + [ + 'Discover and upload all jUnit XML test report files doing recursive search in current directory', + 'datadog-ci junit upload --service my-service --auto-discovery .', + ], + [ + 'Discover and upload all jUnit XML test report files doing recursive search in current directory, ignoring src/ignored-module-a and src/ignored-module-b', + 'datadog-ci junit upload --service my-service --ignored-paths src/ignored-module-a,src/ignored-module-b --auto-discovery .', + ], + [ + 'Upload all jUnit XML test report files in src/unit-test-reports and src/acceptance-test-reports', + 'datadog-ci junit upload --service my-service src/unit-test-reports src/acceptance-test-reports', + ], + [ + 'Upload all jUnit XML test report files in current directory and add extra tags globally', + 'datadog-ci junit upload --service my-service --tags key1:value1 --tags key2:value2 .', + ], + [ + 'Upload all jUnit XML test report files in current directory and add extra measures globally', + 'datadog-ci junit upload --service my-service --measures key1:123 --measures key2:321 .', + ], + [ + 'Upload all jUnit XML test report files in current directory to the datadoghq.eu site', + 'DD_SITE=datadoghq.eu datadog-ci junit upload --service my-service .', + ], + [ + 'Upload all jUnit XML test report files in current directory while also collecting logs', + 'datadog-ci junit upload --service my-service --logs .', + ], + [ + 'Upload all jUnit XML test report files in current directory customizing test suite with xpath', + 'datadog-ci junit upload --service my-service --xpath-tag test.suite=/testcase/@classname .', + ], + [ + 'Upload all jUnit XML test report files in current directory adding a custom tag from property with xpath', + "datadog-ci junit upload --service my-service --xpath-tag custom_tag=/testcase/..//property[@name='property-name'] .", + ], + [ + 'Upload all jUnit XML test report files in current directory with extra verbosity', + 'datadog-ci junit upload --verbose --service my-service .', + ], + ], + }) + + public basePaths = Option.Rest({required: 1}) + public verbose = Option.Boolean('--verbose', false) + public dryRun = Option.Boolean('--dry-run', false) + public env = Option.String('--env') + public logs = Option.String('--logs', 'false', { + env: 'DD_CIVISIBILITY_LOGS_ENABLED', + tolerateBoolean: true, + validator: t.isBoolean(), + }) + public maxConcurrency = Option.String('--max-concurrency', '20', {validator: validation.isInteger()}) + public measures = Option.Array('--measures') + public service = Option.String('--service', {env: 'DD_SERVICE'}) + public tags = Option.Array('--tags') + public reportTags = Option.Array('--report-tags') + public reportMeasures = Option.Array('--report-measures') + public rawXPathTags = Option.Array('--xpath-tag') + public gitRepositoryURL = Option.String('--git-repository-url') + public skipGitMetadataUpload = Option.String('--skip-git-metadata-upload', 'false', { + validator: t.isBoolean(), + tolerateBoolean: true, + }) + public automaticReportsDiscovery = Option.String('--auto-discovery', 'false', { + validator: t.isBoolean(), + tolerateBoolean: true, + }) + public ignoredPaths = Option.String('--ignored-paths') + + public fips = Option.Boolean('--fips', false) + public fipsIgnoreError = Option.Boolean('--fips-ignore-error', false) + + public async execute(): Promise { + return executePluginCommand(this) + } +} diff --git a/packages/datadog-ci/src/commands/cli.ts b/packages/datadog-ci/src/commands/cli.ts index 907dc7ff6..8b3a0f88d 100644 --- a/packages/datadog-ci/src/commands/cli.ts +++ b/packages/datadog-ci/src/commands/cli.ts @@ -4,7 +4,6 @@ import type {RecordWithKebabCaseKeys} from '@datadog/datadog-ci-base/helpers/typ import {commands as coverageCommands} from './coverage/cli' import {commands as elfSymbolsCommands} from './elf-symbols/cli' import {commands as flutterSymbolsCommands} from './flutter-symbols/cli' -import {commands as junitCommands} from './junit/cli' import {commands as measureCommands} from './measure/cli' import {commands as peSymbolsCommands} from './pe-symbols/cli' import {commands as reactNativeCommands} from './react-native/cli' @@ -19,7 +18,6 @@ export const commands = { 'coverage': coverageCommands, 'elf-symbols': elfSymbolsCommands, 'flutter-symbols': flutterSymbolsCommands, - 'junit': junitCommands, 'measure': measureCommands, 'pe-symbols': peSymbolsCommands, 'react-native': reactNativeCommands, diff --git a/packages/datadog-ci/src/commands/coverage/api.ts b/packages/datadog-ci/src/commands/coverage/api.ts index 21f7399cc..ece47cb78 100644 --- a/packages/datadog-ci/src/commands/coverage/api.ts +++ b/packages/datadog-ci/src/commands/coverage/api.ts @@ -15,6 +15,7 @@ const maxBodyLength = Infinity export const datadogSite = process.env.DATADOG_SITE || process.env.DD_SITE || 'datadoghq.com' export const intakeUrl = `https://ci-intake.${datadogSite}` +export const apiUrl = `https://api.${datadogSite}` export const uploadCodeCoverageReport = (request: (args: AxiosRequestConfig) => AxiosPromise) => async (payload: Payload) => { diff --git a/packages/datadog-ci/src/commands/coverage/upload.ts b/packages/datadog-ci/src/commands/coverage/upload.ts index ac74b3d9d..e28345541 100644 --- a/packages/datadog-ci/src/commands/coverage/upload.ts +++ b/packages/datadog-ci/src/commands/coverage/upload.ts @@ -38,9 +38,7 @@ import {Command, Option} from 'clipanion' import * as simpleGit from 'simple-git' import upath from 'upath' -import {apiUrl} from '../junit/api' - -import {apiConstructor, intakeUrl} from './api' +import {apiConstructor, apiUrl, intakeUrl} from './api' import {APIHelper, Payload, RepoFile} from './interfaces' import { renderCommandInfo, diff --git a/packages/plugin-junit/src/__tests__/upload.test.ts b/packages/plugin-junit/src/__tests__/upload.test.ts index df110cbd7..6c30696bb 100644 --- a/packages/plugin-junit/src/__tests__/upload.test.ts +++ b/packages/plugin-junit/src/__tests__/upload.test.ts @@ -6,7 +6,7 @@ import {SpanTags} from '@datadog/datadog-ci-base/helpers/interfaces' import upath from 'upath' import {renderInvalidFile} from '../renderer' -import {JunitUploadCommand} from '../upload' +import {PluginCommand as JunitUploadCommand} from '../upload' jest.mock('@datadog/datadog-ci-base/helpers/id', () => jest.fn()) @@ -47,9 +47,7 @@ describe('upload', () => { expect(filePaths).toContain('src/__tests__/fixtures/java-report.xml') const output = command.context.stdout.toString() - expect(output).toContain( - renderInvalidFile('src/__tests__/fixtures/empty.xml', 'Start tag expected.') - ) + expect(output).toContain(renderInvalidFile('src/__tests__/fixtures/empty.xml', 'Start tag expected.')) expect(output).toContain( renderInvalidFile( 'src/__tests__/fixtures/invalid.xml', @@ -104,10 +102,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: [ - 'src/__tests__/fixtures', - 'src/__tests__/fixtures/subfolder/js-report.xml', - ], + basePaths: ['src/__tests__/fixtures', 'src/__tests__/fixtures/subfolder/js-report.xml'], config: {}, context: command.context, service: 'service', diff --git a/packages/plugin-junit/src/upload.ts b/packages/plugin-junit/src/upload.ts index 1d0856305..61f35a26d 100644 --- a/packages/plugin-junit/src/upload.ts +++ b/packages/plugin-junit/src/upload.ts @@ -1,10 +1,10 @@ import fs from 'fs' import os from 'os' -import {BaseCommand} from '@datadog/datadog-ci-base' import {newSimpleGit} from '@datadog/datadog-ci-base/commands/git-metadata/git' import {uploadToGitDB} from '@datadog/datadog-ci-base/commands/git-metadata/gitdb' import {isGitRepo} from '@datadog/datadog-ci-base/commands/git-metadata/library' +import {JunitUploadCommand} from '@datadog/datadog-ci-base/commands/junit/upload' import {FIPS_ENV_VAR, FIPS_IGNORE_ERROR_ENV_VAR} from '@datadog/datadog-ci-base/constants' import {getCISpanTags} from '@datadog/datadog-ci-base/helpers/ci' import {doWithMaxConcurrency} from '@datadog/datadog-ci-base/helpers/concurrency' @@ -20,11 +20,8 @@ import {retryRequest} from '@datadog/datadog-ci-base/helpers/retry' import {parseTags, parseMetrics} from '@datadog/datadog-ci-base/helpers/tags' import {getUserGitSpanTags} from '@datadog/datadog-ci-base/helpers/user-provided-git' import {getRequestBuilder, timedExecAsync} from '@datadog/datadog-ci-base/helpers/utils' -import * as validation from '@datadog/datadog-ci-base/helpers/validation' import chalk from 'chalk' -import {Command, Option} from 'clipanion' import {XMLParser, XMLValidator} from 'fast-xml-parser' -import * as t from 'typanion' import upath from 'upath' import {apiConstructor, apiUrl, intakeUrl} from './api' @@ -77,92 +74,7 @@ const validateXml = (xmlFilePath: string) => { return undefined } -export class JunitUploadCommand extends BaseCommand { - public static paths = [['junit', 'upload']] - - public static usage = Command.Usage({ - category: 'CI Visibility', - description: 'Upload jUnit XML test reports files to Datadog.', - details: ` - This command will upload to jUnit XML test reports files to Datadog.\n - See README for details. - `, - examples: [ - ['Upload all jUnit XML test report files in current directory', 'datadog-ci junit upload --service my-service .'], - [ - 'Discover and upload all jUnit XML test report files doing recursive search in current directory', - 'datadog-ci junit upload --service my-service --auto-discovery .', - ], - [ - 'Discover and upload all jUnit XML test report files doing recursive search in current directory, ignoring src/ignored-module-a and src/ignored-module-b', - 'datadog-ci junit upload --service my-service --ignored-paths src/ignored-module-a,src/ignored-module-b --auto-discovery .', - ], - [ - 'Upload all jUnit XML test report files in src/unit-test-reports and src/acceptance-test-reports', - 'datadog-ci junit upload --service my-service src/unit-test-reports src/acceptance-test-reports', - ], - [ - 'Upload all jUnit XML test report files in current directory and add extra tags globally', - 'datadog-ci junit upload --service my-service --tags key1:value1 --tags key2:value2 .', - ], - [ - 'Upload all jUnit XML test report files in current directory and add extra measures globally', - 'datadog-ci junit upload --service my-service --measures key1:123 --measures key2:321 .', - ], - [ - 'Upload all jUnit XML test report files in current directory to the datadoghq.eu site', - 'DD_SITE=datadoghq.eu datadog-ci junit upload --service my-service .', - ], - [ - 'Upload all jUnit XML test report files in current directory while also collecting logs', - 'datadog-ci junit upload --service my-service --logs .', - ], - [ - 'Upload all jUnit XML test report files in current directory customizing test suite with xpath', - 'datadog-ci junit upload --service my-service --xpath-tag test.suite=/testcase/@classname .', - ], - [ - 'Upload all jUnit XML test report files in current directory adding a custom tag from property with xpath', - "datadog-ci junit upload --service my-service --xpath-tag custom_tag=/testcase/..//property[@name='property-name'] .", - ], - [ - 'Upload all jUnit XML test report files in current directory with extra verbosity', - 'datadog-ci junit upload --verbose --service my-service .', - ], - ], - }) - - private basePaths = Option.Rest({required: 1}) - private verbose = Option.Boolean('--verbose', false) - private dryRun = Option.Boolean('--dry-run', false) - private env = Option.String('--env') - private logs = Option.String('--logs', 'false', { - env: 'DD_CIVISIBILITY_LOGS_ENABLED', - tolerateBoolean: true, - validator: t.isBoolean(), - }) - private maxConcurrency = Option.String('--max-concurrency', '20', {validator: validation.isInteger()}) - private measures = Option.Array('--measures') - private service = Option.String('--service', {env: 'DD_SERVICE'}) - private tags = Option.Array('--tags') - private reportTags = Option.Array('--report-tags') - private reportMeasures = Option.Array('--report-measures') - private rawXPathTags = Option.Array('--xpath-tag') - private gitRepositoryURL = Option.String('--git-repository-url') - private skipGitMetadataUpload = Option.String('--skip-git-metadata-upload', 'false', { - validator: t.isBoolean(), - tolerateBoolean: true, - }) - - private fips = Option.Boolean('--fips', false) - private fipsIgnoreError = Option.Boolean('--fips-ignore-error', false) - - private automaticReportsDiscovery = Option.String('--auto-discovery', 'false', { - validator: t.isBoolean(), - tolerateBoolean: true, - }) - private ignoredPaths = Option.String('--ignored-paths') - +export class PluginCommand extends JunitUploadCommand { private config = { apiKey: process.env.DATADOG_API_KEY || process.env.DD_API_KEY, env: process.env.DD_ENV, From 67e4e19b6be22038166bf34f4ddff996d08dfa8a Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 20:19:41 +0100 Subject: [PATCH 13/29] Run `yarn lint:packages --fix` --- .github/workflows/ci.yml | 2 ++ packages/plugin-junit/package.json | 2 -- yarn.lock | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85f89b4de..20ffe471e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,7 @@ jobs: "@datadog/datadog-ci-plugin-deployment": "file:./artifacts/@datadog-datadog-ci-plugin-deployment-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-dora": "file:./artifacts/@datadog-datadog-ci-plugin-dora-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-gate": "file:./artifacts/@datadog-datadog-ci-plugin-gate-${{ matrix.version }}.tgz", + "@datadog/datadog-ci-plugin-junit": "file:./artifacts/@datadog-datadog-ci-plugin-junit-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-sarif": "file:./artifacts/@datadog-datadog-ci-plugin-sarif-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-sbom": "file:./artifacts/@datadog-datadog-ci-plugin-sbom-${{ matrix.version }}.tgz" }, @@ -322,6 +323,7 @@ jobs: -p ./artifacts/@datadog-datadog-ci-plugin-deployment-20.tgz \ -p ./artifacts/@datadog-datadog-ci-plugin-dora-20.tgz \ -p ./artifacts/@datadog-datadog-ci-plugin-gate-20.tgz \ + -p ./artifacts/@datadog-datadog-ci-plugin-junit-20.tgz \ -p ./artifacts/@datadog-datadog-ci-plugin-sarif-20.tgz \ -p ./artifacts/@datadog-datadog-ci-plugin-sbom-20.tgz \ datadog-ci aas instrument || true diff --git a/packages/plugin-junit/package.json b/packages/plugin-junit/package.json index c6ff5e5eb..1880094de 100644 --- a/packages/plugin-junit/package.json +++ b/packages/plugin-junit/package.json @@ -40,10 +40,8 @@ "dependencies": { "axios": "^1.12.1", "chalk": "3.0.0", - "clipanion": "^3.2.1", "fast-xml-parser": "^4.4.1", "form-data": "^4.0.4", - "typanion": "^3.14.0", "upath": "^2.0.1", "uuid": "^9.0.0" } diff --git a/yarn.lock b/yarn.lock index bc0c3e01a..2493e9f37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1651,10 +1651,8 @@ __metadata: dependencies: axios: "npm:^1.12.1" chalk: "npm:3.0.0" - clipanion: "npm:^3.2.1" fast-xml-parser: "npm:^4.4.1" form-data: "npm:^4.0.4" - typanion: "npm:^3.14.0" upath: "npm:^2.0.1" uuid: "npm:^9.0.0" peerDependencies: From 918c36f526bef7c3ead15598019a71be51ae296f Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 20:33:10 +0100 Subject: [PATCH 14/29] Fix tests --- packages/base/src/commands/plugin/__tests__/list.test.ts | 8 ++++---- packages/plugin-junit/src/__tests__/upload.test.ts | 6 +++--- packages/plugin-junit/src/{ => commands}/upload.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) rename packages/plugin-junit/src/{ => commands}/upload.ts (98%) diff --git a/packages/base/src/commands/plugin/__tests__/list.test.ts b/packages/base/src/commands/plugin/__tests__/list.test.ts index ebd30fa9f..3b7880d51 100644 --- a/packages/base/src/commands/plugin/__tests__/list.test.ts +++ b/packages/base/src/commands/plugin/__tests__/list.test.ts @@ -21,12 +21,12 @@ describe('PluginListCommand', () => { { title: '--all', args: ['--all'], - expectedOutput: `The following plugins are available:\n\n - (built-in) @datadog/datadog-ci-plugin-aas\n - (built-in) @datadog/datadog-ci-plugin-cloud-run\n - (built-in) @datadog/datadog-ci-plugin-container-app\n - (built-in) @datadog/datadog-ci-plugin-deployment\n - (built-in) @datadog/datadog-ci-plugin-dora\n - (built-in) @datadog/datadog-ci-plugin-gate\n - (built-in) @datadog/datadog-ci-plugin-lambda\n - (built-in) @datadog/datadog-ci-plugin-sarif\n - (built-in) @datadog/datadog-ci-plugin-sbom\n - (built-in) @datadog/datadog-ci-plugin-stepfunctions\n - (built-in) @datadog/datadog-ci-plugin-synthetics\n`, + expectedOutput: `The following plugins are available:\n\n - (built-in) @datadog/datadog-ci-plugin-aas\n - (built-in) @datadog/datadog-ci-plugin-cloud-run\n - (built-in) @datadog/datadog-ci-plugin-container-app\n - (built-in) @datadog/datadog-ci-plugin-deployment\n - (built-in) @datadog/datadog-ci-plugin-dora\n - (built-in) @datadog/datadog-ci-plugin-gate\n - (built-in) @datadog/datadog-ci-plugin-junit\n - (built-in) @datadog/datadog-ci-plugin-lambda\n - (built-in) @datadog/datadog-ci-plugin-sarif\n - (built-in) @datadog/datadog-ci-plugin-sbom\n - (built-in) @datadog/datadog-ci-plugin-stepfunctions\n - (built-in) @datadog/datadog-ci-plugin-synthetics\n`, }, { title: '--all --json', args: ['--all', '--json'], - expectedOutput: `[{"name":"@datadog/datadog-ci-plugin-aas","scope":"aas","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-cloud-run","scope":"cloud-run","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-container-app","scope":"container-app","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-deployment","scope":"deployment","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-dora","scope":"dora","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-gate","scope":"gate","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-lambda","scope":"lambda","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sarif","scope":"sarif","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sbom","scope":"sbom","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-stepfunctions","scope":"stepfunctions","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-synthetics","scope":"synthetics","isBuiltin":true}]\n`, + expectedOutput: `[{"name":"@datadog/datadog-ci-plugin-aas","scope":"aas","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-cloud-run","scope":"cloud-run","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-container-app","scope":"container-app","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-deployment","scope":"deployment","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-dora","scope":"dora","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-gate","scope":"gate","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-junit","scope":"junit","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-lambda","scope":"lambda","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sarif","scope":"sarif","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sbom","scope":"sbom","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-stepfunctions","scope":"stepfunctions","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-synthetics","scope":"synthetics","isBuiltin":true}]\n`, }, ])('all plugins are built-in ($title)', async ({args, expectedOutput}) => { const cli = new Cli() @@ -55,12 +55,12 @@ describe('PluginListCommand', () => { { title: '--all', args: ['--all'], - expectedOutput: `The following plugins are available:\n\n - @datadog/datadog-ci-plugin-aas (install with datadog-ci plugin install aas)\n - (built-in) @datadog/datadog-ci-plugin-cloud-run\n - (built-in) @datadog/datadog-ci-plugin-container-app\n - (built-in) @datadog/datadog-ci-plugin-deployment\n - (built-in) @datadog/datadog-ci-plugin-dora\n - (built-in) @datadog/datadog-ci-plugin-gate\n - (built-in) @datadog/datadog-ci-plugin-lambda\n - (built-in) @datadog/datadog-ci-plugin-sarif\n - (built-in) @datadog/datadog-ci-plugin-sbom\n - (built-in) @datadog/datadog-ci-plugin-stepfunctions\n - (built-in) @datadog/datadog-ci-plugin-synthetics\n`, + expectedOutput: `The following plugins are available:\n\n - @datadog/datadog-ci-plugin-aas (install with datadog-ci plugin install aas)\n - (built-in) @datadog/datadog-ci-plugin-cloud-run\n - (built-in) @datadog/datadog-ci-plugin-container-app\n - (built-in) @datadog/datadog-ci-plugin-deployment\n - (built-in) @datadog/datadog-ci-plugin-dora\n - (built-in) @datadog/datadog-ci-plugin-gate\n - (built-in) @datadog/datadog-ci-plugin-junit\n - (built-in) @datadog/datadog-ci-plugin-lambda\n - (built-in) @datadog/datadog-ci-plugin-sarif\n - (built-in) @datadog/datadog-ci-plugin-sbom\n - (built-in) @datadog/datadog-ci-plugin-stepfunctions\n - (built-in) @datadog/datadog-ci-plugin-synthetics\n`, }, { title: '--all --json', args: ['--all', '--json'], - expectedOutput: `[{"name":"@datadog/datadog-ci-plugin-aas","scope":"aas","isBuiltin":false},{"name":"@datadog/datadog-ci-plugin-cloud-run","scope":"cloud-run","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-container-app","scope":"container-app","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-deployment","scope":"deployment","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-dora","scope":"dora","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-gate","scope":"gate","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-lambda","scope":"lambda","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sarif","scope":"sarif","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sbom","scope":"sbom","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-stepfunctions","scope":"stepfunctions","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-synthetics","scope":"synthetics","isBuiltin":true}]\n`, + expectedOutput: `[{"name":"@datadog/datadog-ci-plugin-aas","scope":"aas","isBuiltin":false},{"name":"@datadog/datadog-ci-plugin-cloud-run","scope":"cloud-run","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-container-app","scope":"container-app","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-deployment","scope":"deployment","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-dora","scope":"dora","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-gate","scope":"gate","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-junit","scope":"junit","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-lambda","scope":"lambda","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sarif","scope":"sarif","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-sbom","scope":"sbom","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-stepfunctions","scope":"stepfunctions","isBuiltin":true},{"name":"@datadog/datadog-ci-plugin-synthetics","scope":"synthetics","isBuiltin":true}]\n`, }, ])('list available plugins ($title)', async ({args, expectedOutput}) => { const cli = new Cli() diff --git a/packages/plugin-junit/src/__tests__/upload.test.ts b/packages/plugin-junit/src/__tests__/upload.test.ts index 6c30696bb..458250244 100644 --- a/packages/plugin-junit/src/__tests__/upload.test.ts +++ b/packages/plugin-junit/src/__tests__/upload.test.ts @@ -5,8 +5,8 @@ import id from '@datadog/datadog-ci-base/helpers/id' import {SpanTags} from '@datadog/datadog-ci-base/helpers/interfaces' import upath from 'upath' +import {PluginCommand as JunitUploadCommand} from '../commands/upload' import {renderInvalidFile} from '../renderer' -import {PluginCommand as JunitUploadCommand} from '../upload' jest.mock('@datadog/datadog-ci-base/helpers/id', () => jest.fn()) @@ -239,7 +239,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['**/junit/**/*.xml'], + basePaths: ['**/*.xml'], config: {}, context: command.context, service: 'service', @@ -267,7 +267,7 @@ describe('upload', () => { const command = createCommand(JunitUploadCommand) const files = await command['getMatchingJUnitXMLFiles'].call( { - basePaths: ['**/junit/**'], + basePaths: ['**'], config: {}, context: command.context, service: 'service', diff --git a/packages/plugin-junit/src/upload.ts b/packages/plugin-junit/src/commands/upload.ts similarity index 98% rename from packages/plugin-junit/src/upload.ts rename to packages/plugin-junit/src/commands/upload.ts index 61f35a26d..18d6e008e 100644 --- a/packages/plugin-junit/src/upload.ts +++ b/packages/plugin-junit/src/commands/upload.ts @@ -24,8 +24,8 @@ import chalk from 'chalk' import {XMLParser, XMLValidator} from 'fast-xml-parser' import upath from 'upath' -import {apiConstructor, apiUrl, intakeUrl} from './api' -import {APIHelper, Payload} from './interfaces' +import {apiConstructor, apiUrl, intakeUrl} from '../api' +import {APIHelper, Payload} from '../interfaces' import { renderCommandInfo, renderDryRunUpload, @@ -37,7 +37,7 @@ import { renderSuccessfulGitDBSync, renderSuccessfulUpload, renderUpload, -} from './renderer' +} from '../renderer' const TRACE_ID_HTTP_HEADER = 'x-datadog-trace-id' const PARENT_ID_HTTP_HEADER = 'x-datadog-parent-id' From d02698be897341062d4cc738c7c065b42c73037f Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 21:02:57 +0100 Subject: [PATCH 15/29] Run `yarn lint:packages --fix` again --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20ffe471e..22f3ebb1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: "@datadog/datadog-ci-plugin-deployment": "file:./artifacts/@datadog-datadog-ci-plugin-deployment-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-dora": "file:./artifacts/@datadog-datadog-ci-plugin-dora-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-gate": "file:./artifacts/@datadog-datadog-ci-plugin-gate-${{ matrix.version }}.tgz", + "@datadog/datadog-ci-plugin-junit": "file:./artifacts/@datadog-datadog-ci-plugin-junit-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-sarif": "file:./artifacts/@datadog-datadog-ci-plugin-sarif-${{ matrix.version }}.tgz", "@datadog/datadog-ci-plugin-sbom": "file:./artifacts/@datadog-datadog-ci-plugin-sbom-${{ matrix.version }}.tgz" }, From ea53ee57afede5767e4e2513e6c145533d08a84d Mon Sep 17 00:00:00 2001 From: corentin Date: Thu, 15 Jan 2026 21:12:30 +0100 Subject: [PATCH 16/29] Change version at the end of init script --- bin/init-package.sh | 23 +++++++++++++++-------- bin/migrate.sh | 15 +++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/bin/init-package.sh b/bin/init-package.sh index eabe00ada..d4a6471c0 100755 --- a/bin/init-package.sh +++ b/bin/init-package.sh @@ -43,7 +43,7 @@ NC='\033[0m' # No Color echo -e "This script will initialize and publish an empty package for ${BLUE}${BOLD}$PLUGIN_PKG${NC}" echo -echo -e "${BOLD}Please follow the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before running this script." +echo -e "${BOLD}Please read the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before running this script." echo read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN @@ -60,7 +60,7 @@ export INIT_NPM_AUTH_TOKEN yarn config set npmAuthToken '${INIT_NPM_AUTH_TOKEN}' echo -echo "1. Creating plugin directory structure" +echo -e "${BOLD}1. Creating plugin directory structure${NC}" mkdir -p "$PLUGIN_DIR" env cp LICENSE "$PLUGIN_DIR" echo "Empty package" > "$PLUGIN_DIR/README.md" @@ -107,7 +107,8 @@ cat > "$PLUGIN_DIR/package.json" < "$PLUGIN_DIR/tsconfig.json" < Date: Thu, 15 Jan 2026 21:27:28 +0100 Subject: [PATCH 17/29] Fail CI on PRs with empty initialized packages --- bin/lint-packages.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/bin/lint-packages.ts b/bin/lint-packages.ts index a68d5f14b..b7e0074f9 100644 --- a/bin/lint-packages.ts +++ b/bin/lint-packages.ts @@ -102,8 +102,8 @@ const findCommands = (folder: string, scope: string): string[] => { } catch (e) { if (e instanceof Error && e.message.includes('no such file or directory')) { const alternateFile = path.join('packages/base/src/commands', scope, 'cli.ts') - console.log(chalk.yellow(`Could not find commands in ${chalk.bold(folder)}, this means it's being migrated...`)) - console.log(chalk.yellow(`Reading imports in ${chalk.bold(alternateFile)} instead.\n`)) + console.log(chalk.yellow(`Could not find commands in ${chalk.bold(folder)}. A migration may be in progress...`)) + console.log(chalk.yellow(`Trying to read imports in ${chalk.bold(alternateFile)} instead.\n`)) const content = fs.readFileSync(alternateFile, 'utf8') @@ -218,10 +218,19 @@ const pluginPackages = fs .readdirSync('packages') .filter((dir) => dir.startsWith('plugin-')) .map((dir) => { + let loadedPackage: Package try { - const {folder, packageJson} = loadPackage(dir) + loadedPackage = loadPackage(dir) + } catch { + console.log(chalk.yellow(`Could not load ${chalk.bold(dir)} package. Skipping it...\n`)) + + return undefined + } + + const {folder, packageJson} = loadedPackage + const scope = dir.replace('plugin-', '') - const scope = dir.replace('plugin-', '') + try { const commands = findCommands(folder, scope) return { @@ -231,9 +240,11 @@ const pluginPackages = fs commands, } } catch { - console.log(chalk.yellow(`Could not load ${chalk.bold(dir)} package. Skipping it...\n`)) - - return undefined + console.log(chalk.bold.red(`Invalid state for ${folder}.`)) + console.log( + `Did you recently run ${chalk.bold(`bin/init-package.sh ${scope}`)}? Please either run ${chalk.bold(`bin/migrate.sh ${scope}`)} and finish the migration, or complete the structure of the plugin before merging your PR.` + ) + process.exit(1) } }) .filter((p): p is PluginPackage => p !== undefined) From 251d8d4801e2e6065f9ec5c4c3d3cab449f67d2a Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 11:32:06 +0100 Subject: [PATCH 18/29] Rename `init-package.sh` to `create-package.sh` --- bin/{init-package.sh => create-package.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bin/{init-package.sh => create-package.sh} (100%) diff --git a/bin/init-package.sh b/bin/create-package.sh similarity index 100% rename from bin/init-package.sh rename to bin/create-package.sh From 9c59805bd7fa75d2b110b6cf29c7a53449e50092 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 11:33:44 +0100 Subject: [PATCH 19/29] Add `package:create` and `package:migrate` aliases --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 7b3a48649..08dd3da8e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "package:build": "yarn tsc:build $INIT_CWD", "package:clean": "cd $INIT_CWD && rimraf dist tsconfig.tsbuildinfo", "package:clean-dist": "cd $INIT_CWD && rimraf --glob 'dist/**/__tests__'", + "package:create": "bin/create-package.sh", "package:lint": "eslint --cache --quiet $INIT_CWD/src/**/*.ts", + "package:migrate": "bin/migrate.sh", "publish:all": "yarn loop-packages --topological --verbose npm publish", "test": "node --experimental-vm-modules node_modules/jest/bin/jest --colors", "test:debug": "node --experimental-vm-modules --inspect-brk node_modules/jest/bin/jest --colors --runInBand", From 4b54a3a6cd9b39d30efd4390251d85855a0cefc1 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 12:00:14 +0100 Subject: [PATCH 20/29] Reduce the role of `create-package.sh` --- bin/create-package.sh | 55 ++++--------------------------------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/bin/create-package.sh b/bin/create-package.sh index d4a6471c0..860f2f9f0 100755 --- a/bin/create-package.sh +++ b/bin/create-package.sh @@ -2,18 +2,13 @@ set -euo pipefail -DRY_RUN=false SCOPE="" while [[ $# -gt 0 ]]; do case $1 in - --dry-run) - DRY_RUN=true - shift - ;; -*) echo "Unknown option: $1" - echo "Usage: $0 [--dry-run] " + echo "Usage: $0 " exit 1 ;; *) @@ -24,7 +19,7 @@ while [[ $# -gt 0 ]]; do done if [ -z "$SCOPE" ]; then - echo "Usage: $0 [--dry-run] " + echo "Usage: $0 " exit 1 fi PLUGIN_PKG="@datadog/datadog-ci-plugin-$SCOPE" @@ -41,23 +36,7 @@ GREEN='\033[0;32m' BLUE='\033[0;34m' NC='\033[0m' # No Color -echo -e "This script will initialize and publish an empty package for ${BLUE}${BOLD}$PLUGIN_PKG${NC}" -echo -echo -e "${BOLD}Please read the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before running this script." -echo - -read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN -echo -if [ -z "$INIT_NPM_AUTH_TOKEN" ]; then - echo "Error: NPM auth token cannot be empty" - exit 1 -fi - -# Export this for subsequent yarn commands in the script -export INIT_NPM_AUTH_TOKEN - -# Do not hardcode the token in .yarnrc.yml, it will be read from the environment variable -yarn config set npmAuthToken '${INIT_NPM_AUTH_TOKEN}' +echo -e "This script will initialize an empty package for ${BLUE}${BOLD}$PLUGIN_PKG${NC}" echo echo -e "${BOLD}1. Creating plugin directory structure${NC}" @@ -67,7 +46,7 @@ echo "Empty package" > "$PLUGIN_DIR/README.md" cat > "$PLUGIN_DIR/package.json" < "$PLUGIN_DIR/package.json" < Date: Mon, 19 Jan 2026 12:08:14 +0100 Subject: [PATCH 21/29] Make new script to check NPM packages --- .github/workflows/ci.yml | 22 +----- bin/check-npm-packages.sh | 153 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 156 insertions(+), 20 deletions(-) create mode 100755 bin/check-npm-packages.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fa60b6fc..d6f0d6d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -652,23 +652,5 @@ jobs: - run: yarn install --immutable - name: Check licenses run: yarn check-licenses - - name: Check published packages - run: | - local=$(yarn workspaces list --json --no-private | jq '.name' | sort) - remote=$(npm search 'maintainer:datadog keywords:datadog-ci' --json | jq '.[].name' | sort) - - DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" <(echo "$remote") <(echo "$local")) || true - - if [ -z "$DIFF_OUTPUT" ]; then - echo "All local packages exist on NPM ✅" - exit 0 - else - echo "$DIFF_OUTPUT" - echo "### Some local packages were not published to NPM yet ❌" >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - echo "$DIFF_OUTPUT" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo >> $GITHUB_STEP_SUMMARY - echo "**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" >> $GITHUB_STEP_SUMMARY - exit 1 - fi + - name: Check NPM packages + run: yarn check-npm-packages diff --git a/bin/check-npm-packages.sh b/bin/check-npm-packages.sh new file mode 100755 index 000000000..b5b14faa7 --- /dev/null +++ b/bin/check-npm-packages.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +set -euo pipefail + +# This script checks if all local packages are published to NPM. +# It can also first-time publish missing packages when run with --fix. +# +# Usage: +# ./bin/check-npm-packages.sh # Check mode (default) - exits 1 if packages are missing +# ./bin/check-npm-packages.sh --fix # Fix mode - publishes missing packages +# ./bin/check-npm-packages.sh --fix --dry-run # Fix mode with dry-run - simulates publishing + +MODE="check" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --fix) + MODE="fix" + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -*) + echo "Unknown option: $1" + echo "Usage: $0 [--fix] [--dry-run]" + exit 1 + ;; + *) + echo "Unknown argument: $1" + echo "Usage: $0 [--fix] [--dry-run]" + exit 1 + ;; + esac +done + +if [ "$DRY_RUN" = true ] && [ "$MODE" != "fix" ]; then + echo "Error: --dry-run can only be used with --fix" + exit 1 +fi + +# Colors +BOLD='\033[1m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${BOLD}Checking for unpublished packages...${NC}" +echo + +# Get local and remote packages +local_packages=$(yarn workspaces list --json --no-private | jq -r '.name' | sort) +remote_packages=$(npm search 'maintainer:datadog keywords:datadog-ci' --json | jq -r '.[].name' | sort) + +# Find packages that exist locally but not on NPM +missing_packages=() +while IFS= read -r pkg; do + if [ -n "$pkg" ] && ! echo "$remote_packages" | grep -q "^${pkg}$"; then + missing_packages+=("$pkg") + fi +done <<< "$local_packages" + +if [ ${#missing_packages[@]} -eq 0 ]; then + echo -e "${GREEN}All local packages exist on NPM ✅${NC}" + exit 0 +fi + +# Report missing packages +echo -e "${RED}The following packages are not published to NPM yet:${NC}" +for pkg in "${missing_packages[@]}"; do + echo " - $pkg" +done + +# In CI environment, write to GitHub step summary +if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" \ + <(echo "$remote_packages") <(echo "$local_packages")) || true + { + echo "### Some local packages were not published to NPM yet ❌" + echo '```diff' + echo "$DIFF_OUTPUT" + echo '```' + echo + echo "**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" + } >> "$GITHUB_STEP_SUMMARY" +fi + +if [ "$MODE" = "check" ]; then + echo + echo -e "${BOLD}Run with --fix to publish these packages${NC}" + echo -e "See instructions at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC}" + exit 1 +fi + +# Fix mode - publish missing packages +echo +echo -e "${BOLD}Publishing missing packages to NPM...${NC}" +echo +echo -e "${BOLD}Please read the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before proceeding." +echo + +read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN +echo +if [ -z "$INIT_NPM_AUTH_TOKEN" ]; then + echo "Error: NPM auth token cannot be empty" + exit 1 +fi + +# Export this for subsequent yarn commands in the script +export INIT_NPM_AUTH_TOKEN + +# Do not hardcode the token in .yarnrc.yml, it will be read from the environment variable +yarn config set npmAuthToken '${INIT_NPM_AUTH_TOKEN}' +echo + +for pkg in "${missing_packages[@]}"; do + echo -e "${BLUE}Publishing ${BOLD}$pkg${NC}${BLUE}...${NC}" + + # Get the package directory + pkg_dir=$(yarn workspaces list --json | jq -r "select(.name == \"$pkg\") | .location") + pkg_json="$pkg_dir/package.json" + + # Save original version + original_version=$(jq -r .version "$pkg_json") + + # Set version to 0.0.1 for first-time publish + jq '.version = "0.0.1"' "$pkg_json" | sponge "$pkg_json" + + if [ "$DRY_RUN" = true ]; then + echo " [DRY-RUN] Would publish $pkg@0.0.1" + yarn workspace "$pkg" npm publish --dry-run 2>&1 | sed 's/^/ /' + else + yarn workspace "$pkg" npm publish 2>&1 | sed 's/^/ /' + echo -e " ${GREEN}Successfully published $pkg@0.0.1${NC}" + fi + + # Restore original version + jq --arg version "$original_version" '.version = $version' "$pkg_json" | sponge "$pkg_json" + echo +done + +echo -e "${BOLD}Cleaning up...${NC}" +yarn config unset npmAuthToken + +echo +if [ "$DRY_RUN" = true ]; then + echo -e "${GREEN}[DRY-RUN] Would have published ${#missing_packages[@]} package(s)${NC}" +else + echo -e "${GREEN}Successfully published ${#missing_packages[@]} package(s)${NC}" +fi diff --git a/package.json b/package.json index 08dd3da8e..f52e5ef29 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "build": "yarn tsc:build", "check-junit-upload": "node bin/check-junit-upload.js", "check-licenses": "node bin/check-licenses.js", + "check-npm-packages": "bin/check-npm-packages.sh", "clean": "rimraf --glob .eslintcache .jest-cache 'packages/*/{dist,tsconfig.tsbuildinfo}'", "compare-binary-size": "FORCE_COLOR=1 node bin/compare-binary-size.js", "dev": "yarn tsc:build --watch", From 1dfd4e430cf4e428f81e7897603efaf78901afa5 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 12:34:32 +0100 Subject: [PATCH 22/29] Undo ci.yml changes --- .github/workflows/ci.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2905f8efc..5f34b4f47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,8 +161,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: '@testmeasure1:20' - EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure1:40' + EXTRA_TEST_QUERY_FILTER: "@testmeasure1:20" + EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure1:40" DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -175,8 +175,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: '@testmeasure2:60' - EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure2:80' + EXTRA_TEST_QUERY_FILTER: "@testmeasure2:60" + EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure2:80" DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -268,8 +268,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: '@testmeasure1:20' - EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure1:40' + EXTRA_TEST_QUERY_FILTER: "@testmeasure1:20" + EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure1:40" DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -282,8 +282,8 @@ jobs: yarn add @datadog/datadog-api-client yarn node bin/check-junit-upload.js env: - EXTRA_TEST_QUERY_FILTER: '@testmeasure2:60' - EXTRA_SESSION_QUERY_FILTER: '@sessionmeasure2:80' + EXTRA_TEST_QUERY_FILTER: "@testmeasure2:60" + EXTRA_SESSION_QUERY_FILTER: "@sessionmeasure2:80" DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} DD_SERVICE: datadog-ci-e2e-tests-junit @@ -644,8 +644,8 @@ jobs: cpu_count: 2 diff_aware: false - additional-checks: - name: Additional checks + check-licenses: + name: Check licenses runs-on: ubuntu-latest steps: @@ -654,8 +654,7 @@ jobs: uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 22.19.0 + # The install step has been added here such that the `.yarn/install-state.gz` file is generated. This file is used + # by the script `check-licenses` below. - run: yarn install --immutable - - name: Check licenses - run: yarn check-licenses - - name: Check NPM packages - run: yarn check-npm-packages + - run: yarn check-licenses From ada3efaefab9113c50115afb9cc0d04e54cc4c86 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 12:42:44 +0100 Subject: [PATCH 23/29] Change publish-release.yml instead --- .github/workflows/publish-release.yml | 21 ++++++++++++++ bin/check-npm-packages.sh | 40 +++++++++++++++++++-------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 49fb795b3..a6993ba11 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,8 +10,29 @@ permissions: contents: write # Required for the draft release jobs: + pre-release-checks: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Install node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: 22.19.0 + - name: Install project dependencies + run: yarn install --immutable + - name: Check NPM packages + run: yarn check-npm-packages + env: + # Used to post comments on the PR + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Temporary fail safe + run: exit 1 + create-draft-release: runs-on: ubuntu-latest + needs: pre-release-checks outputs: release-id: ${{ steps.draft-release.outputs.result }} steps: diff --git a/bin/check-npm-packages.sh b/bin/check-npm-packages.sh index b5b14faa7..6df462bae 100755 --- a/bin/check-npm-packages.sh +++ b/bin/check-npm-packages.sh @@ -74,18 +74,34 @@ for pkg in "${missing_packages[@]}"; do echo " - $pkg" done -# In CI environment, write to GitHub step summary -if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" \ - <(echo "$remote_packages") <(echo "$local_packages")) || true - { - echo "### Some local packages were not published to NPM yet ❌" - echo '```diff' - echo "$DIFF_OUTPUT" - echo '```' - echo - echo "**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" - } >> "$GITHUB_STEP_SUMMARY" +# In CI environment, post a comment on the PR +if [ -n "${GITHUB_TOKEN:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_SHA:-}" ]; then + # Find the PR associated with this commit + PR_NUMBER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \ + | jq -r '.[0].number // empty') + + if [ -n "$PR_NUMBER" ]; then + DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" \ + <(echo "$remote_packages") <(echo "$local_packages")) || true + + COMMENT_BODY="### Some local packages were not published to NPM yet ❌ + +\`\`\`diff +$DIFF_OUTPUT +\`\`\` + +**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" + + # Post comment on the PR + curl -s -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + -d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')" > /dev/null + + echo -e "${BLUE}Posted comment on PR #$PR_NUMBER${NC}" + fi fi if [ "$MODE" = "check" ]; then From 7ad9a712a07c7b34f81fb0677c5ce9b07bcb20aa Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 12:56:05 +0100 Subject: [PATCH 24/29] Stricter regex for tag --- .github/workflows/publish-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index a6993ba11..c59a505f0 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -3,7 +3,7 @@ name: Publish package on NPM on: push: tags: - - v* # Any version tag + - 'v[0-9]+.[0-9]+.[0-9]+' permissions: id-token: write # For OIDC publishing with provenance and to federate tokens From 1256623fbfeacad0a78a7f4d92803c22824a9154 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 13:02:13 +0100 Subject: [PATCH 25/29] Still run `yarn install` in create package script --- bin/create-package.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/create-package.sh b/bin/create-package.sh index 860f2f9f0..ca2fbf659 100755 --- a/bin/create-package.sh +++ b/bin/create-package.sh @@ -86,6 +86,9 @@ cat > "$PLUGIN_DIR/package.json" <&1 | sed 's/^/ /' + echo echo -e "${GREEN}Package created successfully${NC}" From 573c7191d94d8e9ea4f1ab94194c1be5c0b608cc Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 13:03:34 +0100 Subject: [PATCH 26/29] Rename scripts --- bin/{create-package.sh => create-plugin.sh} | 0 package.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename bin/{create-package.sh => create-plugin.sh} (100%) diff --git a/bin/create-package.sh b/bin/create-plugin.sh similarity index 100% rename from bin/create-package.sh rename to bin/create-plugin.sh diff --git a/package.json b/package.json index f52e5ef29..c3b965670 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "package:build": "yarn tsc:build $INIT_CWD", "package:clean": "cd $INIT_CWD && rimraf dist tsconfig.tsbuildinfo", "package:clean-dist": "cd $INIT_CWD && rimraf --glob 'dist/**/__tests__'", - "package:create": "bin/create-package.sh", "package:lint": "eslint --cache --quiet $INIT_CWD/src/**/*.ts", - "package:migrate": "bin/migrate.sh", + "plugin:create": "bin/create-plugin.sh", + "plugin:migrate": "bin/migrate.sh", "publish:all": "yarn loop-packages --topological --verbose npm publish", "test": "node --experimental-vm-modules node_modules/jest/bin/jest --colors", "test:debug": "node --experimental-vm-modules --inspect-brk node_modules/jest/bin/jest --colors --runInBand", From 48d387ff4df5a8be35722e86cbe157df97315401 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 13:10:16 +0100 Subject: [PATCH 27/29] Update naming --- bin/lint-packages.ts | 2 +- bin/migrate.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/lint-packages.ts b/bin/lint-packages.ts index b7e0074f9..513804d6d 100644 --- a/bin/lint-packages.ts +++ b/bin/lint-packages.ts @@ -242,7 +242,7 @@ const pluginPackages = fs } catch { console.log(chalk.bold.red(`Invalid state for ${folder}.`)) console.log( - `Did you recently run ${chalk.bold(`bin/init-package.sh ${scope}`)}? Please either run ${chalk.bold(`bin/migrate.sh ${scope}`)} and finish the migration, or complete the structure of the plugin before merging your PR.` + `Did you recently run ${chalk.bold(`yarn plugin:create ${scope}`)}? Please either run ${chalk.bold(`bin/migrate.sh ${scope}`)} and finish the migration, or complete the structure of the plugin before merging your PR.` ) process.exit(1) } diff --git a/bin/migrate.sh b/bin/migrate.sh index 1e0780a7f..4175ebd4e 100755 --- a/bin/migrate.sh +++ b/bin/migrate.sh @@ -35,7 +35,7 @@ NC='\033[0m' # No Color if [ ! -d "$PLUGIN_DIR" ]; then echo -e "${RED}Plugin directory ${BOLD}$PLUGIN_DIR${NC} does not exist!${NC}" echo - echo -e "${BLUE}To initialize and ${BOLD}publish an initial version${NC}${BLUE} of the package, please run ${BOLD}bin/init-package.sh $SCOPE${NC}" + echo -e "${BLUE}Please run ${BOLD}yarn plugin:create $SCOPE${NC} to initialize the package." exit 1 fi From af78369872f2ae6dcb25414f5f2f1e9c4efaeced Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 13:07:06 +0100 Subject: [PATCH 28/29] Extract PR author and ping them --- .github/workflows/publish-release.yml | 2 +- bin/check-npm-packages.sh | 36 +++++++++++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c59a505f0..a2173262f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -21,7 +21,7 @@ jobs: with: node-version: 22.19.0 - name: Install project dependencies - run: yarn install --immutable + run: yarn install --immutable --mode=skip-build - name: Check NPM packages run: yarn check-npm-packages env: diff --git a/bin/check-npm-packages.sh b/bin/check-npm-packages.sh index 6df462bae..61ab49c20 100755 --- a/bin/check-npm-packages.sh +++ b/bin/check-npm-packages.sh @@ -63,12 +63,13 @@ while IFS= read -r pkg; do fi done <<< "$local_packages" +# Exit early if everything is good if [ ${#missing_packages[@]} -eq 0 ]; then echo -e "${GREEN}All local packages exist on NPM ✅${NC}" exit 0 fi -# Report missing packages +# Otherwise, report missing packages echo -e "${RED}The following packages are not published to NPM yet:${NC}" for pkg in "${missing_packages[@]}"; do echo " - $pkg" @@ -76,23 +77,25 @@ done # In CI environment, post a comment on the PR if [ -n "${GITHUB_TOKEN:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_SHA:-}" ]; then - # Find the PR associated with this commit - PR_NUMBER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \ - | jq -r '.[0].number // empty') + # Get the PR number and author associated with this commit + PR_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls") - if [ -n "$PR_NUMBER" ]; then - DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" \ - <(echo "$remote_packages") <(echo "$local_packages")) || true + PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.[0].number // empty') + PR_AUTHOR=$(echo "$PR_RESPONSE" | jq -r '.[0].user.login // empty') + + DIFF_OUTPUT=$(diff -u --label "Published packages (Actual)" --label "Local packages (Expected)" \ + <(echo "$remote_packages") <(echo "$local_packages")) || true - COMMENT_BODY="### Some local packages were not published to NPM yet ❌ + COMMENT_BODY="### Some packages were not first-time published to NPM yet ❌ \`\`\`diff $DIFF_OUTPUT \`\`\` -**Please follow the instructions** at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" +Hi @$PR_AUTHOR, please **ask an admin** to follow the instructions at https://datadoghq.atlassian.net/wiki/x/QYDRaQE" + if [ -n "$PR_NUMBER" ]; then # Post comment on the PR curl -s -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ @@ -100,10 +103,16 @@ $DIFF_OUTPUT "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ -d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')" > /dev/null - echo -e "${BLUE}Posted comment on PR #$PR_NUMBER${NC}" + echo -e "${BLUE}Posted comment on PR #$PR_NUMBER (author: @$PR_AUTHOR)${NC}" + else + # Fallback when PR is not found + echo -e "${RED}No PR found for commit $GITHUB_SHA${NC}" + echo -e "${BLUE}This would be the comment body:${NC}" + echo "$COMMENT_BODY" fi fi +# Do not continue if we are in check mode if [ "$MODE" = "check" ]; then echo echo -e "${BOLD}Run with --fix to publish these packages${NC}" @@ -118,6 +127,11 @@ echo echo -e "${BOLD}Please read the instructions${NC} at ${BLUE}https://datadoghq.atlassian.net/wiki/x/QYDRaQE${NC} before proceeding." echo +if [ "$DRY_RUN" = true ]; then + echo -e "${BOLD}[DRY-RUN]${NC} None of the packages will actually be published." + echo +fi + read -rsp "Enter your NPM auth token: " INIT_NPM_AUTH_TOKEN echo if [ -z "$INIT_NPM_AUTH_TOKEN" ]; then From 55cc372e6e406e5aa9b528cae6391e89606f1e96 Mon Sep 17 00:00:00 2001 From: corentin Date: Mon, 19 Jan 2026 13:55:04 +0100 Subject: [PATCH 29/29] Remove temporary fail safe --- .github/workflows/publish-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index a2173262f..1ab024e77 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -27,8 +27,6 @@ jobs: env: # Used to post comments on the PR GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Temporary fail safe - run: exit 1 create-draft-release: runs-on: ubuntu-latest