From d2bee4f07e8ca410d6b196d00f90c12e7d48c33a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:13:22 +0000 Subject: [PATCH] feat: support OCI artifact compose-file inputs Signed-off-by: Emilien Escalle --- .github/workflows/__check-action.yml | 42 ++++++++++++++++++++++++++++ README.md | 32 ++++++++++----------- action.yml | 2 +- dist/index.js | 6 +++- dist/post.js | 6 +++- src/services/input.service.test.ts | 25 +++++++++++++++++ src/services/input.service.ts | 8 +++++- 7 files changed, 101 insertions(+), 20 deletions(-) diff --git a/.github/workflows/__check-action.yml b/.github/workflows/__check-action.yml index 138da96..db285d7 100644 --- a/.github/workflows/__check-action.yml +++ b/.github/workflows/__check-action.yml @@ -170,6 +170,14 @@ jobs: docker compose -f ./test/docker-compose.yml ps | grep test-service-a-1 || (echo "Service service-a is not running under custom context" && exit 1) (docker compose -f ./test/docker-compose.yml ps | grep test-service-b-1 && echo "Service service-b should not be running without profile" && exit 1) || true + - name: Given OCI compose artifact when running action + assertion-name: "Then the OCI compose application runs" + compose-file: oci://localhost:5000/compose-action-test:latest + publish-oci-artifact: true + source-compose-file: ./test/docker-compose.yml + assertion: | + docker compose -f "$OCI_COMPOSE_FILE" ps service-a | grep service-a | grep "Up" || (echo "Service service-a is not running from the OCI artifact" && exit 1) + env: DOCKER_COMPOSE_VERSION: ${{ matrix.expected-compose-version || '' }} steps: @@ -200,6 +208,39 @@ jobs: core.exportVariable('DOCKER_COMPOSE_VERSION', dockerComposeVersion); + - name: "Arrange: start local OCI registry" + if: ${{ matrix.publish-oci-artifact }} + # Keep the registry alive until the job ends because the action post hook + # reuses the OCI reference for docker compose logs and down. + run: | + OCI_REGISTRY_IMAGE=registry:2.8.3 + OCI_REGISTRY_MAX_RETRIES=10 + OCI_REGISTRY_RETRY_DELAY_SECONDS=1 + + # Keep the registry on port 5000 so it matches the OCI reference configured in the test matrix. + OCI_REGISTRY_CONTAINER_ID=$(docker run -d -p 5000:5000 "$OCI_REGISTRY_IMAGE") + echo "OCI_REGISTRY_CONTAINER_ID=$OCI_REGISTRY_CONTAINER_ID" >> "$GITHUB_ENV" + + retry_count=0 + while [ "$retry_count" -lt "$OCI_REGISTRY_MAX_RETRIES" ]; do + if curl --fail --silent http://localhost:5000/v2/ >/dev/null; then + exit 0 + fi + + retry_count=$((retry_count + 1)) + sleep "$OCI_REGISTRY_RETRY_DELAY_SECONDS" + done + + echo "Local OCI registry did not become ready on localhost:5000" + exit 1 + + - name: "Arrange: publish compose application as OCI artifact" + if: ${{ matrix.publish-oci-artifact }} + run: | + OCI_REPOSITORY="${{ matrix.compose-file }}" + OCI_PUBLISH_TARGET="${OCI_REPOSITORY#oci://}" + docker compose -f "${{ matrix.source-compose-file }}" publish "$OCI_PUBLISH_TARGET" + - name: "Arrange: ensure original docker compose version is not the expected one" if: ${{ matrix.ensure-version-mismatch }} run: | @@ -230,3 +271,4 @@ jobs: run: ${{ matrix.assertion }} env: IMAGE_NAME: ${{ matrix.image-name || '' }} + OCI_COMPOSE_FILE: ${{ matrix.compose-file || '' }} diff --git a/README.md b/README.md index 747716d..400158d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Some extra options can be passed to the `docker compose down` command using the # Additional options to pass to `docker` command. docker-flags: "" - # Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd). + # Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd), or an OCI artifact reference starting with `oci://`. # Default: `./docker-compose.yml` compose-file: ./docker-compose.yml @@ -96,21 +96,21 @@ Some extra options can be passed to the `docker compose down` command using the ## Inputs -| **Input** | **Description** | **Required** | **Default** | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------- | -| **`docker-flags`** | Additional options to pass to `docker` command. | **false** | - | -| **`compose-file`** | Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd). | **false** | `./docker-compose.yml` | -| **`services`** | Services to perform `docker compose up`. | **false** | - | -| **`up-flags`** | Additional options to pass to `docker compose up` command. | **false** | - | -| **`down-flags`** | Additional options to pass to `docker compose down` command. | **false** | - | -| **`compose-flags`** | Additional options to pass to `docker compose` command. | **false** | - | -| **`cwd`** | Current working directory | **false** | `${{ github.workspace }}` | -| **`compose-version`** | Compose version to use. | **false** | - | -| | If null (default), it will use the current installed version. | | | -| | If "latest", it will install the latest version. | | | -| **`services-log-level`** | The log level used for Docker Compose service logs. | **false** | `debug` | -| | Can be one of "debug", "info". | | | -| **`github-token`** | The GitHub token used to create an authenticated client (to fetch the latest version of Docker Compose). | **false** | `${{ github.token }}` | +| **Input** | **Description** | **Required** | **Default** | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------- | +| **`docker-flags`** | Additional options to pass to `docker` command. | **false** | - | +| **`compose-file`** | Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd), or an OCI artifact reference starting with `oci://`. | **false** | `./docker-compose.yml` | +| **`services`** | Services to perform `docker compose up`. | **false** | - | +| **`up-flags`** | Additional options to pass to `docker compose up` command. | **false** | - | +| **`down-flags`** | Additional options to pass to `docker compose down` command. | **false** | - | +| **`compose-flags`** | Additional options to pass to `docker compose` command. | **false** | - | +| **`cwd`** | Current working directory | **false** | `${{ github.workspace }}` | +| **`compose-version`** | Compose version to use. | **false** | - | +| | If null (default), it will use the current installed version. | | | +| | If "latest", it will install the latest version. | | | +| **`services-log-level`** | The log level used for Docker Compose service logs. | **false** | `debug` | +| | Can be one of "debug", "info". | | | +| **`github-token`** | The GitHub token used to create an authenticated client (to fetch the latest version of Docker Compose). | **false** | `${{ github.token }}` | diff --git a/action.yml b/action.yml index ffd8ace..ff35b1b 100644 --- a/action.yml +++ b/action.yml @@ -25,7 +25,7 @@ inputs: description: "Additional options to pass to `docker` command." required: false compose-file: - description: "Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd)." + description: "Path to compose file(s). It can be a list of files. It can be absolute or relative to the current working directory (cwd), or an OCI artifact reference starting with `oci://`." required: false default: "./docker-compose.yml" services: diff --git a/dist/index.js b/dist/index.js index 93c44c3..abef3f5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -43808,9 +43808,13 @@ class InputService { getComposeFiles() { const cwd = this.getCwd(); const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => { - if (!composeFile.trim().length) { + const trimmedComposeFile = composeFile.trim(); + if (!trimmedComposeFile.length) { return false; } + if (trimmedComposeFile.startsWith("oci://")) { + return true; + } const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile]; for (const path of possiblePaths) { if ((0,external_node_fs_namespaceObject.existsSync)(path)) { diff --git a/dist/post.js b/dist/post.js index 2454544..c3536d8 100644 --- a/dist/post.js +++ b/dist/post.js @@ -40060,9 +40060,13 @@ class InputService { getComposeFiles() { const cwd = this.getCwd(); const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => { - if (!composeFile.trim().length) { + const trimmedComposeFile = composeFile.trim(); + if (!trimmedComposeFile.length) { return false; } + if (trimmedComposeFile.startsWith("oci://")) { + return true; + } const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile]; for (const path of possiblePaths) { if ((0,external_node_fs_namespaceObject.existsSync)(path)) { diff --git a/src/services/input.service.test.ts b/src/services/input.service.test.ts index e65d74b..dacb842 100644 --- a/src/services/input.service.test.ts +++ b/src/services/input.service.test.ts @@ -139,6 +139,31 @@ describe("InputService", () => { expect(inputs.composeFiles).toEqual(["./compose.yml"]); }); + it("should accept OCI compose files without checking the file system", () => { + getMultilineInputMock.mockImplementation((inputName) => { + switch (inputName) { + case InputNames.ComposeFile: + return ["oci://docker.io/hoverkraft/compose-app:latest"]; + default: + return []; + } + }); + + getInputMock.mockImplementation((inputName) => { + switch (inputName) { + case InputNames.Cwd: + return "/current/working/directory"; + default: + return ""; + } + }); + + const inputs = service.getInputs(); + + expect(inputs.composeFiles).toEqual(["oci://docker.io/hoverkraft/compose-app:latest"]); + expect(existsSyncMock).not.toHaveBeenCalled(); + }); + it("should throws an error when a compose file does not exist", () => { getMultilineInputMock.mockImplementation((inputName) => { switch (inputName) { diff --git a/src/services/input.service.ts b/src/services/input.service.ts index af922dc..7666901 100644 --- a/src/services/input.service.ts +++ b/src/services/input.service.ts @@ -54,10 +54,16 @@ export class InputService { private getComposeFiles(): string[] { const cwd = this.getCwd(); const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => { - if (!composeFile.trim().length) { + const trimmedComposeFile = composeFile.trim(); + + if (!trimmedComposeFile.length) { return false; } + if (trimmedComposeFile.startsWith("oci://")) { + return true; + } + const possiblePaths = [join(cwd, composeFile), composeFile]; for (const path of possiblePaths) {