├── .devcontainer └── devcontainer.json ├── .ghadocs.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── ghadocs │ ├── branding.svg │ └── examples │ │ ├── 1_environment-variables.md │ │ ├── 2_services.md │ │ ├── 3_up-flags.md │ │ ├── 4_down-flags.md │ │ └── 5_compose-flags.md ├── linters │ └── .jscpd.json └── workflows │ ├── __check-action.yml │ ├── __check-dist.yml │ ├── __check-nodejs.yml │ ├── __shared-ci.yml │ ├── greetings.yml │ ├── main-ci.yml │ ├── need-fix-to-issue.yml │ ├── pull-request-ci.yml │ ├── release-new-action-version.yml │ ├── semantic-pull-request.yml │ └── stale.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── dist ├── index.js ├── licenses.txt └── post.js ├── package-lock.json ├── package.json ├── src ├── index-runner.test.ts ├── index-runner.ts ├── index.test.ts ├── index.ts ├── post-runner.test.ts ├── post-runner.ts ├── post.test.ts ├── post.ts └── services │ ├── docker-compose-installer.service.test.ts │ ├── docker-compose-installer.service.ts │ ├── docker-compose.service.test.ts │ ├── docker-compose.service.ts │ ├── input.service.test.ts │ ├── input.service.ts │ ├── installer-adapter │ ├── docker-compose-installer-adapter.ts │ ├── manual-installer-adapter.test.ts │ └── manual-installer-adapter.ts │ ├── logger.service.test.ts │ └── logger.service.ts ├── test ├── Dockerfile ├── docker-compose-fail.yml ├── docker-compose-web-mysql.yml ├── docker-compose-with-env.yml ├── docker-compose.ci.yml ├── docker-compose.yml └── entrypoint.sh └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHub Actions (TypeScript)", 3 | "image": "mcr.microsoft.com/devcontainers/typescript-node:20", 4 | "postCreateCommand": "npm install", 5 | "customizations": { 6 | "codespaces": { 7 | "openFiles": ["README.md"] 8 | }, 9 | "vscode": { 10 | "extensions": [ 11 | "bierner.markdown-preview-github-styles", 12 | "davidanson.vscode-markdownlint", 13 | "dbaeumer.vscode-eslint", 14 | "esbenp.prettier-vscode", 15 | "github.copilot", 16 | "github.copilot-chat", 17 | "github.vscode-github-actions", 18 | "github.vscode-pull-request-github", 19 | "me-dutour-mathieu.vscode-github-actions", 20 | "redhat.vscode-yaml", 21 | "rvest.vs-code-prettier-eslint", 22 | "yzhang.markdown-all-in-one" 23 | ], 24 | "settings": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode", 26 | "editor.tabSize": 2, 27 | "editor.formatOnSave": true, 28 | "markdown.extension.list.indentationSize": "adaptive", 29 | "markdown.extension.italic.indicator": "_", 30 | "markdown.extension.orderedList.marker": "one" 31 | } 32 | } 33 | }, 34 | "remoteEnv": { 35 | "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" 36 | }, 37 | "features": { 38 | "ghcr.io/devcontainers/features/github-cli:1": {}, 39 | "ghcr.io/devcontainers-contrib/features/prettier:1": {}, 40 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.ghadocs.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "action": "action.yml", 4 | "readme": "README.md" 5 | }, 6 | "show_logo": true, 7 | "versioning": { 8 | "enabled": true, 9 | "override": "", 10 | "prefix": "v", 11 | "branch": "main" 12 | }, 13 | "owner": "hoverkraft-tech", 14 | "repo": "compose-action", 15 | "title_prefix": "GitHub Action: ", 16 | "prettier": true 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | dist/** -diff linguist-generated=true 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | 12 | **Description:** A clear and concise description of what the bug is. 13 | 14 | **Action version:** Specify the action version 15 | 16 | **Platform:** 17 | 18 | - [ ] Ubuntu 19 | - [ ] macOS 20 | - [ ] Windows 21 | 22 | **Runner type:** 23 | 24 | - [ ] Hosted 25 | - [ ] Self-hosted 26 | 27 | **Tools version:** 28 | 29 | 30 | 31 | **Repro steps:** 32 | A description with steps to reproduce the issue. If you have a public example or 33 | repository to share, please provide the link. 34 | 35 | **Expected behavior:** A description of what you expected to happen. 36 | 37 | **Actual behavior:** A description of what is actually happening. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** A clear and 10 | concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** A clear and concise description of what you 13 | want to happen. 14 | 15 | **Describe alternatives you've considered** A clear and concise description of 16 | any alternative solutions or features you've considered. 17 | 18 | **Additional context** Add any other context or screenshots about the feature 19 | request here. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | open-pull-requests-limit: 20 6 | schedule: 7 | interval: weekly 8 | day: friday 9 | time: "04:00" 10 | groups: 11 | github-actions-dependencies: 12 | patterns: 13 | - "*" 14 | 15 | - package-ecosystem: docker 16 | directories: 17 | - "/test" 18 | - "/" 19 | open-pull-requests-limit: 20 20 | schedule: 21 | interval: weekly 22 | day: friday 23 | time: "04:00" 24 | groups: 25 | docker-dependencies: 26 | patterns: 27 | - "*" 28 | 29 | - package-ecosystem: docker-compose 30 | directory: "/test" 31 | open-pull-requests-limit: 20 32 | schedule: 33 | interval: weekly 34 | day: friday 35 | time: "04:00" 36 | groups: 37 | docker-compose-dependencies: 38 | patterns: 39 | - "*" 40 | 41 | - package-ecosystem: npm 42 | directory: "/" 43 | open-pull-requests-limit: 20 44 | versioning-strategy: widen 45 | schedule: 46 | interval: weekly 47 | day: friday 48 | time: "04:00" 49 | groups: 50 | actions-dependencies: 51 | patterns: 52 | - "@actions/*" 53 | npm-dev-dependencies: 54 | dependency-type: development 55 | 56 | - package-ecosystem: "devcontainers" 57 | open-pull-requests-limit: 20 58 | directory: "/" 59 | schedule: 60 | interval: weekly 61 | day: friday 62 | time: "04:00" 63 | groups: 64 | devcontainers-dependencies: 65 | patterns: 66 | - "*" 67 | -------------------------------------------------------------------------------- /.github/ghadocs/branding.svg: -------------------------------------------------------------------------------- 1 | 2 | anchor 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/ghadocs/examples/1_environment-variables.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Example Using environment variables 4 | 5 | ```yaml 6 | steps: 7 | - uses: actions/checkout@v4.2.2 8 | - uses: hoverkraft-tech/compose-action@v1.5.1 9 | with: 10 | compose-file: "./docker/docker-compose.yml" 11 | env: 12 | CUSTOM_VARIABLE: "test" 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/ghadocs/examples/2_services.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Example using `services` 4 | 5 | Perform `docker compose up` to some given service instead of all of them 6 | 7 | ```yaml 8 | steps: 9 | # need checkout before using compose-action 10 | - uses: actions/checkout@v3 11 | - uses: hoverkraft-tech/compose-action@v1.5.1 12 | with: 13 | compose-file: "./docker/docker-compose.yml" 14 | services: | 15 | helloworld2 16 | helloworld3 17 | ``` 18 | -------------------------------------------------------------------------------- /.github/ghadocs/examples/3_up-flags.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Example using `up-flags` 4 | 5 | Specify flags to pass to the `docker compose up`. Default is none. Can be used 6 | to pass the `--build` flag, for example, if you want persistent volumes to be 7 | deleted as well during cleanup. A full list of flags can be found in the 8 | [docker compose up documentation](https://docs.docker.com/compose/reference/up/). 9 | -------------------------------------------------------------------------------- /.github/ghadocs/examples/4_down-flags.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Example using `down-flags` 4 | 5 | Specify flags to pass to the `docker-compose down` command during cleanup. 6 | Default is none. Can be used to pass the `--volumes` flag, for example, if you 7 | want persistent volumes to be deleted as well during cleanup. A full list of 8 | flags can be found in the 9 | [docker-compose down documentation](https://docs.docker.com/compose/reference/down/). 10 | -------------------------------------------------------------------------------- /.github/ghadocs/examples/5_compose-flags.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Example using `compose-flags` 4 | 5 | Specify flags to pass to the `docker compose` command. Default is none. A full 6 | list of flags can be found in the 7 | [docker compose documentation](https://docs.docker.com/compose/reference/#command-options-overview-and-help). 8 | 9 | ```yaml 10 | steps: 11 | # need checkout before using compose-action 12 | - uses: actions/checkout@v3 13 | - uses: hoverkraft-tech/compose-action@v1.5.1 14 | with: 15 | compose-file: "./docker/docker-compose.yml" 16 | services: | 17 | helloworld2 18 | helloworld3 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/linters/.jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["**/dist/**"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/__check-action.yml: -------------------------------------------------------------------------------- 1 | name: Internal - Tests for action 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test-action-with-services: 11 | runs-on: ubuntu-latest 12 | name: Test with services 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Act 17 | uses: ./ 18 | with: 19 | compose-file: "./test/docker-compose.yml" 20 | services: | 21 | service-b 22 | service-c 23 | 24 | - name: "Assert: only expected services are running" 25 | run: | 26 | docker compose -f ./test/docker-compose.yml ps 27 | 28 | docker compose -f ./test/docker-compose.yml ps | grep test-service-b-1 || (echo "Service service-b is not running" && exit 1) 29 | docker compose -f ./test/docker-compose.yml ps | grep test-service-c-1 || (echo "Service service-c is not running" && exit 1) 30 | (docker compose -f ./test/docker-compose.yml ps | grep test-service-a-1 && echo "Unexpected service service-a is running" && exit 1) || true 31 | 32 | test-action-with-down-flags: 33 | runs-on: ubuntu-latest 34 | name: Test compose action 35 | steps: 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | 38 | - name: Act 39 | uses: ./ 40 | with: 41 | compose-file: "./test/docker-compose.yml" 42 | down-flags: "--volumes" 43 | 44 | test-action-with-compose-flags: 45 | runs-on: ubuntu-latest 46 | name: Test with compose flags 47 | steps: 48 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | 50 | - name: Act 51 | uses: ./ 52 | with: 53 | compose-file: "./test/docker-compose.yml" 54 | compose-flags: "--profile profile-1" 55 | down-flags: "--volumes" 56 | 57 | - name: "Assert: profile is used" 58 | run: | 59 | docker compose -f ./test/docker-compose.yml -p profile-1 ps || (echo "Profile not used" && exit 1) 60 | 61 | test-action-with-env: 62 | runs-on: ubuntu-latest 63 | name: Test with env 64 | steps: 65 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | 67 | - name: Act 68 | uses: ./ 69 | with: 70 | compose-file: "./test/docker-compose-with-env.yml" 71 | env: 72 | IMAGE_NAME: busybox 73 | 74 | - name: "Assert: env is used" 75 | env: 76 | IMAGE_NAME: busybox 77 | run: | 78 | docker compose -f ./test/docker-compose-with-env.yml ps 79 | 80 | docker compose -f ./test/docker-compose-with-env.yml ps | grep test-service-a-1 || (echo "Service service-a is not running" && exit 1) 81 | 82 | test-action-with-multiple-compose-files: 83 | runs-on: ubuntu-latest 84 | name: Test with multiple compose files 85 | steps: 86 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 87 | 88 | - name: Act 89 | uses: ./ 90 | with: 91 | compose-file: | 92 | ./test/docker-compose.yml 93 | ./test/docker-compose.ci.yml 94 | services: | 95 | service-b 96 | service-d 97 | 98 | - name: "Assert: only expected services are running" 99 | run: | 100 | docker compose -f ./test/docker-compose.yml -f ./test/docker-compose.ci.yml ps 101 | 102 | docker compose -f ./test/docker-compose.yml -f ./test/docker-compose.ci.yml ps | grep test-service-b-1 || (echo "Service service-b is not running" && exit 1) 103 | docker compose -f ./test/docker-compose.yml -f ./test/docker-compose.ci.yml ps | grep test-service-d-1 || (echo "Service service-d is not running" && exit 1) 104 | (docker compose -f ./test/docker-compose.yml -f ./test/docker-compose.ci.yml ps | grep test-service-a-1 && echo "Unexpected service service-a is running" && exit 1) || true 105 | (docker compose -f ./test/docker-compose.yml -f ./test/docker-compose.ci.yml ps | grep test-service-c-1 && echo "Unexpected service service-c is running" && exit 1) || true 106 | 107 | test-action-with-cwd: 108 | runs-on: ubuntu-latest 109 | name: Test with cwd 110 | steps: 111 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 112 | 113 | - name: Act 114 | uses: ./ 115 | with: 116 | compose-file: "docker-compose.yml" 117 | cwd: "./test" 118 | services: | 119 | service-b 120 | service-c 121 | 122 | - name: "Assert: only expected services are running" 123 | run: | 124 | docker compose -f ./test/docker-compose.yml ps 125 | 126 | docker compose -f ./test/docker-compose.yml ps | grep test-service-b-1 || (echo "Service service-b is not running" && exit 1) 127 | docker compose -f ./test/docker-compose.yml ps | grep test-service-c-1 || (echo "Service service-c is not running" && exit 1) 128 | (docker compose -f ./test/docker-compose.yml ps | grep test-service-a-1 && echo "Unexpected service service-a is running" && exit 1) || true 129 | 130 | test-action-with-absolute-path: 131 | runs-on: ubuntu-latest 132 | name: Test with absolute path 133 | steps: 134 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 135 | 136 | - name: Act 137 | uses: ./ 138 | with: 139 | compose-file: "${{ github.workspace }}/test/docker-compose.yml" 140 | services: | 141 | service-b 142 | service-c 143 | 144 | - name: "Assert: only expected services are running" 145 | run: | 146 | docker compose -f ./test/docker-compose.yml ps 147 | 148 | docker compose -f ./test/docker-compose.yml ps | grep test-service-b-1 || (echo "Service service-b is not running" && exit 1) 149 | docker compose -f ./test/docker-compose.yml ps | grep test-service-c-1 || (echo "Service service-c is not running" && exit 1) 150 | (docker compose -f ./test/docker-compose.yml ps | grep test-service-a-1 && echo "Unexpected service service-a is running" && exit 1) || true 151 | 152 | test-abort-on-container-exit: 153 | runs-on: ubuntu-latest 154 | name: Test with --abort-on-container-exit 155 | steps: 156 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 157 | 158 | - name: Act 159 | uses: ./ 160 | with: 161 | compose-file: "test/docker-compose-web-mysql.yml" 162 | up-flags: "--build --abort-on-container-exit --exit-code-from=web" 163 | 164 | test-attach-dependencies-failure: 165 | runs-on: ubuntu-latest 166 | name: Test with --attach-dependencies and service failure 167 | steps: 168 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 169 | 170 | - name: Act 171 | uses: ./ 172 | with: 173 | compose-file: "test/docker-compose-fail.yml" 174 | up-flags: "--attach-dependencies" 175 | 176 | - name: Assert 177 | run: | 178 | EXIT_CODE=$(docker compose -f ./test/docker-compose-fail.yml ps service-a --all --format json | jq ".ExitCode") 179 | [ "$EXIT_CODE" == "1" ] || (echo "Service service-a did not exit with code 1" && exit 1) 180 | 181 | test-action-with-compose-version: 182 | runs-on: ubuntu-latest 183 | name: Test with compose version 184 | env: 185 | DOCKER_COMPOSE_VERSION: "2.29.0" 186 | steps: 187 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 188 | 189 | - name: "Arrange: ensure original docker compose version is not the expected one" 190 | run: | 191 | CURRENT_DOCKER_COMPOSE_VERSION=$(docker compose version --short) 192 | echo "Current docker compose version: $CURRENT_DOCKER_COMPOSE_VERSION" 193 | 194 | if [ "$CURRENT_DOCKER_COMPOSE_VERSION" == "$DOCKER_COMPOSE_VERSION" ]; then 195 | echo "Docker compose version is already in $DOCKER_COMPOSE_VERSION version" 196 | exit 1 197 | fi 198 | 199 | - name: Act 200 | uses: ./ 201 | with: 202 | compose-file: "./test/docker-compose.yml" 203 | compose-version: "2.29.0" 204 | 205 | - name: "Assert: compose version is used" 206 | run: | 207 | CURRENT_DOCKER_COMPOSE_VERSION=$(docker compose version --short) 208 | echo "Current docker compose version: $CURRENT_DOCKER_COMPOSE_VERSION" 209 | 210 | if [ "$CURRENT_DOCKER_COMPOSE_VERSION" != "$DOCKER_COMPOSE_VERSION" ]; then 211 | echo "Docker compose version is not in $DOCKER_COMPOSE_VERSION version" 212 | exit 1 213 | fi 214 | 215 | test-action-with-compose-version-latest: 216 | runs-on: ubuntu-latest 217 | name: Test with compose version latest 218 | steps: 219 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 220 | 221 | - name: "Arrange: retrieve latest version of docker compose" 222 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 223 | with: 224 | script: | 225 | const dockerComposeVersion = (await github.rest.repos.getLatestRelease({ 226 | owner: "docker", 227 | repo: "compose", 228 | })).data.tag_name.replace("v", ""); 229 | 230 | core.exportVariable('DOCKER_COMPOSE_VERSION', dockerComposeVersion); 231 | 232 | - name: "Arrange: ensure original docker compose version is not the expected one" 233 | run: | 234 | CURRENT_DOCKER_COMPOSE_VERSION=$(docker compose version --short) 235 | echo "Current docker compose version: $CURRENT_DOCKER_COMPOSE_VERSION" 236 | 237 | if [ "$CURRENT_DOCKER_COMPOSE_VERSION" == "$DOCKER_COMPOSE_VERSION" ]; then 238 | echo "Docker compose version is already in $DOCKER_COMPOSE_VERSION version" 239 | exit 1 240 | fi 241 | 242 | - name: Act 243 | uses: ./ 244 | with: 245 | compose-file: "./test/docker-compose.yml" 246 | compose-version: "latest" 247 | 248 | - name: "Assert: compose version is used" 249 | run: | 250 | CURRENT_DOCKER_COMPOSE_VERSION=$(docker compose version --short) 251 | echo "Current docker compose version: $CURRENT_DOCKER_COMPOSE_VERSION" 252 | 253 | if [ "$CURRENT_DOCKER_COMPOSE_VERSION" != "$DOCKER_COMPOSE_VERSION" ]; then 254 | echo "Docker compose version is not in $DOCKER_COMPOSE_VERSION version" 255 | exit 1 256 | fi 257 | 258 | test-action-with-docker-context: 259 | runs-on: ubuntu-latest 260 | name: Test with docker context 261 | steps: 262 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 263 | 264 | - name: Set up Docker 265 | uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0 266 | with: 267 | context: test-context 268 | 269 | - name: Act 270 | uses: ./ 271 | with: 272 | docker-flags: "--context test-context" 273 | compose-file: "./test/docker-compose.yml" 274 | compose-version: "latest" 275 | -------------------------------------------------------------------------------- /.github/workflows/__check-dist.yml: -------------------------------------------------------------------------------- 1 | name: Internal - Checks for dist 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | check-dist: 11 | name: Check dist 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - uses: hoverkraft-tech/ci-github-nodejs/actions/setup-node@77c905a25700b1ca630037812b5df42d2d7c40ae # 0.12.0 16 | 17 | - name: Build dist/ Directory 18 | id: package 19 | run: npm run package 20 | 21 | # This will fail the workflow if the PR wasn't created by Dependabot. 22 | - name: Compare Directories 23 | id: diff 24 | run: | 25 | if [ "$(git diff --ignore-space-at-eol --text dist/ | wc -l)" -gt "0" ]; then 26 | echo "Detected uncommitted changes after package. See status below:" 27 | git diff --ignore-space-at-eol --text dist/ 28 | exit 1 29 | fi 30 | 31 | # If `dist/` was different than expected, and this was not a Dependabot 32 | # PR, upload the expected version as a workflow artifact. 33 | - if: ${{ failure() && steps.diff.outcome == 'failure' }} 34 | name: Upload Artifact 35 | id: upload 36 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 37 | with: 38 | name: dist 39 | path: dist/ 40 | -------------------------------------------------------------------------------- /.github/workflows/__check-nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Internal - Checks for nodejs 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | security-events: write 9 | id-token: write 10 | 11 | jobs: 12 | test-nodejs: 13 | uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@77c905a25700b1ca630037812b5df42d2d7c40ae # 0.12.0 14 | permissions: 15 | id-token: write 16 | security-events: write 17 | contents: read 18 | with: 19 | build: "" 20 | -------------------------------------------------------------------------------- /.github/workflows/__shared-ci.yml: -------------------------------------------------------------------------------- 1 | name: Common Continuous Integration tasks 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | packages: read 10 | security-events: write 11 | statuses: write 12 | id-token: write 13 | 14 | jobs: 15 | linter: 16 | uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 17 | with: 18 | linter-env: | 19 | FILTER_REGEX_EXCLUDE=dist/**/* 20 | VALIDATE_JSCPD=false 21 | VALIDATE_TYPESCRIPT_STANDARD=false 22 | VALIDATE_TYPESCRIPT_ES=false 23 | VALIDATE_TYPESCRIPT_PRETTIER=false 24 | VALIDATE_JAVASCRIPT_ES=false 25 | VALIDATE_JAVASCRIPT_STANDARD=false 26 | 27 | check-nodejs: 28 | name: Test nodejs 29 | needs: linter 30 | uses: ./.github/workflows/__check-nodejs.yml 31 | secrets: inherit 32 | 33 | check-dist: 34 | name: Test nodejs 35 | needs: linter 36 | uses: ./.github/workflows/__check-dist.yml 37 | 38 | check-action: 39 | name: Test action 40 | needs: [check-nodejs, check-dist] 41 | uses: ./.github/workflows/__check-action.yml 42 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | greetings: 16 | uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 17 | -------------------------------------------------------------------------------- /.github/workflows/main-ci.yml: -------------------------------------------------------------------------------- 1 | name: Internal - Main - Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["*"] 7 | 8 | workflow_dispatch: 9 | 10 | schedule: 11 | - cron: "25 8 * * 1" 12 | 13 | permissions: 14 | actions: read 15 | contents: read 16 | packages: read 17 | security-events: write 18 | statuses: write 19 | # FIXME: This is a workaround for having workflow ref. See https://github.com/orgs/community/discussions/38659 20 | id-token: write 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | ci: 28 | uses: ./.github/workflows/__shared-ci.yml 29 | secrets: inherit 30 | 31 | release: 32 | needs: ci 33 | if: github.event_name != 'schedule' 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | - uses: bitflight-devops/github-action-readme-generator@f750ff0ac8a4b68a3c2d622cc50a5ad20bcebaa1 # v1.8.0 38 | with: 39 | owner: ${{ github.repository_owner }} 40 | repo: ${{ github.event.repository.name }} 41 | 42 | - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 43 | id: generate-token 44 | with: 45 | app-id: ${{ vars.CI_BOT_APP_ID }} 46 | private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} 47 | 48 | - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 49 | with: 50 | github-token: ${{ steps.generate-token.outputs.token }} 51 | branch: docs/actions-workflows-documentation-update 52 | title: "docs: update actions and workflows documentation" 53 | body: Update actions and workflows documentation 54 | commit-message: | 55 | docs: update actions and workflows documentation 56 | 57 | [skip ci] 58 | -------------------------------------------------------------------------------- /.github/workflows/need-fix-to-issue.yml: -------------------------------------------------------------------------------- 1 | name: Need fix to Issue 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | #checkov:skip=CKV_GHA_7: required 10 | manual-commit-ref: 11 | description: "The SHA of the commit to get the diff for" 12 | required: true 13 | manual-base-ref: 14 | description: "By default, the commit entered above is compared to the one directly 15 | before it; to go back further, enter an earlier SHA here" 16 | required: false 17 | 18 | permissions: 19 | contents: read 20 | issues: write 21 | 22 | jobs: 23 | main: 24 | uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 25 | with: 26 | manual-commit-ref: ${{ inputs.manual-commit-ref }} 27 | manual-base-ref: ${{ inputs.manual-base-ref }} 28 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-ci.yml: -------------------------------------------------------------------------------- 1 | name: Pull request - Continuous Integration 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | branches: [main] 7 | 8 | permissions: 9 | actions: read 10 | contents: read 11 | packages: read 12 | statuses: write 13 | security-events: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | ci: 22 | uses: ./.github/workflows/__shared-ci.yml 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /.github/workflows/release-new-action-version.yml: -------------------------------------------------------------------------------- 1 | name: Release new action version 2 | 3 | on: 4 | release: 5 | types: [released] 6 | workflow_dispatch: 7 | inputs: 8 | #checkov:skip=CKV_GHA_7: required 9 | TAG_NAME: 10 | description: "Tag name that the major tag will point to" 11 | required: true 12 | 13 | env: 14 | TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | update_tag: 21 | name: Update the major tag to include the ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} changes 22 | environment: 23 | name: releaseNewActionVersion 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Update the ${{ env.TAG_NAME }} tag 27 | uses: actions/publish-action@f784495ce78a41bac4ed7e34a73f0034015764bb # v0.3.0 28 | with: 29 | source-tag: ${{ env.TAG_NAME }} 30 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request - Semantic Lint" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | main: 16 | uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 17 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | main: 13 | uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@95664be4ec235bfc221c4356c7153cbab3fb8f93 # 0.22.3 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | 100 | # IDE files 101 | .idea 102 | .vscode 103 | *.code-workspace 104 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at 128 | [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Ensure that the changes are well tested and continuous integration checks are succeeded. 15 | 4. Ensure the build assets have been updated. 16 | 5. You may merge the Pull Request in once you have the sign-off of one maintainer, or if you 17 | do not have permission to do that, you may request the reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | - Using welcoming and inclusive language 36 | - Being respectful of differing viewpoints and experiences 37 | - Gracefully accepting constructive criticism 38 | - Focusing on what is best for the community 39 | - Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | - The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | - Trolling, insulting/derogatory comments, and personal or political attacks 46 | - Public or private harassment 47 | - Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | - Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project email 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #checkov:skip=CKV_DOCKER_2: required 2 | FROM ghcr.io/super-linter/super-linter:slim-v7 3 | 4 | ARG UID=1000 5 | ARG GID=1000 6 | RUN chown -R ${UID}:${GID} /github/home 7 | USER ${UID}:${GID} 8 | 9 | ENV RUN_LOCAL=true 10 | ENV USE_FIND_ALGORITHM=true 11 | ENV LOG_LEVEL=WARN 12 | ENV LOG_FILE="../logs" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Hoverkraft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | 3 | help: ## Display help 4 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | lint: ## Execute linting 7 | $(call run_linter,) 8 | 9 | lint-fix: ## Execute linting and fix 10 | $(call run_linter, \ 11 | -e FIX_JSON_PRETTIER=true \ 12 | -e FIX_YAML_PRETTIER=true \ 13 | -e FIX_MARKDOWN=true \ 14 | -e FIX_MARKDOWN_PRETTIER=true \ 15 | -e FIX_NATURAL_LANGUAGE=true) 16 | 17 | all: ## Execute all formats and checks 18 | @npm run all 19 | $(MAKE) lint-fix 20 | 21 | define run_linter 22 | DEFAULT_WORKSPACE="$(CURDIR)"; \ 23 | LINTER_IMAGE="linter:latest"; \ 24 | VOLUME="$$DEFAULT_WORKSPACE:$$DEFAULT_WORKSPACE"; \ 25 | docker build --build-arg UID=$(shell id -u) --build-arg GID=$(shell id -g) --tag $$LINTER_IMAGE .; \ 26 | docker run \ 27 | -e DEFAULT_WORKSPACE="$$DEFAULT_WORKSPACE" \ 28 | -e FILTER_REGEX_INCLUDE="$(filter-out $@,$(MAKECMDGOALS))" \ 29 | -e IGNORE_GITIGNORED_FILES=true \ 30 | -e KUBERNETES_KUBECONFORM_OPTIONS="--schema-location default --schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'" \ 31 | -e FILTER_REGEX_EXCLUDE=dist/**/* \ 32 | -e VALIDATE_TYPESCRIPT_STANDARD=false \ 33 | -e VALIDATE_TYPESCRIPT_ES=false \ 34 | -e VALIDATE_TYPESCRIPT_PRETTIER=false \ 35 | -e VALIDATE_JAVASCRIPT_ES=false \ 36 | -e VALIDATE_JAVASCRIPT_STANDARD=false \ 37 | $(1) \ 38 | -v $$VOLUME \ 39 | --rm \ 40 | $$LINTER_IMAGE 41 | endef 42 | 43 | ############################# 44 | # Argument fix workaround 45 | ############################# 46 | %: 47 | @: -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | branding<icon:anchor color:gray-dark> 6 | 7 | 8 | 9 | 10 | # branding<icon:anchor color:gray-dark> GitHub Action: Docker Compose Action 11 | 12 | 13 | 14 | 15 | 16 | Release%20by%20tagRelease%20by%20dateCommitOpen%20IssuesDownloads 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | This action runs your compose file(s) and clean up before action finished 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Usage 32 | 33 | ### Action 34 | 35 | The action will run `docker compose up` to start the services defined in the given compose file(s). 36 | The compose file(s) can be specified using the `compose-file` input. 37 | Some extra options can be passed to the `docker compose up` command using the `up-flags` input. 38 | 39 | ### Post hook 40 | 41 | On post hook, the action will run `docker compose down` to clean up the services. 42 | In [debug mode](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/troubleshooting-workflows/enabling-debug-logging), the logs of the running services are printed before the cleanup. 43 | 44 | Some extra options can be passed to the `docker compose down` command using the `down-flags` input. 45 | 46 | 47 | 48 | ```yaml 49 | - uses: hoverkraft-tech/compose-action@v0.0.0 50 | with: 51 | # Description: Additional options to pass to `docker` command. 52 | # 53 | docker-flags: "" 54 | 55 | # Description: Path to compose file(s). It can be a list of files. It can be 56 | # absolute or relative to the current working directory (cwd). 57 | # 58 | # Default: ./docker-compose.yml 59 | compose-file: "" 60 | 61 | # Description: Services to perform docker compose up. 62 | # 63 | services: "" 64 | 65 | # Description: Additional options to pass to `docker compose up` command. 66 | # 67 | # Default: 68 | up-flags: "" 69 | 70 | # Description: Additional options to pass to `docker compose down` command. 71 | # 72 | # Default: 73 | down-flags: "" 74 | 75 | # Description: Additional options to pass to `docker compose` command. 76 | # 77 | # Default: 78 | compose-flags: "" 79 | 80 | # Description: Current working directory 81 | # 82 | # Default: ${{ github.workspace }} 83 | cwd: "" 84 | 85 | # Description: Compose version to use. If null (default), it will use the current 86 | # installed version. If "latest", it will install the latest version. 87 | # 88 | compose-version: "" 89 | 90 | # Description: The GitHub token used to create an authenticated client (to fetch 91 | # the latest version of docker compose). 92 | # 93 | # Default: ${{ github.token }} 94 | github-token: "" 95 | ``` 96 | 97 | 98 | 99 | ## Inputs 100 | 101 | 102 | 103 | | **Input** | **Description** | **Default** | **Required** | 104 | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ------------ | 105 | | docker-flags | Additional options to pass to docker command. | | **false** | 106 | | 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). | ./docker-compose.yml | **false** | 107 | | services | Services to perform docker compose up. | | **false** | 108 | | up-flags | Additional options to pass to docker compose up command. | | **false** | 109 | | down-flags | Additional options to pass to docker compose down command. | | **false** | 110 | | compose-flags | Additional options to pass to docker compose command. | | **false** | 111 | | cwd | Current working directory | ${{ github.workspace }} | **false** | 112 | | compose-version | Compose version to use.
If null (default), it will use the current installed version.
If "latest", it will install the latest version. | | **false** | 113 | | github-token | The GitHub token used to create an authenticated client (to fetch the latest version of docker compose). | ${{ github.token }} | **false** | 114 | 115 | 116 | 117 | 118 | 119 | ## Examples 120 | 121 | ### Example using in a full workflow 122 | 123 | ```yaml 124 | name: Docker Compose Action 125 | 126 | on: [push] 127 | 128 | jobs: 129 | build: 130 | runs-on: ubuntu-latest 131 | 132 | steps: 133 | - uses: actions/checkout@v4.2.2 134 | 135 | - name: Run docker compose 136 | uses: hoverkraft-tech/compose-action@v2.0.1 137 | with: 138 | compose-file: "./docker/docker-compose.yml" 139 | 140 | - name: Execute tests in the running services 141 | run: | 142 | docker compose exec test-app pytest 143 | ``` 144 | 145 | 146 | 147 | 148 | ### Example Using environment variables 149 | 150 | ```yaml 151 | steps: 152 | - uses: actions/checkout@v4.2.2 153 | - uses: hoverkraft-tech/compose-action@v2.0.1 154 | with: 155 | compose-file: "./docker/docker-compose.yml" 156 | env: 157 | CUSTOM_VARIABLE: "test" 158 | ``` 159 | 160 | ### Example using `services` 161 | 162 | Perform `docker compose up` to some given service instead of all of them 163 | 164 | ```yaml 165 | steps: 166 | # need checkout before using compose-action 167 | - uses: actions/checkout@v3 168 | - uses: hoverkraft-tech/compose-action@v2.0.1 169 | with: 170 | compose-file: "./docker/docker-compose.yml" 171 | services: | 172 | helloworld2 173 | helloworld3 174 | ``` 175 | 176 | ### Example using `up-flags` 177 | 178 | Specify flags to pass to the `docker compose up`. Default is none. Can be used 179 | to pass the `--build` flag, for example, if you want persistent volumes to be 180 | deleted as well during cleanup. A full list of flags can be found in the 181 | [docker compose up documentation](https://docs.docker.com/compose/reference/up/). 182 | 183 | ### Example using `down-flags` 184 | 185 | Specify flags to pass to the `docker compose down` command during cleanup. 186 | Default is none. Can be used to pass the `--volumes` flag, for example, if you 187 | want persistent volumes to be deleted as well during cleanup. A full list of 188 | flags can be found in the 189 | [docker compose down documentation](https://docs.docker.com/compose/reference/down/). 190 | 191 | ### Example using `compose-flags` 192 | 193 | Specify flags to pass to the `docker compose` command. Default is none. A full 194 | list of flags can be found in the 195 | [docker compose documentation](https://docs.docker.com/compose/reference/#command-options-overview-and-help). 196 | 197 | ```yaml 198 | steps: 199 | # need checkout before using compose-action 200 | - uses: actions/checkout@v3 201 | - uses: hoverkraft-tech/compose-action@v2.0.1 202 | with: 203 | compose-file: "./docker/docker-compose.yml" 204 | services: | 205 | helloworld2 206 | helloworld3 207 | ``` 208 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Docker Compose Action" 2 | description: "This action runs your compose file(s) and clean up before action finished" 3 | author: "Hoverkraft" 4 | branding: 5 | icon: anchor 6 | color: gray-dark 7 | 8 | inputs: 9 | docker-flags: 10 | description: "Additional options to pass to `docker` command." 11 | required: false 12 | compose-file: 13 | 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)." 14 | required: false 15 | default: "./docker-compose.yml" 16 | services: 17 | description: "Services to perform docker compose up." 18 | required: false 19 | up-flags: 20 | description: "Additional options to pass to `docker compose up` command." 21 | required: false 22 | default: "" 23 | down-flags: 24 | description: "Additional options to pass to `docker compose down` command." 25 | required: false 26 | default: "" 27 | compose-flags: 28 | description: "Additional options to pass to `docker compose` command." 29 | required: false 30 | default: "" 31 | cwd: 32 | description: "Current working directory" 33 | required: false 34 | default: ${{ github.workspace }} 35 | compose-version: 36 | description: | 37 | Compose version to use. 38 | If null (default), it will use the current installed version. 39 | If "latest", it will install the latest version. 40 | required: false 41 | github-token: 42 | description: The GitHub token used to create an authenticated client (to fetch the latest version of docker compose). 43 | default: ${{ github.token }} 44 | required: false 45 | 46 | runs: 47 | using: node20 48 | main: dist/index.js 49 | post: dist/post.js 50 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/github 26 | MIT 27 | The MIT License (MIT) 28 | 29 | Copyright 2019 GitHub 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | 37 | @actions/http-client 38 | MIT 39 | Actions Http Client for Node.js 40 | 41 | Copyright (c) GitHub, Inc. 42 | 43 | All rights reserved. 44 | 45 | MIT License 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 48 | associated documentation files (the "Software"), to deal in the Software without restriction, 49 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 50 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 51 | subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 56 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 57 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 58 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 59 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 60 | 61 | 62 | @actions/io 63 | MIT 64 | The MIT License (MIT) 65 | 66 | Copyright 2019 GitHub 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 73 | 74 | @actions/tool-cache 75 | MIT 76 | The MIT License (MIT) 77 | 78 | Copyright 2019 GitHub 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 81 | 82 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 83 | 84 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 85 | 86 | @fastify/busboy 87 | MIT 88 | Copyright Brian White. All rights reserved. 89 | 90 | Permission is hereby granted, free of charge, to any person obtaining a copy 91 | of this software and associated documentation files (the "Software"), to 92 | deal in the Software without restriction, including without limitation the 93 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 94 | sell copies of the Software, and to permit persons to whom the Software is 95 | furnished to do so, subject to the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included in 98 | all copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 101 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 102 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 103 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 104 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 105 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 106 | IN THE SOFTWARE. 107 | 108 | @octokit/auth-token 109 | MIT 110 | The MIT License 111 | 112 | Copyright (c) 2019 Octokit contributors 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining a copy 115 | of this software and associated documentation files (the "Software"), to deal 116 | in the Software without restriction, including without limitation the rights 117 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 118 | copies of the Software, and to permit persons to whom the Software is 119 | furnished to do so, subject to the following conditions: 120 | 121 | The above copyright notice and this permission notice shall be included in 122 | all copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 130 | THE SOFTWARE. 131 | 132 | 133 | @octokit/core 134 | MIT 135 | The MIT License 136 | 137 | Copyright (c) 2019 Octokit contributors 138 | 139 | Permission is hereby granted, free of charge, to any person obtaining a copy 140 | of this software and associated documentation files (the "Software"), to deal 141 | in the Software without restriction, including without limitation the rights 142 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 143 | copies of the Software, and to permit persons to whom the Software is 144 | furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in 147 | all copies or substantial portions of the Software. 148 | 149 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 150 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 151 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 152 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 153 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 154 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 155 | THE SOFTWARE. 156 | 157 | 158 | @octokit/endpoint 159 | MIT 160 | The MIT License 161 | 162 | Copyright (c) 2018 Octokit contributors 163 | 164 | Permission is hereby granted, free of charge, to any person obtaining a copy 165 | of this software and associated documentation files (the "Software"), to deal 166 | in the Software without restriction, including without limitation the rights 167 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 168 | copies of the Software, and to permit persons to whom the Software is 169 | furnished to do so, subject to the following conditions: 170 | 171 | The above copyright notice and this permission notice shall be included in 172 | all copies or substantial portions of the Software. 173 | 174 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 175 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 176 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 177 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 178 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 179 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 180 | THE SOFTWARE. 181 | 182 | 183 | @octokit/graphql 184 | MIT 185 | The MIT License 186 | 187 | Copyright (c) 2018 Octokit contributors 188 | 189 | Permission is hereby granted, free of charge, to any person obtaining a copy 190 | of this software and associated documentation files (the "Software"), to deal 191 | in the Software without restriction, including without limitation the rights 192 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 193 | copies of the Software, and to permit persons to whom the Software is 194 | furnished to do so, subject to the following conditions: 195 | 196 | The above copyright notice and this permission notice shall be included in 197 | all copies or substantial portions of the Software. 198 | 199 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 200 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 201 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 202 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 203 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 204 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 205 | THE SOFTWARE. 206 | 207 | 208 | @octokit/plugin-paginate-rest 209 | MIT 210 | MIT License Copyright (c) 2019 Octokit contributors 211 | 212 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 213 | 214 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 217 | 218 | 219 | @octokit/plugin-rest-endpoint-methods 220 | MIT 221 | MIT License Copyright (c) 2019 Octokit contributors 222 | 223 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 224 | 225 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 226 | 227 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 228 | 229 | 230 | @octokit/request 231 | MIT 232 | The MIT License 233 | 234 | Copyright (c) 2018 Octokit contributors 235 | 236 | Permission is hereby granted, free of charge, to any person obtaining a copy 237 | of this software and associated documentation files (the "Software"), to deal 238 | in the Software without restriction, including without limitation the rights 239 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 240 | copies of the Software, and to permit persons to whom the Software is 241 | furnished to do so, subject to the following conditions: 242 | 243 | The above copyright notice and this permission notice shall be included in 244 | all copies or substantial portions of the Software. 245 | 246 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 247 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 248 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 249 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 250 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 251 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 252 | THE SOFTWARE. 253 | 254 | 255 | @octokit/request-error 256 | MIT 257 | The MIT License 258 | 259 | Copyright (c) 2019 Octokit contributors 260 | 261 | Permission is hereby granted, free of charge, to any person obtaining a copy 262 | of this software and associated documentation files (the "Software"), to deal 263 | in the Software without restriction, including without limitation the rights 264 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 265 | copies of the Software, and to permit persons to whom the Software is 266 | furnished to do so, subject to the following conditions: 267 | 268 | The above copyright notice and this permission notice shall be included in 269 | all copies or substantial portions of the Software. 270 | 271 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 272 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 273 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 274 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 275 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 276 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 277 | THE SOFTWARE. 278 | 279 | 280 | before-after-hook 281 | Apache-2.0 282 | Apache License 283 | Version 2.0, January 2004 284 | http://www.apache.org/licenses/ 285 | 286 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 287 | 288 | 1. Definitions. 289 | 290 | "License" shall mean the terms and conditions for use, reproduction, 291 | and distribution as defined by Sections 1 through 9 of this document. 292 | 293 | "Licensor" shall mean the copyright owner or entity authorized by 294 | the copyright owner that is granting the License. 295 | 296 | "Legal Entity" shall mean the union of the acting entity and all 297 | other entities that control, are controlled by, or are under common 298 | control with that entity. For the purposes of this definition, 299 | "control" means (i) the power, direct or indirect, to cause the 300 | direction or management of such entity, whether by contract or 301 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 302 | outstanding shares, or (iii) beneficial ownership of such entity. 303 | 304 | "You" (or "Your") shall mean an individual or Legal Entity 305 | exercising permissions granted by this License. 306 | 307 | "Source" form shall mean the preferred form for making modifications, 308 | including but not limited to software source code, documentation 309 | source, and configuration files. 310 | 311 | "Object" form shall mean any form resulting from mechanical 312 | transformation or translation of a Source form, including but 313 | not limited to compiled object code, generated documentation, 314 | and conversions to other media types. 315 | 316 | "Work" shall mean the work of authorship, whether in Source or 317 | Object form, made available under the License, as indicated by a 318 | copyright notice that is included in or attached to the work 319 | (an example is provided in the Appendix below). 320 | 321 | "Derivative Works" shall mean any work, whether in Source or Object 322 | form, that is based on (or derived from) the Work and for which the 323 | editorial revisions, annotations, elaborations, or other modifications 324 | represent, as a whole, an original work of authorship. For the purposes 325 | of this License, Derivative Works shall not include works that remain 326 | separable from, or merely link (or bind by name) to the interfaces of, 327 | the Work and Derivative Works thereof. 328 | 329 | "Contribution" shall mean any work of authorship, including 330 | the original version of the Work and any modifications or additions 331 | to that Work or Derivative Works thereof, that is intentionally 332 | submitted to Licensor for inclusion in the Work by the copyright owner 333 | or by an individual or Legal Entity authorized to submit on behalf of 334 | the copyright owner. For the purposes of this definition, "submitted" 335 | means any form of electronic, verbal, or written communication sent 336 | to the Licensor or its representatives, including but not limited to 337 | communication on electronic mailing lists, source code control systems, 338 | and issue tracking systems that are managed by, or on behalf of, the 339 | Licensor for the purpose of discussing and improving the Work, but 340 | excluding communication that is conspicuously marked or otherwise 341 | designated in writing by the copyright owner as "Not a Contribution." 342 | 343 | "Contributor" shall mean Licensor and any individual or Legal Entity 344 | on behalf of whom a Contribution has been received by Licensor and 345 | subsequently incorporated within the Work. 346 | 347 | 2. Grant of Copyright License. Subject to the terms and conditions of 348 | this License, each Contributor hereby grants to You a perpetual, 349 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 350 | copyright license to reproduce, prepare Derivative Works of, 351 | publicly display, publicly perform, sublicense, and distribute the 352 | Work and such Derivative Works in Source or Object form. 353 | 354 | 3. Grant of Patent License. Subject to the terms and conditions of 355 | this License, each Contributor hereby grants to You a perpetual, 356 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 357 | (except as stated in this section) patent license to make, have made, 358 | use, offer to sell, sell, import, and otherwise transfer the Work, 359 | where such license applies only to those patent claims licensable 360 | by such Contributor that are necessarily infringed by their 361 | Contribution(s) alone or by combination of their Contribution(s) 362 | with the Work to which such Contribution(s) was submitted. If You 363 | institute patent litigation against any entity (including a 364 | cross-claim or counterclaim in a lawsuit) alleging that the Work 365 | or a Contribution incorporated within the Work constitutes direct 366 | or contributory patent infringement, then any patent licenses 367 | granted to You under this License for that Work shall terminate 368 | as of the date such litigation is filed. 369 | 370 | 4. Redistribution. You may reproduce and distribute copies of the 371 | Work or Derivative Works thereof in any medium, with or without 372 | modifications, and in Source or Object form, provided that You 373 | meet the following conditions: 374 | 375 | (a) You must give any other recipients of the Work or 376 | Derivative Works a copy of this License; and 377 | 378 | (b) You must cause any modified files to carry prominent notices 379 | stating that You changed the files; and 380 | 381 | (c) You must retain, in the Source form of any Derivative Works 382 | that You distribute, all copyright, patent, trademark, and 383 | attribution notices from the Source form of the Work, 384 | excluding those notices that do not pertain to any part of 385 | the Derivative Works; and 386 | 387 | (d) If the Work includes a "NOTICE" text file as part of its 388 | distribution, then any Derivative Works that You distribute must 389 | include a readable copy of the attribution notices contained 390 | within such NOTICE file, excluding those notices that do not 391 | pertain to any part of the Derivative Works, in at least one 392 | of the following places: within a NOTICE text file distributed 393 | as part of the Derivative Works; within the Source form or 394 | documentation, if provided along with the Derivative Works; or, 395 | within a display generated by the Derivative Works, if and 396 | wherever such third-party notices normally appear. The contents 397 | of the NOTICE file are for informational purposes only and 398 | do not modify the License. You may add Your own attribution 399 | notices within Derivative Works that You distribute, alongside 400 | or as an addendum to the NOTICE text from the Work, provided 401 | that such additional attribution notices cannot be construed 402 | as modifying the License. 403 | 404 | You may add Your own copyright statement to Your modifications and 405 | may provide additional or different license terms and conditions 406 | for use, reproduction, or distribution of Your modifications, or 407 | for any such Derivative Works as a whole, provided Your use, 408 | reproduction, and distribution of the Work otherwise complies with 409 | the conditions stated in this License. 410 | 411 | 5. Submission of Contributions. Unless You explicitly state otherwise, 412 | any Contribution intentionally submitted for inclusion in the Work 413 | by You to the Licensor shall be under the terms and conditions of 414 | this License, without any additional terms or conditions. 415 | Notwithstanding the above, nothing herein shall supersede or modify 416 | the terms of any separate license agreement you may have executed 417 | with Licensor regarding such Contributions. 418 | 419 | 6. Trademarks. This License does not grant permission to use the trade 420 | names, trademarks, service marks, or product names of the Licensor, 421 | except as required for reasonable and customary use in describing the 422 | origin of the Work and reproducing the content of the NOTICE file. 423 | 424 | 7. Disclaimer of Warranty. Unless required by applicable law or 425 | agreed to in writing, Licensor provides the Work (and each 426 | Contributor provides its Contributions) on an "AS IS" BASIS, 427 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 428 | implied, including, without limitation, any warranties or conditions 429 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 430 | PARTICULAR PURPOSE. You are solely responsible for determining the 431 | appropriateness of using or redistributing the Work and assume any 432 | risks associated with Your exercise of permissions under this License. 433 | 434 | 8. Limitation of Liability. In no event and under no legal theory, 435 | whether in tort (including negligence), contract, or otherwise, 436 | unless required by applicable law (such as deliberate and grossly 437 | negligent acts) or agreed to in writing, shall any Contributor be 438 | liable to You for damages, including any direct, indirect, special, 439 | incidental, or consequential damages of any character arising as a 440 | result of this License or out of the use or inability to use the 441 | Work (including but not limited to damages for loss of goodwill, 442 | work stoppage, computer failure or malfunction, or any and all 443 | other commercial damages or losses), even if such Contributor 444 | has been advised of the possibility of such damages. 445 | 446 | 9. Accepting Warranty or Additional Liability. While redistributing 447 | the Work or Derivative Works thereof, You may choose to offer, 448 | and charge a fee for, acceptance of support, warranty, indemnity, 449 | or other liability obligations and/or rights consistent with this 450 | License. However, in accepting such obligations, You may act only 451 | on Your own behalf and on Your sole responsibility, not on behalf 452 | of any other Contributor, and only if You agree to indemnify, 453 | defend, and hold each Contributor harmless for any liability 454 | incurred by, or claims asserted against, such Contributor by reason 455 | of your accepting any such warranty or additional liability. 456 | 457 | END OF TERMS AND CONDITIONS 458 | 459 | APPENDIX: How to apply the Apache License to your work. 460 | 461 | To apply the Apache License to your work, attach the following 462 | boilerplate notice, with the fields enclosed by brackets "{}" 463 | replaced with your own identifying information. (Don't include 464 | the brackets!) The text should be enclosed in the appropriate 465 | comment syntax for the file format. We also recommend that a 466 | file or class name and description of purpose be included on the 467 | same "printed page" as the copyright notice for easier 468 | identification within third-party archives. 469 | 470 | Copyright 2018 Gregor Martynus and other contributors. 471 | 472 | Licensed under the Apache License, Version 2.0 (the "License"); 473 | you may not use this file except in compliance with the License. 474 | You may obtain a copy of the License at 475 | 476 | http://www.apache.org/licenses/LICENSE-2.0 477 | 478 | Unless required by applicable law or agreed to in writing, software 479 | distributed under the License is distributed on an "AS IS" BASIS, 480 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 481 | See the License for the specific language governing permissions and 482 | limitations under the License. 483 | 484 | 485 | deprecation 486 | ISC 487 | The ISC License 488 | 489 | Copyright (c) Gregor Martynus and contributors 490 | 491 | Permission to use, copy, modify, and/or distribute this software for any 492 | purpose with or without fee is hereby granted, provided that the above 493 | copyright notice and this permission notice appear in all copies. 494 | 495 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 496 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 497 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 498 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 499 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 500 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 501 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 502 | 503 | 504 | docker-compose 505 | MIT 506 | MIT License 507 | 508 | Copyright (c) 2017 - 2021 PDMLab 509 | 510 | Permission is hereby granted, free of charge, to any person obtaining a copy 511 | of this software and associated documentation files (the "Software"), to deal 512 | in the Software without restriction, including without limitation the rights 513 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 514 | copies of the Software, and to permit persons to whom the Software is 515 | furnished to do so, subject to the following conditions: 516 | 517 | The above copyright notice and this permission notice shall be included in all 518 | copies or substantial portions of the Software. 519 | 520 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 521 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 522 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 523 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 524 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 525 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 526 | SOFTWARE. 527 | 528 | 529 | once 530 | ISC 531 | The ISC License 532 | 533 | Copyright (c) Isaac Z. Schlueter and Contributors 534 | 535 | Permission to use, copy, modify, and/or distribute this software for any 536 | purpose with or without fee is hereby granted, provided that the above 537 | copyright notice and this permission notice appear in all copies. 538 | 539 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 540 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 541 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 542 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 543 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 544 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 545 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 546 | 547 | 548 | semver 549 | ISC 550 | The ISC License 551 | 552 | Copyright (c) Isaac Z. Schlueter and Contributors 553 | 554 | Permission to use, copy, modify, and/or distribute this software for any 555 | purpose with or without fee is hereby granted, provided that the above 556 | copyright notice and this permission notice appear in all copies. 557 | 558 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 559 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 560 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 561 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 562 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 563 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 564 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 565 | 566 | 567 | tunnel 568 | MIT 569 | The MIT License (MIT) 570 | 571 | Copyright (c) 2012 Koichi Kobayashi 572 | 573 | Permission is hereby granted, free of charge, to any person obtaining a copy 574 | of this software and associated documentation files (the "Software"), to deal 575 | in the Software without restriction, including without limitation the rights 576 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 577 | copies of the Software, and to permit persons to whom the Software is 578 | furnished to do so, subject to the following conditions: 579 | 580 | The above copyright notice and this permission notice shall be included in 581 | all copies or substantial portions of the Software. 582 | 583 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 584 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 585 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 586 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 587 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 588 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 589 | THE SOFTWARE. 590 | 591 | 592 | undici 593 | MIT 594 | MIT License 595 | 596 | Copyright (c) Matteo Collina and Undici contributors 597 | 598 | Permission is hereby granted, free of charge, to any person obtaining a copy 599 | of this software and associated documentation files (the "Software"), to deal 600 | in the Software without restriction, including without limitation the rights 601 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 602 | copies of the Software, and to permit persons to whom the Software is 603 | furnished to do so, subject to the following conditions: 604 | 605 | The above copyright notice and this permission notice shall be included in all 606 | copies or substantial portions of the Software. 607 | 608 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 609 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 610 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 611 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 612 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 613 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 614 | SOFTWARE. 615 | 616 | 617 | universal-user-agent 618 | ISC 619 | # [ISC License](https://spdx.org/licenses/ISC) 620 | 621 | Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m) 622 | 623 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 624 | 625 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 626 | 627 | 628 | wrappy 629 | ISC 630 | The ISC License 631 | 632 | Copyright (c) Isaac Z. Schlueter and Contributors 633 | 634 | Permission to use, copy, modify, and/or distribute this software for any 635 | purpose with or without fee is hereby granted, provided that the above 636 | copyright notice and this permission notice appear in all copies. 637 | 638 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 639 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 640 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 641 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 642 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 643 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 644 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 645 | 646 | 647 | yaml 648 | ISC 649 | Copyright Eemeli Aro 650 | 651 | Permission to use, copy, modify, and/or distribute this software for any purpose 652 | with or without fee is hereby granted, provided that the above copyright notice 653 | and this permission notice appear in all copies. 654 | 655 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 656 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 657 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 658 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 659 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 660 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 661 | THIS SOFTWARE. 662 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compose-action", 3 | "description": "Docker Compose Action", 4 | "version": "0.0.0", 5 | "author": "", 6 | "private": true, 7 | "homepage": "https://github.com/hoverkraft-tech/compose-action", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hoverkraft-tech/compose-action.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/hoverkraft-tech/compose-action/issues" 14 | }, 15 | "keywords": [ 16 | "actions", 17 | "docker-compose" 18 | ], 19 | "exports": { 20 | ".": "./dist/index.js" 21 | }, 22 | "engines": { 23 | "node": ">=20" 24 | }, 25 | "scripts": { 26 | "package": "npm run package:index && npm run package:post", 27 | "package:index": "ncc build src/index.ts -o dist --license licenses.txt", 28 | "package:post": "ncc build src/post.ts -o dist/post && mv dist/post/index.js dist/post.js && rm -rf dist/post", 29 | "package:watch": "npm run package -- --watch", 30 | "lint": "eslint \"src/**/*.{ts,tsx}\"", 31 | "all": "npm run format && npm run lint && npm run test && npm run package", 32 | "build": "tsc --noEmit", 33 | "format": "prettier --cache --write '**/*.ts'", 34 | "jest": "jest --detectOpenHandles --forceExit", 35 | "test": "npm run jest --maxWorkers=50%", 36 | "test:watch": "npm run jest --watch --maxWorkers=25%", 37 | "test:cov": "npm run test --coverage", 38 | "test:ci": "npm run test:cov --runInBand", 39 | "prepare": "ts-dev-tools install" 40 | }, 41 | "license": "MIT", 42 | "jest": { 43 | "preset": "ts-jest", 44 | "verbose": true, 45 | "clearMocks": true, 46 | "testEnvironment": "node", 47 | "moduleFileExtensions": [ 48 | "js", 49 | "ts" 50 | ], 51 | "testMatch": [ 52 | "**/*.test.ts", 53 | "**/__tests__/**/*.[jt]s?(x)", 54 | "**/?(*.)+(spec|test)?(.*).+(ts|tsx|js)" 55 | ], 56 | "testPathIgnorePatterns": [ 57 | "/node_modules/", 58 | "/dist/" 59 | ], 60 | "transform": { 61 | "^.+\\.ts$": "ts-jest" 62 | }, 63 | "coverageReporters": [ 64 | "json-summary", 65 | "text", 66 | "lcov" 67 | ], 68 | "collectCoverage": true, 69 | "collectCoverageFrom": [ 70 | "./src/**", 71 | "**/src/**/*.[jt]s?(x)" 72 | ] 73 | }, 74 | "dependencies": { 75 | "@actions/core": "^1.11.1", 76 | "@actions/github": "^6.0.0", 77 | "@actions/tool-cache": "^2.0.2", 78 | "@octokit/action": "^8.0.1", 79 | "docker-compose": "^1.2.0" 80 | }, 81 | "devDependencies": { 82 | "@ts-dev-tools/core": "^1.6.2", 83 | "@vercel/ncc": "^0.38.3", 84 | "eslint-plugin-github": "^6.0.0", 85 | "eslint-plugin-jsonc": "^2.20.0" 86 | }, 87 | "eslintConfig": { 88 | "root": true, 89 | "parser": "@typescript-eslint/parser", 90 | "plugins": [ 91 | "@typescript-eslint", 92 | "jest" 93 | ], 94 | "extends": [ 95 | "eslint:recommended", 96 | "plugin:@typescript-eslint/recommended", 97 | "plugin:jest/recommended", 98 | "prettier" 99 | ], 100 | "env": { 101 | "es2021": true 102 | }, 103 | "parserOptions": { 104 | "ecmaFeatures": { 105 | "jsx": true 106 | }, 107 | "ecmaVersion": 12, 108 | "sourceType": "module" 109 | }, 110 | "settings": { 111 | "jest": { 112 | "version": "detect" 113 | } 114 | }, 115 | "ignorePatterns": [ 116 | "dist", 117 | "node_modules" 118 | ] 119 | }, 120 | "prettier": { 121 | "semi": true, 122 | "printWidth": 100, 123 | "trailingComma": "es5" 124 | }, 125 | "commitlint": { 126 | "extends": [ 127 | "@commitlint/config-conventional" 128 | ] 129 | }, 130 | "lint-staged": { 131 | "*.{js,ts,tsx}": [ 132 | "eslint --fix" 133 | ] 134 | }, 135 | "importSort": { 136 | ".js, .jsx, .ts, .tsx": { 137 | "style": "module", 138 | "parser": "typescript" 139 | } 140 | }, 141 | "tsDevTools": { 142 | "version": "20220617100200-prettier-cache" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/index-runner.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { InputService } from "./services/input.service"; 3 | import { LoggerService } from "./services/logger.service"; 4 | import { DockerComposeInstallerService } from "./services/docker-compose-installer.service"; 5 | import * as indexRunner from "./index-runner"; 6 | import { DockerComposeService } from "./services/docker-compose.service"; 7 | 8 | describe("run", () => { 9 | // Mock the external libraries and services used by the action 10 | let infoMock: jest.SpiedFunction; 11 | let debugMock: jest.SpiedFunction; 12 | let setFailedMock: jest.SpiedFunction; 13 | let getInputsMock: jest.SpiedFunction; 14 | let installMock: jest.SpiedFunction; 15 | let upMock: jest.SpiedFunction; 16 | 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | 20 | infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(); 21 | debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(); 22 | setFailedMock = jest.spyOn(core, "setFailed").mockImplementation(); 23 | getInputsMock = jest.spyOn(InputService.prototype, "getInputs"); 24 | installMock = jest.spyOn(DockerComposeInstallerService.prototype, "install"); 25 | upMock = jest.spyOn(DockerComposeService.prototype, "up"); 26 | }); 27 | 28 | it("should install docker compose with specified version", async () => { 29 | // Arrange 30 | getInputsMock.mockImplementation(() => ({ 31 | dockerFlags: [], 32 | composeFiles: ["docker-compose.yml"], 33 | services: [], 34 | composeFlags: [], 35 | upFlags: [], 36 | downFlags: [], 37 | cwd: "/current/working/dir", 38 | composeVersion: "1.29.2", 39 | githubToken: null, 40 | })); 41 | 42 | installMock.mockResolvedValue("1.29.2"); 43 | 44 | upMock.mockResolvedValue(); 45 | 46 | // Act 47 | await indexRunner.run(); 48 | 49 | // Assert 50 | expect(infoMock).toHaveBeenCalledWith("Setting up docker compose version 1.29.2"); 51 | 52 | expect(debugMock).toHaveBeenCalledWith( 53 | 'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":"1.29.2","githubToken":null}' 54 | ); 55 | 56 | expect(installMock).toHaveBeenCalledWith({ 57 | composeVersion: "1.29.2", 58 | cwd: "/current/working/dir", 59 | githubToken: null, 60 | }); 61 | 62 | expect(upMock).toHaveBeenCalledWith({ 63 | dockerFlags: [], 64 | composeFiles: ["docker-compose.yml"], 65 | composeFlags: [], 66 | cwd: "/current/working/dir", 67 | upFlags: [], 68 | services: [], 69 | debug: debugMock, 70 | }); 71 | 72 | expect(setFailedMock).not.toHaveBeenCalled(); 73 | }); 74 | 75 | it("should bring up docker compose services", async () => { 76 | // Arrange 77 | getInputsMock.mockImplementation(() => ({ 78 | dockerFlags: [], 79 | composeFiles: ["docker-compose.yml"], 80 | services: ["web"], 81 | composeFlags: [], 82 | upFlags: [], 83 | downFlags: [], 84 | cwd: "/current/working/dir", 85 | composeVersion: null, 86 | githubToken: null, 87 | })); 88 | 89 | // Act 90 | await indexRunner.run(); 91 | 92 | // Assert 93 | expect(upMock).toHaveBeenCalledWith({ 94 | dockerFlags: [], 95 | composeFiles: ["docker-compose.yml"], 96 | composeFlags: [], 97 | cwd: "/current/working/dir", 98 | upFlags: [], 99 | services: ["web"], 100 | debug: debugMock, 101 | }); 102 | expect(setFailedMock).not.toHaveBeenCalled(); 103 | }); 104 | 105 | it("should handle errors and call setFailed", async () => { 106 | // Arrange 107 | const error = new Error("Test error"); 108 | upMock.mockRejectedValue(error); 109 | 110 | getInputsMock.mockImplementation(() => ({ 111 | dockerFlags: [], 112 | composeFiles: ["docker-compose.yml"], 113 | services: ["web"], 114 | composeFlags: [], 115 | upFlags: [], 116 | downFlags: [], 117 | cwd: "/current/working/dir", 118 | composeVersion: null, 119 | githubToken: null, 120 | })); 121 | 122 | // Act 123 | await indexRunner.run(); 124 | 125 | // Assert 126 | expect(setFailedMock).toHaveBeenCalledWith("Error: Test error"); 127 | }); 128 | 129 | it("should handle unknown errors and call setFailed", async () => { 130 | // Arrange 131 | const error = "Test error"; 132 | upMock.mockRejectedValue(error); 133 | 134 | getInputsMock.mockImplementation(() => ({ 135 | dockerFlags: [], 136 | composeFiles: ["docker-compose.yml"], 137 | services: ["web"], 138 | composeFlags: [], 139 | upFlags: [], 140 | downFlags: [], 141 | cwd: "/current/working/dir", 142 | composeVersion: null, 143 | githubToken: null, 144 | })); 145 | 146 | // Act 147 | await indexRunner.run(); 148 | 149 | // Assert 150 | expect(setFailedMock).toHaveBeenCalledWith('"Test error"'); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/index-runner.ts: -------------------------------------------------------------------------------- 1 | import { setFailed } from "@actions/core"; 2 | import { InputService } from "./services/input.service"; 3 | import { LoggerService } from "./services/logger.service"; 4 | import { DockerComposeService } from "./services/docker-compose.service"; 5 | import { DockerComposeInstallerService } from "./services/docker-compose-installer.service"; 6 | import { ManualInstallerAdapter } from "./services/installer-adapter/manual-installer-adapter"; 7 | 8 | /** 9 | * The run function for the action. 10 | * @returns {Promise} Resolves when the action is complete. 11 | */ 12 | export async function run(): Promise { 13 | try { 14 | const loggerService = new LoggerService(); 15 | const inputService = new InputService(); 16 | const dockerComposeInstallerService = new DockerComposeInstallerService( 17 | new ManualInstallerAdapter() 18 | ); 19 | const dockerComposeService = new DockerComposeService(); 20 | 21 | const inputs = inputService.getInputs(); 22 | loggerService.debug(`inputs: ${JSON.stringify(inputs)}`); 23 | 24 | loggerService.info( 25 | "Setting up docker compose" + 26 | (inputs.composeVersion ? ` version ${inputs.composeVersion}` : "") 27 | ); 28 | 29 | const installedVersion = await dockerComposeInstallerService.install({ 30 | composeVersion: inputs.composeVersion, 31 | cwd: inputs.cwd, 32 | githubToken: inputs.githubToken, 33 | }); 34 | 35 | loggerService.info(`docker compose version: ${installedVersion}`); 36 | 37 | loggerService.info("Bringing up docker compose service(s)"); 38 | await dockerComposeService.up({ 39 | dockerFlags: inputs.dockerFlags, 40 | composeFiles: inputs.composeFiles, 41 | composeFlags: inputs.composeFlags, 42 | cwd: inputs.cwd, 43 | upFlags: inputs.upFlags, 44 | services: inputs.services, 45 | debug: loggerService.debug, 46 | }); 47 | loggerService.info("docker compose service(s) are up"); 48 | } catch (error) { 49 | setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { DockerComposeService } from "./services/docker-compose.service"; 3 | import { InputService } from "./services/input.service"; 4 | import { LoggerService } from "./services/logger.service"; 5 | import { DockerComposeInstallerService } from "./services/docker-compose-installer.service"; 6 | 7 | let setFailedMock: jest.SpiedFunction; 8 | let getInputsMock: jest.SpiedFunction; 9 | let debugMock: jest.SpiedFunction; 10 | let infoMock: jest.SpiedFunction; 11 | let installMock: jest.SpiedFunction; 12 | let upMock: jest.SpiedFunction; 13 | 14 | describe("index", () => { 15 | beforeEach(() => { 16 | jest.clearAllMocks(); 17 | 18 | setFailedMock = jest.spyOn(core, "setFailed").mockImplementation(); 19 | infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(); 20 | debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(); 21 | getInputsMock = jest.spyOn(InputService.prototype, "getInputs"); 22 | installMock = jest.spyOn(DockerComposeInstallerService.prototype, "install"); 23 | upMock = jest.spyOn(DockerComposeService.prototype, "up"); 24 | }); 25 | 26 | it("calls run when imported", async () => { 27 | getInputsMock.mockImplementation(() => ({ 28 | dockerFlags: [], 29 | composeFiles: ["docker-compose.yml"], 30 | services: [], 31 | composeFlags: [], 32 | upFlags: [], 33 | downFlags: [], 34 | cwd: "/current/working/dir", 35 | composeVersion: null, 36 | githubToken: null, 37 | })); 38 | 39 | installMock.mockResolvedValue("1.2.3"); 40 | upMock.mockResolvedValueOnce(); 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-require-imports 43 | await require("../src/index"); 44 | await new Promise((resolve) => setTimeout(resolve, 0)); 45 | 46 | expect(infoMock).toHaveBeenNthCalledWith(1, "Setting up docker compose"); 47 | expect(infoMock).toHaveBeenNthCalledWith(2, "docker compose version: 1.2.3"); 48 | 49 | // Verify that all of the functions were called correctly 50 | expect(debugMock).toHaveBeenNthCalledWith( 51 | 1, 52 | 'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":null,"githubToken":null}' 53 | ); 54 | 55 | expect(infoMock).toHaveBeenNthCalledWith(3, "Bringing up docker compose service(s)"); 56 | 57 | expect(upMock).toHaveBeenCalledWith({ 58 | dockerFlags: [], 59 | composeFiles: ["docker-compose.yml"], 60 | services: [], 61 | composeFlags: [], 62 | upFlags: [], 63 | cwd: "/current/working/dir", 64 | debug: debugMock, 65 | }); 66 | 67 | expect(setFailedMock).not.toHaveBeenCalled(); 68 | 69 | expect(infoMock).toHaveBeenNthCalledWith(4, "docker compose service(s) are up"); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The entrypoint for the action. 3 | */ 4 | import { run } from "./index-runner"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 7 | run(); 8 | -------------------------------------------------------------------------------- /src/post-runner.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { InputService } from "./services/input.service"; 3 | import { LoggerService } from "./services/logger.service"; 4 | import * as postRunner from "./post-runner"; 5 | import { DockerComposeService } from "./services/docker-compose.service"; 6 | 7 | // Mock the external libraries and services used by the action 8 | let infoMock: jest.SpiedFunction; 9 | let debugMock: jest.SpiedFunction; 10 | let setFailedMock: jest.SpiedFunction; 11 | let getInputsMock: jest.SpiedFunction; 12 | let downMock: jest.SpiedFunction; 13 | let logsMock: jest.SpiedFunction; 14 | 15 | describe("run", () => { 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | 19 | infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(); 20 | debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(); 21 | setFailedMock = jest.spyOn(core, "setFailed").mockImplementation(); 22 | getInputsMock = jest.spyOn(InputService.prototype, "getInputs"); 23 | downMock = jest.spyOn(DockerComposeService.prototype, "down"); 24 | logsMock = jest.spyOn(DockerComposeService.prototype, "logs"); 25 | }); 26 | 27 | it("should bring down docker compose service(s) and log output", async () => { 28 | // Arrange 29 | getInputsMock.mockImplementation(() => ({ 30 | dockerFlags: [], 31 | composeFiles: ["docker-compose.yml"], 32 | services: [], 33 | composeFlags: [], 34 | upFlags: [], 35 | downFlags: [], 36 | cwd: "/current/working/dir", 37 | composeVersion: null, 38 | githubToken: null, 39 | })); 40 | 41 | logsMock.mockResolvedValue({ error: "", output: "test logs" }); 42 | 43 | downMock.mockResolvedValue(); 44 | 45 | // Act 46 | await postRunner.run(); 47 | 48 | // Assert 49 | expect(logsMock).toHaveBeenCalledWith({ 50 | dockerFlags: [], 51 | composeFiles: ["docker-compose.yml"], 52 | composeFlags: [], 53 | cwd: "/current/working/dir", 54 | services: [], 55 | debug: debugMock, 56 | }); 57 | 58 | expect(downMock).toHaveBeenCalledWith({ 59 | dockerFlags: [], 60 | composeFiles: ["docker-compose.yml"], 61 | composeFlags: [], 62 | cwd: "/current/working/dir", 63 | downFlags: [], 64 | debug: debugMock, 65 | }); 66 | 67 | expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs"); 68 | 69 | expect(infoMock).toHaveBeenCalledWith("docker compose is down"); 70 | 71 | expect(setFailedMock).not.toHaveBeenCalled(); 72 | }); 73 | 74 | it("should log docker composer errors if any", async () => { 75 | // Arrange 76 | getInputsMock.mockImplementation(() => ({ 77 | dockerFlags: [], 78 | composeFiles: ["docker-compose.yml"], 79 | services: [], 80 | composeFlags: [], 81 | upFlags: [], 82 | downFlags: [], 83 | cwd: "/current/working/dir", 84 | composeVersion: null, 85 | githubToken: null, 86 | })); 87 | 88 | logsMock.mockResolvedValue({ 89 | error: "test logs error", 90 | output: "test logs output", 91 | }); 92 | 93 | downMock.mockResolvedValue(); 94 | 95 | // Act 96 | await postRunner.run(); 97 | 98 | // Assert 99 | expect(logsMock).toHaveBeenCalledWith({ 100 | composeFiles: ["docker-compose.yml"], 101 | composeFlags: [], 102 | cwd: "/current/working/dir", 103 | dockerFlags: [], 104 | services: [], 105 | debug: debugMock, 106 | }); 107 | 108 | expect(downMock).toHaveBeenCalledWith({ 109 | composeFiles: ["docker-compose.yml"], 110 | composeFlags: [], 111 | cwd: "/current/working/dir", 112 | dockerFlags: [], 113 | downFlags: [], 114 | debug: debugMock, 115 | }); 116 | 117 | expect(debugMock).toHaveBeenCalledWith("docker compose error:\ntest logs error"); 118 | expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs output"); 119 | 120 | expect(infoMock).toHaveBeenCalledWith("docker compose is down"); 121 | }); 122 | 123 | it("should set failed when an error occurs", async () => { 124 | // Arrange 125 | getInputsMock.mockImplementation(() => { 126 | throw new Error("An error occurred"); 127 | }); 128 | 129 | // Act 130 | await postRunner.run(); 131 | 132 | // Assert 133 | expect(setFailedMock).toHaveBeenCalledWith("Error: An error occurred"); 134 | }); 135 | 136 | it("should handle errors and call setFailed", async () => { 137 | // Arrange 138 | const error = new Error("Test error"); 139 | downMock.mockRejectedValue(error); 140 | 141 | getInputsMock.mockImplementation(() => ({ 142 | dockerFlags: [], 143 | composeFiles: ["docker-compose.yml"], 144 | services: ["web"], 145 | composeFlags: [], 146 | upFlags: [], 147 | downFlags: [], 148 | cwd: "/current/working/dir", 149 | composeVersion: null, 150 | githubToken: null, 151 | })); 152 | 153 | // Act 154 | await postRunner.run(); 155 | 156 | // Assert 157 | expect(setFailedMock).toHaveBeenCalledWith("Error: Test error"); 158 | }); 159 | 160 | it("should handle unknown errors and call setFailed", async () => { 161 | // Arrange 162 | const error = "Test error"; 163 | downMock.mockRejectedValue(error); 164 | 165 | getInputsMock.mockImplementation(() => ({ 166 | dockerFlags: [], 167 | composeFiles: ["docker-compose.yml"], 168 | services: ["web"], 169 | composeFlags: [], 170 | upFlags: [], 171 | downFlags: [], 172 | cwd: "/current/working/dir", 173 | composeVersion: null, 174 | githubToken: null, 175 | })); 176 | 177 | // Act 178 | await postRunner.run(); 179 | 180 | // Assert 181 | expect(setFailedMock).toHaveBeenCalledWith('"Test error"'); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/post-runner.ts: -------------------------------------------------------------------------------- 1 | import { setFailed } from "@actions/core"; 2 | import { InputService } from "./services/input.service"; 3 | import { LoggerService } from "./services/logger.service"; 4 | import { DockerComposeService } from "./services/docker-compose.service"; 5 | 6 | /** 7 | * The run function for the action. 8 | * @returns {Promise} Resolves when the action is complete. 9 | */ 10 | export async function run(): Promise { 11 | try { 12 | const loggerService = new LoggerService(); 13 | const inputService = new InputService(); 14 | const dockerComposeService = new DockerComposeService(); 15 | 16 | const inputs = inputService.getInputs(); 17 | 18 | const { error, output } = await dockerComposeService.logs({ 19 | dockerFlags: inputs.dockerFlags, 20 | composeFiles: inputs.composeFiles, 21 | composeFlags: inputs.composeFlags, 22 | cwd: inputs.cwd, 23 | services: inputs.services, 24 | debug: loggerService.debug, 25 | }); 26 | 27 | if (error) { 28 | loggerService.debug("docker compose error:\n" + error); 29 | } 30 | 31 | loggerService.debug("docker compose logs:\n" + output); 32 | 33 | await dockerComposeService.down({ 34 | dockerFlags: inputs.dockerFlags, 35 | composeFiles: inputs.composeFiles, 36 | composeFlags: inputs.composeFlags, 37 | cwd: inputs.cwd, 38 | downFlags: inputs.downFlags, 39 | debug: loggerService.debug, 40 | }); 41 | 42 | loggerService.info("docker compose is down"); 43 | } catch (error) { 44 | setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/post.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { DockerComposeService } from "./services/docker-compose.service"; 3 | import { InputService } from "./services/input.service"; 4 | import { LoggerService } from "./services/logger.service"; 5 | 6 | let setFailedMock: jest.SpiedFunction; 7 | let getInputsMock: jest.SpiedFunction; 8 | let debugMock: jest.SpiedFunction; 9 | let infoMock: jest.SpiedFunction; 10 | let logsMock: jest.SpiedFunction; 11 | let downMock: jest.SpiedFunction; 12 | 13 | describe("post", () => { 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | 17 | setFailedMock = jest.spyOn(core, "setFailed").mockImplementation(); 18 | infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(); 19 | debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(); 20 | getInputsMock = jest.spyOn(InputService.prototype, "getInputs"); 21 | logsMock = jest.spyOn(DockerComposeService.prototype, "logs"); 22 | downMock = jest.spyOn(DockerComposeService.prototype, "down"); 23 | }); 24 | 25 | it("calls run when imported", async () => { 26 | getInputsMock.mockImplementation(() => ({ 27 | dockerFlags: [], 28 | composeFiles: ["docker-compose.yml"], 29 | services: [], 30 | composeFlags: [], 31 | upFlags: [], 32 | downFlags: [], 33 | cwd: "/current/working/dir", 34 | composeVersion: null, 35 | githubToken: null, 36 | })); 37 | 38 | logsMock.mockResolvedValue({ error: "", output: "test logs" }); 39 | downMock.mockResolvedValueOnce(); 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-require-imports 42 | await require("../src/post"); 43 | await new Promise((resolve) => setTimeout(resolve, 0)); 44 | 45 | expect(logsMock).toHaveBeenCalledWith({ 46 | dockerFlags: [], 47 | composeFiles: ["docker-compose.yml"], 48 | composeFlags: [], 49 | cwd: "/current/working/dir", 50 | services: [], 51 | debug: debugMock, 52 | }); 53 | 54 | expect(downMock).toHaveBeenCalledWith({ 55 | dockerFlags: [], 56 | composeFiles: ["docker-compose.yml"], 57 | composeFlags: [], 58 | cwd: "/current/working/dir", 59 | downFlags: [], 60 | debug: debugMock, 61 | }); 62 | 63 | expect(debugMock).toHaveBeenNthCalledWith(1, "docker compose logs:\ntest logs"); 64 | expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down"); 65 | 66 | expect(setFailedMock).not.toHaveBeenCalled(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The entrypoint for the post action. 3 | */ 4 | import { run } from "./post-runner"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 7 | run(); 8 | -------------------------------------------------------------------------------- /src/services/docker-compose-installer.service.test.ts: -------------------------------------------------------------------------------- 1 | import * as dockerCompose from "docker-compose"; 2 | import { DockerComposeInstallerService } from "./docker-compose-installer.service"; 3 | import { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter"; 4 | import { MockAgent, setGlobalDispatcher } from "undici"; 5 | 6 | jest.mock("docker-compose"); 7 | 8 | describe("DockerComposeInstallerService", () => { 9 | let mockAgent: MockAgent; 10 | let versionMock: jest.SpiedFunction; 11 | let manualInstallerAdapterMock: jest.Mocked; 12 | let service: DockerComposeInstallerService; 13 | 14 | beforeEach(() => { 15 | mockAgent = new MockAgent(); 16 | mockAgent.disableNetConnect(); 17 | 18 | versionMock = jest.spyOn(dockerCompose, "version").mockImplementation(); 19 | 20 | manualInstallerAdapterMock = { 21 | install: jest.fn(), 22 | } as unknown as jest.Mocked; 23 | 24 | service = new DockerComposeInstallerService(manualInstallerAdapterMock); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | describe("install", () => { 32 | it("should not install anything when expected version is already installed", async () => { 33 | // Arrange 34 | versionMock.mockResolvedValue({ 35 | exitCode: 0, 36 | out: "", 37 | err: "", 38 | data: { 39 | version: "1.2.3", 40 | }, 41 | }); 42 | 43 | // Act 44 | const result = await service.install({ 45 | composeVersion: "1.2.3", 46 | cwd: "/path/to/cwd", 47 | githubToken: null, 48 | }); 49 | 50 | // Assert 51 | expect(result).toBe("1.2.3"); 52 | expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled(); 53 | }); 54 | 55 | it("should install the requested version if it is not already installed", async () => { 56 | // Arrange 57 | versionMock.mockResolvedValueOnce({ 58 | exitCode: 0, 59 | out: "", 60 | err: "", 61 | data: { 62 | version: "1.2.3", 63 | }, 64 | }); 65 | 66 | const expectedVersion = "1.3.0"; 67 | versionMock.mockResolvedValueOnce({ 68 | exitCode: 0, 69 | out: "", 70 | err: "", 71 | data: { 72 | version: expectedVersion, 73 | }, 74 | }); 75 | 76 | Object.defineProperty(process, "platform", { 77 | value: "linux", 78 | }); 79 | 80 | // Act 81 | const result = await service.install({ 82 | composeVersion: expectedVersion, 83 | cwd: "/path/to/cwd", 84 | githubToken: null, 85 | }); 86 | 87 | // Assert 88 | expect(result).toBe(expectedVersion); 89 | expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(expectedVersion); 90 | }); 91 | 92 | it("should install the latest version if requested", async () => { 93 | // Arrange 94 | versionMock.mockResolvedValueOnce({ 95 | exitCode: 0, 96 | out: "", 97 | err: "", 98 | data: { 99 | version: "1.2.3", 100 | }, 101 | }); 102 | 103 | const latestVersion = "v1.4.0"; 104 | 105 | const mockClient = mockAgent.get("https://api.github.com"); 106 | mockClient 107 | .intercept({ 108 | path: "/repos/docker/compose/releases/latest", 109 | method: "GET", 110 | }) 111 | .reply( 112 | 200, 113 | { 114 | tag_name: latestVersion, 115 | }, 116 | { 117 | headers: { 118 | "content-type": "application/json", 119 | }, 120 | } 121 | ); 122 | setGlobalDispatcher(mockClient); 123 | 124 | versionMock.mockResolvedValueOnce({ 125 | exitCode: 0, 126 | out: "", 127 | err: "", 128 | data: { 129 | version: latestVersion, 130 | }, 131 | }); 132 | 133 | Object.defineProperty(process, "platform", { 134 | value: "linux", 135 | }); 136 | Object.defineProperty(globalThis, "fetch", { 137 | value: jest.fn(), 138 | }); 139 | 140 | // Act 141 | const result = await service.install({ 142 | composeVersion: "latest", 143 | cwd: "/path/to/cwd", 144 | githubToken: "token", 145 | }); 146 | 147 | // Assert 148 | expect(result).toBe(latestVersion); 149 | expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion); 150 | }); 151 | 152 | it("should throw an error if the latest version if requested and no Github token is provided", async () => { 153 | // Arrange 154 | versionMock.mockResolvedValueOnce({ 155 | exitCode: 0, 156 | out: "", 157 | err: "", 158 | data: { 159 | version: "1.2.3", 160 | }, 161 | }); 162 | 163 | // Act & Assert 164 | await expect( 165 | service.install({ 166 | composeVersion: "latest", 167 | cwd: "/path/to/cwd", 168 | githubToken: null, 169 | }) 170 | ).rejects.toThrow("GitHub token is required to install the latest version"); 171 | }); 172 | 173 | it("should throw an error on unsupported platforms", async () => { 174 | // Arrange 175 | versionMock.mockResolvedValueOnce({ 176 | exitCode: 0, 177 | out: "", 178 | err: "", 179 | data: { 180 | version: "1.2.3", 181 | }, 182 | }); 183 | 184 | const expectedVersion = "1.3.0"; 185 | versionMock.mockResolvedValueOnce({ 186 | exitCode: 0, 187 | out: "", 188 | err: "", 189 | data: { 190 | version: expectedVersion, 191 | }, 192 | }); 193 | 194 | Object.defineProperty(process, "platform", { 195 | value: "win32", 196 | }); 197 | 198 | // Act & Assert 199 | await expect( 200 | service.install({ 201 | composeVersion: expectedVersion, 202 | cwd: "/path/to/cwd", 203 | githubToken: null, 204 | }) 205 | ).rejects.toThrow(`Unsupported platform: win32`); 206 | 207 | expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled(); 208 | }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /src/services/docker-compose-installer.service.ts: -------------------------------------------------------------------------------- 1 | import * as github from "@actions/github"; 2 | import { version } from "docker-compose"; 3 | import { COMPOSE_VERSION_LATEST, Inputs } from "./input.service"; 4 | import { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter"; 5 | 6 | export type InstallInputs = { 7 | composeVersion: Inputs["composeVersion"]; 8 | cwd: Inputs["cwd"]; 9 | githubToken: Inputs["githubToken"]; 10 | }; 11 | 12 | export type VersionInputs = { 13 | cwd: Inputs["cwd"]; 14 | }; 15 | 16 | export class DockerComposeInstallerService { 17 | constructor(private readonly manualInstallerAdapter: ManualInstallerAdapter) {} 18 | 19 | async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise { 20 | const currentVersion = await this.version({ cwd }); 21 | 22 | if (!composeVersion) { 23 | return currentVersion; 24 | } 25 | 26 | if (currentVersion === composeVersion) { 27 | return currentVersion; 28 | } 29 | 30 | if (composeVersion === COMPOSE_VERSION_LATEST) { 31 | if (!githubToken) { 32 | throw new Error("GitHub token is required to install the latest version"); 33 | } 34 | composeVersion = await this.getLatestVersion(githubToken); 35 | } 36 | 37 | await this.installVersion(composeVersion); 38 | 39 | return this.version({ cwd }); 40 | } 41 | 42 | private async version({ cwd }: VersionInputs): Promise { 43 | const result = await version({ 44 | cwd, 45 | }); 46 | return result.data.version; 47 | } 48 | 49 | private async getLatestVersion(githubToken: string): Promise { 50 | const octokit = github.getOctokit(githubToken); 51 | 52 | const response = await octokit.rest.repos.getLatestRelease({ 53 | owner: "docker", 54 | repo: "compose", 55 | }); 56 | 57 | return response.data.tag_name; 58 | } 59 | 60 | private async installVersion(version: string): Promise { 61 | switch (process.platform) { 62 | case "linux": 63 | case "darwin": 64 | await this.manualInstallerAdapter.install(version); 65 | break; 66 | default: 67 | throw new Error(`Unsupported platform: ${process.platform}`); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/services/docker-compose.service.test.ts: -------------------------------------------------------------------------------- 1 | import * as dockerCompose from "docker-compose"; 2 | import { DockerComposeService, DownInputs, LogsInputs, UpInputs } from "./docker-compose.service"; 3 | 4 | jest.mock("docker-compose"); 5 | 6 | describe("DockerComposeService", () => { 7 | let service: DockerComposeService; 8 | let upAllMock: jest.SpiedFunction; 9 | let upManyMock: jest.SpiedFunction; 10 | let downMock: jest.SpiedFunction; 11 | let logsMock: jest.SpiedFunction; 12 | 13 | beforeEach(() => { 14 | service = new DockerComposeService(); 15 | upAllMock = jest.spyOn(dockerCompose, "upAll").mockImplementation(); 16 | upManyMock = jest.spyOn(dockerCompose, "upMany").mockImplementation(); 17 | downMock = jest.spyOn(dockerCompose, "down").mockImplementation(); 18 | logsMock = jest.spyOn(dockerCompose, "logs").mockImplementation(); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | describe("up", () => { 26 | it("should call up with correct options", async () => { 27 | const upInputs: UpInputs = { 28 | dockerFlags: [], 29 | composeFiles: ["docker-compose.yml"], 30 | services: [], 31 | composeFlags: [], 32 | upFlags: [], 33 | cwd: "/current/working/dir", 34 | debug: jest.fn(), 35 | }; 36 | 37 | await service.up(upInputs); 38 | 39 | expect(upAllMock).toHaveBeenCalledWith({ 40 | composeOptions: [], 41 | commandOptions: [], 42 | config: ["docker-compose.yml"], 43 | executable: { 44 | executablePath: "docker", 45 | options: [], 46 | }, 47 | cwd: "/current/working/dir", 48 | callback: expect.any(Function), 49 | }); 50 | }); 51 | 52 | it("should call up with specific docker flags", async () => { 53 | const upInputs: UpInputs = { 54 | dockerFlags: ["--context", "dev"], 55 | composeFiles: ["docker-compose.yml"], 56 | services: [], 57 | composeFlags: [], 58 | upFlags: [], 59 | cwd: "/current/working/dir", 60 | debug: jest.fn(), 61 | }; 62 | 63 | await service.up(upInputs); 64 | 65 | expect(upAllMock).toHaveBeenCalledWith({ 66 | composeOptions: [], 67 | commandOptions: [], 68 | config: ["docker-compose.yml"], 69 | executable: { 70 | executablePath: "docker", 71 | options: ["--context", "dev"], 72 | }, 73 | cwd: "/current/working/dir", 74 | callback: expect.any(Function), 75 | }); 76 | }); 77 | 78 | it("should call up with specific services", async () => { 79 | const upInputs: UpInputs = { 80 | dockerFlags: [], 81 | composeFiles: ["docker-compose.yml"], 82 | services: ["helloworld2", "helloworld3"], 83 | composeFlags: [], 84 | upFlags: ["--build"], 85 | cwd: "/current/working/dir", 86 | debug: jest.fn(), 87 | }; 88 | 89 | await service.up(upInputs); 90 | 91 | expect(upManyMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], { 92 | composeOptions: [], 93 | commandOptions: ["--build"], 94 | config: ["docker-compose.yml"], 95 | cwd: "/current/working/dir", 96 | callback: expect.any(Function), 97 | executable: { 98 | executablePath: "docker", 99 | options: [], 100 | }, 101 | }); 102 | }); 103 | }); 104 | 105 | describe("down", () => { 106 | it("should call down with correct options", async () => { 107 | const downInputs: DownInputs = { 108 | dockerFlags: [], 109 | composeFiles: [], 110 | composeFlags: [], 111 | downFlags: ["--volumes", "--remove-orphans"], 112 | cwd: "/current/working/dir", 113 | debug: jest.fn(), 114 | }; 115 | 116 | await service.down(downInputs); 117 | 118 | expect(downMock).toHaveBeenCalledWith({ 119 | composeOptions: [], 120 | commandOptions: ["--volumes", "--remove-orphans"], 121 | config: [], 122 | executable: { 123 | executablePath: "docker", 124 | options: [], 125 | }, 126 | cwd: "/current/working/dir", 127 | callback: expect.any(Function), 128 | }); 129 | }); 130 | }); 131 | 132 | describe("logs", () => { 133 | it("should call logs with correct options", async () => { 134 | const debugMock = jest.fn(); 135 | const logsInputs: LogsInputs = { 136 | dockerFlags: [], 137 | composeFiles: ["docker-compose.yml"], 138 | services: ["helloworld2", "helloworld3"], 139 | composeFlags: [], 140 | cwd: "/current/working/dir", 141 | debug: debugMock, 142 | }; 143 | 144 | logsMock.mockResolvedValue({ exitCode: 0, err: "", out: "logs" }); 145 | 146 | await service.logs(logsInputs); 147 | 148 | expect(dockerCompose.logs).toHaveBeenCalledWith(["helloworld2", "helloworld3"], { 149 | composeOptions: [], 150 | config: ["docker-compose.yml"], 151 | cwd: "/current/working/dir", 152 | executable: { 153 | executablePath: "docker", 154 | options: [], 155 | }, 156 | follow: false, 157 | callback: expect.any(Function), 158 | }); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /src/services/docker-compose.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | down, 3 | IDockerComposeLogOptions, 4 | IDockerComposeOptions, 5 | logs, 6 | upAll, 7 | upMany, 8 | } from "docker-compose"; 9 | import { Inputs } from "./input.service"; 10 | 11 | type OptionsInputs = { 12 | dockerFlags: Inputs["dockerFlags"]; 13 | composeFiles: Inputs["composeFiles"]; 14 | composeFlags: Inputs["composeFlags"]; 15 | cwd: Inputs["cwd"]; 16 | debug: (message: string) => void; 17 | }; 18 | 19 | export type UpInputs = OptionsInputs & { upFlags: Inputs["upFlags"]; services: Inputs["services"] }; 20 | export type DownInputs = OptionsInputs & { downFlags: Inputs["downFlags"] }; 21 | export type LogsInputs = OptionsInputs & { services: Inputs["services"] }; 22 | 23 | export class DockerComposeService { 24 | async up({ upFlags, services, ...optionsInputs }: UpInputs): Promise { 25 | const options: IDockerComposeOptions = { 26 | ...this.getCommonOptions(optionsInputs), 27 | commandOptions: upFlags, 28 | }; 29 | 30 | if (services.length > 0) { 31 | await upMany(services, options); 32 | return; 33 | } 34 | 35 | await upAll(options); 36 | } 37 | 38 | async down({ downFlags, ...optionsInputs }: DownInputs): Promise { 39 | const options: IDockerComposeOptions = { 40 | ...this.getCommonOptions(optionsInputs), 41 | commandOptions: downFlags, 42 | }; 43 | 44 | await down(options); 45 | } 46 | 47 | async logs({ services, ...optionsInputs }: LogsInputs): Promise<{ 48 | error: string; 49 | output: string; 50 | }> { 51 | const options: IDockerComposeLogOptions = { 52 | ...this.getCommonOptions(optionsInputs), 53 | follow: false, 54 | }; 55 | 56 | const { err, out } = await logs(services, options); 57 | 58 | return { 59 | error: err, 60 | output: out, 61 | }; 62 | } 63 | 64 | private getCommonOptions({ 65 | dockerFlags, 66 | composeFiles, 67 | composeFlags, 68 | cwd, 69 | debug, 70 | }: OptionsInputs): IDockerComposeOptions { 71 | return { 72 | config: composeFiles, 73 | composeOptions: composeFlags, 74 | cwd: cwd, 75 | callback: (chunk) => debug(chunk.toString()), 76 | executable: { 77 | executablePath: "docker", 78 | options: dockerFlags, 79 | }, 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/services/input.service.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import fs from "fs"; 3 | import { InputService, InputNames } from "./input.service"; 4 | 5 | describe("InputService", () => { 6 | let service: InputService; 7 | let getInputMock: jest.SpiedFunction; 8 | let getMultilineInputMock: jest.SpiedFunction; 9 | let existsSyncMock: jest.SpiedFunction; 10 | 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | 14 | existsSyncMock = jest.spyOn(fs, "existsSync").mockImplementation(); 15 | getInputMock = jest.spyOn(core, "getInput").mockImplementation(); 16 | getMultilineInputMock = jest.spyOn(core, "getMultilineInput").mockImplementation(); 17 | 18 | service = new InputService(); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | describe("getInputs", () => { 26 | describe("docker-flags", () => { 27 | it("should return given docker-flags input", () => { 28 | getMultilineInputMock.mockImplementation((inputName) => { 29 | switch (inputName) { 30 | case InputNames.ComposeFile: 31 | return ["file1"]; 32 | default: 33 | return []; 34 | } 35 | }); 36 | 37 | getInputMock.mockImplementation((inputName) => { 38 | switch (inputName) { 39 | case InputNames.DockerFlags: 40 | return "docker-flag1 docker-flag2"; 41 | default: 42 | return ""; 43 | } 44 | }); 45 | 46 | existsSyncMock.mockReturnValue(true); 47 | 48 | const inputs = service.getInputs(); 49 | 50 | expect(inputs.dockerFlags).toEqual(["docker-flag1", "docker-flag2"]); 51 | }); 52 | 53 | it("should return empty array when no docker-flags input", () => { 54 | getMultilineInputMock.mockImplementation((inputName) => { 55 | switch (inputName) { 56 | case InputNames.ComposeFile: 57 | return ["file1"]; 58 | default: 59 | return []; 60 | } 61 | }); 62 | 63 | getInputMock.mockReturnValue(""); 64 | 65 | existsSyncMock.mockReturnValue(true); 66 | 67 | const inputs = service.getInputs(); 68 | 69 | expect(inputs.dockerFlags).toEqual([]); 70 | }); 71 | }); 72 | 73 | describe("composeFiles", () => { 74 | it("should return given composeFiles input", () => { 75 | getMultilineInputMock.mockImplementation((inputName) => { 76 | switch (inputName) { 77 | case InputNames.ComposeFile: 78 | return ["file1", "file2"]; 79 | default: 80 | return []; 81 | } 82 | }); 83 | 84 | getInputMock.mockReturnValue(""); 85 | 86 | existsSyncMock.mockReturnValue(true); 87 | 88 | const inputs = service.getInputs(); 89 | 90 | expect(inputs.composeFiles).toEqual(["file1", "file2"]); 91 | }); 92 | 93 | it("should throws an error when a compose file does not exist", () => { 94 | getMultilineInputMock.mockImplementation((inputName) => { 95 | switch (inputName) { 96 | case InputNames.ComposeFile: 97 | return ["file1", "file2"]; 98 | default: 99 | return []; 100 | } 101 | }); 102 | 103 | getInputMock.mockImplementation((inputName) => { 104 | switch (inputName) { 105 | case InputNames.Cwd: 106 | return "/current/working/directory"; 107 | default: 108 | return ""; 109 | } 110 | }); 111 | 112 | existsSyncMock.mockImplementation((file) => file === "/current/working/directory/file1"); 113 | 114 | expect(() => service.getInputs()).toThrow( 115 | 'Compose file not found in "/current/working/directory/file2", "file2"' 116 | ); 117 | }); 118 | 119 | it("should throws an error when no composeFiles input", () => { 120 | getMultilineInputMock.mockReturnValue([]); 121 | 122 | getInputMock.mockReturnValue(""); 123 | 124 | expect(() => service.getInputs()).toThrow("No compose files found"); 125 | }); 126 | }); 127 | 128 | describe("services", () => { 129 | it("should return given services input", () => { 130 | getMultilineInputMock.mockImplementation((inputName) => { 131 | switch (inputName) { 132 | case InputNames.Services: 133 | return ["service1", "service2"]; 134 | case InputNames.ComposeFile: 135 | return ["file1"]; 136 | default: 137 | return []; 138 | } 139 | }); 140 | 141 | getInputMock.mockReturnValue(""); 142 | existsSyncMock.mockReturnValue(true); 143 | 144 | const inputs = service.getInputs(); 145 | 146 | expect(inputs.services).toEqual(["service1", "service2"]); 147 | }); 148 | }); 149 | 150 | describe("compose-flags", () => { 151 | it("should return given compose-flags input", () => { 152 | getMultilineInputMock.mockImplementation((inputName) => { 153 | switch (inputName) { 154 | case InputNames.ComposeFile: 155 | return ["file1"]; 156 | default: 157 | return []; 158 | } 159 | }); 160 | 161 | getInputMock.mockImplementation((inputName) => { 162 | switch (inputName) { 163 | case InputNames.ComposeFlags: 164 | return "compose-flag1 compose-flag2"; 165 | default: 166 | return ""; 167 | } 168 | }); 169 | 170 | existsSyncMock.mockReturnValue(true); 171 | 172 | const inputs = service.getInputs(); 173 | 174 | expect(inputs.composeFlags).toEqual(["compose-flag1", "compose-flag2"]); 175 | }); 176 | 177 | it("should return empty array when no compose-flags input", () => { 178 | getMultilineInputMock.mockImplementation((inputName) => { 179 | switch (inputName) { 180 | case InputNames.ComposeFile: 181 | return ["file1"]; 182 | default: 183 | return []; 184 | } 185 | }); 186 | 187 | getInputMock.mockReturnValue(""); 188 | 189 | existsSyncMock.mockReturnValue(true); 190 | 191 | const inputs = service.getInputs(); 192 | 193 | expect(inputs.composeFlags).toEqual([]); 194 | }); 195 | }); 196 | 197 | describe("up-flags", () => { 198 | it("should return given up-flags input", () => { 199 | getMultilineInputMock.mockImplementation((inputName) => { 200 | switch (inputName) { 201 | case InputNames.ComposeFile: 202 | return ["file1"]; 203 | default: 204 | return []; 205 | } 206 | }); 207 | 208 | getInputMock.mockImplementation((inputName) => { 209 | switch (inputName) { 210 | case InputNames.UpFlags: 211 | return "up-flag1 up-flag2"; 212 | default: 213 | return ""; 214 | } 215 | }); 216 | 217 | existsSyncMock.mockReturnValue(true); 218 | 219 | const inputs = service.getInputs(); 220 | 221 | expect(inputs.upFlags).toEqual(["up-flag1", "up-flag2"]); 222 | }); 223 | 224 | it("should return empty array when no up-flags input", () => { 225 | getMultilineInputMock.mockImplementation((inputName) => { 226 | switch (inputName) { 227 | case InputNames.ComposeFile: 228 | return ["file1"]; 229 | default: 230 | return []; 231 | } 232 | }); 233 | 234 | getInputMock.mockReturnValue(""); 235 | 236 | existsSyncMock.mockReturnValue(true); 237 | 238 | const inputs = service.getInputs(); 239 | 240 | expect(inputs.upFlags).toEqual([]); 241 | }); 242 | }); 243 | 244 | describe("down-flags", () => { 245 | it("should return given down-flags input", () => { 246 | getMultilineInputMock.mockImplementation((inputName) => { 247 | switch (inputName) { 248 | case InputNames.ComposeFile: 249 | return ["file1"]; 250 | default: 251 | return []; 252 | } 253 | }); 254 | 255 | getInputMock.mockImplementation((inputName) => { 256 | switch (inputName) { 257 | case InputNames.DownFlags: 258 | return "down-flag1 down-flag2"; 259 | default: 260 | return ""; 261 | } 262 | }); 263 | 264 | existsSyncMock.mockReturnValue(true); 265 | 266 | const inputs = service.getInputs(); 267 | 268 | expect(inputs.downFlags).toEqual(["down-flag1", "down-flag2"]); 269 | }); 270 | 271 | it("should return empty array when no down-flags input", () => { 272 | getMultilineInputMock.mockImplementation((inputName) => { 273 | switch (inputName) { 274 | case InputNames.ComposeFile: 275 | return ["file1"]; 276 | default: 277 | return []; 278 | } 279 | }); 280 | 281 | getInputMock.mockReturnValue(""); 282 | existsSyncMock.mockReturnValue(true); 283 | 284 | const inputs = service.getInputs(); 285 | 286 | expect(inputs.downFlags).toEqual([]); 287 | }); 288 | }); 289 | 290 | describe("cwd", () => { 291 | it("should return given cwd input", () => { 292 | getMultilineInputMock.mockImplementation((inputName) => { 293 | switch (inputName) { 294 | case InputNames.ComposeFile: 295 | return ["file1"]; 296 | default: 297 | return []; 298 | } 299 | }); 300 | getInputMock.mockImplementation((inputName) => { 301 | switch (inputName) { 302 | case InputNames.Cwd: 303 | return "cwd"; 304 | default: 305 | return ""; 306 | } 307 | }); 308 | existsSyncMock.mockReturnValue(true); 309 | 310 | const inputs = service.getInputs(); 311 | 312 | expect(inputs.cwd).toEqual("cwd"); 313 | }); 314 | }); 315 | 316 | describe("compose-version", () => { 317 | it("should return given compose-version input", () => { 318 | getMultilineInputMock.mockImplementation((inputName) => { 319 | switch (inputName) { 320 | case InputNames.ComposeFile: 321 | return ["file1"]; 322 | default: 323 | return []; 324 | } 325 | }); 326 | getInputMock.mockImplementation((inputName) => { 327 | switch (inputName) { 328 | case InputNames.ComposeVersion: 329 | return "compose-version"; 330 | default: 331 | return ""; 332 | } 333 | }); 334 | existsSyncMock.mockReturnValue(true); 335 | 336 | const inputs = service.getInputs(); 337 | 338 | expect(inputs.composeVersion).toEqual("compose-version"); 339 | }); 340 | }); 341 | }); 342 | }); 343 | -------------------------------------------------------------------------------- /src/services/input.service.ts: -------------------------------------------------------------------------------- 1 | import { getInput, getMultilineInput } from "@actions/core"; 2 | import { existsSync } from "fs"; 3 | import { join } from "path"; 4 | 5 | export type Inputs = { 6 | dockerFlags: string[]; 7 | composeFiles: string[]; 8 | services: string[]; 9 | composeFlags: string[]; 10 | upFlags: string[]; 11 | downFlags: string[]; 12 | cwd: string; 13 | composeVersion: string | null; 14 | githubToken: string | null; 15 | }; 16 | 17 | export enum InputNames { 18 | DockerFlags = "docker-flags", 19 | ComposeFile = "compose-file", 20 | Services = "services", 21 | ComposeFlags = "compose-flags", 22 | UpFlags = "up-flags", 23 | DownFlags = "down-flags", 24 | Cwd = "cwd", 25 | ComposeVersion = "compose-version", 26 | GithubToken = "github-token", 27 | } 28 | 29 | export const COMPOSE_VERSION_LATEST = "latest"; 30 | 31 | export class InputService { 32 | getInputs(): Inputs { 33 | return { 34 | dockerFlags: this.getDockerFlags(), 35 | composeFiles: this.getComposeFiles(), 36 | services: this.getServices(), 37 | composeFlags: this.getComposeFlags(), 38 | upFlags: this.getUpFlags(), 39 | downFlags: this.getDownFlags(), 40 | cwd: this.getCwd(), 41 | composeVersion: this.getComposeVersion(), 42 | githubToken: this.getGithubToken(), 43 | }; 44 | } 45 | 46 | private getDockerFlags(): string[] { 47 | return this.parseFlags(getInput(InputNames.DockerFlags)); 48 | } 49 | 50 | private getComposeFiles(): string[] { 51 | const cwd = this.getCwd(); 52 | const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => { 53 | if (!composeFile.trim().length) { 54 | return false; 55 | } 56 | 57 | const possiblePaths = [join(cwd, composeFile), composeFile]; 58 | 59 | for (const path of possiblePaths) { 60 | if (existsSync(path)) { 61 | return true; 62 | } 63 | } 64 | 65 | throw new Error(`Compose file not found in "${possiblePaths.join('", "')}"`); 66 | }); 67 | 68 | if (!composeFiles.length) { 69 | throw new Error("No compose files found"); 70 | } 71 | 72 | return composeFiles; 73 | } 74 | 75 | private getServices(): string[] { 76 | return getMultilineInput(InputNames.Services, { required: false }); 77 | } 78 | 79 | private getComposeFlags(): string[] { 80 | return this.parseFlags(getInput(InputNames.ComposeFlags)); 81 | } 82 | 83 | private getUpFlags(): string[] { 84 | return this.parseFlags(getInput(InputNames.UpFlags)); 85 | } 86 | 87 | private getDownFlags(): string[] { 88 | return this.parseFlags(getInput(InputNames.DownFlags)); 89 | } 90 | 91 | private parseFlags(flags: string | null): string[] { 92 | if (!flags) { 93 | return []; 94 | } 95 | 96 | return flags.trim().split(" "); 97 | } 98 | 99 | private getCwd(): string { 100 | return getInput(InputNames.Cwd); 101 | } 102 | 103 | private getComposeVersion(): string | null { 104 | return ( 105 | getInput(InputNames.ComposeVersion, { 106 | required: false, 107 | }) || null 108 | ); 109 | } 110 | 111 | private getGithubToken(): string | null { 112 | return ( 113 | getInput(InputNames.GithubToken, { 114 | required: false, 115 | }) || null 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/services/installer-adapter/docker-compose-installer-adapter.ts: -------------------------------------------------------------------------------- 1 | export interface DockerComposeInstallerAdapter { 2 | install(version: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/services/installer-adapter/manual-installer-adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { ManualInstallerAdapter } from "./manual-installer-adapter"; 2 | import * as exec from "@actions/exec"; 3 | import * as io from "@actions/io"; 4 | import * as toolCache from "@actions/tool-cache"; 5 | 6 | jest.mock("@actions/exec"); 7 | jest.mock("@actions/io"); 8 | jest.mock("@actions/tool-cache"); 9 | 10 | describe("ManualInstallerAdapter", () => { 11 | let mkdirPMock: jest.SpiedFunction; 12 | let execMock: jest.SpiedFunction; 13 | let cacheFileMock: jest.SpiedFunction; 14 | let downloadToolMock: jest.SpiedFunction; 15 | 16 | let adapter: ManualInstallerAdapter; 17 | 18 | beforeEach(() => { 19 | mkdirPMock = jest.spyOn(io, "mkdirP").mockImplementation(); 20 | execMock = jest.spyOn(exec, "exec").mockImplementation(); 21 | cacheFileMock = jest.spyOn(toolCache, "cacheFile").mockImplementation(); 22 | downloadToolMock = jest.spyOn(toolCache, "downloadTool").mockImplementation(); 23 | 24 | adapter = new ManualInstallerAdapter(); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | describe("install", () => { 32 | it("should install docker compose correctly", async () => { 33 | // Arrange 34 | const version = "v2.29.0"; 35 | 36 | // Uname -s 37 | execMock.mockResolvedValueOnce(0); 38 | 39 | // Uname -m 40 | execMock.mockResolvedValueOnce(0); 41 | 42 | Object.defineProperty(process.env, "HOME", { 43 | value: "/home/test", 44 | }); 45 | 46 | // Act 47 | await adapter.install(version); 48 | 49 | // Assert 50 | expect(mkdirPMock).toHaveBeenCalledWith("docker-compose"); 51 | expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], { 52 | listeners: { stdout: expect.any(Function) }, 53 | }); 54 | expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], { 55 | listeners: { stdout: expect.any(Function) }, 56 | }); 57 | 58 | expect(downloadToolMock).toHaveBeenCalledWith( 59 | "https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--", 60 | "/home/test/.docker/cli-plugins/docker-compose" 61 | ); 62 | 63 | expect(cacheFileMock).toHaveBeenCalledWith( 64 | "/home/test/.docker/cli-plugins/docker-compose", 65 | "docker-compose", 66 | "docker-compose", 67 | version 68 | ); 69 | }); 70 | 71 | it("should handle version without 'v' prefix", async () => { 72 | // Arrange 73 | const version = "2.29.0"; 74 | 75 | // Uname -s 76 | execMock.mockResolvedValueOnce(0); 77 | 78 | // Uname -m 79 | execMock.mockResolvedValueOnce(0); 80 | 81 | Object.defineProperty(process.env, "HOME", { 82 | value: "/home/test", 83 | }); 84 | 85 | // Act 86 | await adapter.install(version); 87 | 88 | // Assert 89 | expect(mkdirPMock).toHaveBeenCalledWith("docker-compose"); 90 | expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], { 91 | listeners: { stdout: expect.any(Function) }, 92 | }); 93 | expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], { 94 | listeners: { stdout: expect.any(Function) }, 95 | }); 96 | 97 | expect(downloadToolMock).toHaveBeenCalledWith( 98 | "https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--", 99 | "/home/test/.docker/cli-plugins/docker-compose" 100 | ); 101 | }); 102 | 103 | it("should throw an error if a command fails", async () => { 104 | // Arrange 105 | const version = "v2.29.0"; 106 | 107 | // Uname -s 108 | execMock.mockResolvedValueOnce(1); 109 | 110 | // Act 111 | await expect(adapter.install(version)).rejects.toThrow("Failed to run command: uname -s"); 112 | 113 | // Assert 114 | expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], { 115 | listeners: { stdout: expect.any(Function) }, 116 | }); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/services/installer-adapter/manual-installer-adapter.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "@actions/exec"; 2 | import { mkdirP } from "@actions/io"; 3 | import { basename } from "path"; 4 | import { cacheFile, downloadTool } from "@actions/tool-cache"; 5 | import { DockerComposeInstallerAdapter } from "./docker-compose-installer-adapter"; 6 | 7 | export class ManualInstallerAdapter implements DockerComposeInstallerAdapter { 8 | async install(version: string): Promise { 9 | const dockerComposePluginPath = await this.getDockerComposePluginPath(); 10 | 11 | // Create the directory if it doesn't exist 12 | await mkdirP(basename(dockerComposePluginPath)); 13 | 14 | await this.downloadFile(version, dockerComposePluginPath); 15 | await exec(`chmod +x ${dockerComposePluginPath}`); 16 | await cacheFile(dockerComposePluginPath, "docker-compose", "docker-compose", version); 17 | } 18 | 19 | private async getDockerComposePluginPath(): Promise { 20 | const dockerConfig = process.env.DOCKER_CONFIG || `${process.env.HOME}/.docker`; 21 | 22 | const dockerComposePluginPath = `${dockerConfig}/cli-plugins/docker-compose`; 23 | return dockerComposePluginPath; 24 | } 25 | 26 | private async downloadFile(version: string, installerPath: string): Promise { 27 | if (!version.startsWith("v") && parseInt(version.split(".")[0], 10) >= 2) { 28 | version = `v${version}`; 29 | } 30 | 31 | const system = await this.getSystem(); 32 | const hardware = await this.getHardware(); 33 | 34 | const url = `https://github.com/docker/compose/releases/download/${version}/docker-compose-${system}-${hardware}`; 35 | await downloadTool(url, installerPath); 36 | } 37 | 38 | private async getSystem(): Promise { 39 | return this.runCommand("uname -s"); 40 | } 41 | 42 | private async getHardware(): Promise { 43 | return this.runCommand("uname -m"); 44 | } 45 | 46 | private async runCommand(command: string): Promise { 47 | let output = ""; 48 | const result = await exec(command, [], { 49 | listeners: { 50 | stdout: (data: Buffer) => { 51 | output += data.toString(); 52 | }, 53 | }, 54 | }); 55 | if (result !== 0) { 56 | throw new Error(`Failed to run command: ${command}`); 57 | } 58 | return output.trim(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/services/logger.service.test.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from "./logger.service"; 2 | import { debug, info, warning } from "@actions/core"; 3 | 4 | jest.mock("@actions/core", () => ({ 5 | warning: jest.fn(), 6 | info: jest.fn(), 7 | debug: jest.fn(), 8 | })); 9 | 10 | describe("LoggerService", () => { 11 | let loggerService: LoggerService; 12 | 13 | beforeEach(() => { 14 | loggerService = new LoggerService(); 15 | }); 16 | 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | describe("warn", () => { 22 | it("should call warning with the correct message", () => { 23 | const message = "This is a warning message"; 24 | loggerService.warn(message); 25 | expect(warning).toHaveBeenCalledWith(message); 26 | }); 27 | }); 28 | 29 | describe("info", () => { 30 | it("should call info with the correct message", () => { 31 | const message = "This is an info message"; 32 | 33 | loggerService.info(message); 34 | expect(info).toHaveBeenCalledWith(message); 35 | }); 36 | }); 37 | 38 | describe("debug", () => { 39 | it("should call debug with the correct message", () => { 40 | const message = "This is a debug message"; 41 | 42 | loggerService.debug(message); 43 | expect(debug).toHaveBeenCalledWith(message); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/services/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { debug, info, warning } from "@actions/core"; 2 | 3 | export class LoggerService { 4 | warn(message: string): void { 5 | warning(message); 6 | } 7 | 8 | info(message: string): void { 9 | info(message); 10 | } 11 | 12 | debug(message: string) { 13 | debug(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | #checkov:skip=CKV_DOCKER_2: required 2 | FROM alpine:3 3 | 4 | WORKDIR /app 5 | 6 | COPY entrypoint.sh . 7 | RUN chmod +x entrypoint.sh 8 | 9 | CMD ["/bin/sh", "entrypoint.sh"] 10 | 11 | USER 1000:1000 12 | -------------------------------------------------------------------------------- /test/docker-compose-fail.yml: -------------------------------------------------------------------------------- 1 | services: 2 | service-a: 3 | image: busybox 4 | command: ["sh", "-c", "exit 1"] 5 | -------------------------------------------------------------------------------- /test/docker-compose-web-mysql.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - .:/app 8 | - /app/vendor 9 | environment: 10 | - DB_HOST=mysql 11 | - DB_USER=root 12 | - DB_PASSWORD=12345 13 | - DATABASE=testing 14 | depends_on: 15 | - mysql 16 | 17 | mysql: 18 | image: mariadb:latest 19 | environment: 20 | - MYSQL_HOST=127.0.0.1 21 | - MYSQL_USER=root 22 | - MARIADB_ROOT_PASSWORD=12345 23 | - MYSQL_DB=testing 24 | -------------------------------------------------------------------------------- /test/docker-compose-with-env.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | test_volume: {} 3 | 4 | services: 5 | service-a: 6 | image: ${IMAGE_NAME} 7 | command: ["tail", "-f", "/dev/null"] 8 | -------------------------------------------------------------------------------- /test/docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | services: 2 | service-d: 3 | image: busybox 4 | command: ["tail", "-f", "/dev/null"] 5 | profiles: [profile-2] 6 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | service-a: 3 | image: busybox 4 | command: ["tail", "-f", "/dev/null"] 5 | volumes: 6 | - test_volume:/test:Z 7 | service-b: 8 | image: busybox 9 | command: ["tail", "-f", "/dev/null"] 10 | profiles: [profile-1] 11 | service-c: 12 | image: busybox 13 | command: ["tail", "-f", "/dev/null"] 14 | profiles: [profile-2] 15 | 16 | volumes: 17 | test_volume: {} 18 | -------------------------------------------------------------------------------- /test/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sleep 2 && cat < 6 | < --abort-on-container-exit --exit-code-from=web > 7 | ------------------------------------------------- 8 | \ ^__^ 9 | \ (oo)\_______ 10 | (__)\ )\\/\\ 11 | ||----w | 12 | || || 13 | EOF 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "NodeNext", 6 | "rootDir": "./src", 7 | "moduleResolution": "NodeNext", 8 | "baseUrl": "./", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "newLine": "lf", 17 | "noEmit": true 18 | }, 19 | "exclude": ["./dist", "./node_modules", "./src/**/*.test.ts", "./coverage"], 20 | "include": ["./src/**/*"] 21 | } 22 | --------------------------------------------------------------------------------