diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index 1395033..873b220 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -3,7 +3,7 @@ # GitHub Reusable Workflow: Node.js Continuous Integration
- Node.js Continuous Integration + Node.js Continuous Integration
--- @@ -53,7 +53,7 @@ on: permissions: {} jobs: continuous-integration: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@2b8788166256f66b42262e273b4be22a3fc162e8 # copilot/configure-lint-and-test-commands + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@7b558a563bfd8b37268ab6e820219294a2bb8474 # fix/continuous-integration-reports permissions: {} secrets: # Secrets to be used during the build step. @@ -114,6 +114,15 @@ jobs: # Whether to enable linting. # Set to `null` or empty to disable. # Accepts a JSON object for lint options. See [lint action](../actions/lint/README.md). + # It should generate lint reports in standard formats. + # + # Example: + # + # ```json:package.json + # { + # "lint:ci": "eslint . --output-file eslint-report.json --format json" + # } + # ``` # # Default: `true` lint: "true" @@ -134,6 +143,15 @@ jobs: # Whether to enable testing. # Set to `null` or empty to disable. # Accepts a JSON object for test options. See [test action](../actions/test/README.md). + # If coverage is enabled, it should generate test and coverage reports in standard formats. + # + # Example: + # + # ```json:package.json + # { + # "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=junit.xml --coverage.enabled --coverage.reporter=lcov --coverage.reporter=text" + # } + # ``` # # Default: `true` test: "true" @@ -164,6 +182,9 @@ jobs: # "volumes": ["/tmp:/tmp", "/cache:/cache"], # "credentials": { # "username": "myusername" + # }, + # pathMapping: { + # "/app": "./relative/path/to/app" # } # } # ``` @@ -176,6 +197,7 @@ jobs: # - `ports` (array) # - `volumes` (array) # - `credentials` (object with `username`). + # - `pathMapping` (object) path mapping from container paths to repository paths. Defaults is working directory is mapped with repository root. # # See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container. # @@ -186,64 +208,79 @@ jobs: + + ## Inputs ### Workflow Call Inputs -| **Input** | **Description** | **Required** | **Type** | **Default** | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | -| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` | -| | See . | | | | -| **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | -| | For string, provide a list of commands to run during the build step, one per line. | | | | -| | For JSON object, provide the following properties: | | | | -| | | | | | -| | - `commands`: Array of commands to run during the build step. | | | | -| | - `env`: Object of environment variables to set during the build step. | | | | -| | - `artifact`: String or array of strings specifying paths to artifacts to upload after the build | | | | -| | | | | | -| | Example: | | | | -| |
{
 "commands": [
 "build",
 "generate-artifacts"
 ],
 "env": {
 "CUSTOM_ENV_VAR": "value"
 },
 "artifact": [
 "dist/",
 "packages/package-a/build/"
 ]
}
| | | | -| **`checks`** | Optional flag to enable check steps. | **false** | **boolean** | `true` | -| **`lint`** | Whether to enable linting. | **false** | **string** | `true` | -| | Set to `null` or empty to disable. | | | | -| | Accepts a JSON object for lint options. See [lint action](../actions/lint/README.md). | | | | -| **`code-ql`** | Code QL analysis language. | **false** | **string** | `typescript` | -| | See . | | | | -| **`dependency-review`** | Enable dependency review scan. | **false** | **boolean** | `true` | -| | Works with public repositories and private repositories with a GitHub Advanced Security license. | | | | -| | See . | | | | -| **`test`** | Whether to enable testing. | **false** | **string** | `true` | -| | Set to `null` or empty to disable. | | | | -| | Accepts a JSON object for test options. See [test action](../actions/test/README.md). | | | | -| **`working-directory`** | Working directory where the dependencies are installed. | **false** | **string** | `.` | -| **`container`** | Container configuration to run CI steps in. | **false** | **string** | - | -| | Accepts either a string (container image name) or a JSON object with container options. | | | | -| | | | | | -| | String format (simple): | | | | -| | | | | | -| |
container: "node:18"
| | | | -| | JSON object format (advanced): | | | | -| | | | | | -| |
{
 "image": "node:18",
 "env": {
 "NODE_ENV": "production"
 },
 "options": "--cpus 2",
 "ports": [8080, 3000],
 "volumes": ["/tmp:/tmp", "/cache:/cache"],
 "credentials": {
 "username": "myusername"
 }
}
| | | | -| | Supported properties: | | | | -| | | | | | -| | - `image` (required) | | | | -| | - `env` (object) | | | | -| | - `options` (string) | | | | -| | - `ports` (array) | | | | -| | - `volumes` (array) | | | | -| | - `credentials` (object with `username`). | | | | -| | | | | | -| | See . | | | | -| | | | | | -| | When specified, steps will execute inside this container instead of checking out code. | | | | -| | The container should have the project code and dependencies pre-installed. | | | | +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | +| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` | +| | See . | | | | +| **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | +| | For string, provide a list of commands to run during the build step, one per line. | | | | +| | For JSON object, provide the following properties: | | | | +| | | | | | +| | - `commands`: Array of commands to run during the build step. | | | | +| | - `env`: Object of environment variables to set during the build step. | | | | +| | - `artifact`: String or array of strings specifying paths to artifacts to upload after the build | | | | +| | | | | | +| | Example: | | | | +| |
{
 "commands": [
 "build",
 "generate-artifacts"
 ],
 "env": {
 "CUSTOM_ENV_VAR": "value"
 },
 "artifact": [
 "dist/",
 "packages/package-a/build/"
 ]
}
| | | | +| **`checks`** | Optional flag to enable check steps. | **false** | **boolean** | `true` | +| **`lint`** | Whether to enable linting. | **false** | **string** | `true` | +| | Set to `null` or empty to disable. | | | | +| | Accepts a JSON object for lint options. See [lint action](../actions/lint/README.md). | | | | +| | It should generate lint reports in standard formats. | | | | +| | | | | | +| | Example: | | | | +| | | | | | +| |
{
 "lint:ci": "eslint . --output-file eslint-report.json --format json"
}
| | | | +| **`code-ql`** | Code QL analysis language. | **false** | **string** | `typescript` | +| | See . | | | | +| **`dependency-review`** | Enable dependency review scan. | **false** | **boolean** | `true` | +| | Works with public repositories and private repositories with a GitHub Advanced Security license. | | | | +| | See . | | | | +| **`test`** | Whether to enable testing. | **false** | **string** | `true` | +| | Set to `null` or empty to disable. | | | | +| | Accepts a JSON object for test options. See [test action](../actions/test/README.md). | | | | +| | If coverage is enabled, it should generate test and coverage reports in standard formats. | | | | +| | | | | | +| | Example: | | | | +| | | | | | +| |
{
 "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=junit.xml --coverage.enabled --coverage.reporter=lcov --coverage.reporter=text"
}
| | | | +| **`working-directory`** | Working directory where the dependencies are installed. | **false** | **string** | `.` | +| **`container`** | Container configuration to run CI steps in. | **false** | **string** | - | +| | Accepts either a string (container image name) or a JSON object with container options. | | | | +| | | | | | +| | String format (simple): | | | | +| | | | | | +| |
container: "node:18"
| | | | +| | JSON object format (advanced): | | | | +| | | | | | +| |
{
 "image": "node:18",
 "env": {
 "NODE_ENV": "production"
 },
 "options": "--cpus 2",
 "ports": [8080, 3000],
 "volumes": ["/tmp:/tmp", "/cache:/cache"],
 "credentials": {
 "username": "myusername"
 },
 pathMapping: {
 "/app": "./relative/path/to/app"
 }
}
| | | | +| | Supported properties: | | | | +| | | | | | +| | - `image` (required) | | | | +| | - `env` (object) | | | | +| | - `options` (string) | | | | +| | - `ports` (array) | | | | +| | - `volumes` (array) | | | | +| | - `credentials` (object with `username`). | | | | +| | - `pathMapping` (object) path mapping from container paths to repository paths. Defaults is working directory is mapped with repository root. | | | | +| | | | | | +| | See . | | | | +| | | | | | +| | When specified, steps will execute inside this container instead of checking out code. | | | | +| | The container should have the project code and dependencies pre-installed. | | | | + + ### Container Configuration The `container` input accepts either: @@ -350,7 +387,7 @@ on: jobs: continuous-integration: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@2b8788166256f66b42262e273b4be22a3fc162e8 # copilot/configure-lint-and-test-commands + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@7b558a563bfd8b37268ab6e820219294a2bb8474 # fix/continuous-integration-reports permissions: id-token: write security-events: write @@ -416,21 +453,43 @@ jobs: # Run CI checks inside the Docker container continuous-integration: needs: build-image - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@2b8788166256f66b42262e273b4be22a3fc162e8 # copilot/configure-lint-and-test-commands + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@7b558a563bfd8b37268ab6e820219294a2bb8474 # fix/continuous-integration-reports permissions: id-token: write security-events: write contents: read with: container: ghcr.io/${{ github.repository }}:${{ github.sha }} - # When using container mode, code-ql and dependency-review are typically disabled - # as they require repository checkout - code-ql: "" - dependency-review: false # Specify which build/test commands to run (they should exist in package.json) build: "" # Skip build as it was done in the Docker image - lint: true - test: true +``` + +### Continuous Integration with custom container path mapping + +This example shows how to use custom path mappings when running CI steps inside a container. + +It is useful when the project code is not located in the root folder of the repository. + +```yaml +name: Continuous Integration - Custom Container Path Mapping +on: + push: + branches: [main] +jobs: + continuous-integration: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@7b558a563bfd8b37268ab6e820219294a2bb8474 # fix/continuous-integration-reports + permissions: + id-token: write + security-events: write + contents: read + with: + container: | + { + "image": "ghcr.io/myorg/node-image:18-alpine", + "pathMapping": { + "/app": "./relative/path/to/app" + } + } ``` ### Continuous Integration with Advanced Container Options @@ -446,7 +505,7 @@ on: jobs: continuous-integration: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@2b8788166256f66b42262e273b4be22a3fc162e8 # copilot/configure-lint-and-test-commands + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@7b558a563bfd8b37268ab6e820219294a2bb8474 # fix/continuous-integration-reports permissions: id-token: write security-events: write @@ -466,15 +525,8 @@ jobs: "volumes": ["/tmp:/tmp", "/cache:/workspace/cache"], "credentials": { "username": "myusername" - } + } } - # When using container mode, code-ql and dependency-review are typically disabled - # as they require repository checkout - code-ql: "" - dependency-review: false - build: "build" - lint: true - test: true ``` diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index d24eb24..8139124 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -56,6 +56,15 @@ on: Whether to enable linting. Set to `null` or empty to disable. Accepts a JSON object for lint options. See [lint action](../actions/lint/README.md). + It should generate lint reports in standard formats. + + Example: + + ```json:package.json + { + "lint:ci": "eslint . --output-file eslint-report.json --format json" + } + ``` type: string required: false default: "true" @@ -79,6 +88,15 @@ on: Whether to enable testing. Set to `null` or empty to disable. Accepts a JSON object for test options. See [test action](../actions/test/README.md). + If coverage is enabled, it should generate test and coverage reports in standard formats. + + Example: + + ```json:package.json + { + "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=junit.xml --coverage.enabled --coverage.reporter=lcov --coverage.reporter=text" + } + ``` type: string required: false default: "true" @@ -111,6 +129,9 @@ on: "volumes": ["/tmp:/tmp", "/cache:/cache"], "credentials": { "username": "myusername" + }, + pathMapping: { + "/app": "./relative/path/to/app" } } ``` @@ -123,6 +144,7 @@ on: - `ports` (array) - `volumes` (array) - `credentials` (object with `username`). + - `pathMapping` (object) path mapping from container paths to repository paths. Defaults is working directory is mapped with repository root. See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container. @@ -171,12 +193,14 @@ jobs: container-ports: ${{ steps.parse.outputs.container-ports }} container-volumes: ${{ steps.parse.outputs.container-volumes }} container-username: ${{ steps.parse.outputs.container-username }} + path-mapping: ${{ steps.parse.outputs.path-mapping }} steps: - id: parse uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: CONTAINER_INPUT: ${{ inputs.container }} CONTAINER_PASSWORD: ${{ secrets.container-password }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} with: script: | const containerInput = process.env.CONTAINER_INPUT.trim(); @@ -192,16 +216,26 @@ jobs: options: '--user root:root' }; + let pathMapping = { + [process.env.WORKING_DIRECTORY || '.']: process.env.GITHUB_WORKSPACE, + }; + if (isJson) { try { - const parsedContainer = JSON.parse(containerInput); + const {pathMapping: parsedPathMapping, ...parsedContainer} = JSON.parse(containerInput); core.debug(`Parsed container input as JSON: ${JSON.stringify(parsedContainer)}`); + container = { ...container, ...parsedContainer, options: `${container.options} ${parsedContainer.options || ''}`.trim() }; + core.debug(`Parsed path mapping: ${JSON.stringify(parsedPathMapping)}`); + if (parsedPathMapping){ + pathMapping = parsedPathMapping; + } + } catch (error) { return core.setFailed(`Failed to parse container input as JSON: ${error.message}`,{ cause: error }); } @@ -242,6 +276,16 @@ jobs: return core.setFailed('Container credentials username must be provided when container password is specified.'); } + core.debug(`Parsed path mapping: ${JSON.stringify(pathMapping)}`); + if (!pathMapping || typeof pathMapping !== 'object' || Object.keys(pathMapping).length === 0){ + return core.setFailed('At least one path mapping must be specified in the container configuration.'); + } + + core.setOutput('path-mapping', Object.entries(pathMapping).reduce((acc, [containerPath, repoPath]) => { + acc += `${acc?',' : ''}${containerPath}:${repoPath}`; + return acc; + }, "")); + code-ql: name: ๐Ÿ›ก๏ธ CodeQL Analysis if: inputs.checks == true && inputs.code-ql != '' @@ -449,6 +493,7 @@ jobs: container: ${{ inputs.container != '' && 'true' || 'false' }} command: ${{ steps.preparel-lint-options.outputs.command }} report-file: ${{ steps.preparel-lint-options.outputs.report-file }} + path-mapping: ${{ needs.prepare.outputs.path-mapping || '' }} build: if: inputs.checks == true @@ -559,7 +604,7 @@ jobs: testOptions.coverage = 'github'; } core.setOutput('coverage', testOptions.coverage ); - core.setOutput('coverage-files', testOptions['coverage-files'] || ''); + core.setOutput('report-file', testOptions['report-file'] || ''); core.setOutput('command', testOptions.command || 'test:ci'); - name: Run tests @@ -569,5 +614,6 @@ jobs: container: ${{ inputs.container != '' && 'true' || 'false' }} command: ${{ steps.prepare-test-options.outputs.command }} coverage: ${{ steps.prepare-test-options.outputs.coverage }} - coverage-files: ${{ steps.prepare-test-options.outputs.coverage-files }} + report-file: ${{ steps.prepare-test-options.outputs.report-file }} + path-mapping: ${{ needs.prepare.outputs.path-mapping || '' }} github-token: ${{ secrets.github-token || github.token }} diff --git a/actions/lint/action.yml b/actions/lint/action.yml index 1c3f186..3c59caf 100644 --- a/actions/lint/action.yml +++ b/actions/lint/action.yml @@ -20,14 +20,20 @@ inputs: description: | NPM/package manager script command to run for linting. This should be a script defined in your package.json. - The command should generate lint report files in a standard format (ESLint JSON or Checkstyle XML). + The command should generate lint report files in a standard format. required: false default: "lint:ci" report-file: description: | Optional lint report path forwarded to the [parse-ci-reports](https://hoverkraft-tech/ci-github-common/actions/parse-ci-reports) action. Provide an absolute path or one relative to the working directory. - When omitted, the action falls back to "auto:lint" detection (ESLint JSON / Checkstyle XML). + When omitted, the action falls back to "auto:lint" detection. + required: false + default: "" + path-mapping: + description: | + Optional path mapping to adjust file paths in test and coverage reports. + See the [parse-ci-reports documentation](https://hoverkraft-tech/ci-github-common/actions/parse-ci-reports) for details. required: false default: "" @@ -61,25 +67,22 @@ runs: LINT_COMMAND: ${{ inputs.command }} with: script: | - const workingDirectory = process.env.WORKING_DIRECTORY || '.'; + const workingDirectory = process.env.WORKING_DIRECTORY; const runScriptCommand = process.env.RUN_LINT_COMMAND; const lintCommand = process.env.LINT_COMMAND || 'lint:ci'; core.info(`๐Ÿ‘• Running lint command: ${lintCommand}...`); try { - const result = await exec.getExecOutput(runScriptCommand, [lintCommand], { - cwd: require('path').resolve(process.env.GITHUB_WORKSPACE, workingDirectory), + const exitCode = await exec.exec(runScriptCommand, [lintCommand], { + cwd: workingDirectory, ignoreReturnCode: true }); - if (result.stdout) core.info(result.stdout); - if (result.stderr) core.warning(result.stderr); + core.setOutput('lint-exit-code', exitCode); - core.setOutput('lint-exit-code', result.exitCode); - - if (result.exitCode !== 0) { - core.setFailed(`Linting failed with exit code ${result.exitCode}`); + if (exitCode !== 0) { + core.setFailed(`Linting failed with exit code ${exitCode}`); } } catch (error) { core.setOutput('lint-exit-code', 1); @@ -90,7 +93,9 @@ runs: if: always() uses: hoverkraft-tech/ci-github-common/actions/parse-ci-reports@c314229c3ca6914f7023ffca7afc26753ab99b41 # 0.30.1 with: + working-directory: ${{ inputs.working-directory }} report-paths: ${{ inputs.report-file || 'auto:lint' }} + path-mapping: ${{ inputs.path-mapping }} report-name: "Lint Results" output-format: "annotations,summary" diff --git a/actions/test/action.yml b/actions/test/action.yml index 8c5ac9e..8793b07 100644 --- a/actions/test/action.yml +++ b/actions/test/action.yml @@ -31,13 +31,19 @@ inputs: - `""` or `null`: No coverage reporting required: false default: "github" - coverage-files: + report-file: description: | - Optional coverage report paths forwarded to the hoverkraft-tech/ci-github-common/actions/parse-ci-reports action. + Optional test and coverage report paths forwarded to the hoverkraft-tech/ci-github-common/actions/parse-ci-reports action. Supports multiple formats (Cobertura, OpenCover, lcov, etc.). Provide absolute paths or paths relative to the working directory. Multiple entries can be separated by newlines, commas, or semicolons. - When omitted, the action falls back to "auto:test" detection (LCOV / Cobertura / Clover). + When omitted, the action falls back to "auto:test,auto:coverage" detection. + required: false + default: "" + path-mapping: + description: | + Optional path mapping to adjust file paths in test and coverage reports. + See the [parse-ci-reports documentation](https://hoverkraft-tech/ci-github-common/actions/parse-ci-reports) for details. required: false default: "" github-token: @@ -78,26 +84,23 @@ runs: TEST_COMMAND: ${{ inputs.command }} with: script: | - const workingDirectory = process.env.WORKING_DIRECTORY || '.'; + const workingDirectory = process.env.WORKING_DIRECTORY; const runScriptCommand = process.env.RUN_TEST_COMMAND; const testCommand = process.env.TEST_COMMAND || 'test:ci'; core.info(`๐Ÿงช Running test command: ${testCommand}...`); try { - const result = await exec.getExecOutput(runScriptCommand, [testCommand], { - cwd: require('path').resolve(process.env.GITHUB_WORKSPACE, workingDirectory), + const exitCode = await exec.exec(runScriptCommand, [testCommand], { + cwd: workingDirectory, env: { ...process.env, CI: 'true' }, ignoreReturnCode: true }); - if (result.stdout) core.info(result.stdout); - if (result.stderr) core.warning(result.stderr); + core.setOutput('test-exit-code', exitCode); - core.setOutput('test-exit-code', result.exitCode); - - if (result.exitCode !== 0) { - core.setFailed(`Tests failed with exit code ${result.exitCode}`); + if (exitCode !== 0) { + core.setFailed(`Tests failed with exit code ${exitCode}`); } } catch (error) { core.setOutput('test-exit-code', 1); @@ -109,8 +112,10 @@ runs: id: parse-coverage-reports uses: hoverkraft-tech/ci-github-common/actions/parse-ci-reports@c314229c3ca6914f7023ffca7afc26753ab99b41 # 0.30.1 with: - report-paths: ${{ inputs.coverage-files || 'auto:test' }} + working-directory: ${{ inputs.working-directory }} report-name: "Coverage Results" + report-paths: ${{ inputs.report-file || 'auto:test,auto:coverage' }} + path-mapping: ${{ inputs.path-mapping }} output-format: "summary,markdown" - name: ๐Ÿ“Š Add coverage PR comment