├── .github
├── dependabot.yml
└── workflows
│ ├── cleanup.yml
│ ├── draft-release.yml
│ ├── integration.yml
│ ├── publish.yml
│ ├── release.yml
│ └── unit.yml
├── .gitignore
├── .prettierrc.js
├── CHANGELOG.md
├── CODEOWNERS
├── LICENSE
├── README.md
├── action.yml
├── dist
└── main
│ └── index.js
├── eslint.config.mjs
├── example-app
├── .dockerignore
├── Dockerfile
├── index.js
├── package-lock.json
└── package.json
├── package-lock.json
├── package.json
├── src
├── main.ts
└── output-parser.ts
├── tests
├── e2e.test.ts
├── fixtures
│ ├── env_vars.txt
│ ├── job.yaml
│ └── service.yaml
└── unit
│ ├── main.test.ts
│ └── output-parser.test.ts
└── tsconfig.json
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | rebase-strategy: 'disabled'
6 | schedule:
7 | interval: 'daily'
8 | commit-message:
9 | prefix: 'security: '
10 | open-pull-requests-limit: 0 # only check security updates
11 |
--------------------------------------------------------------------------------
/.github/workflows/cleanup.yml:
--------------------------------------------------------------------------------
1 | name: 'Cleanup'
2 |
3 | on:
4 | schedule:
5 | - cron: '0 */6 * * *'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: 'read'
10 | id-token: 'write'
11 |
12 | jobs:
13 | cleanup:
14 | runs-on: 'ubuntu-latest'
15 |
16 | steps:
17 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
18 |
19 | - uses: 'google-github-actions/auth@v2' # ratchet:exclude
20 | with:
21 | workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
22 | service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
23 |
24 | - uses: 'google-github-actions/setup-gcloud@v2' # ratchet:exclude
25 | with:
26 | version: 'latest'
27 |
28 | - name: 'Delete services'
29 | run: |-
30 | gcloud config set core/project "${{ vars.PROJECT_ID }}"
31 | gcloud config set run/region "us-central1"
32 |
33 | # List and delete all services that were deployed 30 minutes ago or
34 | # earlier. The date math here is a little weird, but we're looking for
35 | # deployments "earlier than" 30 minutes ago, so it's less than since
36 | # time increases.
37 | (IFS=$'\n'; for NAME in $(gcloud run services list --format="value(name)" --filter="metadata.creationTimestamp < '-pt30m'"); do
38 | echo "Deleting ${NAME}..."
39 | gcloud run services delete ${NAME} --quiet --async
40 | done)
41 |
--------------------------------------------------------------------------------
/.github/workflows/draft-release.yml:
--------------------------------------------------------------------------------
1 | name: 'Draft release'
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version_strategy:
7 | description: 'Version strategy: The strategy to used to update the version based on semantic versioning (more info at https://semver.org/).'
8 | required: true
9 | default: 'patch'
10 | type: 'choice'
11 | options:
12 | - 'major'
13 | - 'minor'
14 | - 'patch'
15 |
16 | jobs:
17 | draft-release:
18 | uses: 'google-github-actions/.github/.github/workflows/draft-release.yml@v3' # ratchet:exclude
19 | with:
20 | version_strategy: '${{ github.event.inputs.version_strategy }}'
21 | secrets:
22 | ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
23 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: 'Integration'
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | - 'release/**/*'
8 | pull_request:
9 | branches:
10 | - 'main'
11 | - 'release/**/*'
12 | workflow_dispatch:
13 |
14 | concurrency:
15 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
16 | cancel-in-progress: true
17 |
18 | permissions:
19 | contents: 'read'
20 | id-token: 'write'
21 |
22 | jobs:
23 | deploy:
24 | runs-on: 'ubuntu-latest'
25 |
26 | strategy:
27 | fail-fast: false
28 | matrix:
29 | include:
30 | - name: 'image'
31 | image: 'gcr.io/cloudrun/hello'
32 | - name: 'source'
33 | source: 'example-app'
34 |
35 | name: 'from_${{ matrix.name }}'
36 |
37 | steps:
38 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
39 |
40 | - name: 'Compute service name'
41 | run: |-
42 | echo "SERVICE_NAME=${GITHUB_JOB}-${{ matrix.name }}-${GITHUB_SHA::7}-${GITHUB_RUN_NUMBER}" >> ${GITHUB_ENV}
43 |
44 | - uses: 'actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a' # ratchet:actions/setup-node@v4
45 | with:
46 | node-version: '20.12.x' # https://github.com/nodejs/node/issues/53033
47 |
48 | - run: 'npm ci && npm run build'
49 |
50 | - uses: 'google-github-actions/auth@v2' # ratchet:exclude
51 | with:
52 | workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
53 | service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
54 |
55 | - id: 'deploy-cloudrun'
56 | name: 'Deploy'
57 | uses: './'
58 | with:
59 | image: '${{ matrix.image }}'
60 | source: '${{ matrix.source }}'
61 | service: '${{ env.SERVICE_NAME }}'
62 | env_vars: |-
63 | FOO=bar
64 | ZIP=zap\,with|separators\,and&stuff
65 | env_vars_file: './tests/fixtures/env_vars.txt'
66 | secrets: |-
67 | MY_SECRET=${{ vars.SECRET_NAME }}:latest
68 | MY_SECOND_SECRET=${{ vars.SECRET_NAME }}:1
69 | labels: |-
70 | label1=value1
71 | label2=value2
72 | skip_default_labels: true
73 | flags: '--cpu=2 --concurrency=20'
74 |
75 | - name: 'Run initial deploy tests'
76 | run: 'npm run e2e-tests'
77 | env:
78 | PROJECT_ID: ${{ vars.PROJECT_ID }}
79 | SERVICE: '${{ env.SERVICE_NAME }}'
80 | ENV: |-
81 | {
82 | "FOO": "bar",
83 | "ZIP": "zap,with|separators,and&stuff",
84 | "TEXT_FOO": "bar",
85 | "TEXT_ZIP": "zap,with|separators,and&stuff"
86 | }
87 | SECRET_ENV: |-
88 | {
89 | "MY_SECRET": "${{ vars.SECRET_NAME }}:latest",
90 | "MY_SECOND_SECRET": "${{ vars.SECRET_NAME }}:1"
91 | }
92 | PARAMS: |-
93 | {
94 | "cpu": "2",
95 | "containerConcurrency": "20"
96 | }
97 | LABELS: |-
98 | {
99 | "label1": "value1",
100 | "label2": "value2"
101 | }
102 |
103 | - id: 'deploy-cloudrun-again'
104 | name: 'Deploy again'
105 | uses: './'
106 | with:
107 | image: '${{ matrix.image }}'
108 | source: '${{ matrix.source }}'
109 | service: '${{ env.SERVICE_NAME }}'
110 | env_vars: |-
111 | ABC=123
112 | DEF=456
113 | env_vars_update_strategy: 'overwrite'
114 | secrets: /api/secrets/my-secret=${{ vars.SECRET_NAME }}:latest
115 | secrets_update_strategy: 'overwrite'
116 | to_revision: 'LATEST=100'
117 |
118 | - name: 'Run re-deploy tests'
119 | run: 'npm run e2e-tests'
120 | env:
121 | PROJECT_ID: ${{ vars.PROJECT_ID }}
122 | SERVICE: '${{ env.SERVICE_NAME }}'
123 | ENV: |-
124 | {
125 | "ABC": "123",
126 | "DEF": "456"
127 | }
128 | SECRET_ENV: |-
129 | {}
130 | SECRET_VOLUMES: |-
131 | {
132 | "/api/secrets/my-secret": "${{ vars.SECRET_NAME }}:latest"
133 | }
134 | PARAMS: |-
135 | {
136 | "cpu": "2",
137 | "containerConcurrency": "20"
138 | }
139 | LABELS: |-
140 | {
141 | "label1": "value1",
142 | "label2": "value2",
143 | "commit-sha": "${{ github.sha }}",
144 | "managed-by": "github-actions"
145 | }
146 | REVISION_COUNT: 2
147 |
148 | metadata:
149 | runs-on: 'ubuntu-latest'
150 |
151 | steps:
152 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
153 |
154 | - name: 'Compute service name'
155 | run: |-
156 | echo "SERVICE_NAME=${GITHUB_JOB}-metadata-${GITHUB_SHA::7}-${GITHUB_RUN_NUMBER}" >> ${GITHUB_ENV}
157 |
158 | - name: 'Set service name in metadata YAML'
159 | run: |-
160 | sed -i "s/run-full-yaml/${{ env.SERVICE_NAME }}/" ./tests/fixtures/service.yaml
161 |
162 | - uses: 'actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a' # ratchet:actions/setup-node@v4
163 | with:
164 | node-version: '20.12.x' # https://github.com/nodejs/node/issues/53033
165 |
166 | - run: 'npm ci && npm run build'
167 |
168 | - uses: 'google-github-actions/auth@v2' # ratchet:exclude
169 | with:
170 | workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
171 | service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
172 |
173 | - id: 'deploy-cloudrun'
174 | name: 'Deploy'
175 | uses: './'
176 | with:
177 | metadata: './tests/fixtures/service.yaml'
178 |
179 | - name: 'Run initial deploy tests'
180 | run: 'npm run e2e-tests'
181 | env:
182 | PROJECT_ID: '${{ vars.PROJECT_ID }}'
183 | SERVICE: '${{ env.SERVICE_NAME }}'
184 | PARAMS: |-
185 | {
186 | "cpu": "2",
187 | "memory": "1Gi",
188 | "containerConcurrency": "20"
189 | }
190 | ANNOTATIONS: |-
191 | {
192 | "run.googleapis.com/cloudsql-instances": "test-project:us-central1:my-test-instance"
193 | }
194 | LABELS: |-
195 | {
196 | "test_label": "test_value"
197 | }
198 |
199 | - id: 'deploy-cloudrun-again'
200 | name: 'Deploy again'
201 | uses: './'
202 | with:
203 | image: 'gcr.io/cloudrun/hello'
204 | service: '${{ env.SERVICE_NAME }}'
205 | to_revision: 'LATEST=100'
206 |
207 | - name: 'Run re-deploy tests'
208 | run: 'npm run e2e-tests' # Check that config isn't overwritten
209 | env:
210 | PROJECT_ID: '${{ vars.PROJECT_ID }}'
211 | SERVICE: '${{ env.SERVICE_NAME }}'
212 | PARAMS: |-
213 | {
214 | "cpu": "2",
215 | "memory": "1Gi",
216 | "containerConcurrency": "20"
217 | }
218 | ANNOTATIONS: |-
219 | {
220 | "run.googleapis.com/cloudsql-instances": "test-project:us-central1:my-test-instance"
221 | }
222 | REVISION_COUNT: 2
223 |
224 | jobs:
225 | runs-on: 'ubuntu-latest'
226 |
227 | steps:
228 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
229 |
230 | - name: 'Compute job name'
231 | run: |-
232 | echo "JOB_NAME=${GITHUB_JOB}-job-${GITHUB_SHA::7}-${GITHUB_RUN_NUMBER}" >> ${GITHUB_ENV}
233 |
234 | - uses: 'actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a' # ratchet:actions/setup-node@v4
235 | with:
236 | node-version: '20.12.x' # https://github.com/nodejs/node/issues/53033
237 |
238 | - run: 'npm ci && npm run build'
239 |
240 | - uses: 'google-github-actions/auth@v2' # ratchet:exclude
241 | with:
242 | workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
243 | service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
244 |
245 | - id: 'deploy-cloudrun'
246 | name: 'Deploy'
247 | uses: './'
248 | with:
249 | image: 'gcr.io/cloudrun/hello'
250 | job: '${{ env.JOB_NAME }}'
251 | env_vars: |-
252 | FOO=bar
253 | ZIP=zap\,with|separators\,and&stuff
254 | env_vars_file: './tests/fixtures/env_vars.txt'
255 | secrets: |-
256 | MY_SECRET=${{ vars.SECRET_NAME }}:latest
257 | MY_SECOND_SECRET=${{ vars.SECRET_NAME }}:1
258 | labels: |-
259 | label1=value1
260 | label2=value2
261 | skip_default_labels: true
262 | flags: '--cpu=2'
263 |
264 | - name: 'Run initial deploy tests'
265 | run: 'npm run e2e-tests'
266 | env:
267 | PROJECT_ID: ${{ vars.PROJECT_ID }}
268 | JOB: '${{ env.JOB_NAME }}'
269 | ENV: |-
270 | {
271 | "FOO": "bar",
272 | "ZIP": "zap,with|separators,and&stuff",
273 | "TEXT_FOO": "bar",
274 | "TEXT_ZIP": "zap,with|separators,and&stuff"
275 | }
276 | SECRET_ENV: |-
277 | {
278 | "MY_SECRET": "${{ vars.SECRET_NAME }}:latest",
279 | "MY_SECOND_SECRET": "${{ vars.SECRET_NAME }}:1"
280 | }
281 | LABELS: |-
282 | {
283 | "label1": "value1",
284 | "label2": "value2"
285 | }
286 |
287 | - id: 'deploy-cloudrun-again'
288 | name: 'Deploy again'
289 | uses: './'
290 | with:
291 | image: 'gcr.io/cloudrun/hello'
292 | job: '${{ env.JOB_NAME }}'
293 | env_vars: |-
294 | ABC=123
295 | DEF=456
296 | env_vars_update_strategy: 'overwrite'
297 | secrets: /api/secrets/my-secret=${{ vars.SECRET_NAME }}:latest
298 |
299 | - name: 'Run re-deploy tests'
300 | run: 'npm run e2e-tests'
301 | env:
302 | PROJECT_ID: ${{ vars.PROJECT_ID }}
303 | JOB: '${{ env.JOB_NAME }}'
304 | ENV: |-
305 | {
306 | "ABC": "123",
307 | "DEF": "456"
308 | }
309 | SECRET_VOLUMES: |-
310 | {
311 | "/api/secrets/my-secret": "${{ vars.SECRET_NAME }}:latest"
312 | }
313 | LABELS: |-
314 | {
315 | "label1": "value1",
316 | "label2": "value2",
317 | "commit-sha": "${{ github.sha }}",
318 | "managed-by": "github-actions"
319 | }
320 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish immutable action version'
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types:
7 | - 'published'
8 |
9 | jobs:
10 | publish:
11 | runs-on: 'ubuntu-latest'
12 | permissions:
13 | contents: 'read'
14 | id-token: 'write'
15 | packages: 'write'
16 |
17 | steps:
18 | - name: 'Checkout'
19 | uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
20 |
21 | - name: 'Publish'
22 | id: 'publish'
23 | uses: 'actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978' # ratchet:actions/publish-immutable-action@v0.0.4
24 | with:
25 | github-token: '${{ secrets.GITHUB_TOKEN }}'
26 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: 'Release'
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | - 'release/**/*'
8 |
9 | jobs:
10 | release:
11 | uses: 'google-github-actions/.github/.github/workflows/release.yml@v3' # ratchet:exclude
12 | secrets:
13 | ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
14 |
--------------------------------------------------------------------------------
/.github/workflows/unit.yml:
--------------------------------------------------------------------------------
1 | name: 'Unit'
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | - 'release/**/*'
8 | pull_request:
9 | branches:
10 | - 'main'
11 | - 'release/**/*'
12 | workflow_dispatch:
13 |
14 | concurrency:
15 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | unit:
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | os:
24 | - 'ubuntu-latest'
25 | - 'windows-latest'
26 | - 'macos-latest'
27 | runs-on: '${{ matrix.os }}'
28 |
29 | steps:
30 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
31 |
32 | - uses: 'actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a' # ratchet:actions/setup-node@v4
33 | with:
34 | node-version: '20.x'
35 |
36 | - name: 'npm build'
37 | run: 'npm ci && npm run build'
38 |
39 | - name: 'npm lint'
40 | # There's no need to run the linter for each operating system, since it
41 | # will find the same thing 3x and clog up the PR review.
42 | if: ${{ matrix.os == 'ubuntu-latest' }}
43 | run: 'npm run lint'
44 |
45 | - name: 'npm test'
46 | run: 'npm run test'
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | runner/
3 |
4 | # Rest of the file pulled from https://github.com/github/gitignore/blob/main/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 | # TypeScript v1 declaration files
30 | typings/
31 |
32 | # TypeScript cache
33 | *.tsbuildinfo
34 |
35 | # Optional npm cache directory
36 | .npm
37 |
38 | # Optional eslint cache
39 | .eslintcache
40 |
41 | # Optional REPL history
42 | .node_repl_history
43 |
44 | # Output of 'npm pack'
45 | *.tgz
46 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'always',
3 | bracketSpacing: true,
4 | endOfLine: 'auto',
5 | jsxSingleQuote: true,
6 | printWidth: 100,
7 | quoteProps: 'consistent',
8 | semi: true,
9 | singleQuote: true,
10 | tabWidth: 2,
11 | trailingComma: 'all',
12 | useTabs: false,
13 | };
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Changelogs for each release are located on the [releases page](https://github.com/google-github-actions/deploy-cloudrun/releases).
4 |
5 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @google-github-actions/maintainers
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "{}"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright {yyyy} {name of copyright owner}
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # deploy-cloudrun
2 |
3 | The `deploy-cloudrun` GitHub Action deploys to Google [Cloud Run][cloud-run]. It
4 | can deploy a container image or from source, and the resulting service URL is
5 | available as a GitHub Actions output for use in future steps.
6 |
7 | **This is not an officially supported Google product, and it is not covered by a
8 | Google Cloud support contract. To report bugs or request features in a Google
9 | Cloud product, please contact [Google Cloud
10 | support](https://cloud.google.com/support).**
11 |
12 |
13 | ## Prerequisites
14 |
15 | - This action requires Google Cloud credentials that are authorized to access
16 | the secrets being requested. See [Authorization](#authorization) for more
17 | information.
18 |
19 | - This action runs using Node 20. If you are using self-hosted GitHub Actions
20 | runners, you must use a [runner
21 | version](https://github.com/actions/virtual-environments) that supports this
22 | version or newer.
23 |
24 |
25 | ## Usage
26 |
27 | ```yaml
28 | jobs:
29 | job_id:
30 | # ...
31 |
32 | permissions:
33 | contents: 'read'
34 | id-token: 'write'
35 |
36 | steps:
37 | - uses: 'actions/checkout@v4'
38 |
39 | - uses: 'google-github-actions/auth@v2'
40 | with:
41 | workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
42 | service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
43 |
44 | - id: 'deploy'
45 | uses: 'google-github-actions/deploy-cloudrun@v2'
46 | with:
47 | service: 'hello-cloud-run'
48 | image: 'gcr.io/cloudrun/hello'
49 |
50 | - name: 'Use output'
51 | run: 'curl "${{ steps.deploy.outputs.url }}"'
52 | ```
53 |
54 | ## Inputs
55 |
56 |
57 |
58 | - service
: _(Optional)_ ID of the service or fully-qualified identifier of the service. This is
59 | required unless providing `metadata` or `job`.
60 |
61 | - job
: _(Optional)_ ID of the job or fully-qualified identifier of the job. This is required
62 | unless providing `metadata` or `service`.
63 |
64 | - metadata
: _(Optional)_ YAML service description for the Cloud Run service. This is required
65 | unless providing `service` or `job`.
66 |
67 | - image
: _(Optional)_ (Required, unless providing `metadata` or `source`) Fully-qualified name
68 | of the container image to deploy. For example:
69 |
70 | gcr.io/cloudrun/hello:latest
71 |
72 | or
73 |
74 | us-docker.pkg.dev/my-project/my-container/image:1.2.3
75 |
76 | - source
: _(Optional)_ (Required, unless providing `metadata`, `image`, or `job`) Path to source
77 | to deploy. If specified, this will deploy the Cloud Run service from the
78 | code specified at the given source directory.
79 |
80 | Learn more about the required permissions in [Deploying from source
81 | code](https://cloud.google.com/run/docs/deploying-source-code).
82 |
83 | - suffix
: _(Optional)_ String suffix to append to the revision name. Revision names always start
84 | with the service name automatically. For example, specifying `v1` for a
85 | service named `helloworld`, would lead to a revision named
86 | `helloworld-v1`. This option only applies to services.
87 |
88 | - env_vars
: _(Optional)_ List of environment variables that should be set in the environment.
89 | These are comma-separated or newline-separated `KEY=VALUE`. Keys or values
90 | that contain separators must be escaped with a backslash (e.g. `\,` or
91 | `\\n`) unless quoted. Any leading or trailing whitespace is trimmed unless
92 | values are quoted.
93 |
94 | env_vars: |-
95 | FRUIT=apple
96 | SENTENCE=" this will retain leading and trailing spaces "
97 |
98 | This value will only be set if the input is a non-empty value. If a
99 | non-empty value is given, the field values will be overwritten (not
100 | merged). To remove all values, set the value to the literal string `{}`.
101 |
102 | If both `env_vars` and `env_vars_file` are specified, the keys in
103 | `env_vars` will take precedence over the keys in `env_vars_file`.
104 |
105 | - env_vars_file
: _(Optional)_ Path to a file on disk, relative to the workspace, that defines
106 | environment variables. The file can be newline-separated KEY=VALUE pairs,
107 | JSON, or YAML format. If both `env_vars` and `env_vars_file` are
108 | specified, the keys in env_vars will take precedence over the keys in
109 | env_vars_file.
110 |
111 | NAME=person
112 | EMAILS=foo@bar.com\,zip@zap.com
113 |
114 | When specified as KEY=VALUE pairs, the same escaping rules apply as
115 | described in `env_vars`. You do not have to escape YAML or JSON.
116 |
117 | If both `env_vars` and `env_vars_file` are specified, the keys in
118 | `env_vars` will take precedence over the keys in `env_vars_file`.
119 |
120 | **⚠️ DEPRECATION NOTICE:** This input is deprecated and will be removed in
121 | the next major version release.
122 |
123 | - env_vars_update_strategy
: _(Required, default: `merge`)_ Controls how the environment variables are set on the Cloud Run service.
124 | If set to "merge", then the environment variables are _merged_ with any
125 | upstream values. If set to "overwrite", then all environment variables on
126 | the Cloud Run service will be replaced with exactly the values given by
127 | the GitHub Action (making it authoritative).
128 |
129 | - secrets
: _(Optional)_ List of KEY=VALUE pairs to use as secrets. These are comma-separated or
130 | newline-separated `KEY=VALUE`. Keys or values that contain separators must
131 | be escaped with a backslash (e.g. `\,` or `\\n`) unless quoted. Any
132 | leading or trailing whitespace is trimmed unless values are quoted.
133 |
134 | These can either be injected as environment variables or mounted as
135 | volumes. Keys starting with a forward slash '/' are mount paths. All other
136 | keys correspond to environment variables:
137 |
138 | with:
139 | secrets: |-
140 | # As an environment variable:
141 | KEY1=secret-key-1:latest
142 |
143 | # As a volume mount:
144 | /secrets/api/key=secret-key-2:latest
145 |
146 | This value will only be set if the input is a non-empty value. If a
147 | non-empty value is given, the field values will be overwritten (not
148 | merged). To remove all values, set the value to the literal string `{}`.
149 |
150 | - secrets_update_strategy
: _(Required, default: `merge`)_ Controls how the secrets are set on the Cloud Run service. If set to
151 | `merge`, then the secrets are merged with any upstream values. If set to
152 | `overwrite`, then all secrets on the Cloud Run service will be replaced
153 | with exactly the values given by the GitHub Action (making it
154 | authoritative).
155 |
156 | - labels
: _(Optional)_ List of labels that should be set on the function. These are
157 | comma-separated or newline-separated `KEY=VALUE`. Keys or values that
158 | contain separators must be escaped with a backslash (e.g. `\,` or `\\n`)
159 | unless quoted. Any leading or trailing whitespace is trimmed unless values
160 | are quoted.
161 |
162 | labels: |-
163 | labela=my-label
164 | labelb=my-other-label
165 |
166 | This value will only be set if the input is a non-empty value. If a
167 | non-empty value is given, the field values will be overwritten (not
168 | merged). To remove all values, set the value to the literal string `{}`.
169 |
170 | Google Cloud restricts the allowed values and length for labels. Please
171 | see the Google Cloud documentation for labels for more information.
172 |
173 | - skip_default_labels
: _(Optional, default: `false`)_ Skip applying the special annotation labels that indicate the deployment
174 | came from GitHub Actions. The GitHub Action will automatically apply the
175 | following labels which Cloud Run uses to enhance the user experience:
176 |
177 | managed-by: github-actions
178 | commit-sha:
179 |
180 | Setting this to `true` will skip adding these special labels.
181 |
182 | - tag
: _(Optional)_ Traffic tag to assign to the newly-created revision. This option only
183 | applies to services.
184 |
185 | - timeout
: _(Optional)_ Maximum request execution time, specified as a duration like "10m5s" for
186 | ten minutes and 5 seconds.
187 |
188 | - flags
: _(Optional)_ Space separate list of additional Cloud Run flags to pass to the deploy
189 | command. This can be used to apply advanced features that are not exposed
190 | via this GitHub Action. For Cloud Run services, this command will be
191 | `gcloud run deploy`. For Cloud Run jobs, this command will be `gcloud jobs
192 | deploy`.
193 |
194 | with:
195 | flags: '--add-cloudsql-instances=...'
196 |
197 | Flags that include other flags must quote the _entire_ outer flag value. For
198 | example, to pass `--args=-X=123`:
199 |
200 | with:
201 | flags: '--add-cloudsql-instances=... "--args=-X=123"'
202 |
203 | See the [complete list of
204 | flags](https://cloud.google.com/sdk/gcloud/reference/run/deploy#FLAGS) for
205 | more information.
206 |
207 | Please note, this GitHub Action does not parse or validate the flags. You
208 | are responsible for making sure the flags are available on the gcloud
209 | version and subcommand.
210 |
211 | - no_traffic
: _(Optional, default: `false`)_ If true, the newly deployed revision will not receive traffic. This option
212 | only applies to services.
213 |
214 | - revision_traffic
: _(Optional)_ Comma-separated list of revision traffic assignments.
215 |
216 | with:
217 | revision_traffic: 'my-revision=10' # percentage
218 |
219 | To update traffic to the latest revision, use the special tag "LATEST":
220 |
221 | with:
222 | revision_traffic: 'LATEST=100'
223 |
224 | This is mutually-exclusive with `tag_traffic`. This option only applies
225 | to services.
226 |
227 | - tag_traffic
: _(Optional)_ Comma-separated list of tag traffic assignments.
228 |
229 | with:
230 | tag_traffic: 'my-tag=10' # percentage
231 |
232 | This is mutually-exclusive with `revision_traffic`. This option only
233 | applies to services.
234 |
235 | - update_traffic_flags
: _(Optional)_ Space separate list of additional Cloud Run flags to pass to the `gcloud
236 | run services update-traffic` command. This can be used to apply advanced
237 | features that are not exposed via this GitHub Action. This flag only
238 | applies when `revision_traffic` or `tag_traffic` is set.
239 |
240 | with:
241 | traffic_flags: '--set-tags=...'
242 |
243 | Flags that include other flags must quote the _entire_ outer flag value. For
244 | example, to pass `--args=-X=123`:
245 |
246 | with:
247 | flags: '--set-tags=... "--args=-X=123"'
248 |
249 | See the [complete list of
250 | flags](https://cloud.google.com/sdk/gcloud/reference/run/services/update#FLAGS)
251 | for more information.
252 |
253 | Please note, this GitHub Action does not parse or validate the flags. You
254 | are responsible for making sure the flags are available on the gcloud
255 | version and subcommand.
256 |
257 | - project_id
: _(Optional)_ ID of the Google Cloud project in which to deploy the service.
258 |
259 | - region
: _(Optional, default: `us-central1`)_ Region in which the Cloud Run services are deployed.
260 |
261 | - gcloud_version
: _(Optional)_ Version of the Cloud SDK to install. If unspecified or set to "latest",
262 | the latest available gcloud SDK version for the target platform will be
263 | installed. Example: "290.0.1".
264 |
265 | - gcloud_component
: _(Optional)_ Version of the Cloud SDK components to install and use.
266 |
267 |
268 |
269 |
270 | ### Custom metadata YAML
271 |
272 | For advanced use cases, you can define a custom Cloud Run metadata file. This is
273 | a YAML description of the Cloud Run service or job. This allows you to customize your
274 | service configuration, such as [memory
275 | limits](https://cloud.google.com/run/docs/configuring/memory-limits), [CPU
276 | allocation](https://cloud.google.com/run/docs/configuring/cpu), [max
277 | instances](https://cloud.google.com/run/docs/configuring/max-instances), and
278 | [more](https://cloud.google.com/sdk/gcloud/reference/run/deploy#OPTIONAL-FLAGS).
279 |
280 | **⚠️ When using a custom metadata YAML file, all other inputs are ignored!**
281 |
282 | - `metadata`: (Optional) The path to a Cloud Run service or job metadata file.
283 |
284 | To [deploying a new service](https://cloud.google.com/run/docs/deploying#yaml)
285 | to create a new YAML service definition:
286 |
287 | ```yaml
288 | apiVersion: serving.knative.dev/v1
289 | kind: Service
290 | metadata:
291 | name: SERVICE
292 | spec:
293 | template:
294 | spec:
295 | containers:
296 | - image: IMAGE
297 | ```
298 |
299 | To update a revision or to [deploy a new revision of an existing service](https://cloud.google.com/run/docs/deploying#yaml_1), download and modify the YAML service definition:
300 |
301 | ```shell
302 | gcloud run services describe SERVICE --format yaml > service.yaml
303 | ```
304 |
305 | ## Allowing unauthenticated requests
306 |
307 | A Cloud Run product recommendation is that CI/CD systems not set or change
308 | settings for allowing unauthenticated invocations. New deployments are
309 | automatically private services, while deploying a revision of a public
310 | (unauthenticated) service will preserve the IAM setting of public
311 | (unauthenticated). For more information, see [Controlling access on an individual service](https://cloud.google.com/run/docs/securing/managing-access).
312 |
313 | ## Outputs
314 |
315 |
316 |
317 | - `url`: The URL of the Cloud Run service.
318 |
319 |
320 |
321 |
322 |
323 | ## Authorization
324 |
325 | There are a few ways to authenticate this action. The caller must have
326 | permissions to access the secrets being requested.
327 |
328 | You will need to authenticate to Google Cloud as a service account with the
329 | following roles:
330 |
331 | - Cloud Run Admin (`roles/run.admin`):
332 | - Can create, update, and delete services.
333 | - Can get and set IAM policies.
334 |
335 | This service account needs to be a member of the `Compute Engine default service account`,
336 | `(PROJECT_NUMBER-compute@developer.gserviceaccount.com)`, with role
337 | `Service Account User`. To grant a user permissions for a service account, use
338 | one of the methods found in [Configuring Ownership and access to a service account](https://cloud.google.com/iam/docs/granting-roles-to-service-accounts#granting_access_to_a_user_for_a_service_account).
339 |
340 |
341 | ### Via google-github-actions/auth
342 |
343 | Use [google-github-actions/auth](https://github.com/google-github-actions/auth)
344 | to authenticate the action. You can use [Workload Identity Federation][wif] or
345 | traditional [Service Account Key JSON][sa] authentication.
346 |
347 | ```yaml
348 | jobs:
349 | job_id:
350 | permissions:
351 | contents: 'read'
352 | id-token: 'write'
353 |
354 | steps:
355 |
356 | # ...
357 |
358 | - uses: 'google-github-actions/auth@v2'
359 | with:
360 | workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
361 | service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
362 |
363 | - uses: 'google-github-actions/deploy-cloudrun@v2'
364 | with:
365 | image: 'gcr.io/cloudrun/hello'
366 | service: 'hello-cloud-run'
367 | ```
368 |
369 | ### Via Application Default Credentials
370 |
371 | If you are hosting your own runners, **and** those runners are on Google Cloud,
372 | you can leverage the Application Default Credentials of the instance. This will
373 | authenticate requests as the service account attached to the instance. **This
374 | only works using a custom runner hosted on GCP.**
375 |
376 | ```yaml
377 | jobs:
378 | job_id:
379 | steps:
380 | # ...
381 |
382 | - uses: 'google-github-actions/deploy-cloudrun@v2'
383 | with:
384 | image: 'gcr.io/cloudrun/hello'
385 | service: 'hello-cloud-run'
386 | ```
387 |
388 | The action will automatically detect and use the Application Default
389 | Credentials.
390 |
391 | ## Example Workflows
392 |
393 | * [Deploy from source](https://github.com/google-github-actions/example-workflows/blob/main/workflows/deploy-cloudrun/cloudrun-source.yml)
394 |
395 | * [Build and deploy a container](https://github.com/google-github-actions/example-workflows/blob/main/workflows/deploy-cloudrun/cloudrun-docker.yml)
396 |
397 |
398 | [cloud-run]: https://cloud.google.com/run
399 | [sa]: https://cloud.google.com/iam/docs/creating-managing-service-accounts
400 | [wif]: https://cloud.google.com/iam/docs/workload-identity-federation
401 | [create-key]: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
402 | [gh-runners]: https://help.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners
403 | [gh-secret]: https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets
404 | [setup-gcloud]: ./setup-gcloud
405 | [artifact-api]: https://console.cloud.google.com/flows/enableapi?apiid=artifactregistry.googleapis.com
406 | [repo]: https://cloud.google.com/artifact-registry/docs/manage-repos
407 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | name: 'Deploy to Cloud Run'
16 | author: 'Google LLC'
17 | description: |-
18 | Use this action to deploy a container or source code to Google Cloud Run.
19 |
20 | inputs:
21 | service:
22 | description: |-
23 | ID of the service or fully-qualified identifier of the service. This is
24 | required unless providing `metadata` or `job`.
25 | required: false
26 |
27 | job:
28 | description: |-
29 | ID of the job or fully-qualified identifier of the job. This is required
30 | unless providing `metadata` or `service`.
31 | required: false
32 |
33 | metadata:
34 | description: |-
35 | YAML service description for the Cloud Run service. This is required
36 | unless providing `service` or `job`.
37 | required: false
38 |
39 | image:
40 | description: |-
41 | (Required, unless providing `metadata` or `source`) Fully-qualified name
42 | of the container image to deploy. For example:
43 |
44 | gcr.io/cloudrun/hello:latest
45 |
46 | or
47 |
48 | us-docker.pkg.dev/my-project/my-container/image:1.2.3
49 | required: false
50 |
51 | source:
52 | description: |-
53 | (Required, unless providing `metadata`, `image`, or `job`) Path to source
54 | to deploy. If specified, this will deploy the Cloud Run service from the
55 | code specified at the given source directory.
56 |
57 | Learn more about the required permissions in [Deploying from source
58 | code](https://cloud.google.com/run/docs/deploying-source-code).
59 | required: false
60 |
61 | suffix:
62 | description: |-
63 | String suffix to append to the revision name. Revision names always start
64 | with the service name automatically. For example, specifying `v1` for a
65 | service named `helloworld`, would lead to a revision named
66 | `helloworld-v1`. This option only applies to services.
67 | required: false
68 |
69 | env_vars:
70 | description: |-
71 | List of environment variables that should be set in the environment.
72 | These are comma-separated or newline-separated `KEY=VALUE`. Keys or values
73 | that contain separators must be escaped with a backslash (e.g. `\,` or
74 | `\\n`) unless quoted. Any leading or trailing whitespace is trimmed unless
75 | values are quoted.
76 |
77 | env_vars: |-
78 | FRUIT=apple
79 | SENTENCE=" this will retain leading and trailing spaces "
80 |
81 | This value will only be set if the input is a non-empty value. If a
82 | non-empty value is given, the field values will be overwritten (not
83 | merged). To remove all values, set the value to the literal string `{}`.
84 |
85 | If both `env_vars` and `env_vars_file` are specified, the keys in
86 | `env_vars` will take precedence over the keys in `env_vars_file`.
87 | required: false
88 |
89 | env_vars_file:
90 | description: |-
91 | Path to a file on disk, relative to the workspace, that defines
92 | environment variables. The file can be newline-separated KEY=VALUE pairs,
93 | JSON, or YAML format. If both `env_vars` and `env_vars_file` are
94 | specified, the keys in env_vars will take precedence over the keys in
95 | env_vars_file.
96 |
97 | NAME=person
98 | EMAILS=foo@bar.com\,zip@zap.com
99 |
100 | When specified as KEY=VALUE pairs, the same escaping rules apply as
101 | described in `env_vars`. You do not have to escape YAML or JSON.
102 |
103 | If both `env_vars` and `env_vars_file` are specified, the keys in
104 | `env_vars` will take precedence over the keys in `env_vars_file`.
105 |
106 | **⚠️ DEPRECATION NOTICE:** This input is deprecated and will be removed in
107 | the next major version release.
108 | required: false
109 |
110 | env_vars_update_strategy:
111 | description: |-
112 | Controls how the environment variables are set on the Cloud Run service.
113 | If set to "merge", then the environment variables are _merged_ with any
114 | upstream values. If set to "overwrite", then all environment variables on
115 | the Cloud Run service will be replaced with exactly the values given by
116 | the GitHub Action (making it authoritative).
117 | default: 'merge'
118 | required: true
119 |
120 | secrets:
121 | description: |-
122 | List of KEY=VALUE pairs to use as secrets. These are comma-separated or
123 | newline-separated `KEY=VALUE`. Keys or values that contain separators must
124 | be escaped with a backslash (e.g. `\,` or `\\n`) unless quoted. Any
125 | leading or trailing whitespace is trimmed unless values are quoted.
126 |
127 | These can either be injected as environment variables or mounted as
128 | volumes. Keys starting with a forward slash '/' are mount paths. All other
129 | keys correspond to environment variables:
130 |
131 | with:
132 | secrets: |-
133 | # As an environment variable:
134 | KEY1=secret-key-1:latest
135 |
136 | # As a volume mount:
137 | /secrets/api/key=secret-key-2:latest
138 |
139 | This value will only be set if the input is a non-empty value. If a
140 | non-empty value is given, the field values will be overwritten (not
141 | merged). To remove all values, set the value to the literal string `{}`.
142 | required: false
143 |
144 | secrets_update_strategy:
145 | description: |-
146 | Controls how the secrets are set on the Cloud Run service. If set to
147 | `merge`, then the secrets are merged with any upstream values. If set to
148 | `overwrite`, then all secrets on the Cloud Run service will be replaced
149 | with exactly the values given by the GitHub Action (making it
150 | authoritative).
151 | default: 'merge'
152 | required: true
153 |
154 | labels:
155 | description: |-
156 | List of labels that should be set on the function. These are
157 | comma-separated or newline-separated `KEY=VALUE`. Keys or values that
158 | contain separators must be escaped with a backslash (e.g. `\,` or `\\n`)
159 | unless quoted. Any leading or trailing whitespace is trimmed unless values
160 | are quoted.
161 |
162 | labels: |-
163 | labela=my-label
164 | labelb=my-other-label
165 |
166 | This value will only be set if the input is a non-empty value. If a
167 | non-empty value is given, the field values will be overwritten (not
168 | merged). To remove all values, set the value to the literal string `{}`.
169 |
170 | Google Cloud restricts the allowed values and length for labels. Please
171 | see the Google Cloud documentation for labels for more information.
172 | required: false
173 |
174 | skip_default_labels:
175 | description: |-
176 | Skip applying the special annotation labels that indicate the deployment
177 | came from GitHub Actions. The GitHub Action will automatically apply the
178 | following labels which Cloud Run uses to enhance the user experience:
179 |
180 | managed-by: github-actions
181 | commit-sha:
182 |
183 | Setting this to `true` will skip adding these special labels.
184 | required: false
185 | default: 'false'
186 |
187 | tag:
188 | description: |-
189 | Traffic tag to assign to the newly-created revision. This option only
190 | applies to services.
191 | required: false
192 |
193 | timeout:
194 | description: |-
195 | Maximum request execution time, specified as a duration like "10m5s" for
196 | ten minutes and 5 seconds.
197 | required: false
198 |
199 | flags:
200 | description: |-
201 | Space separate list of additional Cloud Run flags to pass to the deploy
202 | command. This can be used to apply advanced features that are not exposed
203 | via this GitHub Action. For Cloud Run services, this command will be
204 | `gcloud run deploy`. For Cloud Run jobs, this command will be `gcloud jobs
205 | deploy`.
206 |
207 | with:
208 | flags: '--add-cloudsql-instances=...'
209 |
210 | Flags that include other flags must quote the _entire_ outer flag value. For
211 | example, to pass `--args=-X=123`:
212 |
213 | with:
214 | flags: '--add-cloudsql-instances=... "--args=-X=123"'
215 |
216 | See the [complete list of
217 | flags](https://cloud.google.com/sdk/gcloud/reference/run/deploy#FLAGS) for
218 | more information.
219 |
220 | Please note, this GitHub Action does not parse or validate the flags. You
221 | are responsible for making sure the flags are available on the gcloud
222 | version and subcommand.
223 | required: false
224 |
225 | no_traffic:
226 | description: |-
227 | If true, the newly deployed revision will not receive traffic. This option
228 | only applies to services.
229 | default: 'false'
230 | required: false
231 |
232 | revision_traffic:
233 | description: |-
234 | Comma-separated list of revision traffic assignments.
235 |
236 | with:
237 | revision_traffic: 'my-revision=10' # percentage
238 |
239 | To update traffic to the latest revision, use the special tag "LATEST":
240 |
241 | with:
242 | revision_traffic: 'LATEST=100'
243 |
244 | This is mutually-exclusive with `tag_traffic`. This option only applies
245 | to services.
246 | required: false
247 |
248 | tag_traffic:
249 | description: |-
250 | Comma-separated list of tag traffic assignments.
251 |
252 | with:
253 | tag_traffic: 'my-tag=10' # percentage
254 |
255 | This is mutually-exclusive with `revision_traffic`. This option only
256 | applies to services.
257 | required: false
258 |
259 | update_traffic_flags:
260 | description: |-
261 | Space separate list of additional Cloud Run flags to pass to the `gcloud
262 | run services update-traffic` command. This can be used to apply advanced
263 | features that are not exposed via this GitHub Action. This flag only
264 | applies when `revision_traffic` or `tag_traffic` is set.
265 |
266 | with:
267 | traffic_flags: '--set-tags=...'
268 |
269 | Flags that include other flags must quote the _entire_ outer flag value. For
270 | example, to pass `--args=-X=123`:
271 |
272 | with:
273 | flags: '--set-tags=... "--args=-X=123"'
274 |
275 | See the [complete list of
276 | flags](https://cloud.google.com/sdk/gcloud/reference/run/services/update#FLAGS)
277 | for more information.
278 |
279 | Please note, this GitHub Action does not parse or validate the flags. You
280 | are responsible for making sure the flags are available on the gcloud
281 | version and subcommand.
282 | required: false
283 |
284 | project_id:
285 | description: |-
286 | ID of the Google Cloud project in which to deploy the service.
287 | required: false
288 |
289 | region:
290 | description: |-
291 | Region in which the Cloud Run services are deployed.
292 | default: 'us-central1'
293 | required: false
294 |
295 | gcloud_version:
296 | description: |-
297 | Version of the Cloud SDK to install. If unspecified or set to "latest",
298 | the latest available gcloud SDK version for the target platform will be
299 | installed. Example: "290.0.1".
300 | required: false
301 |
302 | gcloud_component:
303 | description: |-
304 | Version of the Cloud SDK components to install and use.
305 | required: false
306 |
307 | outputs:
308 | url:
309 | description: |-
310 | The URL of the Cloud Run service.
311 |
312 | branding:
313 | icon: 'chevrons-right'
314 | color: 'blue'
315 |
316 | runs:
317 | using: 'node20'
318 | main: 'dist/main/index.js'
319 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import ts from 'typescript-eslint';
3 | import tsParser from '@typescript-eslint/parser';
4 |
5 | import prettierRecommended from 'eslint-plugin-prettier/recommended';
6 |
7 | export default ts.config(
8 | js.configs.recommended,
9 | ts.configs.eslintRecommended,
10 | {
11 | files: ['**/*.ts', '**/*.tsx'],
12 | languageOptions: {
13 | parser: tsParser,
14 | },
15 | },
16 | { ignores: ['dist/', '**/*.js'] },
17 | {
18 | rules: {
19 | 'no-unused-vars': 'off',
20 | },
21 | },
22 | prettierRecommended,
23 | );
24 |
--------------------------------------------------------------------------------
/example-app/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | README.md
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/example-app/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google, LLC.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Use the official lightweight Node.js 20 image.
16 | # https://hub.docker.com/_/node
17 | FROM node:20-slim
18 |
19 | # Create and change to the app directory.
20 | WORKDIR /usr/src/app
21 |
22 | # Copy application dependency manifests to the container image.
23 | # A wildcard is used to ensure both package.json AND package-lock.json are copied.
24 | # Copying this separately prevents re-running npm install on every code change.
25 | COPY package*.json ./
26 |
27 | # Install production dependencies.
28 | RUN npm ci
29 |
30 | # Copy local code to the container image.
31 | COPY . ./
32 |
33 | # Run the web service on container startup.
34 | CMD [ "npm", "start" ]
35 |
--------------------------------------------------------------------------------
/example-app/index.js:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Google, LLC.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | const package = require('./package.json')
16 | const express = require('express');
17 | const app = express();
18 |
19 | app.get('/', (req, res) => {
20 | console.log(`${package.name} received a request.`);
21 | res.send(`Congratulations, you successfully deployed a container image to Cloud Run!`);
22 | });
23 |
24 | const port = process.env.PORT || 8080;
25 | app.listen(port, () => {
26 | console.log(`${package.name} listening on port: ${port}`);
27 | });
28 |
--------------------------------------------------------------------------------
/example-app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helloworld",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.8",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
10 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
11 | "requires": {
12 | "mime-types": "~2.1.34",
13 | "negotiator": "0.6.3"
14 | }
15 | },
16 | "array-flatten": {
17 | "version": "1.1.1",
18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
20 | },
21 | "body-parser": {
22 | "version": "1.20.3",
23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
24 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
25 | "requires": {
26 | "bytes": "3.1.2",
27 | "content-type": "~1.0.5",
28 | "debug": "2.6.9",
29 | "depd": "2.0.0",
30 | "destroy": "1.2.0",
31 | "http-errors": "2.0.0",
32 | "iconv-lite": "0.4.24",
33 | "on-finished": "2.4.1",
34 | "qs": "6.13.0",
35 | "raw-body": "2.5.2",
36 | "type-is": "~1.6.18",
37 | "unpipe": "1.0.0"
38 | }
39 | },
40 | "bytes": {
41 | "version": "3.1.2",
42 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
43 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
44 | },
45 | "call-bind": {
46 | "version": "1.0.8",
47 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
48 | "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
49 | "requires": {
50 | "call-bind-apply-helpers": "^1.0.0",
51 | "es-define-property": "^1.0.0",
52 | "get-intrinsic": "^1.2.4",
53 | "set-function-length": "^1.2.2"
54 | }
55 | },
56 | "call-bind-apply-helpers": {
57 | "version": "1.0.1",
58 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
59 | "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
60 | "requires": {
61 | "es-errors": "^1.3.0",
62 | "function-bind": "^1.1.2"
63 | }
64 | },
65 | "content-disposition": {
66 | "version": "0.5.4",
67 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
68 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
69 | "requires": {
70 | "safe-buffer": "5.2.1"
71 | }
72 | },
73 | "content-type": {
74 | "version": "1.0.5",
75 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
76 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
77 | },
78 | "cookie": {
79 | "version": "0.7.1",
80 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
81 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
82 | },
83 | "cookie-signature": {
84 | "version": "1.0.6",
85 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
86 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
87 | },
88 | "debug": {
89 | "version": "2.6.9",
90 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
91 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
92 | "requires": {
93 | "ms": "2.0.0"
94 | }
95 | },
96 | "define-data-property": {
97 | "version": "1.1.4",
98 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
99 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
100 | "requires": {
101 | "es-define-property": "^1.0.0",
102 | "es-errors": "^1.3.0",
103 | "gopd": "^1.0.1"
104 | }
105 | },
106 | "depd": {
107 | "version": "2.0.0",
108 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
109 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
110 | },
111 | "destroy": {
112 | "version": "1.2.0",
113 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
114 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
115 | },
116 | "dunder-proto": {
117 | "version": "1.0.0",
118 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
119 | "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
120 | "requires": {
121 | "call-bind-apply-helpers": "^1.0.0",
122 | "es-errors": "^1.3.0",
123 | "gopd": "^1.2.0"
124 | }
125 | },
126 | "ee-first": {
127 | "version": "1.1.1",
128 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
129 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
130 | },
131 | "encodeurl": {
132 | "version": "2.0.0",
133 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
134 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
135 | },
136 | "es-define-property": {
137 | "version": "1.0.1",
138 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
139 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
140 | },
141 | "es-errors": {
142 | "version": "1.3.0",
143 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
144 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
145 | },
146 | "escape-html": {
147 | "version": "1.0.3",
148 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
149 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
150 | },
151 | "etag": {
152 | "version": "1.8.1",
153 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
154 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
155 | },
156 | "express": {
157 | "version": "4.21.2",
158 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
159 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
160 | "requires": {
161 | "accepts": "~1.3.8",
162 | "array-flatten": "1.1.1",
163 | "body-parser": "1.20.3",
164 | "content-disposition": "0.5.4",
165 | "content-type": "~1.0.4",
166 | "cookie": "0.7.1",
167 | "cookie-signature": "1.0.6",
168 | "debug": "2.6.9",
169 | "depd": "2.0.0",
170 | "encodeurl": "~2.0.0",
171 | "escape-html": "~1.0.3",
172 | "etag": "~1.8.1",
173 | "finalhandler": "1.3.1",
174 | "fresh": "0.5.2",
175 | "http-errors": "2.0.0",
176 | "merge-descriptors": "1.0.3",
177 | "methods": "~1.1.2",
178 | "on-finished": "2.4.1",
179 | "parseurl": "~1.3.3",
180 | "path-to-regexp": "0.1.12",
181 | "proxy-addr": "~2.0.7",
182 | "qs": "6.13.0",
183 | "range-parser": "~1.2.1",
184 | "safe-buffer": "5.2.1",
185 | "send": "0.19.0",
186 | "serve-static": "1.16.2",
187 | "setprototypeof": "1.2.0",
188 | "statuses": "2.0.1",
189 | "type-is": "~1.6.18",
190 | "utils-merge": "1.0.1",
191 | "vary": "~1.1.2"
192 | }
193 | },
194 | "finalhandler": {
195 | "version": "1.3.1",
196 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
197 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
198 | "requires": {
199 | "debug": "2.6.9",
200 | "encodeurl": "~2.0.0",
201 | "escape-html": "~1.0.3",
202 | "on-finished": "2.4.1",
203 | "parseurl": "~1.3.3",
204 | "statuses": "2.0.1",
205 | "unpipe": "~1.0.0"
206 | }
207 | },
208 | "forwarded": {
209 | "version": "0.2.0",
210 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
211 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
212 | },
213 | "fresh": {
214 | "version": "0.5.2",
215 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
216 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
217 | },
218 | "function-bind": {
219 | "version": "1.1.2",
220 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
221 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
222 | },
223 | "get-intrinsic": {
224 | "version": "1.2.5",
225 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
226 | "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
227 | "requires": {
228 | "call-bind-apply-helpers": "^1.0.0",
229 | "dunder-proto": "^1.0.0",
230 | "es-define-property": "^1.0.1",
231 | "es-errors": "^1.3.0",
232 | "function-bind": "^1.1.2",
233 | "gopd": "^1.2.0",
234 | "has-symbols": "^1.1.0",
235 | "hasown": "^2.0.2"
236 | }
237 | },
238 | "gopd": {
239 | "version": "1.2.0",
240 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
241 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
242 | },
243 | "has-property-descriptors": {
244 | "version": "1.0.2",
245 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
246 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
247 | "requires": {
248 | "es-define-property": "^1.0.0"
249 | }
250 | },
251 | "has-symbols": {
252 | "version": "1.1.0",
253 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
254 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
255 | },
256 | "hasown": {
257 | "version": "2.0.2",
258 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
259 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
260 | "requires": {
261 | "function-bind": "^1.1.2"
262 | }
263 | },
264 | "http-errors": {
265 | "version": "2.0.0",
266 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
267 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
268 | "requires": {
269 | "depd": "2.0.0",
270 | "inherits": "2.0.4",
271 | "setprototypeof": "1.2.0",
272 | "statuses": "2.0.1",
273 | "toidentifier": "1.0.1"
274 | }
275 | },
276 | "iconv-lite": {
277 | "version": "0.4.24",
278 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
279 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
280 | "requires": {
281 | "safer-buffer": ">= 2.1.2 < 3"
282 | }
283 | },
284 | "inherits": {
285 | "version": "2.0.4",
286 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
287 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
288 | },
289 | "ipaddr.js": {
290 | "version": "1.9.1",
291 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
292 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
293 | },
294 | "media-typer": {
295 | "version": "0.3.0",
296 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
297 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
298 | },
299 | "merge-descriptors": {
300 | "version": "1.0.3",
301 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
302 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
303 | },
304 | "methods": {
305 | "version": "1.1.2",
306 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
307 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
308 | },
309 | "mime": {
310 | "version": "1.6.0",
311 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
312 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
313 | },
314 | "mime-db": {
315 | "version": "1.52.0",
316 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
317 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
318 | },
319 | "mime-types": {
320 | "version": "2.1.35",
321 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
322 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
323 | "requires": {
324 | "mime-db": "1.52.0"
325 | }
326 | },
327 | "ms": {
328 | "version": "2.0.0",
329 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
330 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
331 | },
332 | "negotiator": {
333 | "version": "0.6.3",
334 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
335 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
336 | },
337 | "object-inspect": {
338 | "version": "1.13.3",
339 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
340 | "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="
341 | },
342 | "on-finished": {
343 | "version": "2.4.1",
344 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
345 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
346 | "requires": {
347 | "ee-first": "1.1.1"
348 | }
349 | },
350 | "parseurl": {
351 | "version": "1.3.3",
352 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
353 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
354 | },
355 | "path-to-regexp": {
356 | "version": "0.1.12",
357 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
358 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
359 | },
360 | "proxy-addr": {
361 | "version": "2.0.7",
362 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
363 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
364 | "requires": {
365 | "forwarded": "0.2.0",
366 | "ipaddr.js": "1.9.1"
367 | }
368 | },
369 | "qs": {
370 | "version": "6.13.0",
371 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
372 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
373 | "requires": {
374 | "side-channel": "^1.0.6"
375 | }
376 | },
377 | "range-parser": {
378 | "version": "1.2.1",
379 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
380 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
381 | },
382 | "raw-body": {
383 | "version": "2.5.2",
384 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
385 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
386 | "requires": {
387 | "bytes": "3.1.2",
388 | "http-errors": "2.0.0",
389 | "iconv-lite": "0.4.24",
390 | "unpipe": "1.0.0"
391 | }
392 | },
393 | "safe-buffer": {
394 | "version": "5.2.1",
395 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
396 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
397 | },
398 | "safer-buffer": {
399 | "version": "2.1.2",
400 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
401 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
402 | },
403 | "send": {
404 | "version": "0.19.0",
405 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
406 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
407 | "requires": {
408 | "debug": "2.6.9",
409 | "depd": "2.0.0",
410 | "destroy": "1.2.0",
411 | "encodeurl": "~1.0.2",
412 | "escape-html": "~1.0.3",
413 | "etag": "~1.8.1",
414 | "fresh": "0.5.2",
415 | "http-errors": "2.0.0",
416 | "mime": "1.6.0",
417 | "ms": "2.1.3",
418 | "on-finished": "2.4.1",
419 | "range-parser": "~1.2.1",
420 | "statuses": "2.0.1"
421 | },
422 | "dependencies": {
423 | "encodeurl": {
424 | "version": "1.0.2",
425 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
426 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
427 | },
428 | "ms": {
429 | "version": "2.1.3",
430 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
431 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
432 | }
433 | }
434 | },
435 | "serve-static": {
436 | "version": "1.16.2",
437 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
438 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
439 | "requires": {
440 | "encodeurl": "~2.0.0",
441 | "escape-html": "~1.0.3",
442 | "parseurl": "~1.3.3",
443 | "send": "0.19.0"
444 | }
445 | },
446 | "set-function-length": {
447 | "version": "1.2.2",
448 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
449 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
450 | "requires": {
451 | "define-data-property": "^1.1.4",
452 | "es-errors": "^1.3.0",
453 | "function-bind": "^1.1.2",
454 | "get-intrinsic": "^1.2.4",
455 | "gopd": "^1.0.1",
456 | "has-property-descriptors": "^1.0.2"
457 | }
458 | },
459 | "setprototypeof": {
460 | "version": "1.2.0",
461 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
462 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
463 | },
464 | "side-channel": {
465 | "version": "1.0.6",
466 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
467 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
468 | "requires": {
469 | "call-bind": "^1.0.7",
470 | "es-errors": "^1.3.0",
471 | "get-intrinsic": "^1.2.4",
472 | "object-inspect": "^1.13.1"
473 | }
474 | },
475 | "statuses": {
476 | "version": "2.0.1",
477 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
478 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
479 | },
480 | "toidentifier": {
481 | "version": "1.0.1",
482 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
483 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
484 | },
485 | "type-is": {
486 | "version": "1.6.18",
487 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
488 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
489 | "requires": {
490 | "media-typer": "0.3.0",
491 | "mime-types": "~2.1.24"
492 | }
493 | },
494 | "unpipe": {
495 | "version": "1.0.0",
496 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
497 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
498 | },
499 | "utils-merge": {
500 | "version": "1.0.1",
501 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
502 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
503 | },
504 | "vary": {
505 | "version": "1.1.2",
506 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
507 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
508 | }
509 | }
510 | }
511 |
--------------------------------------------------------------------------------
/example-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helloworld",
3 | "version": "1.0.0",
4 | "description": "Simple hello world sample in Node",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js"
8 | },
9 | "author": "Google LLC",
10 | "license": "Apache-2.0",
11 | "dependencies": {
12 | "express": "^4.21.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deploy-cloudrun",
3 | "version": "2.7.3",
4 | "description": "Github Action: Deploy to Google Cloud Run",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "rm -rf dist/ && ncc build -m src/main.ts -o dist/main",
8 | "docs": "./node_modules/.bin/actions-gen-readme",
9 | "lint": "eslint .",
10 | "format": "eslint . --fix",
11 | "test": "node --require ts-node/register --test-reporter spec --test tests/unit/main.test.ts tests/unit/output-parser.test.ts",
12 | "e2e-tests": "node --require ts-node/register --test-reporter spec --test tests/e2e.test.ts"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/google-github-actions/deploy-cloudrun.git"
17 | },
18 | "keywords": [
19 | "actions",
20 | "google",
21 | "cloud run",
22 | "cloud",
23 | "run",
24 | "containers"
25 | ],
26 | "author": "Google LLC",
27 | "license": "Apache-2.0",
28 | "bugs": {
29 | "url": "https://github.com/google-github-actions/deploy-cloudrun/issues"
30 | },
31 | "homepage": "https://github.com/google-github-actions/deploy-cloudrun#readme",
32 | "dependencies": {
33 | "@actions/core": "^1.11.1",
34 | "@actions/exec": "^1.1.1",
35 | "@actions/tool-cache": "^2.0.2",
36 | "@google-github-actions/actions-utils": "^0.8.6",
37 | "@google-github-actions/setup-cloud-sdk": "^1.1.9",
38 | "yaml": "^2.7.0"
39 | },
40 | "devDependencies": {
41 | "@eslint/eslintrc": "^3.2.0",
42 | "@eslint/js": "^9.19.0",
43 | "@types/node": "^22.13.0",
44 | "@typescript-eslint/eslint-plugin": "^8.22.0",
45 | "@typescript-eslint/parser": "^8.22.0",
46 | "@vercel/ncc": "^0.38.3",
47 | "eslint-config-prettier": "^10.0.1",
48 | "eslint-plugin-prettier": "^5.2.3",
49 | "eslint": "^9.19.0",
50 | "googleapis": "^144.0.0",
51 | "prettier": "^3.4.2",
52 | "ts-node": "^10.9.2",
53 | "typescript-eslint": "^8.22.0",
54 | "typescript": "^5.7.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import path from 'path';
18 |
19 | import {
20 | addPath,
21 | debug as logDebug,
22 | getInput,
23 | info as logInfo,
24 | setFailed,
25 | setOutput,
26 | warning as logWarning,
27 | } from '@actions/core';
28 | import { getExecOutput } from '@actions/exec';
29 | import * as toolCache from '@actions/tool-cache';
30 | import { readFile } from 'fs/promises';
31 | import { parse as parseYAML } from 'yaml';
32 |
33 | import {
34 | errorMessage,
35 | isPinnedToHead,
36 | joinKVStringForGCloud,
37 | KVPair,
38 | parseBoolean,
39 | parseCSV,
40 | parseFlags,
41 | parseKVString,
42 | parseKVStringAndFile,
43 | pinnedToHeadWarning,
44 | presence,
45 | } from '@google-github-actions/actions-utils';
46 | import {
47 | authenticateGcloudSDK,
48 | getLatestGcloudSDKVersion,
49 | getToolCommand,
50 | installComponent as installGcloudComponent,
51 | installGcloudSDK,
52 | isInstalled as isGcloudInstalled,
53 | } from '@google-github-actions/setup-cloud-sdk';
54 |
55 | import { parseDeployResponse, parseUpdateTrafficResponse } from './output-parser';
56 |
57 | // Do not listen to the linter - this can NOT be rewritten as an ES6 import
58 | // statement.
59 | const { version: appVersion } = require('../package.json');
60 |
61 | // isDebug returns true if runner debugging or step debugging is enabled.
62 | const isDebug =
63 | parseBoolean(process.env.ACTIONS_RUNNER_DEBUG) || parseBoolean(process.env.ACTIONS_STEP_DEBUG);
64 |
65 | /**
66 | * DeployCloudRunOutputs are the common GitHub action outputs created by this action
67 | */
68 | export interface DeployCloudRunOutputs {
69 | url?: string | null | undefined; // Type required to match run_v1.Schema$Service.status.url
70 | }
71 |
72 | /**
73 | * ResponseTypes are the gcloud command response formats
74 | */
75 | enum ResponseTypes {
76 | DEPLOY,
77 | UPDATE_TRAFFIC,
78 | }
79 |
80 | /**
81 | * Executes the main action. It includes the main business logic and is the
82 | * primary entry point. It is documented inline.
83 | */
84 | export async function run(): Promise {
85 | // Register metrics
86 | process.env.CLOUDSDK_CORE_DISABLE_PROMPTS = '1';
87 | process.env.CLOUDSDK_METRICS_ENVIRONMENT = 'github-actions-deploy-cloudrun';
88 | process.env.CLOUDSDK_METRICS_ENVIRONMENT_VERSION = appVersion;
89 | process.env.GOOGLE_APIS_USER_AGENT = `google-github-actions:deploy-cloudrun/${appVersion}`;
90 |
91 | // Warn if pinned to HEAD
92 | if (isPinnedToHead()) {
93 | logWarning(pinnedToHeadWarning('v1'));
94 | }
95 |
96 | try {
97 | // Get action inputs
98 | const image = getInput('image'); // Image ie gcr.io/...
99 | let service = getInput('service'); // Service name
100 | const job = getInput('job'); // Job name
101 | const metadata = getInput('metadata'); // YAML file
102 | const projectId = getInput('project_id');
103 | const gcloudVersion = await computeGcloudVersion(getInput('gcloud_version'));
104 | const gcloudComponent = presence(getInput('gcloud_component')); // Cloud SDK component version
105 | const envVars = getInput('env_vars'); // String of env vars KEY=VALUE,...
106 | const envVarsFile = getInput('env_vars_file'); // File that is a string of env vars KEY=VALUE,...
107 | const envVarsUpdateStrategy = getInput('env_vars_update_strategy') || 'merge';
108 | const secrets = parseKVString(getInput('secrets')); // String of secrets KEY=VALUE,...
109 | const secretsUpdateStrategy = getInput('secrets_update_strategy') || 'merge';
110 | const region = parseCSV(getInput('region') || 'us-central1');
111 | const source = getInput('source'); // Source directory
112 | const suffix = getInput('suffix');
113 | const tag = getInput('tag');
114 | const timeout = getInput('timeout');
115 | const noTraffic = (getInput('no_traffic') || '').toLowerCase() === 'true';
116 | const revTraffic = getInput('revision_traffic');
117 | const tagTraffic = getInput('tag_traffic');
118 | const labels = parseKVString(getInput('labels'));
119 | const skipDefaultLabels = parseBoolean(getInput('skip_default_labels'));
120 | const flags = getInput('flags');
121 | const updateTrafficFlags = getInput('update_traffic_flags');
122 |
123 | let deployCmd: string[] = [];
124 |
125 | // Throw errors if inputs aren't valid
126 | if (revTraffic && tagTraffic) {
127 | throw new Error('Only one of `revision_traffic` or `tag_traffic` inputs can be set.');
128 | }
129 | if ((revTraffic || tagTraffic) && !service) {
130 | throw new Error('No service name set.');
131 | }
132 | if (source && image) {
133 | throw new Error('Only one of `source` or `image` inputs can be set.');
134 | }
135 | if (service && job) {
136 | throw new Error('Only one of `service` or `job` inputs can be set.');
137 | }
138 |
139 | // Deprecation notices
140 | if (envVarsFile) {
141 | logWarning(
142 | `The "env_vars_file" input is deprecated and will be removed in a ` +
143 | `future major release. To source values from a file, read the file ` +
144 | `in a separate GitHub Actions step and set the contents as an output. ` +
145 | `Alternatively, there are many community actions that automate ` +
146 | `reading files.`,
147 | );
148 | }
149 |
150 | // Validate gcloud component input
151 | if (gcloudComponent && gcloudComponent !== 'alpha' && gcloudComponent !== 'beta') {
152 | throw new Error(`invalid input received for gcloud_component: ${gcloudComponent}`);
153 | }
154 |
155 | // Find base command
156 | if (metadata) {
157 | const contents = await readFile(metadata, 'utf8');
158 | const parsed = parseYAML(contents);
159 |
160 | // Extract service name from metadata template
161 | const name = parsed?.metadata?.name;
162 | if (!name) {
163 | throw new Error(`${metadata} is missing 'metadata.name'`);
164 | }
165 | if (service && service != name) {
166 | throw new Error(
167 | `service name in ${metadata} ("${name}") does not match GitHub ` +
168 | `Actions service input ("${service}")`,
169 | );
170 | }
171 | service = name;
172 |
173 | const kind = parsed?.kind;
174 | if (kind === 'Service') {
175 | deployCmd = ['run', 'services', 'replace', metadata];
176 | } else if (kind === 'Job') {
177 | deployCmd = ['run', 'jobs', 'replace', metadata];
178 | } else {
179 | throw new Error(`Unkown metadata type "${kind}", expected "Job" or "Service"`);
180 | }
181 | } else if (job) {
182 | logWarning(
183 | `Support for Cloud Run jobs in this GitHub Action is in beta and is ` +
184 | `not covered by the semver backwards compatibility guarantee.`,
185 | );
186 |
187 | deployCmd = ['run', 'jobs', 'deploy', job];
188 |
189 | if (image) {
190 | deployCmd.push('--image', image);
191 | } else if (source) {
192 | deployCmd.push('--source', source);
193 | }
194 |
195 | // Set optional flags from inputs
196 | setEnvVarsFlags(deployCmd, envVars, envVarsFile, envVarsUpdateStrategy);
197 | setSecretsFlags(deployCmd, secrets, secretsUpdateStrategy);
198 |
199 | // There is no --update-secrets flag on jobs, but there will be in the
200 | // future. At that point, we can remove this.
201 | const idx = deployCmd.indexOf('--update-secrets');
202 | if (idx >= 0) {
203 | logWarning(
204 | `Cloud Run does not allow updating secrets on jobs, ignoring ` +
205 | `"secrets_update_strategy" value of "merge"`,
206 | );
207 | deployCmd[idx] = '--set-secrets';
208 | }
209 |
210 | // Compile the labels
211 | const defLabels = skipDefaultLabels ? {} : defaultLabels();
212 | const compiledLabels = Object.assign({}, defLabels, labels);
213 | if (compiledLabels && Object.keys(compiledLabels).length > 0) {
214 | deployCmd.push('--labels', joinKVStringForGCloud(compiledLabels));
215 | }
216 | } else {
217 | deployCmd = ['run', 'deploy', service];
218 |
219 | if (image) {
220 | deployCmd.push('--image', image);
221 | } else if (source) {
222 | deployCmd.push('--source', source);
223 | }
224 |
225 | // Set optional flags from inputs
226 | setEnvVarsFlags(deployCmd, envVars, envVarsFile, envVarsUpdateStrategy);
227 | setSecretsFlags(deployCmd, secrets, secretsUpdateStrategy);
228 |
229 | if (tag) {
230 | deployCmd.push('--tag', tag);
231 | }
232 | if (suffix) deployCmd.push('--revision-suffix', suffix);
233 | if (noTraffic) deployCmd.push('--no-traffic');
234 | if (timeout) deployCmd.push('--timeout', timeout);
235 |
236 | // Compile the labels
237 | const defLabels = skipDefaultLabels ? {} : defaultLabels();
238 | const compiledLabels = Object.assign({}, defLabels, labels);
239 | if (compiledLabels && Object.keys(compiledLabels).length > 0) {
240 | deployCmd.push('--update-labels', joinKVStringForGCloud(compiledLabels));
241 | }
242 | }
243 |
244 | // Traffic flags
245 | let updateTrafficCmd = ['run', 'services', 'update-traffic', service];
246 | if (revTraffic) updateTrafficCmd.push('--to-revisions', revTraffic);
247 | if (tagTraffic) updateTrafficCmd.push('--to-tags', tagTraffic);
248 |
249 | // Push common flags
250 | deployCmd.push('--format', 'json');
251 | updateTrafficCmd.push('--format', 'json');
252 |
253 | if (region?.length > 0) {
254 | const regions = region
255 | .flat()
256 | .filter((e) => e !== undefined && e !== null && e !== '')
257 | .join(',');
258 | deployCmd.push('--region', regions);
259 | updateTrafficCmd.push('--region', regions);
260 | }
261 | if (projectId) {
262 | deployCmd.push('--project', projectId);
263 | updateTrafficCmd.push('--project', projectId);
264 | }
265 |
266 | // Add optional deploy flags
267 | if (flags) {
268 | const flagList = parseFlags(flags);
269 | if (flagList) {
270 | deployCmd = deployCmd.concat(flagList);
271 | }
272 | }
273 |
274 | // Add optional update-traffic flags
275 | if (updateTrafficFlags) {
276 | const flagList = parseFlags(updateTrafficFlags);
277 | if (flagList) {
278 | updateTrafficCmd = updateTrafficCmd.concat(flagList);
279 | }
280 | }
281 |
282 | // Install gcloud if not already installed.
283 | if (!isGcloudInstalled(gcloudVersion)) {
284 | await installGcloudSDK(gcloudVersion);
285 | } else {
286 | const toolPath = toolCache.find('gcloud', gcloudVersion);
287 | addPath(path.join(toolPath, 'bin'));
288 | }
289 |
290 | // Install gcloud component if needed and prepend the command
291 | if (gcloudComponent) {
292 | await installGcloudComponent(gcloudComponent);
293 | deployCmd.unshift(gcloudComponent);
294 | updateTrafficCmd.unshift(gcloudComponent);
295 | }
296 |
297 | // Authenticate - this comes from google-github-actions/auth.
298 | const credFile = process.env.GOOGLE_GHA_CREDS_PATH;
299 | if (credFile) {
300 | await authenticateGcloudSDK(credFile);
301 | logInfo('Successfully authenticated');
302 | } else {
303 | logWarning('No authentication found, authenticate with `google-github-actions/auth`.');
304 | }
305 |
306 | const toolCommand = getToolCommand();
307 | const options = { silent: !isDebug, ignoreReturnCode: true };
308 | const commandString = `${toolCommand} ${deployCmd.join(' ')}`;
309 | logInfo(`Running: ${commandString}`);
310 | logDebug(
311 | JSON.stringify({ toolCommand: toolCommand, args: deployCmd, options: options }, null, ' '),
312 | );
313 |
314 | // Run deploy command
315 | const deployCmdExec = await getExecOutput(toolCommand, deployCmd, options);
316 | if (deployCmdExec.exitCode !== 0) {
317 | const errMsg =
318 | deployCmdExec.stderr ||
319 | `command exited ${deployCmdExec.exitCode}, but stderr had no output`;
320 | throw new Error(`failed to execute gcloud command \`${commandString}\`: ${errMsg}`);
321 | }
322 | setActionOutputs(parseDeployResponse(deployCmdExec.stdout, { tag: tag }));
323 |
324 | // Run revision/tag command
325 | if (revTraffic || tagTraffic) {
326 | const updateTrafficExec = await getExecOutput(toolCommand, updateTrafficCmd, options);
327 | if (updateTrafficExec.exitCode !== 0) {
328 | const errMsg =
329 | updateTrafficExec.stderr ||
330 | `command exited ${updateTrafficExec.exitCode}, but stderr had no output`;
331 | throw new Error(`failed to execute gcloud command \`${commandString}\`: ${errMsg}`);
332 | }
333 | setActionOutputs(parseUpdateTrafficResponse(updateTrafficExec.stdout));
334 | }
335 | } catch (err) {
336 | const msg = errorMessage(err);
337 | setFailed(`google-github-actions/deploy-cloudrun failed with: ${msg}`);
338 | }
339 | }
340 |
341 | // Map output response to GitHub Action outputs
342 | export function setActionOutputs(outputs: DeployCloudRunOutputs): void {
343 | Object.keys(outputs).forEach((key: string) => {
344 | setOutput(key, outputs[key as keyof DeployCloudRunOutputs]);
345 | });
346 | }
347 |
348 | /**
349 | * defaultLabels returns the default labels to apply to the Cloud Run service.
350 | *
351 | * @return KVPair
352 | */
353 | function defaultLabels(): KVPair {
354 | const rawValues: Record = {
355 | 'managed-by': 'github-actions',
356 | 'commit-sha': process.env.GITHUB_SHA,
357 | };
358 |
359 | const labels: KVPair = {};
360 | for (const key in rawValues) {
361 | const value = rawValues[key];
362 | if (value) {
363 | // Labels can only be lowercase
364 | labels[key] = value.toLowerCase();
365 | }
366 | }
367 |
368 | return labels;
369 | }
370 |
371 | /**
372 | * computeGcloudVersion computes the appropriate gcloud version for the given
373 | * string.
374 | */
375 | async function computeGcloudVersion(str: string): Promise {
376 | str = (str || '').trim();
377 | if (str === '' || str === 'latest') {
378 | return await getLatestGcloudSDKVersion();
379 | }
380 | return str;
381 | }
382 |
383 | function setEnvVarsFlags(cmd: string[], envVars: string, envVarsFile: string, strategy: string) {
384 | const compiledEnvVars = parseKVStringAndFile(envVars, envVarsFile);
385 | if (compiledEnvVars && Object.keys(compiledEnvVars).length > 0) {
386 | let flag = '';
387 | if (strategy === 'overwrite') {
388 | flag = '--set-env-vars';
389 | } else if (strategy === 'merge') {
390 | flag = '--update-env-vars';
391 | } else {
392 | throw new Error(
393 | `Invalid "env_vars_update_strategy" value "${strategy}", valid values ` +
394 | `are "overwrite" and "merge".`,
395 | );
396 | }
397 | cmd.push(flag, joinKVStringForGCloud(compiledEnvVars));
398 | }
399 | }
400 |
401 | function setSecretsFlags(cmd: string[], secrets: KVPair | undefined, strategy: string) {
402 | if (secrets && Object.keys(secrets).length > 0) {
403 | let flag = '';
404 | if (strategy === 'overwrite') {
405 | flag = '--set-secrets';
406 | } else if (strategy === 'merge') {
407 | flag = '--update-secrets';
408 | } else {
409 | throw new Error(
410 | `Invalid "secrets_update_strategy" value "${strategy}", valid values ` +
411 | `are "overwrite" and "merge".`,
412 | );
413 | }
414 | cmd.push(flag, joinKVStringForGCloud(secrets));
415 | }
416 | }
417 |
418 | /**
419 | * execute the main function when this module is required directly.
420 | */
421 | if (require.main === module) {
422 | run();
423 | }
424 |
--------------------------------------------------------------------------------
/src/output-parser.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { DeployCloudRunOutputs } from './main';
18 | import { run_v1 } from 'googleapis';
19 | import { errorMessage, presence } from '@google-github-actions/actions-utils';
20 |
21 | /**
22 | * ParseInputs are the input values from GitHub actions used for parsing logic
23 | */
24 | export interface ParseInputs {
25 | [key: string]: string | boolean;
26 | }
27 |
28 | /**
29 | * UpdateTrafficItem is the response type for the gcloud run services update-traffic command
30 | */
31 | interface UpdateTrafficItem {
32 | displayPercent: string;
33 | displayRevisionId: string;
34 | displayTags: string;
35 | key: string;
36 | latestRevision: boolean;
37 | revisionName: string;
38 | serviceUrl: string;
39 | specPercent: string;
40 | specTags: string;
41 | statusPercent: string;
42 | statusTags: string;
43 | tags: string[];
44 | urls: string[];
45 | }
46 |
47 | /**
48 | * parseUpdateTrafficResponse parses the gcloud command response for update-traffic
49 | * into a common DeployCloudRunOutputs object
50 | *
51 | * @param stdout
52 | * @returns DeployCloudRunOutputs
53 | */
54 | export function parseUpdateTrafficResponse(stdout: string | undefined): DeployCloudRunOutputs {
55 | try {
56 | stdout = presence(stdout);
57 | if (!stdout || stdout === '{}' || stdout === '[]') {
58 | return {};
59 | }
60 |
61 | const outputJSON: UpdateTrafficItem[] = JSON.parse(stdout);
62 |
63 | // Default to service url
64 | const responseItem = outputJSON[0];
65 | let url = responseItem?.serviceUrl;
66 |
67 | // Maintain current logic to use first tag URL if present
68 | for (const item of outputJSON) {
69 | if (item?.urls?.length) {
70 | url = item.urls[0];
71 | break;
72 | }
73 | }
74 |
75 | const outputs: DeployCloudRunOutputs = { url: url };
76 |
77 | return outputs;
78 | } catch (err) {
79 | const msg = errorMessage(err);
80 | throw new Error(`failed to parse update traffic response: ${msg}, stdout: ${stdout}`);
81 | }
82 | }
83 |
84 | /**
85 | * parseDeployResponse parses the gcloud command response for gcloud run deploy/replace
86 | * into a common DeployCloudRunOutputs object
87 | *
88 | * @param stdout Standard output from gcloud command
89 | * @param inputs Action inputs used in parsing logic
90 | * @returns DeployCloudRunOutputs
91 | */
92 | export function parseDeployResponse(
93 | stdout: string | undefined,
94 | inputs: ParseInputs | undefined,
95 | ): DeployCloudRunOutputs {
96 | try {
97 | stdout = presence(stdout);
98 | if (!stdout || stdout === '{}' || stdout === '[]') {
99 | return {};
100 | }
101 |
102 | const outputJSON: run_v1.Schema$Service = JSON.parse(stdout);
103 |
104 | // Set outputs
105 | const outputs: DeployCloudRunOutputs = {
106 | url: outputJSON?.status?.url,
107 | };
108 |
109 | // Maintain current logic to use tag url if provided
110 | if (inputs?.tag) {
111 | const tagInfo = outputJSON?.status?.traffic?.find((t) => t.tag === inputs.tag);
112 | if (tagInfo) {
113 | outputs.url = tagInfo.url;
114 | }
115 | }
116 |
117 | return outputs;
118 | } catch (err) {
119 | const msg = errorMessage(err);
120 | throw new Error(
121 | `failed to parse deploy response: ${msg}, stdout: ${stdout}, inputs: ${JSON.stringify(
122 | inputs,
123 | )}`,
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/tests/e2e.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { test } from 'node:test';
18 | import assert from 'node:assert';
19 |
20 | import { getExecOutput } from '@actions/exec';
21 | import { run_v1 } from 'googleapis';
22 |
23 | import { skipIfMissingEnv } from '@google-github-actions/actions-utils';
24 |
25 | test(
26 | 'e2e tests',
27 | {
28 | concurrency: true,
29 | skip: skipIfMissingEnv('PROJECT_ID'),
30 | },
31 | async (suite) => {
32 | let service: run_v1.Schema$Service;
33 | let job: run_v1.Schema$Job;
34 | let metadata: run_v1.Schema$ObjectMeta;
35 | let spec: run_v1.Schema$TaskSpec | run_v1.Schema$RevisionSpec;
36 |
37 | suite.before(async () => {
38 | if (process.env.JOB) {
39 | const output = await getExecOutput('gcloud', [
40 | 'run',
41 | 'jobs',
42 | 'describe',
43 | process.env.JOB!,
44 | '--project',
45 | process.env.PROJECT_ID!,
46 | '--format',
47 | 'json',
48 | '--region',
49 | 'us-central1',
50 | ]);
51 | job = JSON.parse(output.stdout) as run_v1.Schema$Job;
52 | if (!job) {
53 | throw new Error('failed to find job definition');
54 | }
55 | metadata = job.spec!.template!.metadata!;
56 | spec = job.spec!.template!.spec!.template!.spec!;
57 | } else if (process.env.SERVICE) {
58 | const output = await getExecOutput('gcloud', [
59 | 'run',
60 | 'services',
61 | 'describe',
62 | process.env.SERVICE!,
63 | '--project',
64 | process.env.PROJECT_ID!,
65 | '--format',
66 | 'json',
67 | '--region',
68 | 'us-central1',
69 | ]);
70 | service = JSON.parse(output.stdout) as run_v1.Schema$Service;
71 | if (!service) {
72 | throw new Error('failed to find service definition');
73 | }
74 | metadata = service.spec!.template!.metadata!;
75 | spec = service.spec!.template!.spec!;
76 | } else {
77 | throw new Error(`missing service or job`);
78 | }
79 | });
80 |
81 | await suite.test('has the correct envvars', { skip: skipIfMissingEnv('ENV') }, async () => {
82 | const expected = parseEnvVars(process.env.ENV!);
83 | const actual = spec.containers?.at(0)?.env?.filter((e) => e?.value);
84 |
85 | const subset = expected.map((e) => {
86 | return actual?.find((a) => a.name == e.name);
87 | });
88 |
89 | assert.deepStrictEqual(subset, expected);
90 | });
91 |
92 | await suite.test(
93 | 'has the correct secret vars',
94 | { skip: skipIfMissingEnv('SECRET_ENV') },
95 | async () => {
96 | const expected = parseEnvVars(process.env.SECRET_ENV!);
97 | const actual = spec.containers
98 | ?.at(0)
99 | ?.env?.filter((entry) => entry && entry.valueFrom)
100 | .map((entry) => {
101 | const ref = entry.valueFrom?.secretKeyRef;
102 | return { name: entry.name, value: `${ref?.name}:${ref?.key}` };
103 | });
104 |
105 | const subset = expected.map((e) => {
106 | return actual?.find((a) => a.name == e.name);
107 | });
108 |
109 | assert.deepStrictEqual(subset, expected);
110 | },
111 | );
112 |
113 | await suite.test(
114 | 'has the correct secret volumes',
115 | { skip: skipIfMissingEnv('SECRET_VOLUMES') },
116 | async () => {
117 | const expected = parseEnvVars(process.env.SECRET_VOLUMES!);
118 | const actual = spec.containers?.at(0)?.volumeMounts?.map((volumeMount) => {
119 | const secretVolume = spec.volumes?.find(
120 | (volume) => volumeMount.name === volume.name,
121 | )?.secret;
122 | const secretName = secretVolume?.secretName;
123 | const secretData = secretVolume?.items?.at(0);
124 | const secretPath = `${volumeMount.mountPath}/${secretData?.path}`;
125 | const secretRef = `${secretName}:${secretData?.key}`;
126 | return { name: secretPath, value: secretRef };
127 | });
128 |
129 | const subset = expected.map((e) => {
130 | return actual?.find((a) => a.name == e.name);
131 | });
132 |
133 | assert.deepStrictEqual(subset, expected);
134 | },
135 | );
136 |
137 | await suite.test('has the correct params', { skip: skipIfMissingEnv('PARAMS') }, async () => {
138 | const expected = JSON.parse(process.env.PARAMS!);
139 |
140 | if (expected.containerConncurrency) {
141 | assert.deepStrictEqual(
142 | (spec as run_v1.Schema$RevisionSpec).containerConcurrency,
143 | expected.containerConncurrency,
144 | );
145 | }
146 |
147 | if (expected.timeoutSeconds) {
148 | assert.deepStrictEqual(spec.timeoutSeconds, expected.timeoutSeconds);
149 | }
150 |
151 | const limits = spec.containers?.at(0)?.resources?.limits;
152 | if (expected.cpu) {
153 | assert.deepStrictEqual(limits?.cpu, expected.cpu.toString());
154 | }
155 |
156 | if (expected.memory) {
157 | assert.deepStrictEqual(limits?.memory, expected.memory);
158 | }
159 | });
160 |
161 | await suite.test(
162 | 'has the correct annotations',
163 | { skip: skipIfMissingEnv('ANNOTATIONS') },
164 | async () => {
165 | const expected = JSON.parse(process.env.ANNOTATIONS!) as Record;
166 | const actual = metadata.annotations;
167 |
168 | const subset = Object.assign(
169 | {},
170 | ...Object.keys(expected).map((k) => ({ [k]: actual?.[k] })),
171 | );
172 |
173 | assert.deepStrictEqual(subset, expected);
174 | },
175 | );
176 |
177 | await suite.test('has the correct labels', { skip: skipIfMissingEnv('LABELS') }, async () => {
178 | const expected = JSON.parse(process.env.LABELS!) as Record;
179 | const actual = metadata.labels;
180 |
181 | const subset = Object.assign({}, ...Object.keys(expected).map((k) => ({ [k]: actual?.[k] })));
182 |
183 | assert.deepStrictEqual(subset, expected);
184 | });
185 |
186 | await suite.test('has the revision name', { skip: skipIfMissingEnv('REVISION') }, async () => {
187 | const expected = process.env.REVISION! as string;
188 | const actual = service.metadata?.name;
189 | assert.deepStrictEqual(actual, expected);
190 | });
191 |
192 | await suite.test('has the job name', { skip: skipIfMissingEnv('JOB') }, async () => {
193 | const expected = process.env.JOB! as string;
194 | const actual = job.metadata?.name;
195 | assert.deepStrictEqual(actual, expected);
196 | });
197 |
198 | await suite.test('has the correct tag', { skip: skipIfMissingEnv('TAG') }, async () => {
199 | const expected = process.env.TAG!;
200 | const actual = service?.spec?.traffic?.map((revision) => revision.tag);
201 | assert.deepStrictEqual(actual, expected);
202 | });
203 |
204 | await suite.test(
205 | 'has the correct traffic',
206 | { skip: skipIfMissingEnv('TRAFFIC', 'TAG') },
207 | async () => {
208 | const expected = process.env.TRAFFIC!;
209 | const tagged = service?.spec?.traffic?.find((revision) => {
210 | return revision.tag == process.env.TAG!;
211 | });
212 | const percent = tagged?.percent;
213 | assert.deepStrictEqual(percent, expected);
214 | },
215 | );
216 | },
217 | );
218 |
219 | const parseEnvVars = (envVarInput: string): run_v1.Schema$EnvVar[] => {
220 | const m = JSON.parse(envVarInput) as Record;
221 | const envVars = Object.entries(m).map(([key, value]) => {
222 | return { name: key, value: value };
223 | });
224 | return envVars;
225 | };
226 |
--------------------------------------------------------------------------------
/tests/fixtures/env_vars.txt:
--------------------------------------------------------------------------------
1 | TEXT_FOO=bar
2 | TEXT_ZIP=zap\,with|separators\,and&stuff
3 |
--------------------------------------------------------------------------------
/tests/fixtures/job.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 'run.googleapis.com/v1'
2 | kind: 'Job'
3 | metadata:
4 | name: 'job'
5 | labels:
6 | cloud.googleapis.com/location: 'us-east1'
7 | spec:
8 | template:
9 | metadata:
10 | annotations:
11 | run.googleapis.com/execution-environment: 'gen2'
12 | spec:
13 | parallelism: 1
14 | taskCount: 1
15 | template:
16 | spec:
17 | containers:
18 | - image: 'gcr.io/cloudrun/hello'
19 | imagePullPolicy: 'Always'
20 | resources:
21 | limits:
22 | cpu: '1000m'
23 | memory: '512Mi'
24 | maxRetries: 0
25 | timeoutSeconds: '3600'
26 |
--------------------------------------------------------------------------------
/tests/fixtures/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 'serving.knative.dev/v1'
2 | kind: 'Service'
3 | metadata:
4 | name: 'run-full-yaml'
5 | spec:
6 | template:
7 | metadata:
8 | labels:
9 | test_label: 'test_value'
10 | annotations:
11 | run.googleapis.com/cloudsql-instances: 'test-project:us-central1:my-test-instance'
12 | spec:
13 | containerConcurrency: 20
14 | containers:
15 | - image: 'gcr.io/cloudrun/hello'
16 | ports:
17 | - containerPort: 8080
18 | resources:
19 | limits:
20 | cpu: '2'
21 | memory: '1Gi'
22 | timeoutSeconds: 300
23 |
--------------------------------------------------------------------------------
/tests/unit/main.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { mock, test } from 'node:test';
18 | import assert from 'node:assert';
19 |
20 | import * as core from '@actions/core';
21 | import * as exec from '@actions/exec';
22 | import * as setupGcloud from '@google-github-actions/setup-cloud-sdk';
23 | import { TestToolCache } from '@google-github-actions/setup-cloud-sdk';
24 |
25 | import { assertMembers } from '@google-github-actions/actions-utils';
26 |
27 | import { run } from '../../src/main';
28 |
29 | const fakeInputs: { [key: string]: string } = {
30 | image: 'gcr.io/cloudrun/hello',
31 | project_id: 'test',
32 | };
33 |
34 | const defaultMocks = (
35 | m: typeof mock,
36 | overrideInputs?: Record,
37 | ): Record => {
38 | const inputs = Object.assign({}, fakeInputs, overrideInputs);
39 | return {
40 | setFailed: m.method(core, 'setFailed', (msg: string) => {
41 | throw new Error(msg);
42 | }),
43 | getBooleanInput: m.method(core, 'getBooleanInput', (name: string) => {
44 | return !!inputs[name];
45 | }),
46 | getMultilineInput: m.method(core, 'getMultilineInput', (name: string) => {
47 | return inputs[name];
48 | }),
49 | getInput: m.method(core, 'getInput', (name: string) => {
50 | return inputs[name];
51 | }),
52 | getExecOutput: m.method(exec, 'getExecOutput', () => {
53 | return { exitCode: 0, stderr: '', stdout: '{}' };
54 | }),
55 |
56 | authenticateGcloudSDK: m.method(setupGcloud, 'authenticateGcloudSDK', () => {}),
57 | isAuthenticated: m.method(setupGcloud, 'isAuthenticated', () => {}),
58 | isInstalled: m.method(setupGcloud, 'isInstalled', () => {
59 | return true;
60 | }),
61 | installGcloudSDK: m.method(setupGcloud, 'installGcloudSDK', async () => {
62 | return '1.2.3';
63 | }),
64 | installComponent: m.method(setupGcloud, 'installComponent', () => {}),
65 | setProject: m.method(setupGcloud, 'setProject', () => {}),
66 | getLatestGcloudSDKVersion: m.method(setupGcloud, 'getLatestGcloudSDKVersion', () => {
67 | return '1.2.3';
68 | }),
69 | };
70 | };
71 |
72 | test('#run', { concurrency: true }, async (suite) => {
73 | const originalEnv = Object.assign({}, process.env);
74 |
75 | suite.before(() => {
76 | suite.mock.method(core, 'debug', () => {});
77 | suite.mock.method(core, 'info', () => {});
78 | suite.mock.method(core, 'warning', () => {});
79 | suite.mock.method(core, 'setOutput', () => {});
80 | suite.mock.method(core, 'setSecret', () => {});
81 | suite.mock.method(core, 'group', () => {});
82 | suite.mock.method(core, 'startGroup', () => {});
83 | suite.mock.method(core, 'endGroup', () => {});
84 | suite.mock.method(core, 'addPath', () => {});
85 | suite.mock.method(core, 'exportVariable', () => {});
86 | });
87 |
88 | suite.beforeEach(async () => {
89 | await TestToolCache.start();
90 | });
91 |
92 | suite.afterEach(async () => {
93 | process.env = originalEnv;
94 | await TestToolCache.stop();
95 | });
96 |
97 | await suite.test('sets the project ID', async (t) => {
98 | const mocks = defaultMocks(t.mock, {
99 | project_id: 'my-test-project',
100 | service: 'my-test-service',
101 | });
102 | await run();
103 |
104 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
105 | assertMembers(args, ['--project', 'my-test-project']);
106 | });
107 |
108 | await suite.test('sets a single region', async (t) => {
109 | const mocks = defaultMocks(t.mock, {
110 | region: 'us-central1',
111 | service: 'my-test-service',
112 | });
113 | await run();
114 |
115 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
116 | assertMembers(args, ['--region', 'us-central1']);
117 | });
118 |
119 | await suite.test('sets a multiple regions', async (t) => {
120 | const mocks = defaultMocks(t.mock, {
121 | region: 'us-central1, us-east1',
122 | service: 'my-test-service',
123 | });
124 | await run();
125 |
126 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
127 | assertMembers(args, ['--region', 'us-central1,us-east1']);
128 | });
129 |
130 | await suite.test('installs the gcloud SDK if it is not already installed', async (t) => {
131 | const mocks = defaultMocks(t.mock, {
132 | service: 'my-test-service',
133 | });
134 | t.mock.method(setupGcloud, 'isInstalled', () => {
135 | return false;
136 | });
137 |
138 | await run();
139 |
140 | assert.deepStrictEqual(mocks.installGcloudSDK.mock.callCount(), 1);
141 | });
142 |
143 | await suite.test('uses the cached gcloud SDK if it was already installed', async (t) => {
144 | const mocks = defaultMocks(t.mock, {
145 | service: 'my-test-service',
146 | });
147 | t.mock.method(setupGcloud, 'isInstalled', () => {
148 | return true;
149 | });
150 |
151 | await run();
152 |
153 | assert.deepStrictEqual(mocks.installGcloudSDK.mock.callCount(), 0);
154 | });
155 |
156 | await suite.test('uses default components without gcloud_component flag', async (t) => {
157 | const mocks = defaultMocks(t.mock, {
158 | service: 'my-test-service',
159 | });
160 |
161 | await run();
162 |
163 | assert.deepStrictEqual(mocks.installComponent.mock.callCount(), 0);
164 | });
165 |
166 | await suite.test('throws error with invalid gcloud component flag', async (t) => {
167 | defaultMocks(t.mock, {
168 | service: 'my-test-service',
169 | gcloud_component: 'wrong_value',
170 | });
171 |
172 | await assert.rejects(
173 | async () => {
174 | await run();
175 | },
176 | { message: /invalid input received for gcloud_component: wrong_value/ },
177 | );
178 | });
179 |
180 | await suite.test('installs alpha component with alpha flag', async (t) => {
181 | const mocks = defaultMocks(t.mock, {
182 | service: 'my-test-service',
183 | gcloud_component: 'alpha',
184 | });
185 |
186 | await run();
187 |
188 | const args = mocks.installComponent.mock.calls?.at(0).arguments?.at(0);
189 | assert.deepStrictEqual(args, 'alpha');
190 | });
191 |
192 | await suite.test('installs alpha component with beta flag', async (t) => {
193 | const mocks = defaultMocks(t.mock, {
194 | service: 'my-test-service',
195 | gcloud_component: 'beta',
196 | });
197 |
198 | await run();
199 |
200 | const args = mocks.installComponent.mock.calls?.at(0).arguments?.at(0);
201 | assert.deepStrictEqual(args, 'beta');
202 | });
203 |
204 | await suite.test('merges envvars', async (t) => {
205 | const mocks = defaultMocks(t.mock, {
206 | service: 'my-test-service',
207 | env_vars: 'FOO=BAR',
208 | });
209 |
210 | await run();
211 |
212 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
213 | const envVars = splitKV(args.at(args.indexOf('--update-env-vars') + 1));
214 | assert.deepStrictEqual(envVars, { FOO: 'BAR' });
215 | });
216 |
217 | await suite.test('overwrites envvars', async (t) => {
218 | const mocks = defaultMocks(t.mock, {
219 | service: 'my-test-service',
220 | env_vars: 'FOO=BAR',
221 | env_vars_update_strategy: 'overwrite',
222 | });
223 |
224 | await run();
225 |
226 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
227 | const envVars = splitKV(args.at(args.indexOf('--set-env-vars') + 1));
228 | assert.deepStrictEqual(envVars, { FOO: 'BAR' });
229 | });
230 |
231 | await suite.test('merges secrets', async (t) => {
232 | const mocks = defaultMocks(t.mock, {
233 | service: 'my-test-service',
234 | secrets: 'FOO=bar:latest',
235 | });
236 |
237 | await run();
238 |
239 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
240 | const envVars = splitKV(args.at(args.indexOf('--update-secrets') + 1));
241 | assert.deepStrictEqual(envVars, { FOO: 'bar:latest' });
242 | });
243 |
244 | await suite.test('overwrites secrets', async (t) => {
245 | const mocks = defaultMocks(t.mock, {
246 | service: 'my-test-service',
247 | secrets: 'FOO=bar:latest',
248 | secrets_update_strategy: 'overwrite',
249 | });
250 |
251 | await run();
252 |
253 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
254 | const envVars = splitKV(args.at(args.indexOf('--set-secrets') + 1));
255 | assert.deepStrictEqual(envVars, { FOO: 'bar:latest' });
256 | });
257 |
258 | await suite.test('sets labels', async (t) => {
259 | const mocks = defaultMocks(t.mock, {
260 | service: 'my-test-service',
261 | labels: 'foo=bar,zip=zap',
262 | });
263 |
264 | process.env.GITHUB_SHA = 'abcdef123456';
265 |
266 | await run();
267 |
268 | const expectedLabels = {
269 | 'managed-by': 'github-actions',
270 | 'commit-sha': 'abcdef123456',
271 | 'foo': 'bar',
272 | 'zip': 'zap',
273 | };
274 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
275 | const labels = splitKV(args.at(args.indexOf('--update-labels') + 1));
276 | assert.deepStrictEqual(labels, expectedLabels);
277 | });
278 |
279 | await suite.test('skips default labels', async (t) => {
280 | const mocks = defaultMocks(t.mock, {
281 | service: 'my-test-service',
282 | skip_default_labels: 'true',
283 | labels: 'foo=bar,zip=zap',
284 | });
285 |
286 | await run();
287 |
288 | const expectedLabels = {
289 | foo: 'bar',
290 | zip: 'zap',
291 | };
292 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
293 | const labels = splitKV(args.at(args.indexOf('--update-labels') + 1));
294 | assert.deepStrictEqual(labels, expectedLabels);
295 | });
296 |
297 | await suite.test('overwrites default labels', async (t) => {
298 | const mocks = defaultMocks(t.mock, {
299 | service: 'my-test-service',
300 | labels: 'commit-sha=custom-value',
301 | });
302 | process.env.GITHUB_SHA = 'abcdef123456';
303 |
304 | await run();
305 |
306 | const expectedLabels = {
307 | 'managed-by': 'github-actions',
308 | 'commit-sha': 'custom-value',
309 | };
310 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
311 | const labels = splitKV(args.at(args.indexOf('--update-labels') + 1));
312 | assert.deepStrictEqual(labels, expectedLabels);
313 | });
314 |
315 | await suite.test('sets source if given', async (t) => {
316 | const mocks = defaultMocks(t.mock, {
317 | service: 'my-test-service',
318 | source: 'example-app',
319 | image: '',
320 | });
321 |
322 | await run();
323 |
324 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
325 | assertMembers(args, ['--source', 'example-app']);
326 | });
327 |
328 | await suite.test('sets service metadata if given', async (t) => {
329 | const mocks = defaultMocks(t.mock, {
330 | metadata: 'tests/fixtures/service.yaml',
331 | image: '',
332 | });
333 |
334 | await run();
335 |
336 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
337 | assertMembers(args, ['services', 'replace']);
338 | });
339 |
340 | await suite.test('errors if metadata is given and the service names do not match', async (t) => {
341 | defaultMocks(t.mock, {
342 | metadata: 'tests/fixtures/service.yaml',
343 | service: 'not-a-match',
344 | });
345 |
346 | await assert.rejects(
347 | async () => {
348 | await run();
349 | },
350 | { message: /does not match/ },
351 | );
352 | });
353 |
354 | await suite.test('does not error if metadata is given and the service names match', async (t) => {
355 | defaultMocks(t.mock, {
356 | metadata: 'tests/fixtures/service.yaml',
357 | service: 'run-full-yaml',
358 | });
359 |
360 | await assert.doesNotReject(async () => {
361 | await run();
362 | });
363 | });
364 |
365 | await suite.test('sets job metadata if given', async (t) => {
366 | const mocks = defaultMocks(t.mock, {
367 | metadata: 'tests/fixtures/job.yaml',
368 | image: '',
369 | });
370 |
371 | await run();
372 |
373 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
374 | assertMembers(args, ['jobs', 'replace']);
375 | });
376 |
377 | await suite.test('sets timeout if given', async (t) => {
378 | const mocks = defaultMocks(t.mock, {
379 | service: 'my-test-service',
380 | timeout: '55m12s',
381 | });
382 |
383 | await run();
384 |
385 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
386 | assertMembers(args, ['--timeout', '55m12s']);
387 | });
388 |
389 | await suite.test('sets tag if given', async (t) => {
390 | const mocks = defaultMocks(t.mock, {
391 | service: 'my-test-service',
392 | tag: 'test',
393 | });
394 |
395 | await run();
396 |
397 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
398 | assertMembers(args, ['--tag', 'test']);
399 | });
400 |
401 | await suite.test('sets additional flags on the deploy command', async (t) => {
402 | const mocks = defaultMocks(t.mock, {
403 | service: 'my-test-service',
404 | flags: '--arg1=1 --arg2=2',
405 | });
406 |
407 | await run();
408 |
409 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
410 | assertMembers(args, ['--arg1', '1', '--arg2', '2']);
411 | });
412 |
413 | await suite.test('sets tag traffic if given', async (t) => {
414 | const mocks = defaultMocks(t.mock, {
415 | service: 'my-test-service',
416 | tag_traffic: 'TEST=100',
417 | });
418 |
419 | await run();
420 |
421 | const deployArgs = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
422 | assertMembers(deployArgs, ['run', 'deploy', 'my-test-service']);
423 |
424 | const updateTrafficArgs = mocks.getExecOutput.mock.calls?.at(1)?.arguments?.at(1);
425 | assertMembers(updateTrafficArgs, ['--to-tags', 'TEST=100']);
426 | });
427 |
428 | await suite.test('fails if tag traffic and revision traffic are provided', async (t) => {
429 | defaultMocks(t.mock, {
430 | service: 'my-test-service',
431 | revision_traffic: 'TEST=100',
432 | tag_traffic: 'TEST=100',
433 | });
434 |
435 | await assert.rejects(
436 | async () => {
437 | await run();
438 | },
439 | { message: /only one of `revision_traffic` or `tag_traffic` inputs can be set/ },
440 | );
441 | });
442 |
443 | await suite.test('fails if service is not provided with tag traffic', async (t) => {
444 | defaultMocks(t.mock, {
445 | service: '',
446 | tag_traffic: 'TEST=100',
447 | });
448 |
449 | await assert.rejects(
450 | async () => {
451 | await run();
452 | },
453 | { message: /no service name set/ },
454 | );
455 | });
456 |
457 | await suite.test('sets revision traffic if given', async (t) => {
458 | const mocks = defaultMocks(t.mock, {
459 | service: 'my-test-service',
460 | revision_traffic: 'TEST=100',
461 | });
462 |
463 | await run();
464 |
465 | const deployArgs = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
466 | assertMembers(deployArgs, ['run', 'deploy', 'my-test-service']);
467 |
468 | const updateTrafficArgs = mocks.getExecOutput.mock.calls?.at(1)?.arguments?.at(1);
469 | assertMembers(updateTrafficArgs, ['--to-revisions', 'TEST=100']);
470 | });
471 |
472 | await suite.test('sets additional flags on the update-traffic command', async (t) => {
473 | const mocks = defaultMocks(t.mock, {
474 | service: 'my-test-service',
475 | tag_traffic: 'test',
476 | update_traffic_flags: '--arg1=1 --arg2=2',
477 | });
478 |
479 | await run();
480 |
481 | const args = mocks.getExecOutput.mock.calls?.at(1)?.arguments?.at(1);
482 | assertMembers(args, ['--arg1', '1', '--arg2', '2']);
483 | });
484 |
485 | await suite.test('fails if service is not provided with revision traffic', async (t) => {
486 | defaultMocks(t.mock, {
487 | service: '',
488 | revision_traffic: 'TEST=100',
489 | });
490 |
491 | await assert.rejects(
492 | async () => {
493 | await run();
494 | },
495 | { message: /no service name set/ },
496 | );
497 | });
498 |
499 | await suite.test('fails if job and service are both specified', async (t) => {
500 | defaultMocks(t.mock, {
501 | service: 'my-test-service',
502 | job: 'my-test-job',
503 | });
504 |
505 | await assert.rejects(
506 | async () => {
507 | await run();
508 | },
509 | { message: /only one of `service` or `job` inputs can be set/ },
510 | );
511 | });
512 |
513 | await suite.test('deploys a job if job is specified', async (t) => {
514 | const mocks = defaultMocks(t.mock, {
515 | job: 'my-test-job',
516 | });
517 |
518 | await run();
519 |
520 | const args = mocks.getExecOutput.mock.calls?.at(0)?.arguments?.at(1);
521 | assertMembers(args, ['run', 'jobs', 'deploy', 'my-test-job']);
522 | });
523 | });
524 |
525 | const splitKV = (s: string): Record => {
526 | const delim = s.match(/\^(.+)\^/i);
527 | if (!delim || delim.length === 0) {
528 | throw new Error(`Invalid delimiter: ${s}`);
529 | }
530 |
531 | const parts = s.slice(delim[0].length).split(delim[1]);
532 | return Object.fromEntries(parts.map((p) => p.split('=')));
533 | };
534 |
--------------------------------------------------------------------------------
/tests/unit/output-parser.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { test } from 'node:test';
18 | import assert from 'node:assert';
19 |
20 | import { parseUpdateTrafficResponse, parseDeployResponse } from '../../src/output-parser';
21 |
22 | test('#parseUpdateTrafficResponse', { concurrency: true }, async (suite) => {
23 | const cases = [
24 | {
25 | name: 'parses update traffic outputs',
26 | stdout: `
27 | [
28 | {
29 | "displayPercent": "100%",
30 | "displayRevisionId": "LATEST (currently test-basic-yaml-00007-leg)",
31 | "displayTags": "",
32 | "key": "LATEST",
33 | "latestRevision": true,
34 | "revisionName": "test-basic-yaml-00007-leg",
35 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
36 | "specPercent": "100",
37 | "specTags": "-",
38 | "statusPercent": "100",
39 | "statusTags": "-",
40 | "tags": [],
41 | "urls": []
42 | }
43 | ]
44 | `,
45 | expected: { url: 'https://test-basic-yaml-4goqgbaxqq-uc.a.run.app' },
46 | },
47 | {
48 | name: 'parses update traffic with single tag',
49 | stdout: `
50 | [
51 | {
52 | "displayPercent": "0%",
53 | "displayRevisionId": "test-basic-yaml-00005-yus",
54 | "displayTags": "my-tag-1",
55 | "key": "test-basic-yaml-00005-yus",
56 | "latestRevision": false,
57 | "revisionName": "test-basic-yaml-00005-yus",
58 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
59 | "specPercent": "0",
60 | "specTags": "my-tag-1",
61 | "statusPercent": "0",
62 | "statusTags": "my-tag-1",
63 | "tags": [
64 | {
65 | "inSpec": true,
66 | "inStatus": true,
67 | "tag": "my-tag-1",
68 | "url": "https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
69 | }
70 | ],
71 | "urls": [
72 | "https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
73 | ]
74 | },
75 | {
76 | "displayPercent": "100%",
77 | "displayRevisionId": "LATEST (currently test-basic-yaml-00007-leg)",
78 | "displayTags": "",
79 | "key": "LATEST",
80 | "latestRevision": true,
81 | "revisionName": "test-basic-yaml-00007-leg",
82 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
83 | "specPercent": "100",
84 | "specTags": "-",
85 | "statusPercent": "100",
86 | "statusTags": "-",
87 | "tags": [],
88 | "urls": []
89 | }
90 | ]
91 | `,
92 | expected: { url: 'https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app' },
93 | },
94 | {
95 | name: 'parses update traffic with multiple tags',
96 | stdout: `
97 | [
98 | {
99 | "displayPercent": "20%",
100 | "displayRevisionId": "test-basic-yaml-00005-yus",
101 | "displayTags": "my-tag-1, my-tag-2",
102 | "key": "test-basic-yaml-00005-yus",
103 | "latestRevision": false,
104 | "revisionName": "test-basic-yaml-00005-yus",
105 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
106 | "specPercent": "20",
107 | "specTags": "my-tag-1, my-tag-2",
108 | "statusPercent": "20",
109 | "statusTags": "my-tag-1, my-tag-2",
110 | "tags": [
111 | {
112 | "inSpec": true,
113 | "inStatus": true,
114 | "tag": "my-tag-1",
115 | "url": "https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
116 | },
117 | {
118 | "inSpec": true,
119 | "inStatus": true,
120 | "tag": "my-tag-2",
121 | "url": "https://my-tag-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
122 | }
123 | ],
124 | "urls": [
125 | "https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app",
126 | "https://my-tag-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
127 | ]
128 | },
129 | {
130 | "displayPercent": "40%",
131 | "displayRevisionId": "test-basic-yaml-00006-juz",
132 | "displayTags": "another-2, test-2",
133 | "key": "test-basic-yaml-00006-juz",
134 | "latestRevision": false,
135 | "revisionName": "test-basic-yaml-00006-juz",
136 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
137 | "specPercent": "40",
138 | "specTags": "another-2, test-2",
139 | "statusPercent": "40",
140 | "statusTags": "another-2, test-2",
141 | "tags": [
142 | {
143 | "inSpec": true,
144 | "inStatus": true,
145 | "tag": "another-2",
146 | "url": "https://another-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
147 | },
148 | {
149 | "inSpec": true,
150 | "inStatus": true,
151 | "tag": "test-2",
152 | "url": "https://test-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
153 | }
154 | ],
155 | "urls": [
156 | "https://another-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app",
157 | "https://test-2---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
158 | ]
159 | },
160 | {
161 | "displayPercent": "40%",
162 | "displayRevisionId": "test-basic-yaml-00007-leg",
163 | "displayTags": "another-1, test-1",
164 | "key": "test-basic-yaml-00007-leg",
165 | "latestRevision": false,
166 | "revisionName": "test-basic-yaml-00007-leg",
167 | "serviceUrl": "https://test-basic-yaml-4goqgbaxqq-uc.a.run.app",
168 | "specPercent": "40",
169 | "specTags": "another-1, test-1",
170 | "statusPercent": "40",
171 | "statusTags": "another-1, test-1",
172 | "tags": [
173 | {
174 | "inSpec": true,
175 | "inStatus": true,
176 | "tag": "another-1",
177 | "url": "https://another-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
178 | },
179 | {
180 | "inSpec": true,
181 | "inStatus": true,
182 | "tag": "test-1",
183 | "url": "https://test-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
184 | }
185 | ],
186 | "urls": [
187 | "https://another-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app",
188 | "https://test-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app"
189 | ]
190 | }
191 | ]
192 | `,
193 | expected: { url: 'https://my-tag-1---test-basic-yaml-4goqgbaxqq-uc.a.run.app' },
194 | },
195 | {
196 | name: 'handles empty stdout',
197 | stdout: '',
198 | expected: {},
199 | },
200 | {
201 | name: 'handles empty array from stdout',
202 | stdout: '[]',
203 | expected: {},
204 | },
205 | {
206 | name: 'handles empty object from stdout',
207 | stdout: '{}',
208 | expected: {},
209 | },
210 | {
211 | name: 'handles invalid text from stdout',
212 | stdout: 'Some text to fail',
213 | error: `failed to parse update traffic response: unexpected token 'S', "Some text to fail" is not valid JSON, stdout: Some text to fail`,
214 | },
215 | ];
216 |
217 | for await (const tc of cases) {
218 | await suite.test(tc.name, async () => {
219 | if (tc.error) {
220 | assert.throws(
221 | () => {
222 | parseUpdateTrafficResponse(tc.stdout);
223 | },
224 | { message: tc.error },
225 | );
226 | } else {
227 | const actual = parseUpdateTrafficResponse(tc.stdout);
228 | assert.deepStrictEqual(actual, tc.expected);
229 | }
230 | });
231 | }
232 | });
233 |
234 | test('#parseDeployResponse', { concurrency: true }, async (suite) => {
235 | const cases = [
236 | {
237 | name: 'parses deploy outputs',
238 | stdout: `
239 | {
240 | "apiVersion": "serving.knative.dev/v1",
241 | "kind": "Service",
242 | "metadata": {
243 | "annotations": {
244 | "client.knative.dev/user-image": "image-name:1.0.0",
245 | "run.googleapis.com/client-name": "gcloud",
246 | "run.googleapis.com/client-version": "368.0.0",
247 | "run.googleapis.com/ingress": "internal",
248 | "run.googleapis.com/ingress-status": "internal",
249 | "serving.knative.dev/creator": "creator@domain.com",
250 | "serving.knative.dev/lastModifier": "creator@domain.com"
251 | },
252 | "creationTimestamp": "2022-01-25T21:10:53.714758Z",
253 | "generation": 2,
254 | "labels": {
255 | "cloud.googleapis.com/location": "us-central1"
256 | },
257 | "name": "hello",
258 | "namespace": "392520182231",
259 | "resourceVersion": "AAXWbpADof4",
260 | "selfLink": "/apis/serving.knative.dev/v1/namespaces/392520182231/services/hello",
261 | "uid": "a79b8fc9-f05b-468c-809a-7bb28988fb00"
262 | },
263 | "spec": {
264 | "template": {
265 | "metadata": {
266 | "annotations": {
267 | "autoscaling.knative.dev/maxScale": "2",
268 | "client.knative.dev/user-image": "image-name:1.0.0",
269 | "run.googleapis.com/client-name": "gcloud",
270 | "run.googleapis.com/client-version": "368.0.0"
271 | },
272 | "name": "hello-00002-hex"
273 | },
274 | "spec": {
275 | "containerConcurrency": 80,
276 | "containers": [
277 | {
278 | "image": "image-name:1.0.0",
279 | "ports": [
280 | {
281 | "containerPort": 8080,
282 | "name": "http1"
283 | }
284 | ],
285 | "resources": {
286 | "limits": {
287 | "cpu": "1000m",
288 | "memory": "512Mi"
289 | }
290 | }
291 | }
292 | ],
293 | "serviceAccountName": "000000000-compute@developer.gserviceaccount.com",
294 | "timeoutSeconds": 300
295 | }
296 | },
297 | "traffic": [
298 | {
299 | "latestRevision": true,
300 | "percent": 100
301 | }
302 | ]
303 | },
304 | "status": {
305 | "address": {
306 | "url": "https://action-test-cy7cdwrvha-uc.a.run.app"
307 | },
308 | "conditions": [
309 | {
310 | "lastTransitionTime": "2022-01-25T21:13:54.457086Z",
311 | "status": "True",
312 | "type": "Ready"
313 | },
314 | {
315 | "lastTransitionTime": "2022-01-25T21:13:48.190586Z",
316 | "status": "True",
317 | "type": "ConfigurationsReady"
318 | },
319 | {
320 | "lastTransitionTime": "2022-01-25T21:13:54.457086Z",
321 | "status": "True",
322 | "type": "RoutesReady"
323 | }
324 | ],
325 | "latestCreatedRevisionName": "hello-00002-hex",
326 | "latestReadyRevisionName": "hello-00002-hex",
327 | "observedGeneration": 2,
328 | "traffic": [
329 | {
330 | "latestRevision": true,
331 | "percent": 100,
332 | "revisionName": "hello-00002-hex"
333 | }
334 | ],
335 | "url": "https://action-test-cy7cdwrvha-uc.a.run.app"
336 | }
337 | }
338 | `,
339 | expected: { url: 'https://action-test-cy7cdwrvha-uc.a.run.app' },
340 | },
341 | {
342 | name: 'parses deploy outputs with tag input',
343 | parseInputs: { tag: 'test' },
344 | stdout: `
345 | {
346 | "apiVersion": "serving.knative.dev/v1",
347 | "kind": "Service",
348 | "metadata": {
349 | "annotations": {
350 | "client.knative.dev/user-image": "us-docker.pkg.dev/cloudrun/container/hello@sha256:1595248959b1eaac7f793dfcab2adaecf9c14fdf1cc2b60d20539c6b22fd8e4a",
351 | "run.googleapis.com/client-name": "gcloud",
352 | "run.googleapis.com/client-version": "368.0.0",
353 | "run.googleapis.com/ingress": "internal",
354 | "run.googleapis.com/ingress-status": "internal",
355 | "serving.knative.dev/creator": "verbanicm@google.com",
356 | "serving.knative.dev/lastModifier": "verbanicm@google.com"
357 | },
358 | "creationTimestamp": "2022-01-25T21:10:53.714758Z",
359 | "generation": 9,
360 | "labels": {
361 | "cloud.googleapis.com/location": "us-central1"
362 | },
363 | "name": "hello",
364 | "namespace": "392520182231",
365 | "resourceVersion": "AAXWb88QKzQ",
366 | "selfLink": "/apis/serving.knative.dev/v1/namespaces/392520182231/services/hello",
367 | "uid": "a79b8fc9-f05b-468c-809a-7bb28988fb00"
368 | },
369 | "spec": {
370 | "template": {
371 | "metadata": {
372 | "annotations": {
373 | "autoscaling.knative.dev/maxScale": "2",
374 | "client.knative.dev/user-image": "us-docker.pkg.dev/cloudrun/container/hello@sha256:1595248959b1eaac7f793dfcab2adaecf9c14fdf1cc2b60d20539c6b22fd8e4a",
375 | "run.googleapis.com/client-name": "gcloud",
376 | "run.googleapis.com/client-version": "368.0.0"
377 | },
378 | "name": "hello-suffix"
379 | },
380 | "spec": {
381 | "containerConcurrency": 80,
382 | "containers": [
383 | {
384 | "image": "us-docker.pkg.dev/cloudrun/container/hello@sha256:1595248959b1eaac7f793dfcab2adaecf9c14fdf1cc2b60d20539c6b22fd8e4a",
385 | "ports": [
386 | {
387 | "containerPort": 8080,
388 | "name": "http1"
389 | }
390 | ],
391 | "resources": {
392 | "limits": {
393 | "cpu": "1000m",
394 | "memory": "512Mi"
395 | }
396 | }
397 | }
398 | ],
399 | "serviceAccountName": "392520182231-compute@developer.gserviceaccount.com",
400 | "timeoutSeconds": 300
401 | }
402 | },
403 | "traffic": [
404 | {
405 | "percent": 100,
406 | "revisionName": "hello-00007-jec"
407 | },
408 | {
409 | "revisionName": "hello-00005-zay",
410 | "tag": "test"
411 | },
412 | {
413 | "revisionName": "hello-00007-jec",
414 | "tag": "another"
415 | }
416 | ]
417 | },
418 | "status": {
419 | "address": {
420 | "url": "https://hello-4goqgbaxqq-uc.a.run.app"
421 | },
422 | "conditions": [
423 | {
424 | "lastTransitionTime": "2022-01-25T22:43:07.210548Z",
425 | "status": "True",
426 | "type": "Ready"
427 | },
428 | {
429 | "lastTransitionTime": "2022-01-25T22:43:07.210548Z",
430 | "status": "True",
431 | "type": "ConfigurationsReady"
432 | },
433 | {
434 | "lastTransitionTime": "2022-01-25T22:41:16.384919Z",
435 | "status": "True",
436 | "type": "RoutesReady"
437 | }
438 | ],
439 | "latestCreatedRevisionName": "hello-suffix",
440 | "latestReadyRevisionName": "hello-suffix",
441 | "observedGeneration": 9,
442 | "traffic": [
443 | {
444 | "percent": 100,
445 | "revisionName": "hello-00007-jec"
446 | },
447 | {
448 | "revisionName": "hello-00005-zay",
449 | "tag": "test",
450 | "url": "https://test---hello-4goqgbaxqq-uc.a.run.app"
451 | },
452 | {
453 | "revisionName": "hello-00007-jec",
454 | "tag": "another",
455 | "url": "https://another---hello-4goqgbaxqq-uc.a.run.app"
456 | }
457 | ],
458 | "url": "https://hello-4goqgbaxqq-uc.a.run.app"
459 | }
460 | }
461 | `,
462 | expected: { url: 'https://test---hello-4goqgbaxqq-uc.a.run.app' },
463 | },
464 | {
465 | name: 'handles empty stdout',
466 | stdout: ``,
467 | expected: {},
468 | },
469 | {
470 | name: 'handles empty array from stdout',
471 | stdout: `[]`,
472 | expected: {},
473 | },
474 | {
475 | name: 'handles empty object from stdout',
476 | stdout: `{}`,
477 | expected: {},
478 | },
479 | {
480 | name: 'handles invalid text from stdout',
481 | stdout: `Some text to fail`,
482 | error: `failed to parse deploy response: unexpected token 'S', "Some text to fail" is not valid JSON, stdout: Some text to fail, inputs: undefined`,
483 | },
484 | ];
485 |
486 | for await (const tc of cases) {
487 | await suite.test(tc.name, async () => {
488 | if (tc.error) {
489 | assert.throws(
490 | () => {
491 | parseDeployResponse(tc.stdout, tc.parseInputs);
492 | },
493 | { message: tc.error },
494 | );
495 | } else {
496 | const actual = parseDeployResponse(tc.stdout, tc.parseInputs);
497 | assert.deepStrictEqual(actual, tc.expected);
498 | }
499 | });
500 | }
501 | });
502 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | {
17 | "compilerOptions": {
18 | "target": "es6",
19 | "module": "commonjs",
20 | "lib": [
21 | "es6"
22 | ],
23 | "outDir": "./dist",
24 | "rootDir": "./src",
25 | "strict": true,
26 | "noImplicitAny": true,
27 | "esModuleInterop": true
28 | },
29 | "exclude": ["dist/**/*", "node_modules", "**/*.test.ts"],
30 | }
31 |
--------------------------------------------------------------------------------