├── .gitignore ├── .github ├── CODEOWNERS ├── dependabot.yaml └── workflows │ └── test.yaml ├── LICENSE ├── ci └── fixtures │ ├── ruby-minitest.xml │ ├── subfolder │ └── python-unittest.xml │ └── go-gotestsum.xml ├── check-junit-upload.js ├── action.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @DataDog/ci-app-backend @DataDog/ci-app-libraries -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Datadog, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ci/fixtures/ruby-minitest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Failure: 8 | test_that_it_has_a_version_number(Minitest::Result) [/Home/github.com/DataDog/minitest/minitest-hello/test/minitest/hello_test.rb:5]: 9 | NameError: uninitialized constant Minitest::hello 10 | /Users/John/go/src/github.com/DataDog/minitest/minitest-hello/test/minitest/hello_test.rb:5:in `test_that_it_has_a_version_number' 11 | 12 | 13 | 14 | 15 | Failure: 16 | test_it_does_something_useful(Minitest::Result) [/Home/github.com/DataDog/minitest/minitest-hello/test/minitest/hello_test.rb:9]: 17 | Expected false to be truthy. 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ci/fixtures/subfolder/python-unittest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Some output 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /check-junit-upload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { client, v2 } = require("@datadog/datadog-api-client") 4 | 5 | const configuration = client.createConfiguration(); 6 | const apiInstance = new v2.CIVisibilityTestsApi(configuration); 7 | 8 | const EXPECTED_NUM_TESTS = 32 9 | 10 | const params = { 11 | filterQuery: `@test.service:${process.env.DD_SERVICE} @git.commit.sha:${process.env.GITHUB_SHA}`, 12 | filterFrom: new Date(new Date().getTime() + -300 * 1000), // Last 5 minutes 13 | filterTo: new Date(), 14 | pageLimit: 50, 15 | }; 16 | 17 | const CHECK_INTERVAL_SECONDS = 10 18 | const MAX_NUM_ATTEMPTS = 10 19 | 20 | function getTestData (extraFilter) { 21 | const finalFilterQuery = `${params.filterQuery} ${extraFilter}` 22 | console.log(`🔎 Querying CI Visibility tests with ${finalFilterQuery}.`) 23 | return apiInstance 24 | .listCIAppTestEvents({ 25 | ...params, 26 | filterQuery: `${finalFilterQuery}`, 27 | }) 28 | .then(data => data.data) 29 | .catch(error => console.error(error)) 30 | } 31 | 32 | function waitFor (waitSeconds) { 33 | return new Promise(resolve => setTimeout(() => resolve(), waitSeconds * 1000)) 34 | } 35 | 36 | async function checkJunitUpload () { 37 | let numAttempts = 0 38 | let isSuccess = false 39 | let data = [] 40 | while (numAttempts++ < MAX_NUM_ATTEMPTS && !isSuccess) { 41 | data = await getTestData(`test_level:test ${process.env.EXTRA_TAGS}`) 42 | if (data.length === EXPECTED_NUM_TESTS) { 43 | isSuccess = true 44 | } else { 45 | const isLastAttempt = numAttempts === MAX_NUM_ATTEMPTS 46 | if (!isLastAttempt) { 47 | console.log(`🔁 Attempt number ${numAttempts} failed, retrying in ${CHECK_INTERVAL_SECONDS} seconds.`) 48 | await waitFor(CHECK_INTERVAL_SECONDS) 49 | } 50 | } 51 | } 52 | if (isSuccess) { 53 | console.log(`✅ Successful check: the API returned ${data.length} tests.`) 54 | process.exit(0) 55 | } else { 56 | console.log(`❌ Failed check: the API returned ${data.length} tests but ${EXPECTED_NUM_TESTS} were expected.`) 57 | process.exit(1) 58 | } 59 | } 60 | 61 | checkJunitUpload() -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | # Composite action to upload junit test result files to Datadog Test Optimization 2 | name: "Datadog JUnitXML Upload" 3 | description: "Upload JUnitXML reports files to Datadog Test Optimization" 4 | inputs: 5 | api_key: 6 | required: true 7 | description: Datadog API key to use to upload the junit files. 8 | site: 9 | required: false 10 | default: datadoghq.com 11 | description: The Datadog site to upload the files to. 12 | files: 13 | required: true 14 | description: JUnit files to upload. 15 | default: . 16 | auto-discovery: 17 | required: true 18 | description: Do a recursive search and automatic junit files discovery in the folders provided in `files` input (current folder if omitted). 19 | default: 'true' 20 | ignored-paths: 21 | required: false 22 | description: A comma-separated list of paths that are ignored when junit files auto-discovery is done. Glob patterns are supported 23 | concurrency: 24 | required: true 25 | description: Controls the maximum number of concurrent file uploads. 26 | default: "20" 27 | node-version: 28 | required: true 29 | description: The node version used to install datadog-ci 30 | default: "20" 31 | tags: 32 | required: false 33 | description: Datadog tags to associate with the uploaded test results. 34 | service: 35 | required: false 36 | description: Service name to use with the uploaded test results. 37 | env: 38 | required: false 39 | description: Datadog env to use for the tests. 40 | logs: 41 | required: false 42 | description: Set to "true" to enable forwarding content from XML reports as logs. 43 | datadog-ci-version: 44 | required: false 45 | description: The version of the @datadog/datadog-ci package to use. It defaults to the latest release (`latest`). 46 | default: "latest" 47 | extra-args: 48 | default: "" 49 | description: Extra args to be passed to the datadog-ci cli. 50 | required: false 51 | runs: 52 | using: "composite" 53 | steps: 54 | - name: Install node 55 | uses: actions/setup-node@v4 56 | with: 57 | node-version: ${{ inputs.node-version }} 58 | 59 | - name: Upload the JUnit files 60 | shell: bash 61 | run: | 62 | npx @datadog/datadog-ci@${{ inputs.datadog-ci-version}} junit upload \ 63 | --max-concurrency ${{ inputs.concurrency }} \ 64 | ${{ inputs.logs == 'true' && '--logs' || '' }} \ 65 | ${{ inputs.auto-discovery == 'true' && '--auto-discovery' || '' }} \ 66 | ${{ inputs.ignored-paths != '' && format('--ignored-paths {0}', inputs.ignored-paths) || '' }} \ 67 | ${{ inputs.service != '' && format('--service {0}', inputs.service) || '' }} \ 68 | ${{ inputs.extra-args }} \ 69 | ${{ inputs.files }} 70 | env: 71 | DD_API_KEY: ${{ inputs.api_key }} 72 | DD_SITE: ${{ inputs.site }} 73 | DD_ENV: ${{ inputs.env }} 74 | DD_TAGS: ${{ inputs.tags }} 75 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: 'Test Action' 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'release/*' 8 | schedule: 9 | - cron: '0 0 * * *' # Runs at midnight UTC every day 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | version: [14, 16, 18, 20] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Upload reports using a glob pattern 20 | uses: ./ 21 | with: 22 | api_key: ${{secrets.DD_API_KEY_CI_VISIBILITY}} 23 | logs: "true" 24 | files: '**/fixtures/**' 25 | service: junit-upload-github-action-tests 26 | env: ci 27 | tags: "foo:bar,alpha:bravo,test.node.version:${{ matrix.version}}" 28 | node-version: ${{ matrix.version}} 29 | - name: Check that test data can be queried 30 | run: | 31 | npm install @datadog/datadog-api-client 32 | node ./check-junit-upload.js 33 | env: 34 | EXTRA_TAGS: "@foo:bar @alpha:bravo @test.node.version:${{ matrix.version}}" 35 | DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} 36 | DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} 37 | DD_SERVICE: junit-upload-github-action-tests 38 | test-older-datadog-ci-version: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Grab second latest version of @datadog/datadog-ci 43 | run: | 44 | SECOND_LATEST_VERSION=$(curl -s "https://api.github.com/repos/datadog/datadog-ci/releases" | jq '[.[] | {tag_name: .tag_name, published_at: .published_at}] | sort_by(.published_at) | reverse | .[:2] | .[1] | .tag_name') 45 | echo "SECOND_LATEST_VERSION=$SECOND_LATEST_VERSION" >> $GITHUB_ENV 46 | - name: Upload reports using a glob pattern 47 | uses: ./ 48 | with: 49 | # should still work with api-key as input 50 | api-key: ${{secrets.DD_API_KEY_CI_VISIBILITY}} 51 | logs: "true" 52 | files: '**/fixtures/**' 53 | service: junit-upload-github-action-tests 54 | env: ci 55 | tags: "foo:previous,alpha:previous" 56 | datadog-ci-version: ${{ env.SECOND_LATEST_VERSION }} 57 | - name: Check that test data can be queried 58 | run: | 59 | npm install @datadog/datadog-api-client 60 | node ./check-junit-upload.js 61 | env: 62 | EXTRA_TAGS: "@foo:previous @alpha:previous" 63 | DD_API_KEY: ${{ secrets.DD_API_KEY_CI_VISIBILITY }} 64 | DD_APP_KEY: ${{ secrets.DD_APP_KEY_CI_VISIBILITY }} 65 | DD_SERVICE: junit-upload-github-action-tests 66 | 67 | test-should-complain-about-missing-api-key: 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Upload reports using a glob pattern 72 | uses: ./ 73 | id: test_step 74 | with: 75 | logs: "true" 76 | files: '**/fixtures/**' 77 | service: junit-upload-github-action-tests 78 | env: ci 79 | tags: "foo:bar,alpha:bravo" 80 | continue-on-error: true 81 | - name: Check that previous step failed 82 | if: steps.test_step.outcome == 'success' 83 | run: | 84 | echo "The previous step did not fail as expected" 85 | exit 1 86 | -------------------------------------------------------------------------------- /ci/fixtures/go-gotestsum.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datadog JUnitXML Upload Action 2 | 3 | This action downloads the [datadog-ci](https://github.com/DataDog/datadog-ci) and uses it to upload JUnitXML files 4 | to the [Test Optimization product](https://docs.datadoghq.com/tests/). 5 | 6 | This action sets up node and requires node `>=14`. You can configure a specific version of node to use. 7 | Note that if you have set up another version already it will override it. 8 | 9 | ## Usage 10 | 11 | ```yaml 12 | name: Test Code 13 | on: [ push ] 14 | jobs: 15 | test: 16 | steps: 17 | - uses: actions/checkout@v3 18 | - run: make tests 19 | - uses: datadog/junit-upload-github-action@v2 20 | with: 21 | api_key: ${{ secrets.DD_API_KEY }} 22 | ``` 23 | 24 | ## Inputs 25 | 26 | The action has the following options: 27 | 28 | | Name | Description | Required | Default | 29 | |----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------| 30 | | `api_key` | Datadog API key to use to upload the junit files. | True | | 31 | | `site` | The Datadog site to upload the files to. | False | `datadoghq.com` | 32 | | `files` | Path to file or folder containing XML files to upload | False | `.` | 33 | | `auto-discovery` | Do a recursive search and automatic XML files discovery in the folders provided in `files` input (current folder if omitted). Search for filenames that match `*junit*.xml`, `*test*.xml`, `*TEST-*.xml`. | False | `true` | 34 | | `ignored-paths` | A comma-separated list of paths that are ignored when junit files auto-discovery is done. Glob patterns are supported. | False | | 35 | | `concurrency` | Controls the maximum number of concurrent file uploads | False | `20` | 36 | | `node-version` | The node version to use to install the datadog-ci. It must be `>=14` | False | `20` | 37 | | `tags` | Optional extra tags to add to the tests formatted as a comma separated list of tags. Example: `foo:bar,data:dog` | False | | 38 | | `service` | Service name to use with the uploaded test results. | False | | 39 | | `env` | Optional environment to add to the tests | False | | 40 | | `logs` | When set to "true" enables forwarding content from the XML reports as Logs. The content inside ``, ``, and `` is collected as logs. Logs from elements inside a `` are automatically connected to the test. | False | | 41 | | `datadog-ci-version` | Optionally pin the @datadog/datadog-ci version. | False | `latest` | 42 | | `extra-args` | Extra args to be passed to the datadog-ci junit upload command. | False | | 43 | --------------------------------------------------------------------------------