├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yaml
└── workflows
│ ├── check-build.yml
│ ├── deploy-preview-with-version.yml
│ ├── deploy-preview.yml
│ ├── deploy-production.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── action.yml
├── babel.config.ts
├── bin
└── action.min.js
├── demo
├── firebase.json
└── public
│ └── index.html
├── docs
└── service-account.md
├── jest.config.ts
├── package-lock.json
├── package.json
├── src
├── createCheck.ts
├── createGACFile.ts
├── deploy.ts
├── getChannelId.ts
├── hash.ts
├── index.ts
└── postOrUpdateComment.ts
├── test
├── deploy.test.ts
├── hash.test.ts
├── postOrUpdateComment.test.ts
└── samples
│ ├── cliOutputs.ts
│ └── comments.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug in this GitHub action
4 | title: "[BUG] "
5 | labels: "bug"
6 | assignees: ""
7 | ---
8 |
9 | ### Action config
10 |
11 |
23 |
24 | ### Error message
25 |
26 |
27 |
28 | ### Expected behavior
29 |
30 |
31 |
32 | ### Actual behavior
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/free-pro-team@latest/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
2 |
3 | blank_issues_enabled: true
4 | contact_links:
5 | - name: Issues with the `firebase init` command
6 | url: https://github.com/firebase/firebase-tools/issues/new/choose
7 | about: The firebase-tools repository is the place to report issues with the `firebase init` command.
8 | - name: 🔥 Firebase Support
9 | url: https://firebase.google.com/support/
10 | about: If you have an urgent issue with your app in production, please contact support.
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Want to add or change this action's functionality?
4 | title: ""
5 | labels: "enhancement"
6 | assignees: ""
7 | ---
8 |
9 | ### The use case you're trying to solve
10 |
11 |
12 |
13 | ### Change to the action that would solve that use case
14 |
15 |
16 |
17 | ### Other considerations
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | # Keep GitHub Actions up to date with GitHub's Dependabot...
5 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
6 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
7 | version: 2
8 | updates:
9 | - package-ecosystem: "npm" # See documentation for possible values
10 | directory: "/" # Location of package manifests
11 | schedule:
12 | interval: "weekly"
13 | day: "monday"
14 | - package-ecosystem: github-actions
15 | directory: /
16 | groups:
17 | github-actions:
18 | patterns:
19 | - "*" # Group all Actions updates into a single larger pull request
20 | schedule:
21 | interval: weekly
22 | day: monday
23 |
--------------------------------------------------------------------------------
/.github/workflows/check-build.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: Check build
16 |
17 | on: pull_request
18 |
19 | jobs:
20 | test:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Install dependencies
25 | run: npm ci
26 | - name: Build
27 | run: npm run build
28 | - name: Ensure built files in branch match
29 | run: git diff --ignore-space-at-eol --exit-code -- "*.min.js"
30 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-preview-with-version.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 Preview with version
16 |
17 | on: pull_request
18 |
19 | jobs:
20 | preview:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Deploy to Firebase Hosting preview channel
25 | id: firebase_hosting_preview
26 | uses: ./
27 | with:
28 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
29 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}"
30 | expires: 14d
31 | projectId: action-hosting-deploy-demo
32 | entryPoint: "./demo"
33 | firebaseToolsVersion: v11.12.0
34 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-preview.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 Preview
16 |
17 | on: pull_request
18 |
19 | jobs:
20 | preview:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Deploy to Firebase Hosting preview channel
25 | id: firebase_hosting_preview
26 | uses: ./
27 | with:
28 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
29 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}"
30 | expires: 14d
31 | projectId: action-hosting-deploy-demo
32 | entryPoint: "./demo"
33 | - name: Check outputs
34 | run: |
35 | echo urls ${{steps.firebase_hosting_preview.outputs.url}}
36 | echo expire_time ${{steps.firebase_hosting_preview.outputs.expire_time}}
37 | echo details_url ${{steps.firebase_hosting_preview.outputs.details_url}}
38 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-production.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Production
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy_website:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | # - run: npm run build
14 | - uses: ./
15 | with:
16 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}"
17 | channelId: live
18 | projectId: action-hosting-deploy-demo
19 | entryPoint: "./demo"
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.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: Test
16 |
17 | on: pull_request
18 |
19 | jobs:
20 | test:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Install dependencies
25 | run: npm ci
26 | - name: Check that Prettier was run
27 | run: npm run format:check
28 | - name: Test
29 | run: npm run test
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .firebase/*
3 | **/*.d.ts
4 | demo/.firebase/hosting.*.cache
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | bin
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
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 | singleQuote: false
16 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement (CLA). You (or your employer) retain the copyright to your
10 | contribution; this simply gives us permission to use and redistribute your
11 | contributions as part of the project. Head over to
12 | to see your current agreements on file or
13 | to sign a new one.
14 |
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 |
19 | ## Code reviews
20 |
21 | All submissions, including submissions by project members, require review. We
22 | use GitHub pull requests for this purpose. Consult
23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
24 | information on using pull requests.
25 |
26 | ## Community Guidelines
27 |
28 | This project follows
29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
30 |
31 | ## Development guide
32 |
33 | ### Get set up
34 |
35 | 1. [Clone](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) this repository (or a [fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo#propose-changes-to-someone-elses-project)).
36 | 1. At the project root, install all modules by running `npm install`.
37 |
38 | ### Creating a Pull Request
39 |
40 | > Note: You must build from source and check in any changes to the contents of the `bin` directory.
41 |
42 | 1. Before creating a pull request, run the following commands to lint, build, and test your changes:
43 |
44 | ```shell
45 | # run the linter
46 | npm run format:check
47 |
48 | # rebuild source
49 | npm run build
50 |
51 | # run unit tests
52 | npm run test
53 | ```
54 |
55 | 1. If you've forked the repo and want to watch the action run, add secrets to your forked repo that match the secrets specified in [one of the workflow files](https://github.com/FirebaseExtended/action-hosting-deploy/tree/main/.github/workflows) you want to test, and trigger the workflow in your forked repo (for example, by creating a pr or pushing to the `main` branch)
56 | 1. Once you're confident in your changes, create a pull request against the firebaseextended/action-hosting-deploy repo.
57 |
58 | ## Publishing a new version
59 |
60 | A repo owner can publish a new version of the action by following the instructions [in the GitHub docs](https://docs.github.com/en/free-pro-team@latest/actions/creating-actions/publishing-actions-in-github-marketplace#publishing-an-action). Manual releases should follow the convention of our existing releases, like [`v0.4-alpha`](https://github.com/FirebaseExtended/action-hosting-deploy/releases/tag/v0.4-alpha).
61 |
62 | The current major version is `v0`. We move this tag up to the latest release using the method [recommended by GitHub](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#user-content-recommendations:~:text=Make%20the%20new%20release%20available%20to%20those%20binding%20to%20the%20major%20version%20tag).
63 |
64 | It is important to note that [firebase-tools references `v0`](https://github.com/firebase/firebase-tools/blob/a1fd2ee6ab2f7b4ac7de021226781f5a8f913e18/src/init/features/hosting/github.ts#L32), so any change in the major version should have a linked firebase-tools PR.
65 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔥🌎 Firebase Hosting GitHub Action
2 |
3 | - Creates a new preview channel (and its associated preview URL) for every PR on your GitHub repository.
4 | - Adds a comment to the PR with the preview URL so that you and each reviewer can view and test the PR's changes in a "preview" version of your app.
5 | - Updates the preview URL with changes from each commit by automatically deploying to the associated preview channel. The URL doesn't change with each new commit.
6 | - (Optional) Deploys the current state of your GitHub repo to your live channel when the PR is merged.
7 |
8 | ## Setup
9 |
10 | A full setup guide can be found [in the Firebase Hosting docs](https://firebase.google.com/docs/hosting/github-integration).
11 |
12 | The [Firebase CLI](https://firebase.google.com/docs/cli) can get you set up quickly with a default configuration.
13 |
14 | - If you've NOT set up Hosting, run this version of the command from the root of your local directory:
15 |
16 | ```bash
17 | firebase init hosting
18 | ```
19 |
20 | - If you've ALREADY set up Hosting, then you just need to set up the GitHub Action part of Hosting.
21 | Run this version of the command from the root of your local directory:
22 |
23 | ```bash
24 | firebase init hosting:github
25 | ```
26 |
27 | ## Usage
28 |
29 | ### Deploy to a new preview channel for every PR
30 |
31 | Add a workflow (`.github/workflows/deploy-preview.yml`):
32 |
33 | ```yaml
34 | name: Deploy to Preview Channel
35 |
36 | on:
37 | pull_request:
38 | # Optionally configure to run only for specific files. For example:
39 | # paths:
40 | # - "website/**"
41 |
42 | jobs:
43 | build_and_preview:
44 | runs-on: ubuntu-latest
45 | steps:
46 | - uses: actions/checkout@v4
47 | # Add any build steps here. For example:
48 | # - run: npm ci && npm run build
49 | - uses: FirebaseExtended/action-hosting-deploy@v0
50 | with:
51 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
52 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}"
53 | expires: 30d
54 | projectId: your-Firebase-project-ID
55 | ```
56 |
57 | ### Deploy to your live channel on merge
58 |
59 | Add a workflow (`.github/workflows/deploy-prod.yml`):
60 |
61 | ```yaml
62 | name: Deploy to Live Channel
63 |
64 | on:
65 | push:
66 | branches:
67 | - main
68 | # Optionally configure to run only for specific files. For example:
69 | # paths:
70 | # - "website/**"
71 |
72 | jobs:
73 | deploy_live_website:
74 | runs-on: ubuntu-latest
75 | steps:
76 | - uses: actions/checkout@v4
77 | # Add any build steps here. For example:
78 | # - run: npm ci && npm run build
79 | - uses: FirebaseExtended/action-hosting-deploy@v0
80 | with:
81 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}"
82 | projectId: your-Firebase-project-ID
83 | channelId: live
84 | ```
85 |
86 | ## Options
87 |
88 | ### `firebaseServiceAccount` _{string}_ (required)
89 |
90 | This is a service account JSON key. The easiest way to set it up is to run `firebase init hosting:github`. However, it can also be [created manually](./docs/service-account.md).
91 |
92 | It's important to store this token as an
93 | [encrypted secret](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets)
94 | to prevent unintended access to your Firebase project. Set it in the "Secrets" area
95 | of your repository settings and add it as `FIREBASE_SERVICE_ACCOUNT`:
96 | `https://github.com/USERNAME/REPOSITORY/settings/secrets`.
97 |
98 | ### `repoToken` _{string}_
99 |
100 | Adding `repoToken: "${{secrets.GITHUB_TOKEN}}"` lets the action comment on PRs
101 | with the preview URL for the associated preview channel. You don't need to set
102 | this secret yourself - GitHub sets it automatically.
103 |
104 | If you omit this option, you'll need to find the preview URL in the action's
105 | build log.
106 |
107 | ### `expires` _{string}_
108 |
109 | The length of time the preview channel should remain active after the last deploy.
110 | If left blank, the action uses the default expiry of 7 days.
111 | The expiry date will reset to this value on every new deployment.
112 |
113 | ### `projectId` _{string}_
114 |
115 | The Firebase project that contains the Hosting site to which you
116 | want to deploy. If left blank, you need to check in a `.firebaserc`
117 | file so that the Firebase CLI knows which Firebase project to use.
118 |
119 | ### `channelId` _{string}_
120 |
121 | The ID of the channel to deploy to. If you leave this blank,
122 | a preview channel and its ID will be auto-generated per branch or PR.
123 | If you set it to **`live`**, the action deploys to the live channel of your default Hosting site.
124 |
125 | _You usually want to leave this blank_ so that each PR gets its own preview channel.
126 | An exception might be that you always want to deploy a certain branch to a
127 | long-lived preview channel (for example, you may want to deploy every commit
128 | from your `next` branch to a `preprod` preview channel).
129 |
130 | ### `target` _{string}_
131 |
132 | The target name of the Hosting site to deploy to. If you leave this blank,
133 | the default target or all targets defined in the `.firebaserc` will be deployed to.
134 |
135 | You usually want to leave this blank unless you have set up multiple sites in the Firebase Hosting UI
136 | and are trying to target just one of those sites with this action.
137 |
138 | Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites)
139 | for more information about deploy targets.
140 |
141 | ### `entryPoint` _{string}_
142 |
143 | The directory containing your [`firebase.json`](https://firebase.google.com/docs/cli#the_firebasejson_file)
144 | file relative to the root of your repository. Defaults to `.` (the root of your repo).
145 |
146 | ### `firebaseToolsVersion` _{string}_
147 |
148 | The version of `firebase-tools` to use. If not specified, defaults to `latest`.
149 |
150 | ### `disableComment` _{boolean}_
151 |
152 | Disable commenting in a PR with the preview URL.
153 |
154 | ## Outputs
155 |
156 | Values emitted by this action that can be consumed by other actions later in your workflow
157 |
158 | ### `urls`
159 |
160 | The url(s) deployed to
161 |
162 | ### `expire_time`
163 |
164 | The time the deployed preview urls expire, example: 2024-04-10T14:37:53.817800922Z
165 |
166 | ### `expire_time_formatted`
167 |
168 | The time the deployed preview urls expire in the UTC format, example: Tue, 09 Apr 2024 18:24:42 GMT
169 |
170 | ### `details_url`
171 |
172 | A single URL that was deployed to
173 |
174 | ## Status
175 |
176 | 
177 |
178 | This repository is maintained by Googlers but is not a supported Firebase product. Issues here are answered by maintainers and other community members on GitHub on a best-effort basis.
179 |
--------------------------------------------------------------------------------
/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 Firebase Hosting"
16 | description: "A GitHub Action to deploy to Firebase Hosting"
17 | author: "Jason Miller (https://github.com/developit) and Firebase"
18 | branding:
19 | icon: "globe"
20 | color: "blue"
21 | runs:
22 | using: "node20"
23 | main: "bin/action.min.js"
24 | inputs:
25 | repoToken:
26 | description: "The GITHUB_TOKEN secret"
27 | required: false
28 | firebaseServiceAccount:
29 | description: "Firebase service account JSON"
30 | required: true
31 | expires:
32 | description: "How long should a preview live? See the preview channels docs for options"
33 | default: "7d"
34 | required: false
35 | projectId:
36 | description:
37 | "The project to deploy to. If you leave this blank, make sure you check in
38 | a .firebaserc file"
39 | required: false
40 | channelId:
41 | description: "The ID of the channel to deploy to. If you leave this blank,
42 | a preview channel and its ID will be auto-generated per branch or PR."
43 | required: false
44 | target:
45 | description:
46 | "The target name of the Hosting site to deploy to. If you leave this blank,
47 | the default target or all targets defined in the .firebaserc will be deployed to.
48 | Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites)
49 | for more information about deploy targets."
50 | required: false
51 | entryPoint:
52 | description:
53 | "The location of your firebase.json file, relative to the root of your
54 | directory"
55 | default: "."
56 | required: false
57 | firebaseToolsVersion:
58 | description: >-
59 | The version of firebase-tools to use. Defaults to `latest`.
60 | default: latest
61 | required: false
62 | disableComment:
63 | description: >-
64 | Disable auto-commenting with the preview channel URL to the pull request
65 | default: "false"
66 | required: false
67 | outputs:
68 | urls:
69 | description: The url(s) deployed to
70 | expire_time:
71 | description: The time the deployed preview urls expire
72 | expire_time_formatted:
73 | description: The time the deployed preview urls expire in the UTC format
74 | details_url:
75 | description: A single URL that was deployed to
76 |
--------------------------------------------------------------------------------
/babel.config.ts:
--------------------------------------------------------------------------------
1 | // babel config for jest tests
2 | // https://jestjs.io/docs/en/getting-started#using-typescript
3 | module.exports = {
4 | presets: [
5 | ["@babel/preset-env", { targets: { node: "current" } }],
6 | "@babel/preset-typescript",
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/demo/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "public",
4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 | Welcome to Firebase Hosting 🌎
23 |
24 |
25 | Hello World! 🌎
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/service-account.md:
--------------------------------------------------------------------------------
1 | # Setting up the service account
2 |
3 | This action requires a service account to authenticate with Firebase Hosting. The easiest way to set up the service account is to use the Firebase CLI with the `firebase init hosting:github` if that doesn't work for you, you can configure it manually.
4 |
5 | ## Manually configure the service account
6 |
7 | ### 1. Create a service account that the action will use to deploy to Hosting
8 |
9 | 1. Visit the [GCP Service Accounts page](https://console.cloud.google.com/iam-admin/serviceaccounts) and make sure the correct project (same name as your Firebase project) is selected in the top blue bar
10 | 1. Click the "+ CREATE SERVICE ACCOUNT" button
11 | 1. Give the service account a name, id, description. We recommend something like `github-action-`
12 | 1. At the "Grant this service account access to project" step, choose the following [roles](https://firebase.google.com/docs/projects/iam/roles-predefined-product) that the service account will need to deploy on your behalf:
13 | - **Firebase Authentication Admin** (Required to add preview URLs to Auth authorized domains)
14 | - `roles/firebaseauth.admin`
15 | - **Firebase Hosting Admin** (Required to deploy preview channels)
16 | - `roles/firebasehosting.admin`
17 | - **Cloud Run Viewer** (Required for projects that [use Hosting rewrites to Cloud Run or Cloud Functions](https://firebase.google.com/docs/hosting/serverless-overview))
18 | - `roles/run.viewer`
19 | - **API Keys Viewer** (Required for CLI deploys)
20 | - `roles/serviceusage.apiKeysViewer`
21 | 1. Finish the service account creation flow
22 |
23 | ### 2. Get that service account's key and add it to your repository as a secret
24 |
25 | 1. [Create and download](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating_service_account_keys) the new service account's JSON key
26 | 1. Add that JSON key [as a secret in your GitHub repository](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository). We recommend a name like `FIREBASE_SERVICE_ACCOUNT_` (example: `FIREBASE_SERVICE_ACCOUNT_MY_COOL_APP`)
27 |
28 | ### 3. Add a workflow yaml file to your repository
29 |
30 | 1. Add a yml file as described [here](../README.md#deploy-to-a-new-preview-channel-for-every-pr). Be sure to reference your new secret for the `firebaseServiceAccount` option.
31 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/en/configuration.html
4 | */
5 |
6 | export default {
7 | // Automatically clear mock calls and instances between every test
8 | clearMocks: true,
9 |
10 | // A preset that is used as a base for Jest's configuration
11 | preset: "ts-jest",
12 |
13 | // The test environment that will be used for testing
14 | testEnvironment: "node",
15 | };
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "firebase-hosting-preview-action",
4 | "main": "bin/action.min.js",
5 | "source": "src/index.ts",
6 | "devDependencies": {
7 | "@actions/core": "^1.10.0",
8 | "@actions/exec": "^1.1.1",
9 | "@actions/github": "^5.1.1",
10 | "@babel/core": "^7.21.4",
11 | "@babel/preset-env": "^7.24.6",
12 | "@babel/preset-typescript": "^7.24.6",
13 | "@tsconfig/node12": "^1.0.11",
14 | "@types/jest": "^29.5.1",
15 | "@types/tmp": "^0.2.3",
16 | "babel-jest": "^29.5.0",
17 | "husky": "^9.0.11",
18 | "jest": "^29.5.0",
19 | "microbundle": "^0.15.1",
20 | "prettier": "^2.8.7",
21 | "pretty-quick": "^3.3.1",
22 | "tmp": "^0.2.1",
23 | "ts-jest": "^29.1.3",
24 | "ts-node": "^10.9.1",
25 | "typescript": "^5.0.4"
26 | },
27 | "scripts": {
28 | "format:check": "prettier . --list-different",
29 | "format": "prettier --write .",
30 | "build": "microbundle --format cjs --target node --no-compress --no-sourcemap src/index.ts",
31 | "build:watch": "microbundle watch --format cjs --target node --no-compress --no-sourcemap src/index.ts",
32 | "test": "jest"
33 | },
34 | "husky": {
35 | "hooks": {
36 | "pre-commit": "pretty-quick --staged && npm run build"
37 | }
38 | },
39 | "version": "0.8.0"
40 | }
41 |
--------------------------------------------------------------------------------
/src/createCheck.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 | import type { Context } from "@actions/github/lib/context";
17 | import type { GitHub } from "@actions/github/lib/utils";
18 |
19 | // create a check and return a function that updates (completes) it
20 | export async function createCheck(
21 | github: InstanceType,
22 | context: Context
23 | ) {
24 | const check = await github.rest.checks.create({
25 | ...context.repo,
26 | name: "Deploy Preview",
27 | head_sha: context.payload.pull_request?.head.sha,
28 | status: "in_progress",
29 | });
30 |
31 | return async (details: Object) => {
32 | await github.rest.checks.update({
33 | ...context.repo,
34 | check_run_id: check.data.id,
35 | completed_at: new Date().toISOString(),
36 | status: "completed",
37 | ...details,
38 | });
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/createGACFile.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 { fileSync } from "tmp";
18 | import { writeSync } from "fs";
19 |
20 | // Set up Google Application Credentials for use by the Firebase CLI
21 | // https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
22 | export async function createGacFile(googleApplicationCredentials: string) {
23 | const tmpFile = fileSync({ postfix: ".json" });
24 |
25 | writeSync(tmpFile.fd, googleApplicationCredentials);
26 |
27 | return tmpFile.name;
28 | }
29 |
--------------------------------------------------------------------------------
/src/deploy.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 { exec } from "@actions/exec";
18 |
19 | export type SiteDeploy = {
20 | site: string;
21 | target?: string;
22 | url: string;
23 | expireTime: string;
24 | };
25 |
26 | export type ErrorResult = {
27 | status: "error";
28 | error: string;
29 | };
30 |
31 | export type ChannelSuccessResult = {
32 | status: "success";
33 | result: { [key: string]: SiteDeploy };
34 | };
35 |
36 | export type ProductionSuccessResult = {
37 | status: "success";
38 | result: {
39 | hosting: string | string[];
40 | };
41 | };
42 |
43 | type DeployConfig = {
44 | projectId: string;
45 | target?: string;
46 | // Optional version specification for firebase-tools. Defaults to `latest`.
47 | firebaseToolsVersion?: string;
48 | };
49 |
50 | export type ChannelDeployConfig = DeployConfig & {
51 | expires: string;
52 | channelId: string;
53 | };
54 |
55 | export type ProductionDeployConfig = DeployConfig & {};
56 |
57 | export function interpretChannelDeployResult(
58 | deployResult: ChannelSuccessResult
59 | ): { expireTime: string; expire_time_formatted: string; urls: string[] } {
60 | const allSiteResults = Object.values(deployResult.result);
61 |
62 | const expireTime = allSiteResults[0].expireTime;
63 | const expire_time_formatted = new Date(expireTime).toUTCString();
64 | const urls = allSiteResults.map((siteResult) => siteResult.url);
65 |
66 | return {
67 | expireTime,
68 | expire_time_formatted,
69 | urls,
70 | };
71 | }
72 |
73 | async function execWithCredentials(
74 | args: string[],
75 | projectId,
76 | gacFilename,
77 | opts: { debug?: boolean; firebaseToolsVersion?: string }
78 | ) {
79 | let deployOutputBuf: Buffer[] = [];
80 | const debug = opts.debug || false;
81 | const firebaseToolsVersion = opts.firebaseToolsVersion || "latest";
82 |
83 | try {
84 | await exec(
85 | `npx firebase-tools@${firebaseToolsVersion}`,
86 | [
87 | ...args,
88 | ...(projectId ? ["--project", projectId] : []),
89 | debug
90 | ? "--debug" // gives a more thorough error message
91 | : "--json", // allows us to easily parse the output
92 | ],
93 | {
94 | listeners: {
95 | stdout(data: Buffer) {
96 | deployOutputBuf.push(data);
97 | },
98 | },
99 | env: {
100 | ...process.env,
101 | FIREBASE_DEPLOY_AGENT: "action-hosting-deploy",
102 | GOOGLE_APPLICATION_CREDENTIALS: gacFilename, // the CLI will automatically authenticate with this env variable set
103 | },
104 | }
105 | );
106 | } catch (e) {
107 | console.log(Buffer.concat(deployOutputBuf).toString("utf-8"));
108 | console.log(e.message);
109 |
110 | if (!debug) {
111 | console.log(
112 | "Retrying deploy with the --debug flag for better error output"
113 | );
114 | await execWithCredentials(args, projectId, gacFilename, {
115 | debug: true,
116 | firebaseToolsVersion,
117 | });
118 | } else {
119 | throw e;
120 | }
121 | }
122 |
123 | return deployOutputBuf.length
124 | ? deployOutputBuf[deployOutputBuf.length - 1].toString("utf-8")
125 | : ""; // output from the CLI
126 | }
127 |
128 | export async function deployPreview(
129 | gacFilename: string,
130 | deployConfig: ChannelDeployConfig
131 | ) {
132 | const { projectId, channelId, target, expires, firebaseToolsVersion } =
133 | deployConfig;
134 |
135 | const deploymentText = await execWithCredentials(
136 | [
137 | "hosting:channel:deploy",
138 | channelId,
139 | ...(target ? ["--only", target] : []),
140 | ...(expires ? ["--expires", expires] : []),
141 | ],
142 | projectId,
143 | gacFilename,
144 | { firebaseToolsVersion }
145 | );
146 |
147 | const deploymentResult = JSON.parse(deploymentText.trim()) as
148 | | ChannelSuccessResult
149 | | ErrorResult;
150 |
151 | return deploymentResult;
152 | }
153 |
154 | export async function deployProductionSite(
155 | gacFilename,
156 | productionDeployConfig: ProductionDeployConfig
157 | ) {
158 | const { projectId, target, firebaseToolsVersion } = productionDeployConfig;
159 |
160 | const deploymentText = await execWithCredentials(
161 | ["deploy", "--only", `hosting${target ? ":" + target : ""}`],
162 | projectId,
163 | gacFilename,
164 | { firebaseToolsVersion }
165 | );
166 |
167 | const deploymentResult = JSON.parse(deploymentText) as
168 | | ProductionSuccessResult
169 | | ErrorResult;
170 |
171 | return deploymentResult;
172 | }
173 |
--------------------------------------------------------------------------------
/src/getChannelId.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 { Context } from "@actions/github/lib/context";
18 |
19 | export function getChannelId(configuredChannelId: string, ghContext: Context) {
20 | let tmpChannelId: string = "";
21 |
22 | if (!!configuredChannelId) {
23 | tmpChannelId = configuredChannelId;
24 | } else if (ghContext.payload.pull_request) {
25 | const branchName = ghContext.payload.pull_request.head.ref.substr(0, 20);
26 | tmpChannelId = `pr${ghContext.payload.pull_request.number}-${branchName}`;
27 | }
28 |
29 | // Channel IDs can only include letters, numbers, underscores, hyphens, and periods.
30 | const invalidCharactersRegex = /[^a-zA-Z0-9_\-\.]/g;
31 | const correctedChannelId = tmpChannelId.replace(invalidCharactersRegex, "_");
32 | if (correctedChannelId !== tmpChannelId) {
33 | console.log(
34 | `ChannelId "${tmpChannelId}" contains unsupported characters. Using "${correctedChannelId}" instead.`
35 | );
36 | }
37 |
38 | return correctedChannelId;
39 | }
40 |
--------------------------------------------------------------------------------
/src/hash.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 * as crypto from "crypto";
18 | import { ChannelSuccessResult } from "./deploy";
19 |
20 | // Create a stable signature for a deploy used for earmarking.
21 | export function createDeploySignature(deployResult: ChannelSuccessResult) {
22 | const results = Object.values(deployResult.result);
23 | const sites = results.map((result) => result.site).sort();
24 |
25 | const hash = crypto.createHash("sha1");
26 | sites.forEach((site) => {
27 | hash.update(site);
28 | });
29 | return hash.digest("hex");
30 | }
31 |
--------------------------------------------------------------------------------
/src/index.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 {
18 | endGroup,
19 | getInput,
20 | setFailed,
21 | setOutput,
22 | startGroup,
23 | } from "@actions/core";
24 | import { context, getOctokit } from "@actions/github";
25 | import { existsSync } from "fs";
26 | import { createCheck } from "./createCheck";
27 | import { createGacFile } from "./createGACFile";
28 | import {
29 | deployPreview,
30 | deployProductionSite,
31 | ErrorResult,
32 | interpretChannelDeployResult,
33 | } from "./deploy";
34 | import { getChannelId } from "./getChannelId";
35 | import {
36 | getURLsMarkdownFromChannelDeployResult,
37 | postChannelSuccessComment,
38 | } from "./postOrUpdateComment";
39 |
40 | // Inputs defined in action.yml
41 | const expires = getInput("expires");
42 | const projectId = getInput("projectId");
43 | const googleApplicationCredentials = getInput("firebaseServiceAccount", {
44 | required: true,
45 | });
46 | const configuredChannelId = getInput("channelId");
47 | const isProductionDeploy = configuredChannelId === "live";
48 | const token = process.env.GITHUB_TOKEN || getInput("repoToken");
49 | const octokit = token ? getOctokit(token) : undefined;
50 | const entryPoint = getInput("entryPoint");
51 | const target = getInput("target");
52 | const firebaseToolsVersion = getInput("firebaseToolsVersion");
53 | const disableComment = getInput("disableComment");
54 |
55 | async function run() {
56 | const isPullRequest = !!context.payload.pull_request;
57 |
58 | let finish = (details: Object) => console.log(details);
59 | if (token && isPullRequest) {
60 | finish = await createCheck(octokit, context);
61 | }
62 |
63 | try {
64 | startGroup("Verifying firebase.json exists");
65 |
66 | if (entryPoint !== ".") {
67 | console.log(`Changing to directory: ${entryPoint}`);
68 | try {
69 | process.chdir(entryPoint);
70 | } catch (err) {
71 | throw Error(`Error changing to directory ${entryPoint}: ${err}`);
72 | }
73 | }
74 |
75 | if (existsSync("./firebase.json")) {
76 | console.log("firebase.json file found. Continuing deploy.");
77 | } else {
78 | throw Error(
79 | "firebase.json file not found. If your firebase.json file is not in the root of your repo, edit the entryPoint option of this GitHub action."
80 | );
81 | }
82 | endGroup();
83 |
84 | startGroup("Setting up CLI credentials");
85 | const gacFilename = await createGacFile(googleApplicationCredentials);
86 | console.log(
87 | "Created a temporary file with Application Default Credentials."
88 | );
89 | endGroup();
90 |
91 | if (isProductionDeploy) {
92 | startGroup("Deploying to production site");
93 | const deployment = await deployProductionSite(gacFilename, {
94 | projectId,
95 | target,
96 | firebaseToolsVersion,
97 | });
98 | if (deployment.status === "error") {
99 | throw Error((deployment as ErrorResult).error);
100 | }
101 | endGroup();
102 |
103 | const hostname = target ? `${target}.web.app` : `${projectId}.web.app`;
104 | const url = `https://${hostname}/`;
105 | await finish({
106 | details_url: url,
107 | conclusion: "success",
108 | output: {
109 | title: `Production deploy succeeded`,
110 | summary: `[${hostname}](${url})`,
111 | },
112 | });
113 | return;
114 | }
115 |
116 | const channelId = getChannelId(configuredChannelId, context);
117 |
118 | startGroup(`Deploying to Firebase preview channel ${channelId}`);
119 | const deployment = await deployPreview(gacFilename, {
120 | projectId,
121 | expires,
122 | channelId,
123 | target,
124 | firebaseToolsVersion,
125 | });
126 |
127 | if (deployment.status === "error") {
128 | throw Error((deployment as ErrorResult).error);
129 | }
130 | endGroup();
131 |
132 | const { expireTime, expire_time_formatted, urls } =
133 | interpretChannelDeployResult(deployment);
134 |
135 | setOutput("urls", urls);
136 | setOutput("expire_time", expireTime);
137 | setOutput("expire_time_formatted", expire_time_formatted);
138 | setOutput("details_url", urls[0]);
139 |
140 | if (disableComment === "true") {
141 | console.log(
142 | `Commenting on PR is disabled with "disableComment: ${disableComment}"`
143 | );
144 | } else if (token && isPullRequest && !!octokit) {
145 | const commitId = context.payload.pull_request?.head.sha.substring(0, 7);
146 |
147 | await postChannelSuccessComment(octokit, context, deployment, commitId);
148 | }
149 |
150 | await finish({
151 | details_url: urls[0],
152 | conclusion: "success",
153 | output: {
154 | title: `Deploy preview succeeded`,
155 | summary: getURLsMarkdownFromChannelDeployResult(deployment),
156 | },
157 | });
158 | } catch (e) {
159 | setFailed(e.message);
160 |
161 | await finish({
162 | conclusion: "failure",
163 | output: {
164 | title: "Deploy preview failed",
165 | summary: `Error: ${e.message}`,
166 | },
167 | });
168 | }
169 | }
170 |
171 | run();
172 |
--------------------------------------------------------------------------------
/src/postOrUpdateComment.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 { endGroup, startGroup } from "@actions/core";
18 | import type { GitHub } from "@actions/github/lib/utils";
19 | import { Context } from "@actions/github/lib/context";
20 | import { ChannelSuccessResult, interpretChannelDeployResult } from "./deploy";
21 | import { createDeploySignature } from "./hash";
22 |
23 | const BOT_SIGNATURE =
24 | "🔥 via [Firebase Hosting GitHub Action](https://github.com/marketplace/actions/deploy-to-firebase-hosting) 🌎";
25 |
26 | export function createBotCommentIdentifier(signature: string) {
27 | return function isCommentByBot(comment): boolean {
28 | return comment.user.type === "Bot" && comment.body.includes(signature);
29 | };
30 | }
31 |
32 | export function getURLsMarkdownFromChannelDeployResult(
33 | result: ChannelSuccessResult
34 | ): string {
35 | const { urls } = interpretChannelDeployResult(result);
36 |
37 | return urls.length === 1
38 | ? `[${urls[0]}](${urls[0]})`
39 | : urls.map((url) => `- [${url}](${url})`).join("\n");
40 | }
41 |
42 | export function getChannelDeploySuccessComment(
43 | result: ChannelSuccessResult,
44 | commit: string
45 | ) {
46 | const deploySignature = createDeploySignature(result);
47 | const urlList = getURLsMarkdownFromChannelDeployResult(result);
48 | const { expire_time_formatted } = interpretChannelDeployResult(result);
49 |
50 | return `
51 | Visit the preview URL for this PR (updated for commit ${commit}):
52 |
53 | ${urlList}
54 |
55 | (expires ${expire_time_formatted})
56 |
57 | ${BOT_SIGNATURE}
58 |
59 | Sign: ${deploySignature}`.trim();
60 | }
61 |
62 | export async function postChannelSuccessComment(
63 | github: InstanceType,
64 | context: Context,
65 | result: ChannelSuccessResult,
66 | commit: string
67 | ) {
68 | const commentInfo = {
69 | ...context.repo,
70 | issue_number: context.issue.number,
71 | };
72 |
73 | const commentMarkdown = getChannelDeploySuccessComment(result, commit);
74 |
75 | const comment = {
76 | ...commentInfo,
77 | body: commentMarkdown,
78 | };
79 |
80 | startGroup(`Commenting on PR`);
81 | const deploySignature = createDeploySignature(result);
82 | const isCommentByBot = createBotCommentIdentifier(deploySignature);
83 |
84 | let commentId;
85 | try {
86 | const comments = (await github.rest.issues.listComments(commentInfo)).data;
87 | for (let i = comments.length; i--; ) {
88 | const c = comments[i];
89 | if (isCommentByBot(c)) {
90 | commentId = c.id;
91 | break;
92 | }
93 | }
94 | } catch (e) {
95 | console.log("Error checking for previous comments: " + e.message);
96 | }
97 |
98 | if (commentId) {
99 | try {
100 | await github.rest.issues.updateComment({
101 | ...context.repo,
102 | comment_id: commentId,
103 | body: comment.body,
104 | });
105 | } catch (e) {
106 | commentId = null;
107 | }
108 | }
109 |
110 | if (!commentId) {
111 | try {
112 | await github.rest.issues.createComment(comment);
113 | } catch (e) {
114 | console.log(`Error creating comment: ${e.message}`);
115 | }
116 | }
117 | endGroup();
118 | }
119 |
--------------------------------------------------------------------------------
/test/deploy.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChannelSuccessResult,
3 | ChannelDeployConfig,
4 | deployPreview,
5 | deployProductionSite,
6 | ProductionDeployConfig,
7 | ProductionSuccessResult,
8 | } from "../src/deploy";
9 | import * as exec from "@actions/exec";
10 | import {
11 | channelError,
12 | channelMultiSiteSuccess,
13 | channelSingleSiteSuccess,
14 | liveDeployMultiSiteSuccess,
15 | liveDeploySingleSiteSuccess,
16 | } from "./samples/cliOutputs";
17 |
18 | const baseChannelDeployConfig: ChannelDeployConfig = {
19 | projectId: "my-project",
20 | channelId: "my-channel",
21 | expires: undefined,
22 | };
23 |
24 | const baseLiveDeployConfig: ProductionDeployConfig = {
25 | projectId: "my-project",
26 | };
27 |
28 | async function fakeExecFail(
29 | mainCommand: string,
30 | args: string[],
31 | options: exec.ExecOptions
32 | ) {
33 | options?.listeners?.stdout(Buffer.from(JSON.stringify(channelError), "utf8"));
34 |
35 | throw new Error("I am an error");
36 | }
37 |
38 | async function fakeExec(
39 | mainCommand: string,
40 | args: string[],
41 | options: exec.ExecOptions
42 | ) {
43 | if (args.includes("--debug")) {
44 | return options?.listeners?.stdout(
45 | Buffer.from("I am a very long debug output", "utf8")
46 | );
47 | }
48 |
49 | const isChannelDeploy = args[0] === "hosting:channel:deploy";
50 | let successOutput;
51 |
52 | if (args.includes("--target")) {
53 | successOutput = isChannelDeploy
54 | ? channelMultiSiteSuccess
55 | : liveDeployMultiSiteSuccess;
56 | } else {
57 | successOutput = isChannelDeploy
58 | ? channelSingleSiteSuccess
59 | : liveDeploySingleSiteSuccess;
60 | }
61 |
62 | options?.listeners?.stdout(
63 | Buffer.from(JSON.stringify(successOutput), "utf8")
64 | );
65 | }
66 |
67 | describe("deploy", () => {
68 | it("retries with the --debug flag on error", async () => {
69 | // @ts-ignore read-only property
70 | exec.exec = jest.fn(fakeExec).mockImplementationOnce(fakeExecFail);
71 |
72 | const deployOutput: ChannelSuccessResult = (await deployPreview(
73 | "my-file",
74 | baseChannelDeployConfig
75 | )) as ChannelSuccessResult;
76 |
77 | expect(exec.exec).toBeCalledTimes(2);
78 | expect(deployOutput).toEqual(channelError);
79 |
80 | // Check the arguments that exec was called with
81 | // @ts-ignore Jest adds a magic "mock" property
82 | const args = exec.exec.mock.calls;
83 | const firstCallDeployFlags = args[0][1];
84 | const secondCallDeployFlags = args[1][1];
85 | expect(firstCallDeployFlags).toContain("--json");
86 | expect(secondCallDeployFlags).not.toContain("--json");
87 | expect(firstCallDeployFlags).not.toContain("--debug");
88 | expect(secondCallDeployFlags).toContain("--debug");
89 | });
90 |
91 | describe("deploy to preview channel", () => {
92 | it("calls exec and interprets the output", async () => {
93 | // @ts-ignore read-only property
94 | exec.exec = jest.fn(fakeExec);
95 |
96 | const deployOutput: ChannelSuccessResult = (await deployPreview(
97 | "my-file",
98 | baseChannelDeployConfig
99 | )) as ChannelSuccessResult;
100 |
101 | expect(exec.exec).toBeCalled();
102 | expect(deployOutput).toEqual(channelSingleSiteSuccess);
103 |
104 | // Check the arguments that exec was called with
105 | // @ts-ignore Jest adds a magic "mock" property
106 | const args = exec.exec.mock.calls;
107 | const deployFlags = args[0][1];
108 | expect(deployFlags).toContain("hosting:channel:deploy");
109 | });
110 |
111 | it("specifies a target when one is provided", async () => {
112 | // @ts-ignore read-only property
113 | exec.exec = jest.fn(fakeExec);
114 |
115 | const config: ChannelDeployConfig = {
116 | ...baseChannelDeployConfig,
117 | target: "my-second-site",
118 | };
119 |
120 | await deployPreview("my-file", config);
121 |
122 | // Check the arguments that exec was called with
123 | // @ts-ignore Jest adds a magic "mock" property
124 | const args = exec.exec.mock.calls;
125 | const deployFlags = args[0][1];
126 | expect(deployFlags).toContain("--only");
127 | expect(deployFlags).toContain("my-second-site");
128 | });
129 | });
130 |
131 | describe("deploy to live channel", () => {
132 | it("calls exec and interprets the output", async () => {
133 | // @ts-ignore read-only property
134 | exec.exec = jest.fn(fakeExec);
135 |
136 | const deployOutput: ProductionSuccessResult = (await deployProductionSite(
137 | "my-file",
138 | baseLiveDeployConfig
139 | )) as ProductionSuccessResult;
140 |
141 | expect(exec.exec).toBeCalled();
142 | expect(deployOutput).toEqual(liveDeploySingleSiteSuccess);
143 |
144 | // Check the arguments that exec was called with
145 | // @ts-ignore Jest adds a magic "mock" property
146 | const args = exec.exec.mock.calls;
147 | const deployFlags = args[0][1];
148 | expect(deployFlags).toContain("deploy");
149 | expect(deployFlags).toContain("--only");
150 | expect(deployFlags).toContain("hosting");
151 | });
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/test/hash.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | channelSingleSiteSuccess,
3 | channelMultiSiteSuccess,
4 | } from "./samples/cliOutputs";
5 | import { createDeploySignature } from "../src/hash";
6 |
7 | describe("hash", () => {
8 | it("Returns stable signature for single site", () => {
9 | const signSingle = createDeploySignature(channelSingleSiteSuccess);
10 | expect(signSingle).toEqual("ca07ce2c831b1990b78fcf2ecdfe230a486dc973");
11 | });
12 |
13 | it("Returns stable signature for multi site", () => {
14 | const signMulti1 = createDeploySignature(channelMultiSiteSuccess);
15 | expect(signMulti1).toEqual("980f04126fb629deaadace7d6ee8a0628942e3d3");
16 |
17 | const signMulti2 = createDeploySignature({
18 | ...channelMultiSiteSuccess,
19 | result: {
20 | targetX: channelMultiSiteSuccess.result.target2,
21 | targetY: channelMultiSiteSuccess.result.target1,
22 | },
23 | });
24 | expect(signMulti2).toEqual("980f04126fb629deaadace7d6ee8a0628942e3d3");
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/postOrUpdateComment.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | singleSiteComment,
3 | multiSiteComment,
4 | notABotComment,
5 | } from "./samples/comments";
6 | import {
7 | getChannelDeploySuccessComment,
8 | createBotCommentIdentifier,
9 | } from "../src/postOrUpdateComment";
10 | import {
11 | channelSingleSiteSuccess,
12 | channelMultiSiteSuccess,
13 | } from "./samples/cliOutputs";
14 | import { createDeploySignature } from "../src/hash";
15 |
16 | describe("postOrUpdateComment", () => {
17 | it("Creates the expected comment for a single site", () => {
18 | const comment = getChannelDeploySuccessComment(
19 | channelSingleSiteSuccess,
20 | "fe211ff"
21 | );
22 |
23 | expect(comment).toEqual(singleSiteComment);
24 | });
25 |
26 | it("Creates the expected comment for multisite", () => {
27 | const comment = getChannelDeploySuccessComment(
28 | channelMultiSiteSuccess,
29 | "fe211ff"
30 | );
31 |
32 | expect(comment).toEqual(multiSiteComment);
33 | });
34 |
35 | it("Can tell if a comment has been written by itself", () => {
36 | const signature = createDeploySignature(channelSingleSiteSuccess);
37 | const isCommentByBot = createBotCommentIdentifier(signature);
38 | const testComment = {
39 | user: { type: "Bot" },
40 | body: singleSiteComment,
41 | };
42 | expect(isCommentByBot(testComment)).toEqual(true);
43 | });
44 |
45 | it("Can tell if a comment has not been written by itself", () => {
46 | const signature = createDeploySignature(channelMultiSiteSuccess);
47 | const isCommentByBot = createBotCommentIdentifier(signature);
48 | const testComment = {
49 | user: { type: "Bot" },
50 | body: notABotComment,
51 | };
52 | expect(isCommentByBot(testComment)).toEqual(false);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/samples/cliOutputs.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChannelSuccessResult,
3 | ErrorResult,
4 | ProductionSuccessResult,
5 | } from "../../src/deploy";
6 |
7 | export const commitId = "fe211ff";
8 |
9 | export const channelMultiSiteSuccess: ChannelSuccessResult = {
10 | status: "success",
11 | result: {
12 | target1: {
13 | site: "my-main-hosting-site",
14 | target: "target1",
15 | url: "https://action-hosting-deploy-demo--multisite-test-goqvngto.web.app",
16 | expireTime: "2020-10-27T21:32:57.233344586Z",
17 | },
18 | target2: {
19 | site: "my-second-hosting-site",
20 | target: "target2",
21 | url: "https://action-hosting-deploy-demo-2--multisite-test-ksadajci.web.app",
22 | expireTime: "2020-10-27T21:32:57.233344586Z",
23 | },
24 | },
25 | };
26 |
27 | export const channelSingleSiteSuccess: ChannelSuccessResult = {
28 | status: "success",
29 | result: {
30 | "action-hosting-deploy-demo": {
31 | site: "action-hosting-deploy-demo",
32 | url: "https://action-hosting-deploy-demo--singlesite-test-jl98rmie.web.app",
33 | expireTime: "2020-10-27T21:32:57.233344586Z",
34 | },
35 | },
36 | };
37 |
38 | export const channelError: ErrorResult = {
39 | status: "error",
40 | error:
41 | "HTTP Error: 400, Channel IDs can only include letters, numbers, underscores, hyphens, and periods.",
42 | };
43 |
44 | export const liveDeploySingleSiteSuccess: ProductionSuccessResult = {
45 | status: "success",
46 | result: {
47 | hosting: "sites/jeff-test-699d3/versions/7aebddc461b66922",
48 | },
49 | };
50 |
51 | export const liveDeployMultiSiteSuccess: ProductionSuccessResult = {
52 | status: "success",
53 | result: {
54 | hosting: [
55 | "sites/action-hosting-deploy-demo/versions/cd71a5c43ba0921b",
56 | "sites/action-hosting-deploy-demo-2/versions/e843c071a09cecbf",
57 | ],
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/test/samples/comments.ts:
--------------------------------------------------------------------------------
1 | export const multiSiteComment =
2 | `Visit the preview URL for this PR (updated for commit fe211ff):
3 |
4 | - [https://action-hosting-deploy-demo--multisite-test-goqvngto.web.app](https://action-hosting-deploy-demo--multisite-test-goqvngto.web.app)
5 | - [https://action-hosting-deploy-demo-2--multisite-test-ksadajci.web.app](https://action-hosting-deploy-demo-2--multisite-test-ksadajci.web.app)
6 |
7 | (expires Tue, 27 Oct 2020 21:32:57 GMT)
8 |
9 | 🔥 via [Firebase Hosting GitHub Action](https://github.com/marketplace/actions/deploy-to-firebase-hosting) 🌎
10 |
11 | Sign: 980f04126fb629deaadace7d6ee8a0628942e3d3`.trim();
12 |
13 | export const singleSiteComment =
14 | `Visit the preview URL for this PR (updated for commit fe211ff):
15 |
16 | [https://action-hosting-deploy-demo--singlesite-test-jl98rmie.web.app](https://action-hosting-deploy-demo--singlesite-test-jl98rmie.web.app)
17 |
18 | (expires Tue, 27 Oct 2020 21:32:57 GMT)
19 |
20 | 🔥 via [Firebase Hosting GitHub Action](https://github.com/marketplace/actions/deploy-to-firebase-hosting) 🌎
21 |
22 | Sign: ca07ce2c831b1990b78fcf2ecdfe230a486dc973`.trim();
23 |
24 | export const notABotComment = `I am a comment that was not written by action-hosting-deploy!`;
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "declaration": false
3 | }
4 |
--------------------------------------------------------------------------------