feat: support OCI artifact compose-file inputs

Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-15 15:13:22 +00:00 committed by Emilien Escalle
parent b542f028fa
commit d2bee4f07e
7 changed files with 101 additions and 20 deletions

View File

@ -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-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 (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: env:
DOCKER_COMPOSE_VERSION: ${{ matrix.expected-compose-version || '' }} DOCKER_COMPOSE_VERSION: ${{ matrix.expected-compose-version || '' }}
steps: steps:
@ -200,6 +208,39 @@ jobs:
core.exportVariable('DOCKER_COMPOSE_VERSION', dockerComposeVersion); 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" - name: "Arrange: ensure original docker compose version is not the expected one"
if: ${{ matrix.ensure-version-mismatch }} if: ${{ matrix.ensure-version-mismatch }}
run: | run: |
@ -230,3 +271,4 @@ jobs:
run: ${{ matrix.assertion }} run: ${{ matrix.assertion }}
env: env:
IMAGE_NAME: ${{ matrix.image-name || '' }} IMAGE_NAME: ${{ matrix.image-name || '' }}
OCI_COMPOSE_FILE: ${{ matrix.compose-file || '' }}

View File

@ -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. # Additional options to pass to `docker` command.
docker-flags: "" 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` # Default: `./docker-compose.yml`
compose-file: ./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 ## Inputs
| **Input** | **Description** | **Required** | **Default** | | **Input** | **Description** | **Required** | **Default** |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------- | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------- |
| **`docker-flags`** | Additional options to pass to `docker` command. | **false** | - | | **`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` | | **`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** | - | | **`services`** | Services to perform `docker compose up`. | **false** | - |
| **`up-flags`** | Additional options to pass to `docker compose up` command. | **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** | - | | **`down-flags`** | Additional options to pass to `docker compose down` command. | **false** | - |
| **`compose-flags`** | Additional options to pass to `docker compose` command. | **false** | - | | **`compose-flags`** | Additional options to pass to `docker compose` command. | **false** | - |
| **`cwd`** | Current working directory | **false** | `${{ github.workspace }}` | | **`cwd`** | Current working directory | **false** | `${{ github.workspace }}` |
| **`compose-version`** | Compose version to use. | **false** | - | | **`compose-version`** | Compose version to use. | **false** | - |
| | If null (default), it will use the current installed version. | | | | | If null (default), it will use the current installed version. | | |
| | If "latest", it will install the latest version. | | | | | If "latest", it will install the latest version. | | |
| **`services-log-level`** | The log level used for Docker Compose service logs. | **false** | `debug` | | **`services-log-level`** | The log level used for Docker Compose service logs. | **false** | `debug` |
| | Can be one of "debug", "info". | | | | | 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 }}` | | **`github-token`** | The GitHub token used to create an authenticated client (to fetch the latest version of Docker Compose). | **false** | `${{ github.token }}` |
<!-- inputs:end --> <!-- inputs:end -->

View File

@ -25,7 +25,7 @@ inputs:
description: "Additional options to pass to `docker` command." description: "Additional options to pass to `docker` command."
required: false required: false
compose-file: 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 required: false
default: "./docker-compose.yml" default: "./docker-compose.yml"
services: services:

6
dist/index.js generated vendored
View File

@ -43808,9 +43808,13 @@ class InputService {
getComposeFiles() { getComposeFiles() {
const cwd = this.getCwd(); const cwd = this.getCwd();
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => { const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => {
if (!composeFile.trim().length) { const trimmedComposeFile = composeFile.trim();
if (!trimmedComposeFile.length) {
return false; return false;
} }
if (trimmedComposeFile.startsWith("oci://")) {
return true;
}
const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile]; const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile];
for (const path of possiblePaths) { for (const path of possiblePaths) {
if ((0,external_node_fs_namespaceObject.existsSync)(path)) { if ((0,external_node_fs_namespaceObject.existsSync)(path)) {

6
dist/post.js generated vendored
View File

@ -40060,9 +40060,13 @@ class InputService {
getComposeFiles() { getComposeFiles() {
const cwd = this.getCwd(); const cwd = this.getCwd();
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => { const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile) => {
if (!composeFile.trim().length) { const trimmedComposeFile = composeFile.trim();
if (!trimmedComposeFile.length) {
return false; return false;
} }
if (trimmedComposeFile.startsWith("oci://")) {
return true;
}
const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile]; const possiblePaths = [(0,external_node_path_namespaceObject.join)(cwd, composeFile), composeFile];
for (const path of possiblePaths) { for (const path of possiblePaths) {
if ((0,external_node_fs_namespaceObject.existsSync)(path)) { if ((0,external_node_fs_namespaceObject.existsSync)(path)) {

View File

@ -139,6 +139,31 @@ describe("InputService", () => {
expect(inputs.composeFiles).toEqual(["./compose.yml"]); 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", () => { it("should throws an error when a compose file does not exist", () => {
getMultilineInputMock.mockImplementation((inputName) => { getMultilineInputMock.mockImplementation((inputName) => {
switch (inputName) { switch (inputName) {

View File

@ -54,10 +54,16 @@ export class InputService {
private getComposeFiles(): string[] { private getComposeFiles(): string[] {
const cwd = this.getCwd(); const cwd = this.getCwd();
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => { const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => {
if (!composeFile.trim().length) { const trimmedComposeFile = composeFile.trim();
if (!trimmedComposeFile.length) {
return false; return false;
} }
if (trimmedComposeFile.startsWith("oci://")) {
return true;
}
const possiblePaths = [join(cwd, composeFile), composeFile]; const possiblePaths = [join(cwd, composeFile), composeFile];
for (const path of possiblePaths) { for (const path of possiblePaths) {