├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── check-dist.yml │ ├── check-formatting.yml │ ├── check-linter.yml │ ├── codeql-analysis.yml │ ├── draft-release.yml │ ├── publish-immutable-actions.yml │ ├── rebuild-dependabot-prs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── coverage_badge.svg ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── package-lock.json ├── package.json └── src ├── __tests__ ├── index.test.js └── internal │ └── deployment.test.js ├── index.js └── internal ├── api-client.js ├── context.js └── deployment.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "plugins": ["github"], 9 | "extends": ["eslint:recommended", "prettier", "plugin:github/internal"], 10 | "parserOptions": { 11 | "ecmaVersion": 12 12 | }, 13 | "rules": { 14 | "semi": ["error", "never"] 15 | }, 16 | "ignorePatterns": ["/dist/"] 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default PR reviewers 2 | * @actions/pages 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | groups: 8 | non-breaking-changes: 9 | update-types: [minor, patch] 10 | 11 | - package-ecosystem: 'npm' 12 | directory: '/' 13 | schedule: 14 | interval: 'weekly' 15 | groups: 16 | non-breaking-changes: 17 | update-types: [minor, patch] 18 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: 'v$RESOLVED_VERSION' 3 | tag-template: 'v$RESOLVED_VERSION' 4 | template: | 5 | # Changelog 6 | 7 | $CHANGES 8 | 9 | --- 10 | 11 | See details of [all code changes](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release. 12 | 13 | :warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the [compatibility table](https://github.com/$OWNER/$REPOSITORY/#compatibility). 14 | categories: 15 | - title: '🚀 Features' 16 | labels: 17 | - 'feature' 18 | - 'enhancement' 19 | - title: '🐛 Bug Fixes' 20 | labels: 21 | - 'fix' 22 | - 'bugfix' 23 | - 'bug' 24 | - title: '🧰 Maintenance' 25 | labels: 26 | - 'infrastructure' 27 | - 'automation' 28 | - 'documentation' 29 | - title: '🏎 Performance' 30 | label: 'performance' 31 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 32 | version-resolver: 33 | major: 34 | labels: 35 | - 'type: breaking' 36 | minor: 37 | labels: 38 | - 'type: enhancement' 39 | patch: 40 | labels: 41 | - 'type: bug' 42 | - 'type: maintenance' 43 | - 'type: documentation' 44 | default: patch 45 | -------------------------------------------------------------------------------- /.github/workflows/check-dist.yml: -------------------------------------------------------------------------------- 1 | # `dist/index.js` is a special file in Actions. 2 | # When you reference an action with `uses:` in a workflow, 3 | # `index.js` is the code that will run. 4 | # For our project, we generate this file using `ncc` 5 | # We need to make sure the checked-in `index.js` actually matches what we expect it to be. 6 | name: Check distributables 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - '**.md' 14 | pull_request: 15 | paths-ignore: 16 | - '**.md' 17 | workflow_dispatch: 18 | 19 | jobs: 20 | check-dist: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node.JS 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version-file: '.node-version' 31 | cache: npm 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Rebuild the dist/ directory 37 | run: npm run prepare 38 | 39 | - name: Compare the expected and actual dist/ directories 40 | id: diff 41 | run: | 42 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 43 | echo "Detected uncommitted changes after build in dist folder. See status below:" 44 | git diff 45 | exit 1 46 | fi 47 | 48 | # If index.js was different than expected, upload the expected version as an artifact 49 | - uses: actions/upload-artifact@v4 50 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 51 | with: 52 | name: dist 53 | path: dist/ 54 | -------------------------------------------------------------------------------- /.github/workflows/check-formatting.yml: -------------------------------------------------------------------------------- 1 | name: Check formatting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | # This allows a subsequently queued workflow run to interrupt previous runs 13 | concurrency: 14 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | format: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Node.JS 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version-file: '.node-version' 28 | cache: npm 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Verify formatting 34 | run: npm run format:check 35 | -------------------------------------------------------------------------------- /.github/workflows/check-linter.yml: -------------------------------------------------------------------------------- 1 | name: Check linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | # This allows a subsequently queued workflow run to interrupt previous runs 13 | concurrency: 14 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Node.JS 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version-file: '.node-version' 28 | cache: npm 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Verify linter 34 | run: npm run lint:check 35 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: CodeQL 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: '40 0 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | draft-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 # v6.0.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-immutable-actions.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Immutable Action Version' 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | packages: write 14 | 15 | steps: 16 | - name: Checking out 17 | uses: actions/checkout@v4 18 | - name: Publish 19 | id: publish 20 | uses: actions/publish-immutable-action@0.0.3 21 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | name: Rebuild distributables for Dependabot PRs 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'dependabot/npm**' 7 | 8 | # No permissions needed for `GITHUB_TOKEN` since we're using a PAT instead 9 | permissions: {} 10 | 11 | jobs: 12 | rebuild-dist: 13 | if: ${{ github.event.sender.login == 'dependabot[bot]' }} 14 | 15 | # This allows a subsequently queued workflow run to interrupt previous runs. 16 | # It is evaluated AFTER the job's `if` condition, so a push triggered by this 17 | # workflow's PAT will NOT interrupt a run triggered by a push from Dependabot. 18 | concurrency: 19 | group: '${{ github.workflow }} / ${{ github.job }} @ ${{ github.ref }}' 20 | cancel-in-progress: true 21 | 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | token: ${{ secrets.PAGES_AUTOMATION_PAT }} 28 | 29 | - name: Setup Node.JS 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version-file: '.node-version' 33 | cache: npm 34 | 35 | - name: Install dependencies 36 | run: npm ci 37 | 38 | - name: Rebuild the dist/ directory 39 | run: npm run prepare 40 | 41 | - name: Commit any differences present in the dist/ directory 42 | run: | 43 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 44 | echo "Detected uncommitted changes after rebuild in dist folder. Committing..." 45 | git add dist/ 46 | git config --local user.name "github-actions[bot]" 47 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 48 | git commit -m "[dependabot skip] Update distributables after Dependabot 🤖" 49 | echo "Pushing branch ${{ github.ref_name }}" 50 | git push origin ${{ github.ref_name }} 51 | fi 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [released] 5 | workflow_dispatch: 6 | inputs: 7 | TAG_NAME: 8 | description: 'Tag name that the major tag will point to' 9 | required: true 10 | 11 | env: 12 | TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | update_tag: 19 | name: Update the major tag to include the ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} changes 20 | runs-on: ubuntu-latest 21 | environment: 22 | # Note: this environment is protected 23 | name: Release 24 | steps: 25 | - name: Update the ${{ env.TAG_NAME }} tag 26 | id: update-major-tag 27 | uses: actions/publish-action@v0.3.0 28 | with: 29 | source-tag: ${{ env.TAG_NAME }} 30 | slack-webhook: ${{ secrets.SLACK_WEBHOOK }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Node.JS 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version-file: '.node-version' 23 | cache: npm 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Run tests 29 | run: npm run test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Editors 4 | .vscode/ 5 | .idea/ 6 | *.iml 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Other Dependency directories 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.10.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts 2 | /dist/ 3 | /pre/ 4 | 5 | # Ignore all Markdown files 6 | *.md 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # Prettier (formatter) configuration 2 | --- 3 | printWidth: 120 4 | tabWidth: 2 5 | useTabs: false 6 | semi: false 7 | singleQuote: true 8 | trailingComma: none 9 | bracketSpacing: true 10 | arrowParens: avoid 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 💻 2 | 3 | All contributions are welcome and greatly appreciated! 4 | 5 | ## Steps to Contribute 💡 6 | 7 | > Check the `.node-version` file in the root of this repo so see what version of Node.js is required for local development - note, this can be different from the version of Node.js which runs the Action on GitHub runners. It is suggested to download [nodenv](https://github.com/nodenv/nodenv) which uses this file and manages your Node.js versions for you 8 | 9 | 1. Fork this repository 10 | 2. Make your changes 11 | 3. [Test](#testing-) your changes locally 12 | 4. Before opening a pull request, please run `npm run all` to verify formatting, linting, tests, generated files, etc. 13 | 5. Commit and push your changes to your fork 14 | 6. Open a pull request back to this repository 15 | 7. Wait for an approval or changes requested from the maintainers of this repository 16 | 17 | After merging the pull request, the maintainers of this repository will create a new release with those changes included. After that, everyone can utilize the newly integrated changes in their own Actions workflows and enjoy your awesome improvements! 18 | 19 | ## Testing 🧪 20 | 21 | ### Running the test suite (required) 22 | 23 | Simply run the following command to execute the entire test suite: 24 | 25 | ```bash 26 | npm test 27 | ``` 28 | 29 | > Note: This requires that you have already run `npm install`. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GitHub, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deploy-pages 🚀 2 | 3 | [![Release](https://img.shields.io/github/v/release/actions/deploy-pages?label=Release&logo=github)](https://github.com/actions/deploy-pages/releases/latest) [![Linting](https://img.shields.io/github/actions/workflow/status/actions/deploy-pages/check-linter.yml?label=Linting&logo=github)](https://github.com/actions/deploy-pages/actions/workflows/check-linter.yml) [![Formatting](https://img.shields.io/github/actions/workflow/status/actions/deploy-pages/check-formatting.yml?label=Formatting&logo=github)](https://github.com/actions/deploy-pages/actions/workflows/check-formatting.yml) [![Tests](https://img.shields.io/github/actions/workflow/status/actions/deploy-pages/test.yml?label=Tests&logo=github)](https://github.com/actions/deploy-pages/actions/workflows/test.yml) ![Coverage](./coverage_badge.svg) [![Distributables](https://img.shields.io/github/actions/workflow/status/actions/deploy-pages/check-dist.yml?label=Distributables&logo=github)](https://github.com/actions/deploy-pages/actions/workflows/check-dist.yml) [![CodeQL](https://img.shields.io/github/actions/workflow/status/actions/deploy-pages/codeql-analysis.yml?label=CodeQL&logo=github)](https://github.com/actions/deploy-pages/actions/workflows/codeql-analysis.yml) 4 | 5 | This action is used to deploy [Actions artifacts][artifacts] to [GitHub Pages](https://pages.github.com/). 6 | 7 | ## Usage 8 | 9 | See [action.yml](action.yml) for the various `inputs` this action supports (or [below](#inputs-📥)). 10 | 11 | For examples that make use of this action, check out our [starter-workflows][starter-workflows] in a variety of frameworks. 12 | 13 | This action deploys a Pages site previously uploaded as an artifact (e.g. using [`actions/upload-pages-artifact`][upload-pages-artifact]). 14 | 15 | We recommend this action to be used in a dedicated job: 16 | 17 | ```yaml 18 | jobs: 19 | # Build job 20 | build: 21 | # 22 | # At a minimum this job should upload artifacts using actions/upload-pages-artifact 23 | 24 | # Deploy job 25 | deploy: 26 | # Add a dependency to the build job 27 | needs: build 28 | 29 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 30 | permissions: 31 | pages: write # to deploy to Pages 32 | id-token: write # to verify the deployment originates from an appropriate source 33 | 34 | # Deploy to the github-pages environment 35 | environment: 36 | name: github-pages 37 | url: ${{ steps.deployment.outputs.page_url }} 38 | 39 | # Specify runner + deployment step 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action 45 | ``` 46 | 47 | ### Inputs 📥 48 | 49 | | Input | Required? | Default | Description | 50 | | ----- | --------- | ------- | ----------- | 51 | | `token` | `true` | `${{ github.token }}` | The GitHub token used to create an authenticated client - Provided for you by default! | 52 | | `timeout` | `false` | `"600000"` | Time in milliseconds after which to timeout and cancel the deployment (default: 10 minutes) | 53 | | `error_count` | `false` | `"10"` | Maximum number of status report errors before cancelling a deployment (default: 10) | 54 | | `reporting_interval` | `false` | `"5000"` | Time in milliseconds between two deployment status reports (default: 5 seconds) | 55 | | `artifact_name` | `false` | `"github-pages"` | The name of the artifact to deploy | 56 | | `preview` | `false` | `"false"` | Is this attempting to deploy a pull request as a GitHub Pages preview site? (NOTE: This feature is only in alpha currently and is not available to the public!) | 57 | 58 | ### Outputs 📤 59 | 60 | | Output | Description | 61 | | ------ | ----------- | 62 | | `page_url` | The URL of the deployed Pages site | 63 | 64 | ### Environment Variables 🌎 65 | 66 | | Variable | Description | 67 | | -------- | ----------- | 68 | | `GITHUB_PAGES` | This environment variable is created and set to the string value `"true"` so that framework build tools may choose to differentiate their output based on the intended target hosting platform. | 69 | 70 | ## Security Considerations 71 | 72 | There are a few important considerations to be aware of: 73 | 74 | 1. The artifact being deployed must have been uploaded in a previous step, either in the same job or a separate job that doesn't execute until the upload is complete. See [`actions/upload-pages-artifact`][upload-pages-artifact] for more information about the format of the artifact we expect. 75 | 76 | 2. The job that executes the deployment must at minimum have the following permissions: 77 | - `pages: write` 78 | - `id-token: write` 79 | 80 | 3. The deployment should target the `github-pages` environment (you may use a different environment name if needed, but this is not recommended.) 81 | 82 | 4. If your Pages site is using a source branch, the deployment must originate from this source branch unless [your environment is protected][environment-protection] in which case the environment protection rules take precedence over the source branch rule 83 | 84 | 5. If your Pages site is using GitHub Actions as the source, while not required we highly recommend you also [protect your environment][environment-protection] (we will configure it by default for you). 85 | 86 | ## OIDC 87 | When we invoke a job using GitHub Actions the job requests an OIDC token from GitHub's OIDC provider which responds with a JSON web token (JWT). Each token is unique to each workflow job [learn more about OIDC tokens](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token). 88 | 89 | OIDC tokens are minted within the context of a single job, and are used to form a trust relationship which validates properties of the workflow run against a third-party (e.g. cloud providers such as AWS or Azure). In the context of GitHub Pages, this is most relevant to ensure a workflow respects branch protection settings. To do this, the OIDC token includes a claim about which branch/ref is executing the workflow. The token is passed to the pages deployment API as part of the request payload, where it's decoded internally to validate the claims and verify if that workflow is allowed to deploy to pages. 90 | A common question regarding OIDC tokens is the need to use both `pages:write` and `id-token:write`. The pages permission relates to the `GITHUB_TOKEN` by giving it the permissions to create pages deployments when calling the GitHub API. The id-token permission is necessary to request the OIDC JWT token. For more information on the id-token, check the docs on [adding permissions settings](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings). 91 | 92 | ## Compatibility 93 | 94 | This action is primarily designed for use with GitHub.com's Actions workflows and Pages deployments. However, certain releases should also be compatible with GitHub Enterprise Server (GHES) `3.7` and above. 95 | 96 | | Release | GHES Compatibility | 97 | |:---|:---| 98 | | [`v4`](https://github.com/actions/deploy-pages/releases/tag/v4) | :warning: Incompatible at this time | 99 | | [`v3`](https://github.com/actions/deploy-pages/releases/tag/v3) | `>= 3.9` | 100 | | `v3.x.x` | `>= 3.9` | 101 | | [`v2`](https://github.com/actions/deploy-pages/releases/tag/v2) | `>= 3.9` | 102 | | `v2.x.x` | `>= 3.9` | 103 | | [`v1`](https://github.com/actions/deploy-pages/releases/tag/v1) | `>= 3.7` | 104 | | [`v1.2.8`](https://github.com/actions/deploy-pages/releases/tag/v1.2.8) | `>= 3.7` | 105 | | [`v1.2.7`](https://github.com/actions/deploy-pages/releases/tag/v1.2.7) | :warning: `>= 3.9` [Incompatible with prior versions!](https://github.com/actions/deploy-pages/issues/137) | 106 | | [`v1.2.6`](https://github.com/actions/deploy-pages/releases/tag/v1.2.6) | `>= 3.7` | 107 | | `v1.x.x` | `>= 3.7` | 108 | 109 | ## Release Instructions 110 | 111 | In order to release a new version of this Action: 112 | 113 | 1. Locate the semantic version of the [upcoming release][release-list] (a draft is maintained by the [`draft-release` workflow][draft-release]). 114 | 115 | 2. Publish the draft release from the `main` branch with semantic version as the tag name, _with_ the checkbox to publish to the GitHub Marketplace checked. :ballot_box_with_check: 116 | 117 | 3. After publishing the release, the [`release` workflow][release] will automatically run to create/update the corresponding major version tag such as `v1`. 118 | 119 | ⚠️ Environment approval is required. Check the [Release workflow run list][release-workflow-runs]. 120 | 121 | ## License 122 | 123 | The scripts and documentation in this project are released under the [MIT License](LICENSE). 124 | 125 | 126 | [starter-workflows]: https://github.com/actions/starter-workflows/tree/main/pages 127 | [upload-pages-artifact]: https://github.com/actions/upload-pages-artifact 128 | [artifacts]: https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts 129 | [environment-protection]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#environment-protection-rules 130 | [release-list]: https://github.com/actions/deploy-pages/releases 131 | [draft-release]: .github/workflows/draft-release.yml 132 | [release]: .github/workflows/release.yml 133 | [release-workflow-runs]: https://github.com/actions/deploy-pages/actions/workflows/release.yml 134 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy GitHub Pages site' 2 | description: 'A GitHub Action to deploy an artifact as a GitHub Pages site' 3 | author: 'GitHub' 4 | runs: 5 | using: 'node20' 6 | main: 'dist/index.js' 7 | inputs: 8 | token: 9 | description: 'GitHub token' 10 | default: ${{ github.token }} 11 | required: true 12 | timeout: 13 | description: 'Time in milliseconds after which to timeout and cancel the deployment (default: 10 minutes)' 14 | required: false 15 | default: '600000' 16 | error_count: 17 | description: 'Maximum number of status report errors before cancelling a deployment (default: 10)' 18 | required: false 19 | default: '10' 20 | reporting_interval: 21 | description: 'Time in milliseconds between two deployment status report (default: 5 seconds)' 22 | required: false 23 | default: '5000' 24 | artifact_name: 25 | description: 'Name of the artifact to deploy' 26 | required: false 27 | default: 'github-pages' 28 | preview: 29 | description: 'Is this attempting to deploy a pull request as a GitHub Pages preview site? (NOTE: This feature is only in alpha currently and is not available to the public!)' 30 | required: false 31 | default: 'false' 32 | outputs: 33 | page_url: 34 | description: 'URL to deployed GitHub Pages' 35 | -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | Coverage: 80.84%Coverage80.84% -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-pages", 3 | "version": "0.0.0", 4 | "description": "Deploy an actions artifact to GitHub Pages", 5 | "main": "./dist/index.js", 6 | "dependencies": { 7 | "@actions/artifact": "^2.1.8", 8 | "@actions/core": "^1.10.1", 9 | "@actions/github": "^6.0.0", 10 | "@octokit/request-error": "^5.0.1", 11 | "http-status-messages": "^1.1.0" 12 | }, 13 | "devDependencies": { 14 | "@vercel/ncc": "^0.38.1", 15 | "eslint": "^8.57.0", 16 | "eslint-config-prettier": "^9.1.0", 17 | "eslint-plugin-github": "^4.10.2", 18 | "jest": "^29.7.0", 19 | "make-coverage-badge": "^1.2.0", 20 | "nock": "^13.5.4", 21 | "prettier": "^3.3.3", 22 | "undici": "^6.19.2" 23 | }, 24 | "scripts": { 25 | "all": "npm run format && npm run lint && npm run prepare && npm run test && npm run coverage-badge", 26 | "coverage-badge": "make-coverage-badge --output-path ./coverage_badge.svg", 27 | "format": "prettier --write .", 28 | "format:check": "prettier --check .", 29 | "lint": "DEBUG=eslint:cli-engine eslint --fix .", 30 | "lint:check": "DEBUG=eslint:cli-engine eslint .", 31 | "prepare": "ncc build src/index.js -o dist --source-map --license licenses.txt", 32 | "test": "jest" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/actions/deploy-pages.git" 37 | }, 38 | "keywords": [ 39 | "GitHub", 40 | "Pages" 41 | ], 42 | "author": "GitHub", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/actions/deploy-pages/issues" 46 | }, 47 | "homepage": "https://github.com/actions/deploy-pages#readme", 48 | "jest": { 49 | "coverageReporters": [ 50 | "json-summary", 51 | "text", 52 | "lcov" 53 | ], 54 | "collectCoverage": true, 55 | "collectCoverageFrom": [ 56 | "./src/**" 57 | ], 58 | "coverageThreshold": { 59 | "global": { 60 | "lines": 70, 61 | "statements": 70 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const process = require('process') 2 | const cp = require('child_process') 3 | const path = require('path') 4 | 5 | describe('with all environment variables set', () => { 6 | beforeEach(() => { 7 | process.env.GITHUB_RUN_ID = '123' 8 | process.env.GITHUB_REPOSITORY = 'actions/is-awesome' 9 | process.env.GITHUB_TOKEN = 'gha-token' 10 | process.env.GITHUB_SHA = '123abc' 11 | process.env.GITHUB_ACTOR = 'monalisa' 12 | process.env.GITHUB_ACTION = '__monalisa/octocat' 13 | process.env.GITHUB_ACTION_PATH = 'something' 14 | }) 15 | 16 | it('executes cleanly', done => { 17 | const ip = path.join(__dirname, '../index.js') 18 | cp.exec(`node ${ip}`, { env: process.env }, (err, stdout) => { 19 | expect(stdout).toMatch(/::debug::all variables are set/) 20 | done() 21 | }) 22 | }) 23 | }) 24 | 25 | describe('with variables missing', () => { 26 | it('execution fails if there are missing variables', done => { 27 | delete process.env.GITHUB_RUN_ID 28 | const ip = path.join(__dirname, '../index.js') 29 | cp.exec(`node ${ip}`, { env: process.env }, (err, stdout) => { 30 | expect(stdout).toBe('') 31 | expect(err).toBeTruthy() 32 | expect(err.code).toBe(1) 33 | done() 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/__tests__/internal/deployment.test.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core') 2 | // For mocking network calls with core http (http-client) 3 | const nock = require('nock') 4 | // For mocking network calls with native Fetch (octokit) 5 | const { MockAgent, setGlobalDispatcher } = require('undici') 6 | 7 | const { Deployment, MAX_TIMEOUT, ONE_GIGABYTE, SIZE_LIMIT_DESCRIPTION } = require('../../internal/deployment') 8 | 9 | const fakeJwt = 10 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjllMWIxOC1jOGFiLTRhZGQtOGYxOC03MzVlMzVjZGJhZjAiLCJzdWIiOiJyZXBvOnBhcGVyLXNwYS9taW55aTplbnZpcm9ubWVudDpQcm9kdWN0aW9uIiwiYXVkIjoiaHR0cHM6Ly9naXRodWIuY29tL3BhcGVyLXNwYSIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInNoYSI6ImEyODU1MWJmODdiZDk3NTFiMzdiMmM0YjM3M2MxZjU3NjFmYWM2MjYiLCJyZXBvc2l0b3J5IjoicGFwZXItc3BhL21pbnlpIiwicmVwb3NpdG9yeV9vd25lciI6InBhcGVyLXNwYSIsInJ1bl9pZCI6IjE1NDY0NTkzNjQiLCJydW5fbnVtYmVyIjoiMzQiLCJydW5fYXR0ZW1wdCI6IjIiLCJhY3RvciI6IllpTXlzdHkiLCJ3b3JrZmxvdyI6IkNJIiwiaGVhZF9yZWYiOiIiLCJiYXNlX3JlZiI6IiIsImV2ZW50X25hbWUiOiJwdXNoIiwicmVmX3R5cGUiOiJicmFuY2giLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJqb2Jfd29ya2Zsb3dfcmVmIjoicGFwZXItc3BhL21pbnlpLy5naXRodWIvd29ya2Zsb3dzL2JsYW5rLnltbEByZWZzL2hlYWRzL21haW4iLCJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwibmJmIjoxNjM4ODI4MDI4LCJleHAiOjE2Mzg4Mjg5MjgsImlhdCI6MTYzODgyODYyOH0.1wyupfxu1HGoTyIqatYg0hIxy2-0bMO-yVlmLSMuu2w' 11 | 12 | const LIST_ARTIFACTS_TWIRP_PATH = '/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts' 13 | 14 | describe('Deployment', () => { 15 | let mockPool 16 | 17 | beforeEach(() => { 18 | jest.clearAllMocks() 19 | process.env.GITHUB_RUN_ID = '123' 20 | process.env.GITHUB_REPOSITORY = 'actions/is-awesome' 21 | process.env.GITHUB_TOKEN = 'gha-token' 22 | process.env.GITHUB_SHA = '123abc' 23 | process.env.GITHUB_ACTOR = 'monalisa' 24 | process.env.GITHUB_ACTION = '__monalisa/octocat' 25 | process.env.GITHUB_ACTION_PATH = 'something' 26 | // A valid actions token must have an 'scp' field whose value is a space-delimited list of strings 27 | process.env.ACTIONS_RUNTIME_TOKEN = 28 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY3AiOiJBY3Rpb25zLkV4YW1wbGVTY29wZSBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhOmNhMzk1MDg1LTA0MGEtNTI2Yi0yY2U4LWJkYzg1ZjY5Mjc3NCJ9.l-VcBU1PeNk_lWpOhjWehQlYyjCcY2dp_EMt7Rf06io' 29 | process.env.ACTIONS_RESULTS_URL = 'https://actions-results-url.biz' 30 | 31 | jest.spyOn(core, 'getInput').mockImplementation(param => { 32 | switch (param) { 33 | case 'artifact_name': 34 | return 'github-pages' 35 | case 'token': 36 | return process.env.GITHUB_TOKEN 37 | case 'reporting_interval': 38 | return 50 // Lower reporting interval to speed up test 39 | default: 40 | return process.env[`INPUT_${param.toUpperCase()}`] || '' 41 | } 42 | }) 43 | 44 | jest.spyOn(core, 'setOutput').mockImplementation(param => { 45 | return param 46 | }) 47 | 48 | jest.spyOn(core, 'setFailed').mockImplementation(param => { 49 | return param 50 | }) 51 | 52 | // Mock error/warning/info/debug 53 | jest.spyOn(core, 'error').mockImplementation(jest.fn()) 54 | jest.spyOn(core, 'warning').mockImplementation(jest.fn()) 55 | jest.spyOn(core, 'info').mockImplementation(jest.fn()) 56 | jest.spyOn(core, 'debug').mockImplementation(jest.fn()) 57 | 58 | // Set up Fetch mocking 59 | let mockAgent = new MockAgent() 60 | mockAgent.disableNetConnect() 61 | setGlobalDispatcher(mockAgent) 62 | mockPool = mockAgent.get('https://api.github.com') 63 | }) 64 | 65 | describe('#create', () => { 66 | afterEach(() => { 67 | // Remove mock for `core.getInput('preview')` 68 | delete process.env.INPUT_PREVIEW 69 | }) 70 | 71 | it('can successfully create a deployment', async () => { 72 | process.env.GITHUB_SHA = 'valid-build-version' 73 | 74 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 75 | .post(LIST_ARTIFACTS_TWIRP_PATH) 76 | .reply( 77 | 200, 78 | { 79 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 80 | }, 81 | { headers: { 'content-type': 'application/json' } } 82 | ) 83 | 84 | mockPool 85 | .intercept({ 86 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 87 | method: 'POST', 88 | body: bodyString => { 89 | const body = JSON.parse(bodyString) 90 | const keys = Object.keys(body).sort() 91 | return ( 92 | keys.length === 3 && 93 | keys[0] === 'artifact_id' && 94 | keys[1] === 'oidc_token' && 95 | keys[2] === 'pages_build_version' && 96 | body.artifact_id === 11 && 97 | body.pages_build_version === process.env.GITHUB_SHA && 98 | body.oidc_token === fakeJwt 99 | ) 100 | } 101 | }) 102 | .reply( 103 | 200, 104 | { 105 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 106 | page_url: 'https://actions.github.io/is-awesome' 107 | }, 108 | { headers: { 'content-type': 'application/json' } } 109 | ) 110 | 111 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 112 | 113 | // Create the deployment 114 | const deployment = new Deployment() 115 | await deployment.create(fakeJwt) 116 | 117 | expect(core.setFailed).not.toHaveBeenCalled() 118 | expect(core.info).toHaveBeenLastCalledWith( 119 | expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) 120 | ) 121 | twirpScope.done() 122 | }) 123 | 124 | it('can successfully create a preview deployment', async () => { 125 | process.env.GITHUB_SHA = 'valid-build-version' 126 | 127 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 128 | .post(LIST_ARTIFACTS_TWIRP_PATH) 129 | .reply( 130 | 200, 131 | { 132 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 133 | }, 134 | { headers: { 'content-type': 'application/json' } } 135 | ) 136 | 137 | mockPool 138 | .intercept({ 139 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 140 | method: 'POST', 141 | body: bodyString => { 142 | const body = JSON.parse(bodyString) 143 | const keys = Object.keys(body).sort() 144 | return ( 145 | keys.length === 4 && 146 | keys[0] === 'artifact_id' && 147 | keys[1] === 'oidc_token' && 148 | keys[2] === 'pages_build_version' && 149 | keys[3] === 'preview' && 150 | body.artifact_id === 11 && 151 | body.pages_build_version === process.env.GITHUB_SHA && 152 | body.oidc_token === fakeJwt && 153 | body.preview === true 154 | ) 155 | } 156 | }) 157 | .reply( 158 | 200, 159 | { 160 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 161 | page_url: 'https://actions.github.io/is-awesome', 162 | preview_url: 'https://actions.drafts.github.io/is-awesome' 163 | }, 164 | { headers: { 'content-type': 'application/json' } } 165 | ) 166 | 167 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 168 | 169 | // Return `"true"` for `core.getInput("preview")` 170 | process.env.INPUT_PREVIEW = 'true' 171 | 172 | // Create the deployment 173 | const deployment = new Deployment() 174 | await deployment.create(fakeJwt) 175 | 176 | expect(core.setFailed).not.toHaveBeenCalled() 177 | expect(core.info).toHaveBeenLastCalledWith( 178 | expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) 179 | ) 180 | twirpScope.done() 181 | }) 182 | 183 | it('reports errors with failed artifact metadata exchange', async () => { 184 | process.env.GITHUB_SHA = 'invalid-build-version' 185 | 186 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 187 | .post(LIST_ARTIFACTS_TWIRP_PATH) 188 | .reply(400, { msg: 'yikes!' }, { 'content-type': 'application/json' }) 189 | 190 | // Create the deployment 191 | const deployment = new Deployment() 192 | await expect(deployment.create()).rejects.toThrow( 193 | `Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}.` 194 | ) 195 | expect(core.error).toHaveBeenNthCalledWith( 196 | 1, 197 | 'Listing artifact metadata failed', 198 | new Error('Failed to ListArtifacts: Received non-retryable error: Failed request: (400) null: yikes!') 199 | ) 200 | expect(core.error).toHaveBeenNthCalledWith( 201 | 2, 202 | 'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages, or Actions? Please re-run the deployment at a later time.', 203 | expect.any(Error) 204 | ) 205 | twirpScope.done() 206 | }) 207 | 208 | it('reports errors with a failed 500 in a deployment', async () => { 209 | process.env.GITHUB_SHA = 'build-version' 210 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 211 | .post(LIST_ARTIFACTS_TWIRP_PATH) 212 | .reply( 213 | 200, 214 | { 215 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 216 | }, 217 | { headers: { 'content-type': 'application/json' } } 218 | ) 219 | 220 | mockPool 221 | .intercept({ 222 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 223 | method: 'POST', 224 | body: bodyString => { 225 | const body = JSON.parse(bodyString) 226 | const keys = Object.keys(body).sort() 227 | return ( 228 | keys.length === 2 && 229 | keys[0] === 'artifact_id' && 230 | keys[1] === 'pages_build_version' && 231 | body.artifact_id === 11 && 232 | body.pages_build_version === process.env.GITHUB_SHA 233 | ) 234 | } 235 | }) 236 | .reply(500, { message: 'oh no' }, { headers: { 'content-type': 'application/json' } }) 237 | 238 | // Create the deployment 239 | const deployment = new Deployment() 240 | await expect(deployment.create()).rejects.toEqual( 241 | new Error( 242 | `Failed to create deployment (status: 500) with build version ${process.env.GITHUB_SHA}. Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.` 243 | ) 244 | ) 245 | twirpScope.done() 246 | }) 247 | 248 | it('reports errors with an unexpected 403 during deployment', async () => { 249 | process.env.GITHUB_SHA = 'build-version' 250 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 251 | .post(LIST_ARTIFACTS_TWIRP_PATH) 252 | .reply( 253 | 200, 254 | { 255 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 256 | }, 257 | { headers: { 'content-type': 'application/json' } } 258 | ) 259 | 260 | mockPool 261 | .intercept({ 262 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 263 | method: 'POST', 264 | body: bodyString => { 265 | const body = JSON.parse(bodyString) 266 | const keys = Object.keys(body).sort() 267 | return ( 268 | keys.length === 2 && 269 | keys[0] === 'artifact_id' && 270 | keys[1] === 'pages_build_version' && 271 | body.artifact_id === 11 && 272 | body.pages_build_version === process.env.GITHUB_SHA 273 | ) 274 | } 275 | }) 276 | .reply(403, { message: 'You are forbidden' }, { headers: { 'content-type': 'application/json' } }) 277 | 278 | // Create the deployment 279 | const deployment = new Deployment() 280 | await expect(deployment.create()).rejects.toEqual( 281 | new Error( 282 | `Failed to create deployment (status: 403) with build version ${process.env.GITHUB_SHA}. Ensure GITHUB_TOKEN has permission "pages: write".` 283 | ) 284 | ) 285 | twirpScope.done() 286 | }) 287 | 288 | it('reports errors with an unexpected 404 during deployment', async () => { 289 | process.env.GITHUB_SHA = 'build-version' 290 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 291 | .post(LIST_ARTIFACTS_TWIRP_PATH) 292 | .reply( 293 | 200, 294 | { 295 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 296 | }, 297 | { headers: { 'content-type': 'application/json' } } 298 | ) 299 | 300 | mockPool 301 | .intercept({ 302 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 303 | method: 'POST', 304 | body: bodyString => { 305 | const body = JSON.parse(bodyString) 306 | const keys = Object.keys(body).sort() 307 | return ( 308 | keys.length === 2 && 309 | keys[0] === 'artifact_id' && 310 | keys[1] === 'pages_build_version' && 311 | body.artifact_id === 11 && 312 | body.pages_build_version === process.env.GITHUB_SHA 313 | ) 314 | } 315 | }) 316 | .reply(404, { message: 'Not found' }, { headers: { 'content-type': 'application/json' } }) 317 | 318 | // Create the deployment 319 | const deployment = new Deployment() 320 | await expect(deployment.create()).rejects.toEqual( 321 | new Error( 322 | `Failed to create deployment (status: 404) with build version ${process.env.GITHUB_SHA}. Ensure GitHub Pages has been enabled: https://github.com/actions/is-awesome/settings/pages` 323 | ) 324 | ) 325 | twirpScope.done() 326 | }) 327 | 328 | it('reports errors with failed deployments', async () => { 329 | process.env.GITHUB_SHA = 'invalid-build-version' 330 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 331 | .post(LIST_ARTIFACTS_TWIRP_PATH) 332 | .reply( 333 | 200, 334 | { 335 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 336 | }, 337 | { headers: { 'content-type': 'application/json' } } 338 | ) 339 | 340 | mockPool 341 | .intercept({ 342 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 343 | method: 'POST', 344 | body: bodyString => { 345 | const body = JSON.parse(bodyString) 346 | const keys = Object.keys(body).sort() 347 | return ( 348 | keys.length === 2 && 349 | keys[0] === 'artifact_id' && 350 | keys[1] === 'pages_build_version' && 351 | body.artifact_id === 11 && 352 | body.pages_build_version === process.env.GITHUB_SHA 353 | ) 354 | } 355 | }) 356 | .reply(400, { message: 'Bad request' }, { headers: { 'content-type': 'application/json' } }) 357 | 358 | // Create the deployment 359 | const deployment = new Deployment() 360 | await expect(deployment.create()).rejects.toEqual( 361 | new Error( 362 | `Failed to create deployment (status: 400) with build version ${process.env.GITHUB_SHA}. Responded with: Bad request` 363 | ) 364 | ) 365 | twirpScope.done() 366 | }) 367 | 368 | it('fails if there are multiple artifacts with the same name', async () => { 369 | process.env.GITHUB_SHA = 'valid-build-version' 370 | 371 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 372 | .post(LIST_ARTIFACTS_TWIRP_PATH) 373 | .reply( 374 | 200, 375 | { 376 | artifacts: [ 377 | { databaseId: 13, name: 'github-pages', size: 1400 }, 378 | { databaseId: 14, name: 'github-pages', size: 1620 } 379 | ] 380 | }, 381 | { headers: { 'content-type': 'application/json' } } 382 | ) 383 | 384 | const deployment = new Deployment() 385 | await expect(deployment.create(fakeJwt)).rejects.toThrow( 386 | `Multiple artifacts named "github-pages" were unexpectedly found for this workflow run. Artifact count is 2.` 387 | ) 388 | twirpScope.done() 389 | }) 390 | 391 | it('fails if there are no artifacts found', async () => { 392 | process.env.GITHUB_SHA = 'valid-build-version' 393 | 394 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 395 | .post(LIST_ARTIFACTS_TWIRP_PATH) 396 | .reply( 397 | 200, 398 | { 399 | artifacts: [] 400 | }, 401 | { headers: { 'content-type': 'application/json' } } 402 | ) 403 | 404 | const deployment = new Deployment() 405 | await expect(deployment.create(fakeJwt)).rejects.toThrow( 406 | `No artifacts named "github-pages" were found for this workflow run. Ensure artifacts are uploaded with actions/upload-artifact@v4 or later.` 407 | ) 408 | twirpScope.done() 409 | }) 410 | 411 | it('fails with error message if list artifact endpoint returns 501', async () => { 412 | process.env.GITHUB_SHA = 'valid-build-version' 413 | 414 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 415 | .post(LIST_ARTIFACTS_TWIRP_PATH) 416 | .reply(501, { msg: 'oh no' }, { headers: { 'content-type': 'application/json' } }) 417 | 418 | const deployment = new Deployment() 419 | await expect(deployment.create(fakeJwt)).rejects.toThrow( 420 | `Failed to create deployment (status: 501) with build version ${process.env.GITHUB_SHA}. Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.` 421 | ) 422 | expect(core.error).toHaveBeenNthCalledWith( 423 | 1, 424 | 'Listing artifact metadata failed', 425 | new Error('Failed to ListArtifacts: Received non-retryable error: Failed request: (501) null: oh no') 426 | ) 427 | expect(core.error).toHaveBeenNthCalledWith( 428 | 2, 429 | 'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages, or Actions? Please re-run the deployment at a later time.', 430 | expect.any(Error) 431 | ) 432 | 433 | twirpScope.done() 434 | }) 435 | 436 | it('warns if the artifact size is bigger than maximum', async () => { 437 | process.env.GITHUB_SHA = 'valid-build-version' 438 | const artifactSize = ONE_GIGABYTE + 1 439 | 440 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 441 | .post(LIST_ARTIFACTS_TWIRP_PATH) 442 | .reply( 443 | 200, 444 | { 445 | artifacts: [{ databaseId: 12, name: 'github-pages', size: artifactSize }] 446 | }, 447 | { headers: { 'content-type': 'application/json' } } 448 | ) 449 | 450 | mockPool 451 | .intercept({ 452 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 453 | method: 'POST', 454 | body: bodyString => { 455 | const body = JSON.parse(bodyString) 456 | const keys = Object.keys(body).sort() 457 | return ( 458 | keys.length === 3 && 459 | keys[0] === 'artifact_id' && 460 | keys[1] === 'oidc_token' && 461 | keys[2] === 'pages_build_version' && 462 | body.artifact_id === 12 && 463 | body.oidc_token === fakeJwt && 464 | body.pages_build_version === process.env.GITHUB_SHA 465 | ) 466 | } 467 | }) 468 | .reply( 469 | 200, 470 | { 471 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 472 | page_url: 'https://actions.github.io/is-awesome' 473 | }, 474 | { headers: { 'content-type': 'application/json' } } 475 | ) 476 | 477 | const deployment = new Deployment() 478 | await deployment.create(fakeJwt) 479 | 480 | expect(core.warning).toBeCalledWith( 481 | `Uploaded artifact size of ${artifactSize} bytes exceeds the allowed size of ${SIZE_LIMIT_DESCRIPTION}. Deployment might fail.` 482 | ) 483 | expect(core.setFailed).not.toHaveBeenCalled() 484 | expect(core.info).toHaveBeenLastCalledWith( 485 | expect.stringMatching(new RegExp(`^Created deployment for ${process.env.GITHUB_SHA}`)) 486 | ) 487 | twirpScope.done() 488 | }) 489 | 490 | it('warns when the timeout is greater than the maximum allowed', async () => { 491 | process.env.GITHUB_SHA = 'valid-build-version' 492 | 493 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 494 | .post(LIST_ARTIFACTS_TWIRP_PATH) 495 | .reply( 496 | 200, 497 | { 498 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 499 | }, 500 | { headers: { 'content-type': 'application/json' } } 501 | ) 502 | 503 | mockPool 504 | .intercept({ 505 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 506 | method: 'POST', 507 | body: bodyString => { 508 | const body = JSON.parse(bodyString) 509 | const keys = Object.keys(body).sort() 510 | return ( 511 | keys.length === 3 && 512 | keys[0] === 'artifact_id' && 513 | keys[1] === 'oidc_token' && 514 | keys[2] === 'pages_build_version' && 515 | body.artifact_id === 11 && 516 | body.oidc_token === fakeJwt && 517 | body.pages_build_version === process.env.GITHUB_SHA 518 | ) 519 | } 520 | }) 521 | .reply( 522 | 200, 523 | { 524 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 525 | page_url: 'https://actions.github.io/is-awesome' 526 | }, 527 | { headers: { 'content-type': 'application/json' } } 528 | ) 529 | 530 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 531 | 532 | // Set timeout to greater than max 533 | jest.spyOn(core, 'getInput').mockImplementation(param => { 534 | switch (param) { 535 | case 'artifact_name': 536 | return 'github-pages' 537 | case 'token': 538 | return process.env.GITHUB_TOKEN 539 | case 'reporting_interval': 540 | return 50 // Lower reporting interval to speed up test 541 | case 'timeout': 542 | return MAX_TIMEOUT + 1 543 | default: 544 | return process.env[`INPUT_${param.toUpperCase()}`] || '' 545 | } 546 | }) 547 | 548 | const deployment = new Deployment() 549 | await deployment.create(fakeJwt) 550 | 551 | expect(core.warning).toBeCalledWith( 552 | `Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.` 553 | ) 554 | twirpScope.done() 555 | }) 556 | }) 557 | 558 | describe('#check', () => { 559 | it('sets output to success when deployment is successful', async () => { 560 | process.env.GITHUB_SHA = 'valid-build-version' 561 | 562 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 563 | .post(LIST_ARTIFACTS_TWIRP_PATH) 564 | .reply( 565 | 200, 566 | { 567 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 568 | }, 569 | { headers: { 'content-type': 'application/json' } } 570 | ) 571 | 572 | mockPool 573 | .intercept({ 574 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 575 | method: 'POST', 576 | body: bodyString => { 577 | const body = JSON.parse(bodyString) 578 | const keys = Object.keys(body).sort() 579 | return ( 580 | keys.length === 3 && 581 | keys[0] === 'artifact_id' && 582 | keys[1] === 'oidc_token' && 583 | keys[2] === 'pages_build_version' && 584 | body.artifact_id === 11 && 585 | body.oidc_token === fakeJwt && 586 | body.pages_build_version === process.env.GITHUB_SHA 587 | ) 588 | } 589 | }) 590 | .reply( 591 | 200, 592 | { 593 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 594 | page_url: 'https://actions.github.io/is-awesome' 595 | }, 596 | { headers: { 'content-type': 'application/json' } } 597 | ) 598 | 599 | mockPool 600 | .intercept({ 601 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 602 | method: 'GET' 603 | }) 604 | .reply(200, { status: 'succeed' }, { headers: { 'content-type': 'application/json' } }) 605 | 606 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 607 | 608 | // Create the deployment 609 | const deployment = new Deployment() 610 | await deployment.create(fakeJwt) 611 | await deployment.check() 612 | 613 | expect(core.setOutput).toBeCalledWith('status', 'succeed') 614 | expect(core.info).toHaveBeenLastCalledWith('Reported success!') 615 | twirpScope.done() 616 | }) 617 | 618 | it('fails check when no deployment is found', async () => { 619 | process.env.GITHUB_SHA = 'valid-build-version' 620 | const deployment = new Deployment() 621 | await deployment.check() 622 | expect(core.setFailed).toBeCalledWith('Deployment not found.') 623 | }) 624 | 625 | it('exits early when deployment is not in progress', async () => { 626 | process.env.GITHUB_SHA = 'valid-build-version' 627 | 628 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 629 | .post(LIST_ARTIFACTS_TWIRP_PATH) 630 | .reply( 631 | 200, 632 | { 633 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 634 | }, 635 | { headers: { 'content-type': 'application/json' } } 636 | ) 637 | 638 | mockPool 639 | .intercept({ 640 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 641 | method: 'POST', 642 | body: bodyString => { 643 | const body = JSON.parse(bodyString) 644 | const keys = Object.keys(body).sort() 645 | return ( 646 | keys.length === 3 && 647 | keys[0] === 'artifact_id' && 648 | keys[1] === 'oidc_token' && 649 | keys[2] === 'pages_build_version' && 650 | body.artifact_id === 11 && 651 | body.oidc_token === fakeJwt && 652 | body.pages_build_version === process.env.GITHUB_SHA 653 | ) 654 | } 655 | }) 656 | .reply( 657 | 200, 658 | { 659 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 660 | page_url: 'https://actions.github.io/is-awesome' 661 | }, 662 | { headers: { 'content-type': 'application/json' } } 663 | ) 664 | 665 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 666 | 667 | const deployment = new Deployment() 668 | await deployment.create(fakeJwt) 669 | deployment.deploymentInfo.pending = false 670 | await deployment.check() 671 | expect(core.setFailed).toBeCalledWith('Unable to get deployment status.') 672 | twirpScope.done() 673 | }) 674 | 675 | it('enforces max timeout', async () => { 676 | process.env.GITHUB_SHA = 'valid-build-version' 677 | 678 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 679 | .post(LIST_ARTIFACTS_TWIRP_PATH) 680 | .reply( 681 | 200, 682 | { 683 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 684 | }, 685 | { headers: { 'content-type': 'application/json' } } 686 | ) 687 | 688 | mockPool 689 | .intercept({ 690 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 691 | method: 'POST', 692 | body: bodyString => { 693 | const body = JSON.parse(bodyString) 694 | const keys = Object.keys(body).sort() 695 | return ( 696 | keys.length === 3 && 697 | keys[0] === 'artifact_id' && 698 | keys[1] === 'oidc_token' && 699 | keys[2] === 'pages_build_version' && 700 | body.artifact_id === 11 && 701 | body.oidc_token === fakeJwt && 702 | body.pages_build_version === process.env.GITHUB_SHA 703 | ) 704 | } 705 | }) 706 | .reply( 707 | 200, 708 | { 709 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 710 | page_url: 'https://actions.github.io/is-awesome' 711 | }, 712 | { headers: { 'content-type': 'application/json' } } 713 | ) 714 | 715 | mockPool 716 | .intercept({ 717 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 718 | method: 'GET' 719 | }) 720 | .reply(200, { status: 'deployment_in_progress' }, { headers: { 'content-type': 'application/json' } }) 721 | .times(2) 722 | 723 | mockPool 724 | .intercept({ 725 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}/cancel`, 726 | method: 'POST' 727 | }) 728 | .reply(200, {}, { headers: { 'content-type': 'application/json' } }) 729 | 730 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 731 | 732 | // Set timeout to greater than max 733 | jest.spyOn(core, 'getInput').mockImplementation(param => { 734 | switch (param) { 735 | case 'artifact_name': 736 | return 'github-pages' 737 | case 'token': 738 | return process.env.GITHUB_TOKEN 739 | case 'error_count': 740 | return 10 741 | case 'reporting_interval': 742 | return 50 // Lower the interval for the test 743 | case 'timeout': 744 | return MAX_TIMEOUT + 1 745 | default: 746 | return process.env[`INPUT_${param.toUpperCase()}`] || '' 747 | } 748 | }) 749 | 750 | // Jump the "current time" by MAX_TIMEOUT ever time Date.now is called 751 | const _now = Date.now 752 | let nowCalls = 0 753 | const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => { 754 | nowCalls++ 755 | return _now() + MAX_TIMEOUT * nowCalls 756 | }) 757 | 758 | // Create the deployment 759 | const deployment = new Deployment() 760 | await deployment.create(fakeJwt) 761 | await deployment.check() 762 | 763 | nowSpy.mockRestore() 764 | 765 | expect(deployment.timeout).toEqual(MAX_TIMEOUT) 766 | expect(core.error).toBeCalledWith('Timeout reached, aborting!') 767 | expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!') 768 | twirpScope.done() 769 | }) 770 | 771 | it('sets timeout to user timeout if user timeout is less than max timeout', async () => { 772 | process.env.GITHUB_SHA = 'valid-build-version' 773 | 774 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 775 | .post(LIST_ARTIFACTS_TWIRP_PATH) 776 | .reply( 777 | 200, 778 | { 779 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 780 | }, 781 | { headers: { 'content-type': 'application/json' } } 782 | ) 783 | 784 | mockPool 785 | .intercept({ 786 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 787 | method: 'POST', 788 | body: bodyString => { 789 | const body = JSON.parse(bodyString) 790 | const keys = Object.keys(body).sort() 791 | return ( 792 | keys.length === 3 && 793 | keys[0] === 'artifact_id' && 794 | keys[1] === 'oidc_token' && 795 | keys[2] === 'pages_build_version' && 796 | body.artifact_id === 11 && 797 | body.oidc_token === fakeJwt && 798 | body.pages_build_version === process.env.GITHUB_SHA 799 | ) 800 | } 801 | }) 802 | .reply( 803 | 200, 804 | { 805 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 806 | page_url: 'https://actions.github.io/is-awesome' 807 | }, 808 | { headers: { 'content-type': 'application/json' } } 809 | ) 810 | 811 | mockPool 812 | .intercept({ 813 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}/cancel`, 814 | method: 'POST' 815 | }) 816 | .reply(200, {}, { headers: { 'content-type': 'application/json' } }) 817 | 818 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 819 | 820 | // Set timeout to greater than max 821 | jest.spyOn(core, 'getInput').mockImplementation(param => { 822 | switch (param) { 823 | case 'artifact_name': 824 | return 'github-pages' 825 | case 'token': 826 | return process.env.GITHUB_TOKEN 827 | case 'error_count': 828 | return 10 829 | case 'reporting_interval': 830 | return 42 // The default of 5000 is too long for the test 831 | case 'timeout': 832 | return 42 833 | default: 834 | return process.env[`INPUT_${param.toUpperCase()}`] || '' 835 | } 836 | }) 837 | 838 | const now = Date.now() 839 | const mockStartTime = now - 42 840 | jest 841 | .spyOn(Date, 'now') 842 | .mockImplementationOnce(() => mockStartTime) 843 | .mockImplementationOnce(() => now) 844 | 845 | // Create the deployment 846 | const deployment = new Deployment() 847 | await deployment.create(fakeJwt) 848 | await deployment.check() 849 | 850 | expect(deployment.timeout).toEqual(42) 851 | expect(core.error).toBeCalledWith('Timeout reached, aborting!') 852 | expect(core.setFailed).toBeCalledWith('Timeout reached, aborting!') 853 | twirpScope.done() 854 | }) 855 | 856 | it('sets output to success when timeout is set but not reached', async () => { 857 | process.env.GITHUB_SHA = 'valid-build-version' 858 | 859 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 860 | .post(LIST_ARTIFACTS_TWIRP_PATH) 861 | .reply( 862 | 200, 863 | { 864 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 865 | }, 866 | { headers: { 'content-type': 'application/json' } } 867 | ) 868 | 869 | mockPool 870 | .intercept({ 871 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 872 | method: 'POST', 873 | body: bodyString => { 874 | const body = JSON.parse(bodyString) 875 | const keys = Object.keys(body).sort() 876 | return ( 877 | keys.length === 3 && 878 | keys[0] === 'artifact_id' && 879 | keys[1] === 'oidc_token' && 880 | keys[2] === 'pages_build_version' && 881 | body.artifact_id === 11 && 882 | body.oidc_token === fakeJwt && 883 | body.pages_build_version === process.env.GITHUB_SHA 884 | ) 885 | } 886 | }) 887 | .reply( 888 | 200, 889 | { 890 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 891 | page_url: 'https://actions.github.io/is-awesome' 892 | }, 893 | { headers: { 'content-type': 'application/json' } } 894 | ) 895 | 896 | mockPool 897 | .intercept({ 898 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 899 | method: 'GET' 900 | }) 901 | .reply(200, { status: 'succeed' }, { headers: { 'content-type': 'application/json' } }) 902 | 903 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 904 | 905 | // Set timeout to greater than max 906 | jest.spyOn(core, 'getInput').mockImplementation(param => { 907 | switch (param) { 908 | case 'artifact_name': 909 | return 'github-pages' 910 | case 'token': 911 | return process.env.GITHUB_TOKEN 912 | case 'error_count': 913 | return 10 914 | case 'reporting_interval': 915 | return 0 // The default of 5000 is too long for the test 916 | case 'timeout': 917 | return 42 918 | default: 919 | return process.env[`INPUT_${param.toUpperCase()}`] || '' 920 | } 921 | }) 922 | 923 | const now = Date.now() 924 | const mockStartTime = now // No time elapsed 925 | jest 926 | .spyOn(Date, 'now') 927 | .mockImplementationOnce(() => mockStartTime) 928 | .mockImplementationOnce(() => now) 929 | 930 | // Create the deployment 931 | const deployment = new Deployment() 932 | await deployment.create(fakeJwt) 933 | await deployment.check() 934 | 935 | expect(deployment.timeout).toEqual(42) 936 | expect(core.error).not.toBeCalled() 937 | expect(core.setOutput).toBeCalledWith('status', 'succeed') 938 | expect(core.info).toHaveBeenLastCalledWith('Reported success!') 939 | twirpScope.done() 940 | }) 941 | }) 942 | 943 | describe('#cancel', () => { 944 | it('can successfully cancel a deployment', async () => { 945 | process.env.GITHUB_SHA = 'valid-build-version' 946 | 947 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 948 | .post(LIST_ARTIFACTS_TWIRP_PATH) 949 | .reply( 950 | 200, 951 | { 952 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 953 | }, 954 | { headers: { 'content-type': 'application/json' } } 955 | ) 956 | 957 | mockPool 958 | .intercept({ 959 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 960 | method: 'POST', 961 | body: bodyString => { 962 | const body = JSON.parse(bodyString) 963 | const keys = Object.keys(body).sort() 964 | return ( 965 | keys.length === 3 && 966 | keys[0] === 'artifact_id' && 967 | keys[1] === 'oidc_token' && 968 | keys[2] === 'pages_build_version' && 969 | body.artifact_id === 11 && 970 | body.oidc_token === fakeJwt && 971 | body.pages_build_version === process.env.GITHUB_SHA 972 | ) 973 | } 974 | }) 975 | .reply( 976 | 200, 977 | { 978 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 979 | page_url: 'https://actions.github.io/is-awesome' 980 | }, 981 | { headers: { 'content-type': 'application/json' } } 982 | ) 983 | 984 | mockPool 985 | .intercept({ 986 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}/cancel`, 987 | method: 'POST' 988 | }) 989 | .reply(200, {}, { headers: { 'content-type': 'application/json' } }) 990 | 991 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 992 | 993 | // Create the deployment 994 | const deployment = new Deployment() 995 | await deployment.create(fakeJwt) 996 | 997 | // Cancel it 998 | await deployment.cancel() 999 | 1000 | expect(core.info).toHaveBeenLastCalledWith(`Canceled deployment with ID ${process.env.GITHUB_SHA}`) 1001 | twirpScope.done() 1002 | }) 1003 | 1004 | it('can exit if a pages deployment was not created and none need to be cancelled', async () => { 1005 | process.env.GITHUB_SHA = 'valid-build-version' 1006 | 1007 | // Create the deployment 1008 | const deployment = new Deployment() 1009 | 1010 | // Cancel it 1011 | await deployment.cancel() 1012 | 1013 | expect(core.debug).toHaveBeenCalledWith('all variables are set') 1014 | expect(core.debug).toHaveBeenCalledWith(`No deployment to cancel`) 1015 | }) 1016 | 1017 | it('catches an error when trying to cancel a deployment', async () => { 1018 | process.env.GITHUB_SHA = 'valid-build-version' 1019 | 1020 | const twirpScope = nock(process.env.ACTIONS_RESULTS_URL) 1021 | .post(LIST_ARTIFACTS_TWIRP_PATH) 1022 | .reply( 1023 | 200, 1024 | { 1025 | artifacts: [{ databaseId: 11, name: 'github-pages', size: 221 }] 1026 | }, 1027 | { headers: { 'content-type': 'application/json' } } 1028 | ) 1029 | 1030 | mockPool 1031 | .intercept({ 1032 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments`, 1033 | method: 'POST', 1034 | body: bodyString => { 1035 | const body = JSON.parse(bodyString) 1036 | const keys = Object.keys(body).sort() 1037 | return ( 1038 | keys.length === 3 && 1039 | keys[0] === 'artifact_id' && 1040 | keys[1] === 'oidc_token' && 1041 | keys[2] === 'pages_build_version' && 1042 | body.artifact_id === 11 && 1043 | body.oidc_token === fakeJwt && 1044 | body.pages_build_version === process.env.GITHUB_SHA 1045 | ) 1046 | } 1047 | }) 1048 | .reply( 1049 | 200, 1050 | { 1051 | status_url: `https://api.github.com/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}`, 1052 | page_url: 'https://actions.github.io/is-awesome' 1053 | }, 1054 | { headers: { 'content-type': 'application/json' } } 1055 | ) 1056 | 1057 | mockPool 1058 | .intercept({ 1059 | path: `/repos/${process.env.GITHUB_REPOSITORY}/pages/deployments/${process.env.GITHUB_SHA}/cancel`, 1060 | method: 'POST' 1061 | }) 1062 | .reply(500, {}, { headers: { 'content-type': 'application/json' } }) 1063 | 1064 | core.getIDToken = jest.fn().mockResolvedValue(fakeJwt) 1065 | 1066 | // Create the deployment 1067 | const deployment = new Deployment() 1068 | await deployment.create(fakeJwt) 1069 | 1070 | // Cancel it 1071 | await deployment.cancel() 1072 | 1073 | expect(core.error).toHaveBeenCalledWith(`Canceling Pages deployment failed`, expect.anything()) 1074 | twirpScope.done() 1075 | }) 1076 | }) 1077 | }) 1078 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // This package assumes a site has already been built and the files exist in the current workspace 2 | // If there's an artifact named `artifact.tar`, it can upload that to actions on its own, 3 | // without the user having to do the tar process themselves. 4 | 5 | const core = require('@actions/core') 6 | 7 | const { Deployment } = require('./internal/deployment') 8 | const getContext = require('./internal/context') 9 | 10 | const deployment = new Deployment() 11 | 12 | async function cancelHandler(evtOrExitCodeOrError) { 13 | await deployment.cancel() 14 | process.exit(isNaN(+evtOrExitCodeOrError) ? 1 : +evtOrExitCodeOrError) 15 | } 16 | 17 | async function main() { 18 | const { isPreview } = getContext() 19 | 20 | let idToken = '' 21 | try { 22 | idToken = await core.getIDToken() 23 | } catch (error) { 24 | console.log(error) 25 | core.setFailed(`Ensure GITHUB_TOKEN has permission "id-token: write".`) 26 | return 27 | } 28 | 29 | try { 30 | const deploymentInfo = await deployment.create(idToken) 31 | 32 | // Output the deployed Pages URL 33 | let pageUrl = deploymentInfo?.['page_url'] || '' 34 | const previewUrl = deploymentInfo?.['preview_url'] || '' 35 | if (isPreview && previewUrl) { 36 | pageUrl = previewUrl 37 | } 38 | core.setOutput('page_url', pageUrl) 39 | 40 | await deployment.check() 41 | } catch (error) { 42 | core.setFailed(error) 43 | } 44 | } 45 | 46 | // Register signal handlers for workflow cancellation 47 | process.on('SIGINT', cancelHandler) 48 | process.on('SIGTERM', cancelHandler) 49 | 50 | // Main 51 | main() 52 | -------------------------------------------------------------------------------- /src/internal/api-client.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core') 2 | const github = require('@actions/github') 3 | const { DefaultArtifactClient } = require('@actions/artifact') 4 | const { RequestError } = require('@octokit/request-error') 5 | const HttpStatusMessages = require('http-status-messages') 6 | 7 | function wrapTwirpResponseLikeOctokit(twirpResponse, requestOptions) { 8 | // Specific response shape aligned with Octokit 9 | const response = { 10 | url: requestOptions.url, 11 | status: 200, 12 | headers: { 13 | ...requestOptions.headers 14 | }, 15 | data: twirpResponse 16 | } 17 | return response 18 | } 19 | 20 | // Mimic the errors thrown by Octokit for consistency. 21 | function wrapTwirpErrorLikeOctokit(twirpError, requestOptions) { 22 | const rawErrorMsg = twirpError?.message || twirpError?.toString() || '' 23 | const statusCodeMatch = rawErrorMsg.match(/Failed request: \((?\d+)\)/) 24 | const statusCode = statusCodeMatch?.groups?.statusCode ?? 500 25 | 26 | // Try to provide the best error message 27 | const errorMsg = 28 | rawErrorMsg || 29 | // Fallback to the HTTP status message based on the status code 30 | HttpStatusMessages[statusCode] || 31 | // Or if the status code is unexpected... 32 | `Unknown error (${statusCode})` 33 | 34 | // RequestError is an Octokit-specific class 35 | return new RequestError(errorMsg, statusCode, { 36 | response: { 37 | url: requestOptions.url, 38 | status: statusCode, 39 | headers: { 40 | ...requestOptions.headers 41 | }, 42 | data: rawErrorMsg ? { message: rawErrorMsg } : '' 43 | }, 44 | request: requestOptions 45 | }) 46 | } 47 | 48 | function getArtifactsServiceOrigin() { 49 | const resultsUrl = process.env.ACTIONS_RESULTS_URL 50 | return resultsUrl ? new URL(resultsUrl).origin : '' 51 | } 52 | 53 | async function getArtifactMetadata({ artifactName }) { 54 | const artifactClient = new DefaultArtifactClient() 55 | 56 | // Primarily for debugging purposes, accuracy is not critical 57 | const requestOptions = { 58 | method: 'POST', 59 | url: `${getArtifactsServiceOrigin()}/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts`, 60 | headers: { 61 | 'content-type': 'application/json' 62 | }, 63 | body: {} 64 | } 65 | 66 | try { 67 | core.info(`Fetching artifact metadata for "${artifactName}" in this workflow run`) 68 | 69 | let response 70 | try { 71 | const twirpResponse = await artifactClient.listArtifacts() 72 | response = wrapTwirpResponseLikeOctokit(twirpResponse, requestOptions) 73 | } catch (twirpError) { 74 | core.error('Listing artifact metadata failed', twirpError) 75 | const octokitError = wrapTwirpErrorLikeOctokit(twirpError, requestOptions) 76 | throw octokitError 77 | } 78 | 79 | const filteredArtifacts = response.data.artifacts.filter(artifact => artifact.name === artifactName) 80 | 81 | const artifactCount = filteredArtifacts.length 82 | core.debug(`List artifact count: ${artifactCount}`) 83 | 84 | if (artifactCount === 0) { 85 | throw new Error( 86 | `No artifacts named "${artifactName}" were found for this workflow run. Ensure artifacts are uploaded with actions/upload-artifact@v4 or later.` 87 | ) 88 | } else if (artifactCount > 1) { 89 | throw new Error( 90 | `Multiple artifacts named "${artifactName}" were unexpectedly found for this workflow run. Artifact count is ${artifactCount}.` 91 | ) 92 | } 93 | 94 | const artifact = filteredArtifacts[0] 95 | core.debug(`Artifact: ${JSON.stringify(artifact)}`) 96 | 97 | if (!artifact.size) { 98 | core.warning('Artifact size was not found. Unable to verify if artifact size exceeds the allowed size.') 99 | } 100 | 101 | return artifact 102 | } catch (error) { 103 | core.error( 104 | 'Fetching artifact metadata failed. Is githubstatus.com reporting issues with API requests, Pages, or Actions? Please re-run the deployment at a later time.', 105 | error 106 | ) 107 | throw error 108 | } 109 | } 110 | 111 | async function createPagesDeployment({ githubToken, artifactId, buildVersion, idToken, isPreview = false }) { 112 | const octokit = github.getOctokit(githubToken) 113 | 114 | const payload = { 115 | artifact_id: artifactId, 116 | pages_build_version: buildVersion, 117 | oidc_token: idToken 118 | } 119 | if (isPreview === true) { 120 | payload.preview = true 121 | } 122 | core.info(`Creating Pages deployment with payload:\n${JSON.stringify(payload, null, '\t')}`) 123 | 124 | try { 125 | const response = await octokit.request('POST /repos/{owner}/{repo}/pages/deployments', { 126 | owner: github.context.repo.owner, 127 | repo: github.context.repo.repo, 128 | ...payload 129 | }) 130 | 131 | return response.data 132 | } catch (error) { 133 | core.error('Creating Pages deployment failed', error) 134 | throw error 135 | } 136 | } 137 | 138 | async function getPagesDeploymentStatus({ githubToken, deploymentId }) { 139 | const octokit = github.getOctokit(githubToken) 140 | 141 | core.info('Getting Pages deployment status...') 142 | try { 143 | const response = await octokit.request('GET /repos/{owner}/{repo}/pages/deployments/{deploymentId}', { 144 | owner: github.context.repo.owner, 145 | repo: github.context.repo.repo, 146 | deploymentId 147 | }) 148 | 149 | return response.data 150 | } catch (error) { 151 | core.error('Getting Pages deployment status failed', error) 152 | throw error 153 | } 154 | } 155 | 156 | async function cancelPagesDeployment({ githubToken, deploymentId }) { 157 | const octokit = github.getOctokit(githubToken) 158 | 159 | core.info('Canceling Pages deployment...') 160 | try { 161 | const response = await octokit.request('POST /repos/{owner}/{repo}/pages/deployments/{deploymentId}/cancel', { 162 | owner: github.context.repo.owner, 163 | repo: github.context.repo.repo, 164 | deploymentId 165 | }) 166 | 167 | return response.data 168 | } catch (error) { 169 | core.error('Canceling Pages deployment failed', error) 170 | throw error 171 | } 172 | } 173 | 174 | module.exports = { 175 | getArtifactMetadata, 176 | createPagesDeployment, 177 | getPagesDeploymentStatus, 178 | cancelPagesDeployment 179 | } 180 | -------------------------------------------------------------------------------- /src/internal/context.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core') 2 | 3 | // Load variables from Actions runtime 4 | function getRequiredVars() { 5 | return { 6 | workflowRun: process.env.GITHUB_RUN_ID, 7 | repositoryNwo: process.env.GITHUB_REPOSITORY, 8 | buildVersion: process.env.GITHUB_SHA, 9 | buildActor: process.env.GITHUB_ACTOR, 10 | actionsId: process.env.GITHUB_ACTION, 11 | githubToken: core.getInput('token'), 12 | githubApiUrl: process.env.GITHUB_API_URL ?? 'https://api.github.com', 13 | githubServerUrl: process.env.GITHUB_SERVER_URL ?? 'https://github.com', 14 | artifactName: core.getInput('artifact_name') || 'github-pages', 15 | isPreview: core.getInput('preview') === 'true' 16 | } 17 | } 18 | 19 | module.exports = function getContext() { 20 | const requiredVars = getRequiredVars() 21 | for (const variable in requiredVars) { 22 | if (requiredVars[variable] === undefined) { 23 | throw new Error(`${variable} is undefined. Cannot continue.`) 24 | } 25 | } 26 | core.debug('all variables are set') 27 | return requiredVars 28 | } 29 | -------------------------------------------------------------------------------- /src/internal/deployment.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core') 2 | 3 | // All variables we need from the runtime are loaded here 4 | const getContext = require('./context') 5 | const { 6 | getArtifactMetadata, 7 | createPagesDeployment, 8 | getPagesDeploymentStatus, 9 | cancelPagesDeployment 10 | } = require('./api-client') 11 | 12 | const temporaryErrorStatus = { 13 | unknown_status: 'Unable to get deployment status.', 14 | not_found: 'Deployment not found.', 15 | deployment_attempt_error: 'Deployment temporarily failed, a retry will be automatically scheduled...' 16 | } 17 | 18 | const finalErrorStatus = { 19 | deployment_failed: 'Deployment failed, try again later.', 20 | deployment_content_failed: 21 | 'Artifact could not be deployed. Please ensure the content does not contain any hard links, symlinks and total size is less than 10GB.', 22 | deployment_cancelled: 'Deployment cancelled.', 23 | deployment_lost: 'Deployment failed to report final status.' 24 | } 25 | 26 | const MAX_TIMEOUT = 600000 27 | const ONE_GIGABYTE = 1073741824 28 | const SIZE_LIMIT_DESCRIPTION = '1 GB' 29 | 30 | class Deployment { 31 | constructor() { 32 | const context = getContext() 33 | this.repositoryNwo = context.repositoryNwo 34 | this.buildVersion = context.buildVersion 35 | this.buildActor = context.buildActor 36 | this.actionsId = context.actionsId 37 | this.githubToken = context.githubToken 38 | this.workflowRun = context.workflowRun 39 | this.deploymentInfo = null 40 | this.githubApiUrl = context.githubApiUrl 41 | this.githubServerUrl = context.githubServerUrl 42 | this.artifactName = context.artifactName 43 | this.isPreview = context.isPreview === true 44 | this.timeout = MAX_TIMEOUT 45 | this.startTime = null 46 | } 47 | 48 | // Call GitHub api to fetch artifacts matching the provided name and deploy to GitHub Pages 49 | // by creating a deployment with that artifact id 50 | async create(idToken) { 51 | if (Number(core.getInput('timeout')) > MAX_TIMEOUT) { 52 | core.warning( 53 | `Warning: timeout value is greater than the allowed maximum - timeout set to the maximum of ${MAX_TIMEOUT} milliseconds.` 54 | ) 55 | } 56 | 57 | const timeoutInput = Number(core.getInput('timeout')) 58 | this.timeout = !timeoutInput || timeoutInput <= 0 ? MAX_TIMEOUT : Math.min(timeoutInput, MAX_TIMEOUT) 59 | 60 | try { 61 | core.debug(`Actor: ${this.buildActor}`) 62 | core.debug(`Action ID: ${this.actionsId}`) 63 | core.debug(`Actions Workflow Run ID: ${this.workflowRun}`) 64 | 65 | const artifactData = await getArtifactMetadata({ artifactName: this.artifactName }) 66 | 67 | if (artifactData?.size > ONE_GIGABYTE) { 68 | core.warning( 69 | `Uploaded artifact size of ${artifactData?.size} bytes exceeds the allowed size of ${SIZE_LIMIT_DESCRIPTION}. Deployment might fail.` 70 | ) 71 | } 72 | 73 | const deployment = await createPagesDeployment({ 74 | githubToken: this.githubToken, 75 | artifactId: artifactData.id, 76 | buildVersion: this.buildVersion, 77 | idToken, 78 | isPreview: this.isPreview 79 | }) 80 | 81 | if (deployment) { 82 | this.deploymentInfo = { 83 | ...deployment, 84 | id: deployment.id || deployment.status_url?.split('/')?.pop() || this.buildVersion, 85 | pending: true 86 | } 87 | this.startTime = Date.now() 88 | } 89 | 90 | core.info(`Created deployment for ${this.buildVersion}, ID: ${this.deploymentInfo?.id}`) 91 | 92 | core.debug(JSON.stringify(deployment)) 93 | 94 | return deployment 95 | } catch (error) { 96 | core.error(error.stack) 97 | 98 | // build customized error message based on server response 99 | if (error.response) { 100 | let errorMessage = `Failed to create deployment (status: ${error.status}) with build version ${this.buildVersion}.` 101 | if (error.response.headers['x-github-request-id']) { 102 | errorMessage += ` Request ID ${error.response.headers['x-github-request-id']}` 103 | } 104 | if (error.status === 400) { 105 | errorMessage += ` Responded with: ${error.message}` 106 | } else if (error.status === 403) { 107 | errorMessage += ' Ensure GITHUB_TOKEN has permission "pages: write".' 108 | } else if (error.status === 404) { 109 | const pagesSettingsUrl = `${this.githubServerUrl}/${this.repositoryNwo}/settings/pages` 110 | errorMessage += ` Ensure GitHub Pages has been enabled: ${pagesSettingsUrl}` 111 | // If using GHES, add a special note about compatibility 112 | if (new URL(this.githubServerUrl).hostname.toLowerCase() !== 'github.com') { 113 | errorMessage += 114 | '\nNote: This action version may not yet support GitHub Enterprise Server, please check the compatibility table.' 115 | } 116 | } else if (error.status >= 500) { 117 | errorMessage += 118 | ' Server error, is githubstatus.com reporting a Pages outage? Please re-run the deployment at a later time.' 119 | } 120 | throw new Error(errorMessage) 121 | } else { 122 | // istanbul ignore next 123 | throw error 124 | } 125 | } 126 | } 127 | 128 | // Poll the deployment endpoint for status 129 | async check() { 130 | // Don't attempt to check status if no deployment was created 131 | if (!this.deploymentInfo) { 132 | core.setFailed(temporaryErrorStatus.not_found) 133 | return 134 | } 135 | if (this.deploymentInfo.pending !== true) { 136 | core.setFailed(temporaryErrorStatus.unknown_status) 137 | return 138 | } 139 | 140 | const deploymentId = this.deploymentInfo.id || this.buildVersion 141 | const reportingInterval = Number(core.getInput('reporting_interval')) 142 | const maxErrorCount = Number(core.getInput('error_count')) 143 | 144 | let errorCount = 0 145 | 146 | // Time in milliseconds between two deployment status report when status errored, default 0. 147 | let errorReportingInterval = 0 148 | let deployment = null 149 | let errorStatus = 0 150 | 151 | /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ 152 | while (true) { 153 | // Handle reporting interval 154 | await new Promise(resolve => setTimeout(resolve, reportingInterval + errorReportingInterval)) 155 | 156 | // Check status 157 | try { 158 | deployment = await getPagesDeploymentStatus({ 159 | githubToken: this.githubToken, 160 | deploymentId 161 | }) 162 | 163 | if (deployment.status === 'succeed') { 164 | core.info('Reported success!') 165 | core.setOutput('status', 'succeed') 166 | this.deploymentInfo.pending = false 167 | break 168 | } else if (finalErrorStatus[deployment.status]) { 169 | // Fall into permanent error, it may be caused by ongoing incident, malicious deployment content, exhausted automatic retry times, invalid artifact, etc. 170 | core.setFailed(finalErrorStatus[deployment.status]) 171 | this.deploymentInfo.pending = false 172 | break 173 | } else if (temporaryErrorStatus[deployment.status]) { 174 | // A temporary error happened, will query the status again 175 | core.warning(temporaryErrorStatus[deployment.status]) 176 | } else { 177 | core.info('Current status: ' + deployment.status) 178 | } 179 | 180 | // reset the error reporting interval once get the proper status back. 181 | errorReportingInterval = 0 182 | } catch (error) { 183 | core.error(error.stack) 184 | 185 | // build customized error message based on server response 186 | if (error.response) { 187 | errorStatus = error.status || error.response.status 188 | 189 | errorCount++ 190 | 191 | // set the maximum error reporting interval greater than 15 sec but below 30 sec. 192 | if (errorReportingInterval < 1000 * 15) { 193 | errorReportingInterval = (errorReportingInterval << 1) | 1 194 | } 195 | } 196 | } 197 | 198 | if (errorCount >= maxErrorCount) { 199 | core.error('Too many errors, aborting!') 200 | core.setFailed('Failed with status code: ' + errorStatus) 201 | 202 | // Explicitly cancel the deployment 203 | await this.cancel() 204 | return 205 | } 206 | 207 | // Handle timeout 208 | if (Date.now() - this.startTime >= this.timeout) { 209 | core.error('Timeout reached, aborting!') 210 | core.setFailed('Timeout reached, aborting!') 211 | 212 | // Explicitly cancel the deployment 213 | await this.cancel() 214 | return 215 | } 216 | } 217 | } 218 | 219 | async cancel() { 220 | // Don't attempt to cancel if no deployment was created 221 | if (!this.deploymentInfo || this.deploymentInfo.pending !== true) { 222 | core.debug('No deployment to cancel') 223 | return 224 | } 225 | 226 | // Cancel the deployment 227 | try { 228 | const deploymentId = this.deploymentInfo.id || this.buildVersion 229 | await cancelPagesDeployment({ 230 | githubToken: this.githubToken, 231 | deploymentId 232 | }) 233 | core.info(`Canceled deployment with ID ${deploymentId}`) 234 | 235 | this.deploymentInfo.pending = false 236 | } catch (error) { 237 | core.setFailed(error) 238 | if (error.response?.data) { 239 | core.error(JSON.stringify(error.response.data)) 240 | } 241 | } 242 | } 243 | } 244 | 245 | module.exports = { Deployment, MAX_TIMEOUT, ONE_GIGABYTE, SIZE_LIMIT_DESCRIPTION } 246 | --------------------------------------------------------------------------------