From 9a116f426c05a456c1f138889ff7d6922579d2f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:43:33 +0000 Subject: [PATCH 1/2] feat(continuous-integration): support container options with env and docker options Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/linters/actionlint.yml | 7 + ...__test-workflow-continuous-integration.yml | 47 ++++++ .github/workflows/continuous-integration.md | 98 ++++++++++- .github/workflows/continuous-integration.yml | 153 ++++++++++++++---- 4 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 .github/linters/actionlint.yml diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml new file mode 100644 index 0000000..e4464d1 --- /dev/null +++ b/.github/linters/actionlint.yml @@ -0,0 +1,7 @@ +# FIXME: Temporary ignores to bypass actionlint limitations. See https://github.com/rhysd/actionlint/issues/590. +paths: + .github/workflows/continuous-integration.yml: + ignore: + - 'both "username" and "password" must be specified in "credentials" section' + - '"credentials" section is scalar node but mapping node is expected' + - '"container" section is alias node but mapping node is expected' diff --git a/.github/workflows/__test-workflow-continuous-integration.yml b/.github/workflows/__test-workflow-continuous-integration.yml index 9c5e6ec..a3f4f38 100644 --- a/.github/workflows/__test-workflow-continuous-integration.yml +++ b/.github/workflows/__test-workflow-continuous-integration.yml @@ -109,3 +109,50 @@ jobs: - name: Check the build artifacts run: test -f ${{ runner.temp }}/usr/src/app/dist/test.txt + + act-with-container-advanced: + name: Act - Run the continuous integration workflow (with container and advanced options) + uses: ./.github/workflows/continuous-integration.yml + needs: arrange-with-container + permissions: + contents: read + pull-requests: write + security-events: write + # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 + id-token: write + with: + container: | + { + "image": "${{ fromJSON(needs.arrange-with-container.outputs.built-images).ci-npm.images[0] }}", + "env": { + "NODE_ENV": "test", + "CI": "true" + }, + "options": "--cpus 1", + "credentials": { + "username": "${{ github.actor }}" + } + } + working-directory: /usr/src/app/ + build: | + { + "artifact": "dist" + } + test: | + {"coverage": "codecov"} + secrets: + container-password: ${{ secrets.GITHUB_TOKEN }} + + assert-with-container-advanced: + name: Assert - Ensure build artifact has been uploaded (with container advanced) + runs-on: ubuntu-latest + needs: act-with-container-advanced + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + artifact-ids: ${{ needs.act-with-container-advanced.outputs.build-artifact-id }} + path: ${{ runner.temp }} + + - name: Check the build artifacts + run: test -f ${{ runner.temp }}/usr/src/app/dist/test.txt diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index 8667311..94a0ebd 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -126,7 +126,29 @@ jobs: # Default: `.` working-directory: . - # Docker container image to run CI steps in. 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 to run CI steps in. + # Accepts either a string (container image name) or a JSON object with container options. + # + # String format (simple): + # container: "node:18" + # + # JSON object format (advanced): + # container: | + # { + # "image": "node:18", + # "env": { + # "NODE_ENV": "production" + # }, + # "ports": [8080], + # "volumes": ["/tmp:/tmp"], + # "options": "--cpus 2" + # } + # + # All properties from GitHub's container specification are supported except credentials (use secrets instead). + # See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container + # + # 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: "" ```` @@ -162,10 +184,45 @@ jobs: | | 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`** | Docker container image to run CI steps in. When specified, steps will execute inside this container instead of checking out code. The container should have the project code and dependencies pre-installed. | **false** | **string** | - | +| **`container`** | Container configuration to run CI steps in. Accepts string or JSON object. See Container Configuration below | **false** | **string** | - | +### Container Configuration + +The `container` input accepts either: + +**Simple string format** (image name only): + +```yaml +container: "node:18" +``` + +**Advanced JSON format** (with container options): + +```yaml +container: | + { + "image": "node:18", + "env": { + "NODE_ENV": "production" + }, + "options": "--cpus 2" + } +``` + +**Supported properties:** + +- `image` (string, required) - Container image name +- `env` (object) - Environment variables +- `options` (string) - Additional Docker options + +**Note:** `ports`, `volumes`, and `credentials` are not currently supported due to GitHub Actions workflow syntax limitations. + +See [GitHub's container specification](https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container) for more details. + +When specified, steps will execute inside this container instead of checking out code. The container should have the project code and dependencies pre-installed. + ## Secrets @@ -289,6 +346,43 @@ jobs: test: true ``` +### Continuous Integration with Advanced Container Options + +This example shows how to use advanced container options like environment variables, credentials, and additional Docker options. + +```yaml +name: Continuous Integration - Advanced Container Options + +on: + push: + branches: [main] + +jobs: + continuous-integration: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@32a69b7b8fd5f7ab7bf656e7e88aa90ad235cf8d # 0.18.0 + permissions: + id-token: write + security-events: write + contents: read + with: + container: | + { + "image": "node:18-alpine", + "env": { + "NODE_ENV": "production", + "CI": "true" + }, + "options": "--cpus 2 --memory 4g" + } + # 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 60edee9..dc083d7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -83,7 +83,33 @@ on: required: false default: "." container: - description: "Docker container image to run CI steps in. When specified, steps will execute inside this container instead of checking out code. The container should have the project code and dependencies pre-installed." + description: | + Container configuration to run CI steps in. + Accepts either a string (container image name) or a JSON object with container options. + + String format (simple): + ```yml + container: "node:18" + ``` + + JSON object format (advanced): + ```json + { + "image": "node:18", + "env": { + "NODE_ENV": "production" + }, + "options": "--cpus 2" + } + ``` + + Supported properties: image (required), env (object), options (string). + Note: ports, volumes, and credentials are not currently supported due to GitHub Actions limitations. + + See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container + + When specified, steps will execute inside this container instead of checking out code. + The container should have the project code and dependencies pre-installed. type: string required: false default: "" @@ -97,6 +123,12 @@ on: SECRET_EXAMPLE=$\{{ secrets.SECRET_EXAMPLE }} ``` required: false + container-password: + description: | + Password for container registry authentication, if required. + Used when the container image is hosted in a private registry. + See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container#defining-credentials-for-a-container-registry. + required: false outputs: build-artifact-id: description: "ID of the build artifact) uploaded during the build step." @@ -105,6 +137,73 @@ on: permissions: {} jobs: + prepare: + name: ๐Ÿ“ฆ Prepare configuration + runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} + permissions: {} + outputs: + container-image: ${{ steps.parse.outputs.container-image }} + container-options: ${{ steps.parse.outputs.container-options }} + container-username: ${{ steps.parse.outputs.container-username }} + steps: + - id: parse + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + CONTAINER_INPUT: ${{ inputs.container }} + CONTAINER_PASSWORD: ${{ secrets.container-password }} + with: + script: | + const containerInput = process.env.CONTAINER_INPUT.trim(); + if (!containerInput) { + return; + } + core.debug(`Container input: ${containerInput}`); + + // Check if input is a JSON object or a simple string + const isJson = containerInput.startsWith('{'); + + let container = { + options: '--user root:root' + }; + + if (isJson) { + try { + const parsedContainer = JSON.parse(containerInput); + core.debug(`Parsed container input as JSON: ${JSON.stringify(parsedContainer)}`); + container = { + ...container, + ...parsedContainer, + options: `${container.options} ${parsedContainer.options || ''}`.trim() + }; + + } catch (error) { + return core.setFailed(`Failed to parse container input as JSON: ${error.message}`,{ cause: error }); + } + } else { + // Simple string format - just the image name + container.image = containerInput; + } + + core.debug(`Parsed container configuration: ${JSON.stringify(container)}`); + + if (!container.image) { + return core.setFailed('Container image must be specified in the container input.'); + } + core.setOutput('container-image', container.image); + + if (container.options) { + core.setOutput('container-options', container.options); + } + + if (container.credentials?.username) { + core.setOutput('container-username', container.credentials.username); + if (!process.env.CONTAINER_PASSWORD) { + return core.setFailed('Container password must be provided when container credentials username is specified.'); + } + } else if (process.env.CONTAINER_PASSWORD) { + return core.setFailed('Container credentials username must be provided when container password is specified.'); + } + code-ql: name: ๐Ÿ›ก๏ธ CodeQL Analysis if: inputs.checks == true && inputs.code-ql != '' @@ -131,10 +230,11 @@ jobs: setup: name: โš™๏ธ Setup runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} - container: - image: ${{ inputs.container != '' && inputs.container || null }} - # Root user is required to use GitHub Actions features inside the container - options: --user root:root + needs: prepare + container: &container-setup + image: ${{ needs.prepare.outputs.container-image || '' }} + options: ${{ needs.prepare.outputs.container-options || ' ' }} + credentials: ${{ fromJSON(needs.prepare.outputs.container-username && format('{{"username":{0},"password":{1}}}',toJSON(needs.prepare.outputs.container-username),toJSON(secrets.container-password)) || '{}') }} permissions: contents: read # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 @@ -144,7 +244,7 @@ jobs: build-commands: ${{ steps.build-variables.outputs.commands }} build-artifact: ${{ steps.build-variables.outputs.artifact }} steps: - - if: inputs.container == '' + - if: needs.prepare.outputs.container-image == null uses: hoverkraft-tech/ci-github-common/actions/checkout@753288393de1f3d92f687a6761d236ca800f5306 # 0.28.1 - id: build-variables @@ -249,12 +349,11 @@ jobs: lint: name: ๐Ÿ‘• Lint if: inputs.checks == true && inputs.lint + needs: + - prepare + - setup runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} - container: - image: ${{ inputs.container != '' && inputs.container || null }} - # Root user is required to use GitHub Actions features inside the container - options: --user root:root - needs: setup + container: *container-setup # jscpd:ignore-start permissions: contents: read @@ -262,9 +361,8 @@ jobs: id-token: write steps: - uses: hoverkraft-tech/ci-github-common/actions/checkout@753288393de1f3d92f687a6761d236ca800f5306 # 0.28.1 - if: inputs.container == '' + if: needs.prepare.outputs.container-image == null - # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 - id: oidc uses: ChristopherHX/oidc@73eee1ff03fdfce10eda179f617131532209edbd # v3 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -300,18 +398,17 @@ jobs: - uses: ./self-workflow/actions/lint with: working-directory: ${{ inputs.working-directory }} - container: ${{ inputs.container != '' }} + container: ${{ needs.prepare.outputs.container-image && 'true' || 'false' }} build: name: ๐Ÿ—๏ธ Build if: inputs.checks == true runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} + container: *container-setup # jscpd:ignore-start - container: - image: ${{ inputs.container != '' && inputs.container || null }} - # Root user is required to use GitHub Actions features inside the container - options: --user root:root - needs: setup + needs: + - prepare + - setup permissions: contents: read # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 @@ -320,7 +417,7 @@ jobs: artifact-id: ${{ steps.build.outputs.artifact-id }} steps: - uses: hoverkraft-tech/ci-github-common/actions/checkout@753288393de1f3d92f687a6761d236ca800f5306 # 0.28.1 - if: needs.setup.outputs.build-commands && inputs.container == '' + if: needs.setup.outputs.build-commands && needs.prepare.outputs.container-image == null # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 - id: oidc @@ -348,17 +445,15 @@ jobs: build-env: ${{ needs.setup.outputs.build-env }} build-secrets: ${{ secrets.build-secrets }} build-artifact: ${{ needs.setup.outputs.build-artifact }} - container: ${{ inputs.container != '' }} + container: ${{ needs.prepare.outputs.container-image && 'true' || 'false' }} test: name: ๐Ÿงช Test if: inputs.checks == true && inputs.test runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} - container: - image: ${{ inputs.container != '' && inputs.container || null }} - # Root user is required to use GitHub Actions features inside the container - options: --user root:root + container: *container-setup needs: + - prepare - setup - build permissions: @@ -368,9 +463,9 @@ jobs: id-token: write steps: - uses: hoverkraft-tech/ci-github-common/actions/checkout@753288393de1f3d92f687a6761d236ca800f5306 # 0.28.1 - if: inputs.container == '' + if: needs.prepare.outputs.container-image == null - - if: needs.build.outputs.artifact-id && inputs.container == '' + - if: needs.build.outputs.artifact-id && needs.prepare.outputs.container-image == null uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} @@ -419,7 +514,7 @@ jobs: - uses: ./self-workflow/actions/test with: working-directory: ${{ inputs.working-directory }} - container: ${{ inputs.container != '' }} + container: ${{ needs.prepare.outputs.container-image && 'true' || 'false' }} coverage: ${{ steps.prepare-test-options.outputs.coverage }} - coverage-files: ${{ steps.prepare-test-options.outputs['coverage-files'] }} + coverage-files: ${{ steps.prepare-test-options.outputs.coverage-files }} github-token: ${{ github.token }} From 5a246de67232b3a21fb951be0ccfdb5493b52ccd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:22:26 +0000 Subject: [PATCH 2/2] feat: add support for ports, volumes, and credentials in container configuration Co-authored-by: neilime <314088+neilime@users.noreply.github.com> Signed-off-by: Emilien Escalle --- .github/linters/actionlint.yml | 3 ++ .github/workflows/continuous-integration.md | 57 ++++++++++++++++---- .github/workflows/continuous-integration.yml | 28 ++++++++-- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/.github/linters/actionlint.yml b/.github/linters/actionlint.yml index e4464d1..f516136 100644 --- a/.github/linters/actionlint.yml +++ b/.github/linters/actionlint.yml @@ -5,3 +5,6 @@ paths: - 'both "username" and "password" must be specified in "credentials" section' - '"credentials" section is scalar node but mapping node is expected' - '"container" section is alias node but mapping node is expected' + - '"env" section must be mapping node but got scalar node' + - '"ports" section must be sequence node but got scalar node' + - '"volumes" section must be sequence node but got scalar node' diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index 94a0ebd..bcef730 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -207,7 +207,12 @@ container: | "env": { "NODE_ENV": "production" }, - "options": "--cpus 2" + "options": "--cpus 2", + "ports": [8080, 3000], + "volumes": ["/tmp:/tmp", "/cache:/cache"], + "credentials": { + "username": "myusername" + } } ``` @@ -216,8 +221,29 @@ container: | - `image` (string, required) - Container image name - `env` (object) - Environment variables - `options` (string) - Additional Docker options +- `ports` (array) - Port mappings +- `volumes` (array) - Volume mounts +- `credentials` (object) - Registry credentials with `username` property -**Note:** `ports`, `volumes`, and `credentials` are not currently supported due to GitHub Actions workflow syntax limitations. +#### Container Registry Credentials + +For private container images, specify the username in the container input's `credentials.username` property and pass the password via the `container-password` secret: + +```yaml +jobs: + continuous-integration: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@main + secrets: + container-password: ${{ secrets.REGISTRY_PASSWORD }} + with: + container: | + { + "image": "ghcr.io/myorg/my-private-image:latest", + "credentials": { + "username": "myusername" + } + } +``` See [GitHub's container specification](https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container) for more details. @@ -227,12 +253,14 @@ When specified, steps will execute inside this container instead of checking out ## Secrets -| **Secret** | **Description** | **Required** | -| ------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------ | -| **`build-secrets`** | Secrets to be used during the build step. | **false** | -| | Must be a multi-line env formatted string. | | -| | Example: | | -| |
SECRET_EXAMPLE=$\{{ secrets.SECRET_EXAMPLE }}
| | +| **Secret** | **Description** | **Required** | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| **`build-secrets`** | Secrets to be used during the build step. | **false** | +| | Must be a multi-line env formatted string. | | +| | Example: | | +| |
SECRET_EXAMPLE=$\{{ secrets.SECRET_EXAMPLE }}
| | +| **`container-password`** | Password or token for authenticating to the container registry. | **false** | +| | Required when using private container images. The username should be specified in the container input's `credentials.username` property. | | @@ -348,7 +376,7 @@ jobs: ### Continuous Integration with Advanced Container Options -This example shows how to use advanced container options like environment variables, credentials, and additional Docker options. +This example shows how to use advanced container options like environment variables, ports, volumes, credentials, and additional Docker options. ```yaml name: Continuous Integration - Advanced Container Options @@ -364,15 +392,22 @@ jobs: id-token: write security-events: write contents: read + secrets: + container-password: ${{ secrets.REGISTRY_PASSWORD }} with: container: | { - "image": "node:18-alpine", + "image": "ghcr.io/myorg/node-image:18-alpine", "env": { "NODE_ENV": "production", "CI": "true" }, - "options": "--cpus 2 --memory 4g" + "options": "--cpus 2 --memory 4g", + "ports": [3000, 8080], + "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 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dc083d7..d2565c6 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -99,12 +99,16 @@ on: "env": { "NODE_ENV": "production" }, - "options": "--cpus 2" + "options": "--cpus 2", + "ports": [8080, 3000], + "volumes": ["/tmp:/tmp", "/cache:/cache"], + "credentials": { + "username": "myusername" + } } ``` - Supported properties: image (required), env (object), options (string). - Note: ports, volumes, and credentials are not currently supported due to GitHub Actions limitations. + Supported properties: image (required), env (object), options (string), ports (array), volumes (array), credentials (object with username). See https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container @@ -143,7 +147,10 @@ jobs: permissions: {} outputs: container-image: ${{ steps.parse.outputs.container-image }} + container-env: ${{ steps.parse.outputs.container-env }} container-options: ${{ steps.parse.outputs.container-options }} + container-ports: ${{ steps.parse.outputs.container-ports }} + container-volumes: ${{ steps.parse.outputs.container-volumes }} container-username: ${{ steps.parse.outputs.container-username }} steps: - id: parse @@ -191,10 +198,22 @@ jobs: } core.setOutput('container-image', container.image); + if (container.env) { + core.setOutput('container-env', JSON.stringify(container.env)); + } + if (container.options) { core.setOutput('container-options', container.options); } + if (container.ports) { + core.setOutput('container-ports', JSON.stringify(container.ports)); + } + + if (container.volumes) { + core.setOutput('container-volumes', JSON.stringify(container.volumes)); + } + if (container.credentials?.username) { core.setOutput('container-username', container.credentials.username); if (!process.env.CONTAINER_PASSWORD) { @@ -233,7 +252,10 @@ jobs: needs: prepare container: &container-setup image: ${{ needs.prepare.outputs.container-image || '' }} + env: ${{ fromJSON(needs.prepare.outputs.container-env || '{}') }} options: ${{ needs.prepare.outputs.container-options || ' ' }} + ports: ${{ fromJSON(needs.prepare.outputs.container-ports || '[]') }} + volumes: ${{ fromJSON(needs.prepare.outputs.container-volumes || '[]') }} credentials: ${{ fromJSON(needs.prepare.outputs.container-username && format('{{"username":{0},"password":{1}}}',toJSON(needs.prepare.outputs.container-username),toJSON(secrets.container-password)) || '{}') }} permissions: contents: read