├── .nvmrc ├── .husky ├── .gitignore ├── pre-commit └── post-merge ├── .dockerignore ├── .vscode ├── .gitignore └── settings.json ├── .npmrc ├── .prettierignore ├── test_projects └── mdbook │ ├── .gitignore │ ├── src │ ├── chapter_1.md │ └── SUMMARY.md │ └── book.toml ├── __tests__ ├── fixtures │ ├── publish_dir_1 │ │ ├── main.css │ │ ├── main.js │ │ ├── assets │ │ │ ├── lib.css │ │ │ └── lib.js │ │ └── index.html │ └── publish_dir_root │ │ ├── main.css │ │ ├── main.js │ │ ├── assets │ │ ├── lib.css │ │ └── lib.js │ │ ├── .github │ │ ├── workflows │ │ │ └── test.yml │ │ ├── ISSUE_TEMPLATE │ │ │ └── template.md │ │ ├── CODEOWNERS │ │ └── dependabot.yml │ │ └── index.html ├── set-tokens.ghes.test.ts ├── set-tokens.test.ts ├── utils.test.ts ├── get-inputs.test.ts └── git-utils.test.ts ├── .envrc ├── .gitignore ├── images ├── log1.jpg ├── log4.jpg ├── secrets-1.jpg ├── secrets-2.jpg ├── deploy-keys-1.jpg ├── deploy-keys-2.jpg ├── log_overview.jpg ├── log_success.jpg ├── commit_message.jpg ├── settings_select.jpg ├── settings_inactive.jpg ├── committer_github_actions_bot.jpg ├── log_first_deployment_failed_with_github_token.jpg └── ogp.svg ├── .github ├── CODEOWNERS ├── labeler.yml ├── workflows │ ├── release.yml │ ├── purge-readme-image-cache.yml │ ├── pages-status-check.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── label-commenter.yml │ ├── labeler.yml │ ├── update-major-tag.yml │ └── test.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 3_proposal.yml │ ├── 1_user_support.yml │ └── 2_bug_report.txt └── label-commenter-config.yml ├── .editorconfig ├── .prettierrc.json ├── jest.config.js ├── renovate.json ├── src ├── index.ts ├── interfaces.ts ├── utils.ts ├── get-inputs.ts ├── main.ts ├── set-tokens.ts └── git-utils.ts ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── .devcontainer └── devcontainer.json ├── release.sh ├── package.json ├── action.yml └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.1 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | * 3 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /test_projects/mdbook/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_1/main.css: -------------------------------------------------------------------------------- 1 | /* CSS */ 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_1/main.js: -------------------------------------------------------------------------------- 1 | // JavaScript 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/main.css: -------------------------------------------------------------------------------- 1 | /* CSS */ 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | nvmrc=~/.nvm/nvm.sh 2 | source $nvmrc 3 | nvm use 4 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | npm install 2 | git remote prune origin 3 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_1/assets/lib.css: -------------------------------------------------------------------------------- 1 | /* CSS */ 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/main.js: -------------------------------------------------------------------------------- 1 | // JavaScript 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_1/assets/lib.js: -------------------------------------------------------------------------------- 1 | // JavaScript 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/assets/lib.css: -------------------------------------------------------------------------------- 1 | /* CSS */ 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/assets/lib.js: -------------------------------------------------------------------------------- 1 | // JavaScript 2 | -------------------------------------------------------------------------------- /test_projects/mdbook/src/chapter_1.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 2 | 3 | test 7 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | -------------------------------------------------------------------------------- /test_projects/mdbook/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Chapter 1](./chapter_1.md) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage 3 | .npm 4 | .eslintcache 5 | .env 6 | node_modules 7 | -------------------------------------------------------------------------------- /images/log1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/log1.jpg -------------------------------------------------------------------------------- /images/log4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/log4.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "deno.enable": false 4 | } -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/.github/ISSUE_TEMPLATE/template.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/secrets-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/secrets-1.jpg -------------------------------------------------------------------------------- /images/secrets-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/secrets-2.jpg -------------------------------------------------------------------------------- /images/deploy-keys-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/deploy-keys-1.jpg -------------------------------------------------------------------------------- /images/deploy-keys-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/deploy-keys-2.jpg -------------------------------------------------------------------------------- /images/log_overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/log_overview.jpg -------------------------------------------------------------------------------- /images/log_success.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/log_success.jpg -------------------------------------------------------------------------------- /images/commit_message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/commit_message.jpg -------------------------------------------------------------------------------- /images/settings_select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/settings_select.jpg -------------------------------------------------------------------------------- /images/settings_inactive.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/settings_inactive.jpg -------------------------------------------------------------------------------- /images/committer_github_actions_bot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/committer_github_actions_bot.jpg -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | 3 | * @peaceiris 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | -------------------------------------------------------------------------------- /images/log_first_deployment_failed_with_github_token.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/actions-gh-pages/main/images/log_first_deployment_failed_with_github_token.jpg -------------------------------------------------------------------------------- /test_projects/mdbook/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["peaceiris"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "GitHub Actions for GitHub Pages" 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # dependabot config 2 | version: 2 3 | updates: 4 | - package-ecosystem: npm 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [Makefile] 12 | indent_size = 4 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /__tests__/fixtures/publish_dir_root/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | }; 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>peaceiris/renovate-config" 5 | ], 6 | "packageRules": [ 7 | { 8 | "automerge": false, 9 | "matchUpdateTypes": ["minor", "patch"], 10 | "automergeStrategy": "squash" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | cicd: 2 | - .github/workflows/* 3 | 4 | dependencies: 5 | - .nvmrc 6 | - package.json 7 | - package-lock.json 8 | 9 | documentation: 10 | - README.md 11 | 12 | test: 13 | - __tests__ 14 | 15 | docker: 16 | - .devcontainer/* 17 | - .dockerignore 18 | - Dockerfile 19 | - Makefile 20 | - docker-compose.yml 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as main from './main'; 3 | 4 | (async (): Promise => { 5 | try { 6 | await main.run(); 7 | } catch (error) { 8 | if (error instanceof Error) { 9 | core.setFailed(`Action failed with "${error.message}"`); 10 | } else { 11 | core.setFailed('unexpected error'); 12 | } 13 | } 14 | })(); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2019"], 4 | "module": "commonjs", 5 | "target": "ES2019", 6 | "sourceMap": true, 7 | "outDir": "./lib", 8 | "rootDir": "./src", 9 | "removeComments": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true 14 | }, 15 | "exclude": ["node_modules", "**/*.test.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v3.*.*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | # https://github.com/peaceiris/workflows/blob/main/create-release-npm/action.yml 14 | - uses: peaceiris/workflows/create-release-npm@v0.20.1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/purge-readme-image-cache.yml: -------------------------------------------------------------------------------- 1 | name: Purge image cache 2 | 3 | on: 4 | schedule: 5 | - cron: '54 18 * * */7' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | purge: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | 13 | - run: > 14 | curl -sL https://github.com/${GITHUB_REPOSITORY} | 15 | grep -oE ' { 6 | jest.resetModules(); 7 | process.env = {...OLD_ENV}; 8 | }); 9 | 10 | afterAll(() => { 11 | process.env = OLD_ENV; // Restore old environment 12 | }); 13 | 14 | describe('setGithubToken()', () => { 15 | test('return remote url with GITHUB_TOKEN gh-pages', () => { 16 | process.env.GITHUB_SERVER_URL = 'https://github.enterprise.server'; 17 | const expected = 'https://x-access-token:GITHUB_TOKEN@github.enterprise.server/owner/repo.git'; 18 | const test = setGithubToken( 19 | 'GITHUB_TOKEN', 20 | 'owner/repo', 21 | 'gh-pages', 22 | '', 23 | 'refs/heads/master', 24 | 'push' 25 | ); 26 | expect(test).toMatch(expected); 27 | }); 28 | }); 29 | 30 | describe('setPersonalToken()', () => { 31 | test('return remote url with personal access token', () => { 32 | process.env.GITHUB_SERVER_URL = 'https://github.enterprise.server'; 33 | const expected = 'https://x-access-token:pat@github.enterprise.server/owner/repo.git'; 34 | const test = setPersonalToken('pat', 'owner/repo'); 35 | expect(test).toMatch(expected); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js", 3 | "image": "docker.pkg.github.com/peaceiris/actions-gh-pages/dev:latest", 4 | 5 | // Use 'settings' to set *default* container specific settings.json values on container create. 6 | "settings": { 7 | "terminal.integrated.shell.linux": "/bin/bash" 8 | }, 9 | 10 | // Add the IDs of extensions you want installed when the container is created in the array below. 11 | "extensions": [ 12 | "bungcip.better-toml", 13 | "EditorConfig.EditorConfig", 14 | "donjayamanne.githistory", 15 | "eamodio.gitlens", 16 | "oderwat.indent-rainbow", 17 | "yzhang.markdown-all-in-one", 18 | "shd101wyy.markdown-preview-enhanced", 19 | "christian-kohler.path-intellisense", 20 | "lfs.vscode-emacs-friendly", 21 | "ms-azuretools.vscode-docker", 22 | "dbaeumer.vscode-eslint", 23 | "firsttris.vscode-jest-runner", 24 | "VisualStudioExptTeam.vscodeintellicode" 25 | ], 26 | 27 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 28 | // "forwardPorts": [3000], 29 | 30 | // Specifies a command that should be run after the container has been created. 31 | "postCreateCommand": "npm ci", 32 | 33 | // Comment out the next line to run as root instead. 34 | // "remoteUser": "runner" 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_proposal.yml: -------------------------------------------------------------------------------- 1 | name: Proposal 2 | description: Suggest an idea for this project 3 | title: 'proposal: ' 4 | labels: proposal 5 | assignees: peaceiris 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: 10 | Please note we will close your issue without comment if you do not fill out the issue checklist below and provide ALL the requested information. 11 | - type: checkboxes 12 | attributes: 13 | label: Checklist 14 | description: Checklist before creating an issue. 15 | options: 16 | - label: "I am using the latest version of this action." 17 | required: true 18 | - label: "I have read the latest README and followed the instructions." 19 | required: true 20 | - label: "I have read the latest GitHub Actions official documentation and learned the basic spec and concepts." 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: "Describe your proposal" 25 | description: "A clear and concise description of what the proposal is." 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: "Describe the solution you'd like" 31 | description: "A clear and concise description of what you want to happen." 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: "Describe alternatives you've considered" 37 | description: "A clear and concise description of any alternative solutions or features you've considered." 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: "Additional context" 43 | description: "Add any other context or screenshots about the feature request here." 44 | validations: 45 | required: false 46 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # fail on unset variables and command errors 4 | set -eu -o pipefail # -x: is for debugging 5 | 6 | DEFAULT_BRANCH="main" 7 | 8 | CURRENT_BRANCH="$(git branch --show-current)" 9 | if [ "${CURRENT_BRANCH}" != "${DEFAULT_BRANCH}" ]; then 10 | echo "$0: Current branch ${CURRENT_BRANCH} is not ${DEFAULT_BRANCH}, continue? (y/n)" 11 | read -r res 12 | if [ "${res}" = "n" ]; then 13 | echo "$0: Stop script" 14 | exit 0 15 | fi 16 | fi 17 | 18 | PRERELEASE_TYPE_LIST="prerelease prepatch preminor premajor" 19 | if [ "${CURRENT_BRANCH}" != "${DEFAULT_BRANCH}" ]; then 20 | RELEASE_TYPE_LIST="${PRERELEASE_TYPE_LIST}" 21 | else 22 | RELEASE_TYPE_LIST="${PRERELEASE_TYPE_LIST} patch minor major" 23 | fi 24 | 25 | if command -v fzf; then 26 | RELEASE_TYPE=$(echo "${RELEASE_TYPE_LIST}" | tr ' ' '\n' | fzf --layout=reverse) 27 | else 28 | select sel in ${RELEASE_TYPE_LIST}; do 29 | RELEASE_TYPE="${sel}" 30 | break 31 | done 32 | fi 33 | 34 | echo "$0: Create ${RELEASE_TYPE} release, continue? (y/n)" 35 | read -r res 36 | if [ "${res}" = "n" ]; then 37 | echo "$0: Stop script" 38 | exit 0 39 | fi 40 | 41 | git fetch origin 42 | if [ "${CURRENT_BRANCH}" != "${DEFAULT_BRANCH}" ]; then 43 | git pull origin "${CURRENT_BRANCH}" 44 | else 45 | git pull origin ${DEFAULT_BRANCH} 46 | git tag -d v3 || true 47 | git pull origin --tags 48 | fi 49 | 50 | npm install 51 | 52 | mkdir ./lib 53 | npm run build 54 | git add ./lib/index.js 55 | git commit -m "chore(release): Add build assets" 56 | 57 | npm run release -- --release-as "${RELEASE_TYPE}" --preset eslint 58 | 59 | git rm ./lib/index.js 60 | rm -rf ./lib 61 | git commit -m "chore(release): Remove build assets [skip ci]" 62 | 63 | if [ "${CURRENT_BRANCH}" != "${DEFAULT_BRANCH}" ]; then 64 | git push origin "${CURRENT_BRANCH}" 65 | else 66 | git push origin ${DEFAULT_BRANCH} 67 | fi 68 | 69 | TAG_NAME="v$(jq -r '.version' ./package.json)" 70 | git push origin "${TAG_NAME}" 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_user_support.yml: -------------------------------------------------------------------------------- 1 | name: User Support 2 | description: Questions for this action 3 | title: "support: " 4 | labels: support 5 | assignees: peaceiris 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: 10 | Please note we will close your issue without comment if you do not fill out the issue checklist below and provide ALL the requested information. 11 | - type: checkboxes 12 | attributes: 13 | label: Checklist 14 | description: Checklist before creating an issue. 15 | options: 16 | - label: "I am using the latest version of this action." 17 | required: true 18 | - label: "I have read the latest README and followed the instructions." 19 | required: true 20 | - label: "I have read the latest GitHub Actions official documentation and learned the basic spec and concepts." 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Describe your question 25 | description: A clear and concise description of what the question is. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Relevant links 31 | description: 32 | Links to your public repository, YAML config file, and YAML workflow file. 33 | Please use [a permanent link](https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/getting-permanent-links-to-files), not a default branch. 34 | render: markdown 35 | value: | 36 | Public repository: 37 | YAML config: 38 | YAML workflow: 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Relevant log output 44 | description: Copy and paste any relevant log output here. 45 | validations: 46 | required: false 47 | - type: textarea 48 | attributes: 49 | label: Additional context. 50 | description: Write any other context about the question here. 51 | validations: 52 | required: false 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_bug_report.txt: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Try the User Support Issue Template first. 3 | title: 'bug: ' 4 | labels: bug 5 | assignees: peaceiris 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: 10 | Please note we will close your issue without comment if you do not fill out the issue checklist below and provide ALL the requested information. 11 | - type: checkboxes 12 | attributes: 13 | label: Checklist 14 | description: Checklist before creating an issue. 15 | options: 16 | - label: "I am using the latest version of this action." 17 | required: true 18 | - label: "I have read the latest README and followed the instructions." 19 | required: true 20 | - label: "I have read the latest GitHub Actions official documentation and learned the basic spec and concepts." 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: "Describe the bug" 25 | description: "A clear and concise description of what the bug is." 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Relevant links 31 | description: 32 | Links to your public repository, YAML config file, and YAML workflow file. 33 | Please use [a permanent link](https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/getting-permanent-links-to-files), not a default branch. 34 | render: markdown 35 | value: | 36 | Public repository: 37 | YAML config: 38 | YAML workflow: 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Relevant log output 44 | description: Copy and paste any relevant log output here. 45 | validations: 46 | required: false 47 | - type: textarea 48 | attributes: 49 | label: Additional context. 50 | description: Write any other context about the question here. 51 | validations: 52 | required: false 53 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as io from '@actions/io'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | export async function getHomeDir(): Promise { 7 | let homedir = ''; 8 | 9 | if (process.platform === 'win32') { 10 | homedir = process.env['USERPROFILE'] || 'C:\\'; 11 | } else { 12 | homedir = `${process.env.HOME}`; 13 | } 14 | 15 | core.debug(`homeDir: ${homedir}`); 16 | 17 | return homedir; 18 | } 19 | 20 | export async function getWorkDirName(unixTime: string): Promise { 21 | const homeDir = await getHomeDir(); 22 | const workDirName = path.join(homeDir, `actions_github_pages_${unixTime}`); 23 | return workDirName; 24 | } 25 | 26 | export async function createDir(dirPath: string): Promise { 27 | await io.mkdirP(dirPath); 28 | core.debug(`Created directory ${dirPath}`); 29 | return; 30 | } 31 | 32 | export async function addNoJekyll(workDir: string, DisableNoJekyll: boolean): Promise { 33 | if (DisableNoJekyll) { 34 | return; 35 | } 36 | const filepath = path.join(workDir, '.nojekyll'); 37 | if (fs.existsSync(filepath)) { 38 | return; 39 | } 40 | fs.closeSync(fs.openSync(filepath, 'w')); 41 | core.info(`[INFO] Created ${filepath}`); 42 | } 43 | 44 | export async function addCNAME(workDir: string, content: string): Promise { 45 | if (content === '') { 46 | return; 47 | } 48 | const filepath = path.join(workDir, 'CNAME'); 49 | if (fs.existsSync(filepath)) { 50 | core.info(`CNAME already exists, skip adding CNAME`); 51 | return; 52 | } 53 | fs.writeFileSync(filepath, content + '\n'); 54 | core.info(`[INFO] Created ${filepath}`); 55 | } 56 | 57 | export async function skipOnFork( 58 | isForkRepository: boolean, 59 | githubToken: string, 60 | deployKey: string, 61 | personalToken: string 62 | ): Promise { 63 | if (isForkRepository) { 64 | if (githubToken === '' && deployKey === '' && personalToken === '') { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-github-pages", 3 | "version": "4.0.0", 4 | "description": "GitHub Actions for GitHub Pages", 5 | "main": "lib/index.js", 6 | "engines": { 7 | "node": ">=v20.11.0", 8 | "npm": ">=10.2.4" 9 | }, 10 | "scripts": { 11 | "postinstall": "npx husky install", 12 | "all": "npm run format && npm run lint && npm test", 13 | "lint": "eslint ./{src,__tests__}/**/*.ts", 14 | "lint:fix": "eslint --fix ./{src,__tests__}/**/*.ts", 15 | "test": "jest --coverage --verbose --detectOpenHandles", 16 | "build": "ncc build ./src/index.ts -o lib --minify", 17 | "tsc": "tsc", 18 | "format": "prettier --write '**/*.ts'", 19 | "format:check": "prettier --check '**/*.ts'", 20 | "release": "standard-version" 21 | }, 22 | "lint-staged": { 23 | "{src,__tests__}/**/*.ts": [ 24 | "prettier --check", 25 | "eslint" 26 | ], 27 | "README.md": [ 28 | "npx doctoc@2.1.0 --github" 29 | ] 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/peaceiris/actions-gh-pages.git" 34 | }, 35 | "keywords": [ 36 | "GitHub Actions", 37 | "Actions", 38 | "JavaScript Action", 39 | "TypeScript Action", 40 | "GitHub Pages", 41 | "gh-pages" 42 | ], 43 | "author": "peaceiris", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/peaceiris/actions-gh-pages/issues" 47 | }, 48 | "homepage": "https://github.com/peaceiris/actions-gh-pages#readme", 49 | "dependencies": { 50 | "@actions/core": "^1.10.0", 51 | "@actions/exec": "^1.1.1", 52 | "@actions/github": "^5.1.1", 53 | "@actions/glob": "^0.5.0", 54 | "@actions/io": "^1.1.2", 55 | "@types/shelljs": "^0.8.11", 56 | "shelljs": "^0.8.5" 57 | }, 58 | "devDependencies": { 59 | "@types/jest": "^29.2.6", 60 | "@types/js-yaml": "^4.0.5", 61 | "@types/node": "~16", 62 | "@typescript-eslint/eslint-plugin": "^5.48.2", 63 | "@typescript-eslint/parser": "^5.48.2", 64 | "@vercel/ncc": "^0.38.0", 65 | "eslint": "^8.32.0", 66 | "eslint-config-prettier": "^9.0.0", 67 | "eslint-plugin-jest": "^27.2.1", 68 | "eslint-plugin-prettier": "^4.2.1", 69 | "husky": "^8.0.3", 70 | "jest": "^29.3.1", 71 | "jest-circus": "^29.3.1", 72 | "js-yaml": "^4.1.0", 73 | "lint-staged": "^13.1.0", 74 | "prettier": "2.8.8", 75 | "standard-version": "^9.1.1", 76 | "ts-jest": "^29.0.5", 77 | "typescript": "^4.9.4" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'GitHub Pages action' 2 | description: 'GitHub Actions for GitHub Pages 🚀 Deploy static files and publish your site easily. Static-Site-Generators-friendly.' 3 | author: 'peaceiris' 4 | runs: 5 | using: 'node20' 6 | main: 'lib/index.js' 7 | branding: 8 | icon: 'upload-cloud' 9 | color: 'blue' 10 | inputs: 11 | deploy_key: 12 | description: 'Set a SSH private key from repository secret value for pushing to the remote branch.' 13 | required: false 14 | github_token: 15 | description: 'Set a generated GITHUB_TOKEN for pushing to the remote branch.' 16 | required: false 17 | personal_token: 18 | description: 'Set a personal access token for pushing to the remote branch.' 19 | required: false 20 | publish_branch: 21 | description: 'Set a target branch for deployment.' 22 | required: false 23 | default: 'gh-pages' 24 | publish_dir: 25 | description: 'Set an input directory for deployment.' 26 | required: false 27 | default: 'public' 28 | destination_dir: 29 | description: 'Set an destination subdirectory for deployment.' 30 | required: false 31 | default: '' 32 | external_repository: 33 | description: 'Set an external repository (owner/repo).' 34 | required: false 35 | allow_empty_commit: 36 | description: 'If empty commits should be made to the publication branch' 37 | required: false 38 | default: 'false' 39 | keep_files: 40 | description: 'If existing files in the publish branch should be not removed before deploying' 41 | required: false 42 | default: 'false' 43 | force_orphan: 44 | description: 'Keep only the latest commit on a GitHub Pages branch' 45 | required: false 46 | default: 'false' 47 | user_name: 48 | description: 'Set Git user.name' 49 | required: false 50 | user_email: 51 | description: 'Set Git user.email' 52 | required: false 53 | commit_message: 54 | description: 'Set a custom commit message with a triggered commit hash' 55 | required: false 56 | full_commit_message: 57 | description: 'Set a custom full commit message without a triggered commit hash' 58 | required: false 59 | tag_name: 60 | description: 'Set tag name' 61 | required: false 62 | tag_message: 63 | description: 'Set tag message' 64 | required: false 65 | enable_jekyll: 66 | description: 'Enable the GitHub Pages built-in Jekyll' 67 | required: false 68 | default: 'false' 69 | disable_nojekyll: 70 | description: 'An alias for enable_jekyll to disable adding .nojekyll file to a publishing branch' 71 | required: false 72 | default: 'false' 73 | cname: 74 | description: 'Set custom domain' 75 | required: false 76 | exclude_assets: 77 | description: 'Set files or directories to exclude from a publish directory.' 78 | required: false 79 | default: '.github' 80 | -------------------------------------------------------------------------------- /src/get-inputs.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import {Inputs} from './interfaces'; 3 | 4 | export function showInputs(inps: Inputs): void { 5 | let authMethod = ''; 6 | if (inps.DeployKey) { 7 | authMethod = 'DeployKey'; 8 | } else if (inps.GithubToken) { 9 | authMethod = 'GithubToken'; 10 | } else if (inps.PersonalToken) { 11 | authMethod = 'PersonalToken'; 12 | } 13 | 14 | core.info(`\ 15 | [INFO] ${authMethod}: true 16 | [INFO] PublishBranch: ${inps.PublishBranch} 17 | [INFO] PublishDir: ${inps.PublishDir} 18 | [INFO] DestinationDir: ${inps.DestinationDir} 19 | [INFO] ExternalRepository: ${inps.ExternalRepository} 20 | [INFO] AllowEmptyCommit: ${inps.AllowEmptyCommit} 21 | [INFO] KeepFiles: ${inps.KeepFiles} 22 | [INFO] ForceOrphan: ${inps.ForceOrphan} 23 | [INFO] UserName: ${inps.UserName} 24 | [INFO] UserEmail: ${inps.UserEmail} 25 | [INFO] CommitMessage: ${inps.CommitMessage} 26 | [INFO] FullCommitMessage: ${inps.FullCommitMessage} 27 | [INFO] TagName: ${inps.TagName} 28 | [INFO] TagMessage: ${inps.TagMessage} 29 | [INFO] EnableJekyll (DisableNoJekyll): ${inps.DisableNoJekyll} 30 | [INFO] CNAME: ${inps.CNAME} 31 | [INFO] ExcludeAssets ${inps.ExcludeAssets} 32 | `); 33 | } 34 | 35 | export function getInputs(): Inputs { 36 | let useBuiltinJekyll = false; 37 | 38 | const isBoolean = (param: string): boolean => (param || 'false').toUpperCase() === 'TRUE'; 39 | 40 | const enableJekyll: boolean = isBoolean(core.getInput('enable_jekyll')); 41 | const disableNoJekyll: boolean = isBoolean(core.getInput('disable_nojekyll')); 42 | 43 | if (enableJekyll && disableNoJekyll) { 44 | throw new Error(`Use either of enable_jekyll or disable_nojekyll`); 45 | } else if (enableJekyll) { 46 | useBuiltinJekyll = true; 47 | } else if (disableNoJekyll) { 48 | useBuiltinJekyll = true; 49 | } 50 | 51 | const inps: Inputs = { 52 | DeployKey: core.getInput('deploy_key'), 53 | GithubToken: core.getInput('github_token'), 54 | PersonalToken: core.getInput('personal_token'), 55 | PublishBranch: core.getInput('publish_branch'), 56 | PublishDir: core.getInput('publish_dir'), 57 | DestinationDir: core.getInput('destination_dir'), 58 | ExternalRepository: core.getInput('external_repository'), 59 | AllowEmptyCommit: isBoolean(core.getInput('allow_empty_commit')), 60 | KeepFiles: isBoolean(core.getInput('keep_files')), 61 | ForceOrphan: isBoolean(core.getInput('force_orphan')), 62 | UserName: core.getInput('user_name'), 63 | UserEmail: core.getInput('user_email'), 64 | CommitMessage: core.getInput('commit_message'), 65 | FullCommitMessage: core.getInput('full_commit_message'), 66 | TagName: core.getInput('tag_name'), 67 | TagMessage: core.getInput('tag_message'), 68 | DisableNoJekyll: useBuiltinJekyll, 69 | CNAME: core.getInput('cname'), 70 | ExcludeAssets: core.getInput('exclude_assets') 71 | }; 72 | 73 | return inps; 74 | } 75 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {context} from '@actions/github'; 2 | import * as core from '@actions/core'; 3 | import * as exec from '@actions/exec'; 4 | import * as github from '@actions/github'; 5 | import {Inputs} from './interfaces'; 6 | import {showInputs, getInputs} from './get-inputs'; 7 | import {setTokens} from './set-tokens'; 8 | import {setRepo, setCommitAuthor, getCommitMessage, commit, push, pushTag} from './git-utils'; 9 | import {getWorkDirName, addNoJekyll, addCNAME, skipOnFork} from './utils'; 10 | 11 | export async function run(): Promise { 12 | try { 13 | core.info('[INFO] Usage https://github.com/peaceiris/actions-gh-pages#readme'); 14 | 15 | const inps: Inputs = getInputs(); 16 | core.startGroup('Dump inputs'); 17 | showInputs(inps); 18 | core.endGroup(); 19 | 20 | if (core.isDebug()) { 21 | core.startGroup('Debug: dump context'); 22 | console.log(context); 23 | core.endGroup(); 24 | } 25 | 26 | const eventName = context.eventName; 27 | if (eventName === 'pull_request' || eventName === 'push') { 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | const isForkRepository = (context.payload as any).repository.fork; 30 | const isSkipOnFork = await skipOnFork( 31 | isForkRepository, 32 | inps.GithubToken, 33 | inps.DeployKey, 34 | inps.PersonalToken 35 | ); 36 | if (isSkipOnFork) { 37 | core.warning('This action runs on a fork and not found auth token, Skip deployment'); 38 | core.setOutput('skip', 'true'); 39 | return; 40 | } 41 | } 42 | 43 | core.startGroup('Setup auth token'); 44 | const remoteURL = await setTokens(inps); 45 | core.debug(`remoteURL: ${remoteURL}`); 46 | core.endGroup(); 47 | 48 | core.startGroup('Prepare publishing assets'); 49 | const date = new Date(); 50 | const unixTime = date.getTime(); 51 | const workDir = await getWorkDirName(`${unixTime}`); 52 | await setRepo(inps, remoteURL, workDir); 53 | await addNoJekyll(workDir, inps.DisableNoJekyll); 54 | await addCNAME(workDir, inps.CNAME); 55 | core.endGroup(); 56 | 57 | core.startGroup('Setup Git config'); 58 | try { 59 | await exec.exec('git', ['remote', 'rm', 'origin']); 60 | } catch (error) { 61 | if (error instanceof Error) { 62 | core.info(`[INFO] ${error.message}`); 63 | } else { 64 | throw new Error('unexpected error'); 65 | } 66 | } 67 | await exec.exec('git', ['remote', 'add', 'origin', remoteURL]); 68 | await exec.exec('git', ['add', '--all']); 69 | await setCommitAuthor(inps.UserName, inps.UserEmail); 70 | core.endGroup(); 71 | 72 | core.startGroup('Create a commit'); 73 | const hash = `${process.env.GITHUB_SHA}`; 74 | const baseRepo = `${github.context.repo.owner}/${github.context.repo.repo}`; 75 | const commitMessage = getCommitMessage( 76 | inps.CommitMessage, 77 | inps.FullCommitMessage, 78 | inps.ExternalRepository, 79 | baseRepo, 80 | hash 81 | ); 82 | await commit(inps.AllowEmptyCommit, commitMessage); 83 | core.endGroup(); 84 | 85 | core.startGroup('Push the commit or tag'); 86 | await push(inps.PublishBranch, inps.ForceOrphan); 87 | await pushTag(inps.TagName, inps.TagMessage); 88 | core.endGroup(); 89 | 90 | core.info('[INFO] Action successfully completed'); 91 | 92 | return; 93 | } catch (error) { 94 | if (error instanceof Error) { 95 | throw new Error(error.message); 96 | } else { 97 | throw new Error('unexpected error'); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /__tests__/set-tokens.test.ts: -------------------------------------------------------------------------------- 1 | import {getPublishRepo, setPersonalToken, setGithubToken} from '../src/set-tokens'; 2 | 3 | beforeEach(() => { 4 | jest.resetModules(); 5 | }); 6 | 7 | // afterEach(() => { 8 | 9 | // }); 10 | 11 | describe('getPublishRepo()', () => { 12 | test('return repository name', () => { 13 | const test = getPublishRepo('', 'owner', 'repo'); 14 | expect(test).toMatch('owner/repo'); 15 | }); 16 | 17 | test('return external repository name', () => { 18 | const test = getPublishRepo('extOwner/extRepo', 'owner', 'repo'); 19 | expect(test).toMatch('extOwner/extRepo'); 20 | }); 21 | }); 22 | 23 | describe('setGithubToken()', () => { 24 | test('return remote url with GITHUB_TOKEN gh-pages', () => { 25 | const expected = 'https://x-access-token:GITHUB_TOKEN@github.com/owner/repo.git'; 26 | const test = setGithubToken( 27 | 'GITHUB_TOKEN', 28 | 'owner/repo', 29 | 'gh-pages', 30 | '', 31 | 'refs/heads/master', 32 | 'push' 33 | ); 34 | expect(test).toMatch(expected); 35 | }); 36 | 37 | test('return remote url with GITHUB_TOKEN master', () => { 38 | const expected = 'https://x-access-token:GITHUB_TOKEN@github.com/owner/repo.git'; 39 | const test = setGithubToken( 40 | 'GITHUB_TOKEN', 41 | 'owner/repo', 42 | 'master', 43 | '', 44 | 'refs/heads/source', 45 | 'push' 46 | ); 47 | expect(test).toMatch(expected); 48 | }); 49 | 50 | test('return remote url with GITHUB_TOKEN gh-pages (RegExp)', () => { 51 | const expected = 'https://x-access-token:GITHUB_TOKEN@github.com/owner/repo.git'; 52 | const test = setGithubToken( 53 | 'GITHUB_TOKEN', 54 | 'owner/repo', 55 | 'gh-pages', 56 | '', 57 | 'refs/heads/gh-pages-base', 58 | 'push' 59 | ); 60 | expect(test).toMatch(expected); 61 | }); 62 | 63 | test('throw error gh-pages-base to gh-pages-base (RegExp)', () => { 64 | expect(() => { 65 | setGithubToken( 66 | 'GITHUB_TOKEN', 67 | 'owner/repo', 68 | 'gh-pages-base', 69 | '', 70 | 'refs/heads/gh-pages-base', 71 | 'push' 72 | ); 73 | }).toThrow('You deploy from gh-pages-base to gh-pages-base'); 74 | }); 75 | 76 | test('throw error master to master', () => { 77 | expect(() => { 78 | setGithubToken('GITHUB_TOKEN', 'owner/repo', 'master', '', 'refs/heads/master', 'push'); 79 | }).toThrow('You deploy from master to master'); 80 | }); 81 | 82 | test('throw error external repository with GITHUB_TOKEN', () => { 83 | expect(() => { 84 | setGithubToken( 85 | 'GITHUB_TOKEN', 86 | 'owner/repo', 87 | 'gh-pages', 88 | 'extOwner/extRepo', 89 | 'refs/heads/master', 90 | 'push' 91 | ); 92 | }).toThrow(`\ 93 | The generated GITHUB_TOKEN (github_token) does not support to push to an external repository. 94 | Use deploy_key or personal_token. 95 | `); 96 | }); 97 | 98 | test('return remote url with GITHUB_TOKEN pull_request', () => { 99 | const expected = 'https://x-access-token:GITHUB_TOKEN@github.com/owner/repo.git'; 100 | const test = setGithubToken( 101 | 'GITHUB_TOKEN', 102 | 'owner/repo', 103 | 'gh-pages', 104 | '', 105 | 'refs/pull/29/merge', 106 | 'pull_request' 107 | ); 108 | expect(test).toMatch(expected); 109 | }); 110 | }); 111 | 112 | describe('setPersonalToken()', () => { 113 | test('return remote url with personal access token', () => { 114 | const expected = 'https://x-access-token:pat@github.com/owner/repo.git'; 115 | const test = setPersonalToken('pat', 'owner/repo'); 116 | expect(test).toMatch(expected); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { 4 | getHomeDir, 5 | getWorkDirName, 6 | createDir, 7 | addNoJekyll, 8 | addCNAME, 9 | skipOnFork 10 | } from '../src/utils'; 11 | 12 | beforeEach(() => { 13 | jest.resetModules(); 14 | }); 15 | 16 | // afterEach(() => { 17 | 18 | // }); 19 | 20 | async function getTime(): Promise { 21 | const date = new Date(); 22 | const unixTime = date.getTime(); 23 | return `${unixTime}`; 24 | } 25 | 26 | describe('getHomeDir()', () => { 27 | test('get home directory name', async () => { 28 | let test = ''; 29 | if (process.platform === 'win32') { 30 | test = 'C:\\Users\\runneradmin'; 31 | } else { 32 | test = `${process.env.HOME}`; 33 | } 34 | const expected = await getHomeDir(); 35 | expect(test).toMatch(expected); 36 | }); 37 | }); 38 | 39 | describe('getWorkDirName()', () => { 40 | test('get work directory name', async () => { 41 | let home = ''; 42 | if (process.platform === 'win32') { 43 | home = 'C:\\Users\\runneradmin'; 44 | } else { 45 | home = `${process.env.HOME}`; 46 | } 47 | const unixTime = await getTime(); 48 | const expected = path.join(home, `actions_github_pages_${unixTime}`); 49 | const test = await getWorkDirName(`${unixTime}`); 50 | expect(test).toMatch(expected); 51 | }); 52 | }); 53 | 54 | describe('createDir()', () => { 55 | test('create a directory', async () => { 56 | const unixTime = await getTime(); 57 | const workDirName = await getWorkDirName(`${unixTime}`); 58 | await createDir(workDirName); 59 | const test = fs.existsSync(workDirName); 60 | expect(test).toBe(true); 61 | }); 62 | }); 63 | 64 | async function getWorkDir(): Promise { 65 | const unixTime = await getTime(); 66 | let workDir = ''; 67 | workDir = await getWorkDirName(`${unixTime}`); 68 | await createDir(workDir); 69 | return workDir; 70 | } 71 | 72 | describe('addNoJekyll()', () => { 73 | test('add .nojekyll', async () => { 74 | let workDir = ''; 75 | (async (): Promise => { 76 | workDir = await getWorkDir(); 77 | })(); 78 | const filepath = path.join(workDir, '.nojekyll'); 79 | 80 | await addNoJekyll(workDir, false); 81 | const test = fs.existsSync(filepath); 82 | expect(test).toBe(true); 83 | 84 | fs.unlinkSync(filepath); 85 | }); 86 | 87 | test('.nojekyll already exists', async () => { 88 | let workDir = ''; 89 | (async (): Promise => { 90 | workDir = await getWorkDir(); 91 | })(); 92 | const filepath = path.join(workDir, '.nojekyll'); 93 | fs.closeSync(fs.openSync(filepath, 'w')); 94 | 95 | await addNoJekyll(workDir, false); 96 | const test = fs.existsSync(filepath); 97 | expect(test).toBe(true); 98 | 99 | fs.unlinkSync(filepath); 100 | }); 101 | 102 | test('not add .nojekyll disable_nojekyll', async () => { 103 | let workDir = ''; 104 | (async (): Promise => { 105 | workDir = await getWorkDir(); 106 | })(); 107 | const filepath = path.join(workDir, '.nojekyll'); 108 | 109 | await addNoJekyll(workDir, true); 110 | const test = fs.existsSync(filepath); 111 | expect(test).toBe(false); 112 | }); 113 | }); 114 | 115 | describe('addCNAME()', () => { 116 | test('add CNAME', async () => { 117 | let workDir = ''; 118 | (async (): Promise => { 119 | workDir = await getWorkDir(); 120 | })(); 121 | const filepath = path.join(workDir, 'CNAME'); 122 | 123 | await addCNAME(workDir, 'github.com'); 124 | const test = fs.readFileSync(filepath, 'utf8'); 125 | expect(test).toMatch('github.com'); 126 | 127 | fs.unlinkSync(filepath); 128 | }); 129 | 130 | test('do nothing', async () => { 131 | let workDir = ''; 132 | (async (): Promise => { 133 | workDir = await getWorkDir(); 134 | })(); 135 | const filepath = path.join(workDir, 'CNAME'); 136 | 137 | await addCNAME(workDir, ''); 138 | const test = fs.existsSync(filepath); 139 | expect(test).toBe(false); 140 | }); 141 | 142 | test('CNAME already exists', async () => { 143 | let workDir = ''; 144 | (async (): Promise => { 145 | workDir = await getWorkDir(); 146 | })(); 147 | const filepath = path.join(workDir, 'CNAME'); 148 | 149 | await addCNAME(workDir, 'github.io'); 150 | await addCNAME(workDir, 'github.com'); 151 | const test = fs.readFileSync(filepath, 'utf8'); 152 | expect(test).toMatch('github.io'); 153 | 154 | fs.unlinkSync(filepath); 155 | }); 156 | }); 157 | 158 | describe('skipOnFork()', () => { 159 | test('return false on upstream', async () => { 160 | const test = await skipOnFork(false, 'token', '', ''); 161 | expect(test).toBeFalsy(); 162 | }); 163 | 164 | test('return false on fork with github_token', async () => { 165 | const test = await skipOnFork(true, 'token', '', ''); 166 | expect(test).toBeFalsy(); 167 | }); 168 | 169 | test('return false on fork with deploy_key', async () => { 170 | const test = await skipOnFork(true, '', 'deploy_key', ''); 171 | expect(test).toBeFalsy(); 172 | }); 173 | 174 | test('return false on fork with personal_token', async () => { 175 | const test = await skipOnFork(true, '', '', 'personal_token'); 176 | expect(test).toBeFalsy(); 177 | }); 178 | 179 | test('return true on fork with no tokens', async () => { 180 | const test = await skipOnFork(true, '', '', ''); 181 | expect(test).toBeTruthy(); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/set-tokens.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as exec from '@actions/exec'; 3 | import * as github from '@actions/github'; 4 | import * as io from '@actions/io'; 5 | import path from 'path'; 6 | import fs from 'fs'; 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const cpSpawnSync = require('child_process').spawnSync; 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const cpexec = require('child_process').execFileSync; 11 | import {Inputs} from './interfaces'; 12 | import {getHomeDir} from './utils'; 13 | import {getServerUrl} from './git-utils'; 14 | 15 | export async function setSSHKey(inps: Inputs, publishRepo: string): Promise { 16 | core.info('[INFO] setup SSH deploy key'); 17 | 18 | const homeDir = await getHomeDir(); 19 | const sshDir = path.join(homeDir, '.ssh'); 20 | await io.mkdirP(sshDir); 21 | await exec.exec('chmod', ['700', sshDir]); 22 | 23 | const knownHosts = path.join(sshDir, 'known_hosts'); 24 | // ssh-keyscan -t rsa github.com or serverUrl >> ~/.ssh/known_hosts on Ubuntu 25 | const cmdSSHkeyscanOutput = `\ 26 | # ${getServerUrl().host}.com:22 SSH-2.0-babeld-1f0633a6 27 | ${ 28 | getServerUrl().host 29 | } ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= 30 | `; 31 | fs.writeFileSync(knownHosts, cmdSSHkeyscanOutput + '\n'); 32 | core.info(`[INFO] wrote ${knownHosts}`); 33 | await exec.exec('chmod', ['600', knownHosts]); 34 | 35 | const idRSA = path.join(sshDir, 'github'); 36 | fs.writeFileSync(idRSA, inps.DeployKey + '\n'); 37 | core.info(`[INFO] wrote ${idRSA}`); 38 | await exec.exec('chmod', ['600', idRSA]); 39 | 40 | const sshConfigPath = path.join(sshDir, 'config'); 41 | const sshConfigContent = `\ 42 | Host ${getServerUrl().host} 43 | HostName ${getServerUrl().host} 44 | IdentityFile ~/.ssh/github 45 | User git 46 | `; 47 | fs.writeFileSync(sshConfigPath, sshConfigContent + '\n'); 48 | core.info(`[INFO] wrote ${sshConfigPath}`); 49 | await exec.exec('chmod', ['600', sshConfigPath]); 50 | 51 | if (process.platform === 'win32') { 52 | core.warning(`\ 53 | Currently, the deploy_key option is not supported on the windows-latest. 54 | Watch https://github.com/peaceiris/actions-gh-pages/issues/87 55 | `); 56 | 57 | await cpSpawnSync('Start-Process', ['powershell.exe', '-Verb', 'runas']); 58 | await cpSpawnSync('sh', ['-c', '\'eval "$(ssh-agent)"\''], {shell: true}); 59 | await exec.exec('sc', ['config', 'ssh-agent', 'start=auto']); 60 | await exec.exec('sc', ['start', 'ssh-agent']); 61 | } 62 | await cpexec('ssh-agent', ['-a', '/tmp/ssh-auth.sock']); 63 | core.exportVariable('SSH_AUTH_SOCK', '/tmp/ssh-auth.sock'); 64 | await exec.exec('ssh-add', [idRSA]); 65 | 66 | return `git@${getServerUrl().host}:${publishRepo}.git`; 67 | } 68 | 69 | export function setGithubToken( 70 | githubToken: string, 71 | publishRepo: string, 72 | publishBranch: string, 73 | externalRepository: string, 74 | ref: string, 75 | eventName: string 76 | ): string { 77 | core.info('[INFO] setup GITHUB_TOKEN'); 78 | 79 | core.debug(`ref: ${ref}`); 80 | core.debug(`eventName: ${eventName}`); 81 | let isProhibitedBranch = false; 82 | 83 | if (externalRepository) { 84 | throw new Error(`\ 85 | The generated GITHUB_TOKEN (github_token) does not support to push to an external repository. 86 | Use deploy_key or personal_token. 87 | `); 88 | } 89 | 90 | if (eventName === 'push') { 91 | isProhibitedBranch = ref.match(new RegExp(`^refs/heads/${publishBranch}$`)) !== null; 92 | if (isProhibitedBranch) { 93 | throw new Error(`\ 94 | You deploy from ${publishBranch} to ${publishBranch} 95 | This operation is prohibited to protect your contents 96 | `); 97 | } 98 | } 99 | 100 | return `https://x-access-token:${githubToken}@${getServerUrl().host}/${publishRepo}.git`; 101 | } 102 | 103 | export function setPersonalToken(personalToken: string, publishRepo: string): string { 104 | core.info('[INFO] setup personal access token'); 105 | return `https://x-access-token:${personalToken}@${getServerUrl().host}/${publishRepo}.git`; 106 | } 107 | 108 | export function getPublishRepo(externalRepository: string, owner: string, repo: string): string { 109 | if (externalRepository) { 110 | return externalRepository; 111 | } 112 | return `${owner}/${repo}`; 113 | } 114 | 115 | export async function setTokens(inps: Inputs): Promise { 116 | try { 117 | const publishRepo = getPublishRepo( 118 | inps.ExternalRepository, 119 | github.context.repo.owner, 120 | github.context.repo.repo 121 | ); 122 | if (inps.DeployKey) { 123 | return setSSHKey(inps, publishRepo); 124 | } else if (inps.GithubToken) { 125 | const context = github.context; 126 | const ref = context.ref; 127 | const eventName = context.eventName; 128 | return setGithubToken( 129 | inps.GithubToken, 130 | publishRepo, 131 | inps.PublishBranch, 132 | inps.ExternalRepository, 133 | ref, 134 | eventName 135 | ); 136 | } else if (inps.PersonalToken) { 137 | return setPersonalToken(inps.PersonalToken, publishRepo); 138 | } else { 139 | throw new Error('not found deploy key or tokens'); 140 | } 141 | } catch (error) { 142 | if (error instanceof Error) { 143 | throw new Error(error.message); 144 | } else { 145 | throw new Error('unexpected error'); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '*.md' 9 | pull_request: 10 | paths-ignore: 11 | - '*.md' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: 22 | - 'ubuntu-22.04' 23 | - 'ubuntu-20.04' 24 | - 'ubuntu-latest' 25 | - 'macos-latest' 26 | - 'windows-latest' 27 | permissions: 28 | contents: write 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - uses: peaceiris/workflows/setup-node@v0.20.1 33 | with: 34 | node-version-file: ".nvmrc" 35 | 36 | - name: Dump version 37 | run: | 38 | node -v 39 | npm --version 40 | git --version 41 | 42 | - run: npm ci --ignore-scripts 43 | 44 | - name: npm audit 45 | if: startsWith(matrix.os, 'ubuntu-22.04') 46 | run: | 47 | npm audit > ./audit.log || true 48 | if ! [ "$(cat ./audit.log | wc -l)" = 1 ]; then 49 | echo "::warning::$(cat ./audit.log)" 50 | fi 51 | rm ./audit.log 52 | 53 | - name: Run prettier 54 | if: startsWith(matrix.os, 'ubuntu-22.04') 55 | run: npm run format:check 56 | 57 | - name: Run eslint 58 | if: startsWith(matrix.os, 'ubuntu-22.04') 59 | run: npm run lint 60 | 61 | - run: npm test 62 | 63 | - name: Upload test coverage as artifact 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: coverage-${{ matrix.os }} 67 | path: coverage 68 | 69 | - uses: codecov/codecov-action@v4 70 | 71 | - name: Run ncc 72 | run: npm run build 73 | 74 | - name: Remove lint-staged husky 75 | if: ${{ github.ref == 'refs/heads/main' }} 76 | run: | 77 | npm uninstall lint-staged husky 78 | git checkout package-lock.json package.json 79 | 80 | - name: Setup mdBook 81 | if: ${{ github.ref == 'refs/heads/main' }} 82 | uses: peaceiris/actions-mdbook@v1.2.0 83 | with: 84 | mdbook-version: '0.4.5' 85 | 86 | - name: Build site 87 | if: ${{ github.ref == 'refs/heads/main' }} 88 | working-directory: ./test_projects/mdbook 89 | run: mdbook build 90 | 91 | - name: Deploy 92 | if: | 93 | startsWith(matrix.os, 'ubuntu-latest') && 94 | github.ref == 'refs/heads/main' && github.event.repository.fork == false 95 | uses: ./ 96 | with: 97 | # deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 98 | github_token: ${{ secrets.GITHUB_TOKEN }} 99 | # publish_branch: gh-pages 100 | publish_dir: ./test_projects/mdbook/book 101 | # external_repository: '' 102 | allow_empty_commit: true 103 | # keep_files: true 104 | # force_orphan: true 105 | user_name: 'github-actions[bot]' 106 | user_email: 'github-actions[bot]@users.noreply.github.com' 107 | # commit_message: ${{ github.event.head_commit.message }} 108 | cname: 'actions-gh-pages.peaceiris.com' 109 | 110 | - name: Deploy 111 | if: | 112 | startsWith(matrix.os, 'macos') && 113 | github.ref == 'refs/heads/main' && github.event.repository.fork == false 114 | uses: ./ 115 | with: 116 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 117 | # github_token: ${{ secrets.GITHUB_TOKEN }} 118 | # personal_token: ${{ secrets.PERSONAL_TOKEN }} 119 | publish_branch: gh-pages-macos 120 | publish_dir: ./test_projects/mdbook/book 121 | # external_repository: '' 122 | allow_empty_commit: true 123 | # keep_files: true 124 | # force_orphan: true 125 | user_name: 'github-actions[bot]' 126 | user_email: 'github-actions[bot]@users.noreply.github.com' 127 | # commit_message: ${{ github.event.head_commit.message }} 128 | 129 | - name: Deploy 130 | if: | 131 | startsWith(matrix.os, 'windows') && 132 | github.ref == 'refs/heads/main' && github.event.repository.fork == false 133 | uses: ./ 134 | with: 135 | # deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 136 | github_token: ${{ secrets.GITHUB_TOKEN }} 137 | # personal_token: ${{ secrets.PERSONAL_TOKEN }} 138 | publish_branch: gh-pages-windows 139 | publish_dir: ./test_projects/mdbook/book 140 | # external_repository: '' 141 | allow_empty_commit: true 142 | # keep_files: true 143 | # force_orphan: true 144 | user_name: 'github-actions[bot]' 145 | user_email: 'github-actions[bot]@users.noreply.github.com' 146 | # commit_message: ${{ github.event.head_commit.message }} 147 | 148 | - name: Deploy 149 | if: | 150 | startsWith(matrix.os, 'ubuntu-20.04') && 151 | github.ref == 'refs/heads/main' && github.event.repository.fork == false 152 | uses: ./ 153 | with: 154 | # deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 155 | github_token: ${{ secrets.GITHUB_TOKEN }} 156 | publish_branch: gh-pages-ubuntu-20.04 157 | publish_dir: ./test_projects/mdbook/book 158 | # external_repository: '' 159 | allow_empty_commit: true 160 | # keep_files: true 161 | # force_orphan: true 162 | user_name: 'github-actions[bot]' 163 | user_email: 'github-actions[bot]@users.noreply.github.com' 164 | # commit_message: ${{ github.event.head_commit.message }} 165 | 166 | - name: Deploy 167 | if: | 168 | startsWith(matrix.os, 'ubuntu-22.04') && 169 | github.ref == 'refs/heads/main' && github.event.repository.fork == false 170 | uses: ./ 171 | with: 172 | # deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 173 | github_token: ${{ secrets.GITHUB_TOKEN }} 174 | publish_branch: gh-pages-ubuntu-22.04 175 | publish_dir: ./test_projects/mdbook/book 176 | # external_repository: '' 177 | allow_empty_commit: true 178 | # keep_files: true 179 | # force_orphan: true 180 | user_name: 'github-actions[bot]' 181 | user_email: 'github-actions[bot]@users.noreply.github.com' 182 | # commit_message: ${{ github.event.head_commit.message }} 183 | -------------------------------------------------------------------------------- /src/git-utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as exec from '@actions/exec'; 3 | import * as glob from '@actions/glob'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | import {URL} from 'url'; 7 | import {Inputs, CmdResult} from './interfaces'; 8 | import {createDir} from './utils'; 9 | import {cp, rm} from 'shelljs'; 10 | 11 | export async function createBranchForce(branch: string): Promise { 12 | await exec.exec('git', ['init']); 13 | await exec.exec('git', ['checkout', '--orphan', branch]); 14 | return; 15 | } 16 | 17 | export function getServerUrl(): URL { 18 | return new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); 19 | } 20 | 21 | export async function deleteExcludedAssets(destDir: string, excludeAssets: string): Promise { 22 | if (excludeAssets === '') return; 23 | core.info(`[INFO] delete excluded assets`); 24 | const excludedAssetNames: Array = excludeAssets.split(','); 25 | const excludedAssetPaths = ((): Array => { 26 | const paths: Array = []; 27 | for (const pattern of excludedAssetNames) { 28 | paths.push(path.join(destDir, pattern)); 29 | } 30 | return paths; 31 | })(); 32 | const globber = await glob.create(excludedAssetPaths.join('\n')); 33 | const files = await globber.glob(); 34 | for await (const file of globber.globGenerator()) { 35 | core.info(`[INFO] delete ${file}`); 36 | } 37 | rm('-rf', files); 38 | return; 39 | } 40 | 41 | export async function copyAssets( 42 | publishDir: string, 43 | destDir: string, 44 | excludeAssets: string 45 | ): Promise { 46 | core.info(`[INFO] prepare publishing assets`); 47 | 48 | if (!fs.existsSync(destDir)) { 49 | core.info(`[INFO] create ${destDir}`); 50 | await createDir(destDir); 51 | } 52 | 53 | const dotGitPath = path.join(publishDir, '.git'); 54 | if (fs.existsSync(dotGitPath)) { 55 | core.info(`[INFO] delete ${dotGitPath}`); 56 | rm('-rf', dotGitPath); 57 | } 58 | 59 | core.info(`[INFO] copy ${publishDir} to ${destDir}`); 60 | cp('-RfL', [`${publishDir}/*`, `${publishDir}/.*`], destDir); 61 | 62 | await deleteExcludedAssets(destDir, excludeAssets); 63 | 64 | return; 65 | } 66 | 67 | export async function setRepo(inps: Inputs, remoteURL: string, workDir: string): Promise { 68 | const publishDir = path.isAbsolute(inps.PublishDir) 69 | ? inps.PublishDir 70 | : path.join(`${process.env.GITHUB_WORKSPACE}`, inps.PublishDir); 71 | 72 | if (path.isAbsolute(inps.DestinationDir)) { 73 | throw new Error('destination_dir should be a relative path'); 74 | } 75 | const destDir = ((): string => { 76 | if (inps.DestinationDir === '') { 77 | return workDir; 78 | } else { 79 | return path.join(workDir, inps.DestinationDir); 80 | } 81 | })(); 82 | 83 | core.info(`[INFO] ForceOrphan: ${inps.ForceOrphan}`); 84 | if (inps.ForceOrphan) { 85 | await createDir(destDir); 86 | core.info(`[INFO] chdir ${workDir}`); 87 | process.chdir(workDir); 88 | await createBranchForce(inps.PublishBranch); 89 | await copyAssets(publishDir, destDir, inps.ExcludeAssets); 90 | return; 91 | } 92 | 93 | const result: CmdResult = { 94 | exitcode: 0, 95 | output: '' 96 | }; 97 | const options = { 98 | listeners: { 99 | stdout: (data: Buffer): void => { 100 | result.output += data.toString(); 101 | } 102 | } 103 | }; 104 | 105 | try { 106 | result.exitcode = await exec.exec( 107 | 'git', 108 | ['clone', '--depth=1', '--single-branch', '--branch', inps.PublishBranch, remoteURL, workDir], 109 | options 110 | ); 111 | if (result.exitcode === 0) { 112 | await createDir(destDir); 113 | 114 | if (inps.KeepFiles) { 115 | core.info('[INFO] Keep existing files'); 116 | } else { 117 | core.info(`[INFO] clean up ${destDir}`); 118 | core.info(`[INFO] chdir ${destDir}`); 119 | process.chdir(destDir); 120 | await exec.exec('git', ['rm', '-r', '--ignore-unmatch', '*']); 121 | } 122 | 123 | core.info(`[INFO] chdir ${workDir}`); 124 | process.chdir(workDir); 125 | await copyAssets(publishDir, destDir, inps.ExcludeAssets); 126 | return; 127 | } else { 128 | throw new Error(`Failed to clone remote branch ${inps.PublishBranch}`); 129 | } 130 | } catch (error) { 131 | if (error instanceof Error) { 132 | core.info(`[INFO] first deployment, create new branch ${inps.PublishBranch}`); 133 | core.info(`[INFO] ${error.message}`); 134 | await createDir(destDir); 135 | core.info(`[INFO] chdir ${workDir}`); 136 | process.chdir(workDir); 137 | await createBranchForce(inps.PublishBranch); 138 | await copyAssets(publishDir, destDir, inps.ExcludeAssets); 139 | return; 140 | } else { 141 | throw new Error('unexpected error'); 142 | } 143 | } 144 | } 145 | 146 | export function getUserName(userName: string): string { 147 | if (userName) { 148 | return userName; 149 | } else { 150 | return `${process.env.GITHUB_ACTOR}`; 151 | } 152 | } 153 | 154 | export function getUserEmail(userEmail: string): string { 155 | if (userEmail) { 156 | return userEmail; 157 | } else { 158 | return `${process.env.GITHUB_ACTOR}@users.noreply.github.com`; 159 | } 160 | } 161 | 162 | export async function setCommitAuthor(userName: string, userEmail: string): Promise { 163 | if (userName && !userEmail) { 164 | throw new Error('user_email is undefined'); 165 | } 166 | if (!userName && userEmail) { 167 | throw new Error('user_name is undefined'); 168 | } 169 | await exec.exec('git', ['config', 'user.name', getUserName(userName)]); 170 | await exec.exec('git', ['config', 'user.email', getUserEmail(userEmail)]); 171 | } 172 | 173 | export function getCommitMessage( 174 | msg: string, 175 | fullMsg: string, 176 | extRepo: string, 177 | baseRepo: string, 178 | hash: string 179 | ): string { 180 | const msgHash = ((): string => { 181 | if (extRepo) { 182 | return `${baseRepo}@${hash}`; 183 | } else { 184 | return hash; 185 | } 186 | })(); 187 | 188 | const subject = ((): string => { 189 | if (fullMsg) { 190 | return fullMsg; 191 | } else if (msg) { 192 | return `${msg} ${msgHash}`; 193 | } else { 194 | return `deploy: ${msgHash}`; 195 | } 196 | })(); 197 | 198 | return subject; 199 | } 200 | 201 | export async function commit(allowEmptyCommit: boolean, msg: string): Promise { 202 | try { 203 | if (allowEmptyCommit) { 204 | await exec.exec('git', ['commit', '--allow-empty', '-m', `${msg}`]); 205 | } else { 206 | await exec.exec('git', ['commit', '-m', `${msg}`]); 207 | } 208 | } catch (error) { 209 | if (error instanceof Error) { 210 | core.info('[INFO] skip commit'); 211 | core.debug(`[INFO] skip commit ${error.message}`); 212 | } else { 213 | throw new Error('unexpected error'); 214 | } 215 | } 216 | } 217 | 218 | export async function push(branch: string, forceOrphan: boolean): Promise { 219 | if (forceOrphan) { 220 | await exec.exec('git', ['push', 'origin', '--force', branch]); 221 | } else { 222 | await exec.exec('git', ['push', 'origin', branch]); 223 | } 224 | } 225 | 226 | export async function pushTag(tagName: string, tagMessage: string): Promise { 227 | if (tagName === '') { 228 | return; 229 | } 230 | 231 | let msg = ''; 232 | if (tagMessage) { 233 | msg = tagMessage; 234 | } else { 235 | msg = `Deployment ${tagName}`; 236 | } 237 | 238 | await exec.exec('git', ['tag', '-a', `${tagName}`, '-m', `${msg}`]); 239 | await exec.exec('git', ['push', 'origin', `${tagName}`]); 240 | } 241 | -------------------------------------------------------------------------------- /__tests__/get-inputs.test.ts: -------------------------------------------------------------------------------- 1 | // import * as main from '../src/main'; 2 | import {Inputs} from '../src/interfaces'; 3 | import {showInputs, getInputs} from '../src/get-inputs'; 4 | import os from 'os'; 5 | import fs from 'fs'; 6 | import yaml from 'js-yaml'; 7 | 8 | beforeEach(() => { 9 | jest.resetModules(); 10 | process.stdout.write = jest.fn(); 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | const doc: any = yaml.load(fs.readFileSync(__dirname + '/../action.yml', 'utf8')); 14 | Object.keys(doc.inputs).forEach(name => { 15 | const envVar = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`; 16 | process.env[envVar] = doc.inputs[name]['default']; 17 | }); 18 | }); 19 | 20 | afterEach(() => { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | const doc: any = yaml.load(fs.readFileSync(__dirname + '/../action.yml', 'utf8')); 23 | Object.keys(doc.inputs).forEach(name => { 24 | const envVar = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`; 25 | console.debug(`delete ${envVar}\t${process.env[envVar]}`); 26 | delete process.env[envVar]; 27 | }); 28 | }); 29 | 30 | // Assert that process.stdout.write calls called only with the given arguments. 31 | // cf. https://github.com/actions/toolkit/blob/8b0300129f08728419263b016de8630f1d426d5f/packages/core/__tests__/core.test.ts 32 | function assertWriteCalls(calls: string[]): void { 33 | expect(process.stdout.write).toHaveBeenCalledTimes(calls.length); 34 | 35 | for (let i = 0; i < calls.length; i++) { 36 | expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i]); 37 | } 38 | } 39 | 40 | function getInputsLog(authMethod: string, inps: Inputs): string { 41 | return `\ 42 | [INFO] ${authMethod}: true 43 | [INFO] PublishBranch: ${inps.PublishBranch} 44 | [INFO] PublishDir: ${inps.PublishDir} 45 | [INFO] DestinationDir: ${inps.DestinationDir} 46 | [INFO] ExternalRepository: ${inps.ExternalRepository} 47 | [INFO] AllowEmptyCommit: ${inps.AllowEmptyCommit} 48 | [INFO] KeepFiles: ${inps.KeepFiles} 49 | [INFO] ForceOrphan: ${inps.ForceOrphan} 50 | [INFO] UserName: ${inps.UserName} 51 | [INFO] UserEmail: ${inps.UserEmail} 52 | [INFO] CommitMessage: ${inps.CommitMessage} 53 | [INFO] FullCommitMessage: ${inps.FullCommitMessage} 54 | [INFO] TagName: ${inps.TagName} 55 | [INFO] TagMessage: ${inps.TagMessage} 56 | [INFO] EnableJekyll (DisableNoJekyll): ${inps.DisableNoJekyll} 57 | [INFO] CNAME: ${inps.CNAME} 58 | [INFO] ExcludeAssets ${inps.ExcludeAssets} 59 | `; 60 | } 61 | 62 | describe('showInputs()', () => { 63 | // eslint-disable-next-line jest/expect-expect 64 | test('print all inputs DeployKey', () => { 65 | process.env['INPUT_DEPLOY_KEY'] = 'test_deploy_key'; 66 | 67 | const inps: Inputs = getInputs(); 68 | showInputs(inps); 69 | 70 | const authMethod = 'DeployKey'; 71 | const test = getInputsLog(authMethod, inps); 72 | assertWriteCalls([`${test}${os.EOL}`]); 73 | }); 74 | 75 | // eslint-disable-next-line jest/expect-expect 76 | test('print all inputs GithubToken', () => { 77 | delete process.env['INPUT_DEPLOY_KEY']; 78 | process.env['INPUT_GITHUB_TOKEN'] = 'test_github_token'; 79 | 80 | const inps: Inputs = getInputs(); 81 | showInputs(inps); 82 | 83 | const authMethod = 'GithubToken'; 84 | const test = getInputsLog(authMethod, inps); 85 | assertWriteCalls([`${test}${os.EOL}`]); 86 | }); 87 | 88 | // eslint-disable-next-line jest/expect-expect 89 | test('print all inputs PersonalToken', () => { 90 | delete process.env['INPUT_DEPLOY_KEY']; 91 | delete process.env['INPUT_GITHUB_TOKEN']; 92 | process.env['INPUT_PERSONAL_TOKEN'] = 'test_personal_token'; 93 | 94 | const inps: Inputs = getInputs(); 95 | showInputs(inps); 96 | 97 | const authMethod = 'PersonalToken'; 98 | const test = getInputsLog(authMethod, inps); 99 | assertWriteCalls([`${test}${os.EOL}`]); 100 | }); 101 | }); 102 | 103 | describe('getInputs()', () => { 104 | test('get default inputs', () => { 105 | process.env['INPUT_DEPLOY_KEY'] = 'test_deploy_key'; 106 | 107 | const inps: Inputs = getInputs(); 108 | 109 | expect(inps.DeployKey).toMatch('test_deploy_key'); 110 | expect(inps.GithubToken).toMatch(''); 111 | expect(inps.PersonalToken).toMatch(''); 112 | expect(inps.PublishBranch).toMatch('gh-pages'); 113 | expect(inps.PublishDir).toMatch('public'); 114 | expect(inps.DestinationDir).toMatch(''); 115 | expect(inps.ExternalRepository).toMatch(''); 116 | expect(inps.AllowEmptyCommit).toBe(false); 117 | expect(inps.KeepFiles).toBe(false); 118 | expect(inps.ForceOrphan).toBe(false); 119 | expect(inps.UserName).toMatch(''); 120 | expect(inps.UserEmail).toMatch(''); 121 | expect(inps.CommitMessage).toMatch(''); 122 | expect(inps.FullCommitMessage).toMatch(''); 123 | expect(inps.TagName).toMatch(''); 124 | expect(inps.TagMessage).toMatch(''); 125 | expect(inps.DisableNoJekyll).toBe(false); 126 | expect(inps.CNAME).toMatch(''); 127 | expect(inps.ExcludeAssets).toMatch('.github'); 128 | }); 129 | 130 | test('get spec inputs', () => { 131 | // process.env['INPUT_DEPLOY_KEY'] = 'test_deploy_key'; 132 | process.env['INPUT_GITHUB_TOKEN'] = 'test_github_token'; 133 | process.env['INPUT_PERSONAL_TOKEN'] = 'test_personal_token'; 134 | process.env['INPUT_PUBLISH_BRANCH'] = 'master'; 135 | process.env['INPUT_PUBLISH_DIR'] = 'out'; 136 | process.env['INPUT_DESTINATION_DIR'] = 'subdir'; 137 | process.env['INPUT_EXTERNAL_REPOSITORY'] = 'user/repo'; 138 | process.env['INPUT_ALLOW_EMPTY_COMMIT'] = 'true'; 139 | process.env['INPUT_KEEP_FILES'] = 'true'; 140 | process.env['INPUT_FORCE_ORPHAN'] = 'true'; 141 | process.env['INPUT_USER_NAME'] = 'username'; 142 | process.env['INPUT_USER_EMAIL'] = 'github@github.com'; 143 | process.env['INPUT_COMMIT_MESSAGE'] = 'feat: Add new feature'; 144 | process.env['INPUT_FULL_COMMIT_MESSAGE'] = 'feat: Add new feature'; 145 | process.env['INPUT_TAG_NAME'] = 'deploy-v1.2.3'; 146 | process.env['INPUT_TAG_MESSAGE'] = 'Deployment v1.2.3'; 147 | process.env['INPUT_DISABLE_NOJEKYLL'] = 'true'; 148 | process.env['INPUT_CNAME'] = 'github.com'; 149 | process.env['INPUT_EXCLUDE_ASSETS'] = '.github'; 150 | 151 | const inps: Inputs = getInputs(); 152 | 153 | expect(inps.DeployKey).toMatch(''); 154 | expect(inps.GithubToken).toMatch('test_github_token'); 155 | expect(inps.PersonalToken).toMatch('test_personal_token'); 156 | expect(inps.PublishBranch).toMatch('master'); 157 | expect(inps.PublishDir).toMatch('out'); 158 | expect(inps.DestinationDir).toMatch('subdir'); 159 | expect(inps.ExternalRepository).toMatch('user/repo'); 160 | expect(inps.AllowEmptyCommit).toBe(true); 161 | expect(inps.KeepFiles).toBe(true); 162 | expect(inps.ForceOrphan).toBe(true); 163 | expect(inps.UserName).toMatch('username'); 164 | expect(inps.UserEmail).toMatch('github@github.com'); 165 | expect(inps.CommitMessage).toMatch('feat: Add new feature'); 166 | expect(inps.FullCommitMessage).toMatch('feat: Add new feature'); 167 | expect(inps.TagName).toMatch('deploy-v1.2.3'); 168 | expect(inps.TagMessage).toMatch('Deployment v1.2.3'); 169 | expect(inps.DisableNoJekyll).toBe(true); 170 | expect(inps.CNAME).toMatch('github.com'); 171 | expect(inps.ExcludeAssets).toMatch('.github'); 172 | }); 173 | 174 | test('get spec inputs enable_jekyll', () => { 175 | process.env['INPUT_ENABLE_JEKYLL'] = 'true'; 176 | const inps: Inputs = getInputs(); 177 | expect(inps.DisableNoJekyll).toBe(true); 178 | }); 179 | 180 | test('throw error enable_jekyll or disable_nojekyll', () => { 181 | process.env['INPUT_DEPLOY_KEY'] = 'test_deploy_key'; 182 | process.env['INPUT_ENABLE_JEKYLL'] = 'true'; 183 | process.env['INPUT_DISABLE_NOJEKYLL'] = 'true'; 184 | 185 | expect(() => { 186 | getInputs(); 187 | }).toThrow('Use either of enable_jekyll or disable_nojekyll'); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /__tests__/git-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | copyAssets, 3 | setRepo, 4 | getUserName, 5 | getUserEmail, 6 | setCommitAuthor, 7 | getCommitMessage 8 | } from '../src/git-utils'; 9 | import {getInputs} from '../src/get-inputs'; 10 | import {Inputs} from '../src/interfaces'; 11 | import {getWorkDirName, createDir} from '../src/utils'; 12 | import {CmdResult} from '../src/interfaces'; 13 | import * as exec from '@actions/exec'; 14 | import {cp, rm} from 'shelljs'; 15 | import path from 'path'; 16 | import fs from 'fs'; 17 | 18 | const testRoot = path.resolve(__dirname); 19 | 20 | async function createTestDir(name: string): Promise { 21 | const date = new Date(); 22 | const unixTime = date.getTime(); 23 | return await getWorkDirName(`${unixTime}_${name}`); 24 | } 25 | 26 | beforeEach(() => { 27 | jest.resetModules(); 28 | process.env['GITHUB_ACTOR'] = 'default-octocat'; 29 | process.env['GITHUB_REPOSITORY'] = 'owner/repo'; 30 | }); 31 | 32 | afterEach(() => { 33 | delete process.env['GITHUB_ACTOR']; 34 | delete process.env['GITHUB_REPOSITORY']; 35 | }); 36 | 37 | describe('copyAssets', () => { 38 | let gitTempDir = ''; 39 | (async (): Promise => { 40 | const date = new Date(); 41 | const unixTime = date.getTime(); 42 | gitTempDir = await getWorkDirName(`${unixTime}_git`); 43 | })(); 44 | 45 | beforeAll(async () => { 46 | await createDir(gitTempDir); 47 | process.chdir(gitTempDir); 48 | await exec.exec('git', ['init']); 49 | }); 50 | 51 | test('copy assets from publish_dir to root, delete .github', async () => { 52 | const publishDir = await createTestDir('src'); 53 | const destDir = await createTestDir('dst'); 54 | cp('-Rf', path.resolve(testRoot, 'fixtures/publish_dir_1'), publishDir); 55 | cp('-Rf', gitTempDir, destDir); 56 | 57 | await copyAssets(publishDir, destDir, '.github'); 58 | expect(fs.existsSync(path.resolve(destDir, '.github'))).toBeFalsy(); 59 | expect(fs.existsSync(path.resolve(destDir, 'index.html'))).toBeTruthy(); 60 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.css'))).toBeTruthy(); 61 | rm('-rf', publishDir, destDir); 62 | }); 63 | 64 | test('copy assets from publish_dir to root, delete .github,main.js', async () => { 65 | const publishDir = await createTestDir('src'); 66 | const destDir = await createTestDir('dst'); 67 | cp('-Rf', path.resolve(testRoot, 'fixtures/publish_dir_1'), publishDir); 68 | cp('-Rf', gitTempDir, destDir); 69 | 70 | await copyAssets(publishDir, destDir, '.github,main.js'); 71 | expect(fs.existsSync(path.resolve(destDir, '.github'))).toBeFalsy(); 72 | expect(fs.existsSync(path.resolve(destDir, 'index.html'))).toBeTruthy(); 73 | expect(fs.existsSync(path.resolve(destDir, 'main.js'))).toBeFalsy(); 74 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.css'))).toBeTruthy(); 75 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.js'))).toBeTruthy(); 76 | rm('-rf', publishDir, destDir); 77 | }); 78 | 79 | test('copy assets from publish_dir to root, delete nothing', async () => { 80 | const publishDir = await createTestDir('src'); 81 | const destDir = await createTestDir('dst'); 82 | cp('-Rf', path.resolve(testRoot, 'fixtures/publish_dir_root'), publishDir); 83 | cp('-Rf', gitTempDir, destDir); 84 | 85 | await copyAssets(publishDir, destDir, ''); 86 | expect(fs.existsSync(path.resolve(destDir, '.github'))).toBeTruthy(); 87 | expect(fs.existsSync(path.resolve(destDir, 'index.html'))).toBeTruthy(); 88 | expect(fs.existsSync(path.resolve(destDir, 'main.js'))).toBeTruthy(); 89 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.css'))).toBeTruthy(); 90 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.js'))).toBeTruthy(); 91 | rm('-rf', publishDir, destDir); 92 | }); 93 | 94 | test('copy assets from root to root, delete .github', async () => { 95 | const publishDir = await createTestDir('src'); 96 | const destDir = await createTestDir('dst'); 97 | cp('-Rf', path.resolve(testRoot, 'fixtures/publish_dir_root'), publishDir); 98 | cp('-Rf', gitTempDir, destDir); 99 | cp('-Rf', gitTempDir, publishDir); 100 | 101 | await copyAssets(publishDir, destDir, '.github'); 102 | expect(fs.existsSync(path.resolve(destDir, '.github'))).toBeFalsy(); 103 | expect(fs.existsSync(path.resolve(destDir, 'index.html'))).toBeTruthy(); 104 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.css'))).toBeTruthy(); 105 | rm('-rf', publishDir, destDir); 106 | }); 107 | 108 | test('copy assets from root to root, delete nothing', async () => { 109 | const publishDir = await createTestDir('src'); 110 | const destDir = await createTestDir('dst'); 111 | cp('-Rf', path.resolve(testRoot, 'fixtures/publish_dir_root'), publishDir); 112 | cp('-Rf', gitTempDir, destDir); 113 | cp('-Rf', gitTempDir, publishDir); 114 | 115 | await copyAssets(publishDir, destDir, ''); 116 | expect(fs.existsSync(path.resolve(destDir, '.github'))).toBeTruthy(); 117 | expect(fs.existsSync(path.resolve(destDir, 'index.html'))).toBeTruthy(); 118 | expect(fs.existsSync(path.resolve(destDir, 'assets/lib.css'))).toBeTruthy(); 119 | rm('-rf', publishDir, destDir); 120 | }); 121 | 122 | test.todo('copy assets from root to subdir, delete .github'); 123 | test.todo('copy assets from root to subdir, delete .github,main.js'); 124 | test.todo('copy assets from root to subdir, delete nothing'); 125 | }); 126 | 127 | describe('setRepo()', () => { 128 | test('throw error destination_dir should be a relative path', async () => { 129 | process.env['INPUT_GITHUB_TOKEN'] = 'test_github_token'; 130 | process.env['INPUT_PUBLISH_BRANCH'] = 'gh-pages'; 131 | process.env['INPUT_PUBLISH_DIR'] = 'public'; 132 | process.env['INPUT_DESTINATION_DIR'] = '/subdir'; 133 | // process.env['INPUT_EXTERNAL_REPOSITORY'] = 'user/repo'; 134 | // process.env['INPUT_ALLOW_EMPTY_COMMIT'] = 'true'; 135 | // process.env['INPUT_KEEP_FILES'] = 'true'; 136 | // process.env['INPUT_FORCE_ORPHAN'] = 'true'; 137 | // process.env['INPUT_USER_NAME'] = 'username'; 138 | // process.env['INPUT_USER_EMAIL'] = 'github@github.com'; 139 | // process.env['INPUT_COMMIT_MESSAGE'] = 'feat: Add new feature'; 140 | // process.env['INPUT_FULL_COMMIT_MESSAGE'] = 'feat: Add new feature'; 141 | // process.env['INPUT_TAG_NAME'] = 'deploy-v1.2.3'; 142 | // process.env['INPUT_TAG_MESSAGE'] = 'Deployment v1.2.3'; 143 | // process.env['INPUT_DISABLE_NOJEKYLL'] = 'true'; 144 | // process.env['INPUT_CNAME'] = 'github.com'; 145 | process.env['INPUT_EXCLUDE_ASSETS'] = '.github'; 146 | const inps: Inputs = getInputs(); 147 | const remoteURL = 'https://x-access-token:pat@github.com/actions/pages.git'; 148 | const date = new Date(); 149 | const unixTime = date.getTime(); 150 | const workDir = await getWorkDirName(`${unixTime}`); 151 | await expect(setRepo(inps, remoteURL, workDir)).rejects.toThrow( 152 | 'destination_dir should be a relative path' 153 | ); 154 | }); 155 | }); 156 | 157 | describe('getUserName()', () => { 158 | test('get default git user name', () => { 159 | const userName = ''; 160 | const test = getUserName(userName); 161 | expect(test).toMatch('default-octocat'); 162 | }); 163 | 164 | test('get custom git user name', () => { 165 | const userName = 'custom-octocat'; 166 | const test = getUserName(userName); 167 | expect(test).toMatch(userName); 168 | }); 169 | }); 170 | 171 | describe('getUserEmail()', () => { 172 | test('get default git user email', () => { 173 | const userEmail = ''; 174 | const test = getUserEmail(userEmail); 175 | expect(test).toMatch('default-octocat@users.noreply.github.com'); 176 | }); 177 | 178 | test('get custom git user email', () => { 179 | const userEmail = 'custom-octocat@github.com'; 180 | const test = getUserEmail(userEmail); 181 | expect(test).toMatch(userEmail); 182 | }); 183 | }); 184 | 185 | describe('setCommitAuthor()', () => { 186 | let workDirName = ''; 187 | (async (): Promise => { 188 | const date = new Date(); 189 | const unixTime = date.getTime(); 190 | workDirName = await getWorkDirName(`${unixTime}`); 191 | })(); 192 | 193 | beforeEach(async () => { 194 | await createDir(workDirName); 195 | process.chdir(workDirName); 196 | await exec.exec('git', ['init']); 197 | }); 198 | 199 | test('get default commit author', async () => { 200 | const userName = ''; 201 | const userEmail = ''; 202 | const result: CmdResult = { 203 | exitcode: 0, 204 | output: '' 205 | }; 206 | const options = { 207 | listeners: { 208 | stdout: (data: Buffer): void => { 209 | result.output += data.toString(); 210 | } 211 | } 212 | }; 213 | await setCommitAuthor(userName, userEmail); 214 | result.exitcode = await exec.exec('git', ['config', 'user.name'], options); 215 | expect(result.output).toMatch('default-octocat'); 216 | result.exitcode = await exec.exec('git', ['config', 'user.email'], options); 217 | expect(result.output).toMatch('default-octocat@users.noreply.github.com'); 218 | }); 219 | 220 | test('get custom commit author', async () => { 221 | const userName = 'custom-octocat'; 222 | const userEmail = 'custom-octocat@github.com'; 223 | const result: CmdResult = { 224 | exitcode: 0, 225 | output: '' 226 | }; 227 | const options = { 228 | listeners: { 229 | stdout: (data: Buffer): void => { 230 | result.output += data.toString(); 231 | } 232 | } 233 | }; 234 | await setCommitAuthor(userName, userEmail); 235 | result.exitcode = await exec.exec('git', ['config', 'user.name'], options); 236 | expect(result.output).toMatch(userName); 237 | result.exitcode = await exec.exec('git', ['config', 'user.email'], options); 238 | expect(result.output).toMatch(userEmail); 239 | }); 240 | 241 | test('throw error user_email is undefined', async () => { 242 | const userName = 'custom-octocat'; 243 | const userEmail = ''; 244 | await expect(setCommitAuthor(userName, userEmail)).rejects.toThrow('user_email is undefined'); 245 | }); 246 | 247 | test('throw error user_name is undefined', async () => { 248 | const userName = ''; 249 | const userEmail = 'custom-octocat@github.com'; 250 | await expect(setCommitAuthor(userName, userEmail)).rejects.toThrow('user_name is undefined'); 251 | }); 252 | }); 253 | 254 | describe('getCommitMessage()', () => { 255 | test('get default message', () => { 256 | const test = getCommitMessage('', '', '', 'actions/pages', 'commit_hash'); 257 | expect(test).toMatch('deploy: commit_hash'); 258 | }); 259 | 260 | test('get default message for external repository', () => { 261 | const test = getCommitMessage( 262 | '', 263 | '', 264 | 'actions/actions.github.io', 265 | 'actions/pages', 266 | 'commit_hash' 267 | ); 268 | expect(test).toMatch('deploy: actions/pages@commit_hash'); 269 | }); 270 | 271 | test('get custom message', () => { 272 | const test = getCommitMessage('Custom msg', '', '', 'actions/pages', 'commit_hash'); 273 | expect(test).toMatch('Custom msg commit_hash'); 274 | }); 275 | 276 | test('get custom message for external repository', () => { 277 | const test = getCommitMessage( 278 | 'Custom msg', 279 | '', 280 | 'actions/actions.github.io', 281 | 'actions/pages', 282 | 'commit_hash' 283 | ); 284 | expect(test).toMatch('Custom msg actions/pages@commit_hash'); 285 | }); 286 | 287 | test('get full custom message', () => { 288 | const test = getCommitMessage('', 'Full custom msg', '', 'actions/pages', 'commit_hash'); 289 | expect(test).toMatch('Full custom msg'); 290 | }); 291 | 292 | test('get full custom message for external repository', () => { 293 | const test = getCommitMessage( 294 | '', 295 | 'Full custom msg', 296 | 'actions/actions.github.io', 297 | 'actions/pages', 298 | 'commit_hash' 299 | ); 300 | expect(test).toMatch('Full custom msg'); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /images/ogp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | GitHub Pages Action 3 |

4 | 5 |
6 | GitHub Actions for deploying to GitHub Pages with Static Site Generators 7 | 8 | [![license](https://img.shields.io/github/license/peaceiris/actions-gh-pages.svg)](https://github.com/peaceiris/actions-gh-pages/blob/main/LICENSE) 9 | [![release](https://img.shields.io/github/release/peaceiris/actions-gh-pages.svg)](https://github.com/peaceiris/actions-gh-pages/releases/latest) 10 | [![GitHub release date](https://img.shields.io/github/release-date/peaceiris/actions-gh-pages.svg)](https://github.com/peaceiris/actions-gh-pages/releases) 11 | ![Test](https://github.com/peaceiris/actions-gh-pages/workflows/Test/badge.svg?branch=main&event=push) 12 | ![Code Scanning](https://github.com/peaceiris/actions-gh-pages/workflows/Code%20Scanning/badge.svg?event=push) 13 | [![CodeFactor](https://www.codefactor.io/repository/github/peaceiris/actions-gh-pages/badge)](https://www.codefactor.io/repository/github/peaceiris/actions-gh-pages) 14 | 15 |
16 | 17 | > [!NOTE] 18 | > 19 | > See also the GitHub official GitHub Pages Action first. 20 | > 21 | > - [GitHub Pages now uses Actions by default | The GitHub Blog](https://github.blog/2022-08-10-github-pages-now-uses-actions-by-default/) 22 | > - [GitHub Pages: Custom GitHub Actions Workflows (beta) | GitHub Changelog](https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/) 23 | 24 | This is a **GitHub Action** to deploy your static files to **GitHub Pages**. 25 | This deploy action can be combined simply and freely with [Static Site Generators]. (Hugo, MkDocs, Gatsby, mdBook, Next, Nuxt, and so on.) 26 | 27 | [Static Site Generators]: https://jamstack.org/generators/ 28 | 29 | The next example step will deploy `./public` directory to the remote `gh-pages` branch. 30 | 31 | ```yaml 32 | - name: Deploy 33 | uses: peaceiris/actions-gh-pages@v4 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | publish_dir: ./public 37 | ``` 38 | 39 | For newbies of GitHub Actions: 40 | Note that the `GITHUB_TOKEN` is **NOT** a personal access token. 41 | A GitHub Actions runner automatically creates a `GITHUB_TOKEN` secret to authenticate in your workflow. 42 | So, you can start to deploy immediately without any configuration. 43 | 44 | 45 | 46 | ## Supported Tokens 47 | 48 | Three tokens are supported. 49 | 50 | | Token | Private repo | Public repo | Protocol | Setup | 51 | |---|:---:|:---:|---|---| 52 | | `github_token` | ✅️ | ✅️ | HTTPS | Unnecessary | 53 | | `deploy_key` | ✅️ | ✅️ | SSH | Necessary | 54 | | `personal_token` | ✅️ | ✅️ | HTTPS | Necessary | 55 | 56 | Notes: Actually, the `GITHUB_TOKEN` works for deploying to GitHub Pages but it has still some limitations. 57 | For the first deployment, we need to select the `gh-pages` branch or another branch on the repository settings tab. 58 | See [First Deployment with `GITHUB_TOKEN`](#%EF%B8%8F-first-deployment-with-github_token) 59 | 60 | 61 | 62 | ## Supported Platforms 63 | 64 | All Actions runners: Linux (Ubuntu), macOS, and Windows are supported. 65 | 66 | | runs-on | `github_token` | `deploy_key` | `personal_token` | 67 | |---|:---:|:---:|:---:| 68 | | ubuntu-22.04 | ✅️ | ✅️ | ✅️ | 69 | | ubuntu-20.04 | ✅️ | ✅️ | ✅️ | 70 | | ubuntu-latest | ✅️ | ✅️ | ✅️ | 71 | | macos-latest | ✅️ | ✅️ | ✅️ | 72 | | windows-latest | ✅️ | (2) | ✅️ | 73 | 74 | 2. WIP, See [Issue #87](https://github.com/peaceiris/actions-gh-pages/issues/87) 75 | 76 | 77 | 78 | ## GitHub Enterprise Server Support 79 | 80 | ✅️ GitHub Enterprise Server is supported above `2.22.6`. 81 | 82 | Note that the `GITHUB_TOKEN` that is created by the runner might not inherently have push/publish privileges on GHES. You might need to create/request a technical user with write permissions to your target repository. 83 | 84 | 85 | 86 | ## Table of Contents 87 | 88 | 89 | 90 | 91 | - [Getting started](#getting-started) 92 | - [Options](#options) 93 | - [⭐️ Set Runner's Access Token `github_token`](#%EF%B8%8F-set-runners-access-token-github_token) 94 | - [⭐️ Set SSH Private Key `deploy_key`](#%EF%B8%8F-set-ssh-private-key-deploy_key) 95 | - [⭐️ Set Personal Access Token `personal_token`](#%EF%B8%8F-set-personal-access-token-personal_token) 96 | - [⭐️ Set Another GitHub Pages Branch `publish_branch`](#%EF%B8%8F-set-another-github-pages-branch-publish_branch) 97 | - [⭐️ Source Directory `publish_dir`](#%EF%B8%8F-source-directory-publish_dir) 98 | - [⭐️ Deploy to Subdirectory `destination_dir`](#%EF%B8%8F-deploy-to-subdirectory-destination_dir) 99 | - [⭐️ Filter publishing assets `exclude_assets`](#%EF%B8%8F-filter-publishing-assets-exclude_assets) 100 | - [⭐️ Add CNAME file `cname`](#%EF%B8%8F-add-cname-file-cname) 101 | - [⭐️ Enable Built-in Jekyll `enable_jekyll`](#%EF%B8%8F-enable-built-in-jekyll-enable_jekyll) 102 | - [⭐️ Allow empty commits `allow_empty_commit`](#%EF%B8%8F-allow-empty-commits-allow_empty_commit) 103 | - [⭐️ Keeping existing files `keep_files`](#%EF%B8%8F-keeping-existing-files-keep_files) 104 | - [⭐️ Deploy to external repository `external_repository`](#%EF%B8%8F-deploy-to-external-repository-external_repository) 105 | - [⭐️ Force orphan `force_orphan`](#%EF%B8%8F-force-orphan-force_orphan) 106 | - [⭐️ Set Git username and email](#%EF%B8%8F-set-git-username-and-email) 107 | - [⭐️ Set custom commit message](#%EF%B8%8F-set-custom-commit-message) 108 | - [⭐️ Create Git tag](#%EF%B8%8F-create-git-tag) 109 | - [Tips and FAQ](#tips-and-faq) 110 | - [⭐️ Create SSH Deploy Key](#%EF%B8%8F-create-ssh-deploy-key) 111 | - [⭐️ First Deployment with `GITHUB_TOKEN`](#%EF%B8%8F-first-deployment-with-github_token) 112 | - [⭐️ Use the latest and specific release](#%EF%B8%8F-use-the-latest-and-specific-release) 113 | - [⭐️ Schedule and Manual Deployment](#%EF%B8%8F-schedule-and-manual-deployment) 114 | - [⭐️ Release Strategy](#%EF%B8%8F-release-strategy) 115 | - [Examples](#examples) 116 | - [⭐️ Static Site Generators with Node.js](#%EF%B8%8F-static-site-generators-with-nodejs) 117 | - [⭐️ Gatsby](#%EF%B8%8F-gatsby) 118 | - [⭐️ React and Next](#%EF%B8%8F-react-and-next) 119 | - [⭐️ Vue and Nuxt](#%EF%B8%8F-vue-and-nuxt) 120 | - [⭐️ Docusaurus](#%EF%B8%8F-docusaurus) 121 | - [⭐️ Static Site Generators with Python](#%EF%B8%8F-static-site-generators-with-python) 122 | - [⭐️ mdBook (Rust)](#%EF%B8%8F-mdbook-rust) 123 | - [⭐️ Flutter Web](#%EF%B8%8F-flutter-web) 124 | - [⭐️ Elm](#%EF%B8%8F-elm) 125 | - [⭐️ Swift Publish](#%EF%B8%8F-swift-publish) 126 | - [License](#license) 127 | - [Maintainer](#maintainer) 128 | 129 | 130 | 131 | 132 | 133 | ## Getting started 134 | 135 | Add your workflow file `.github/workflows/gh-pages.yml` and push it to your remote default branch. 136 | 137 | Here is an example workflow for Hugo. 138 | 139 | - [peaceiris/actions-hugo: GitHub Actions for Hugo](https://github.com/peaceiris/actions-hugo) 140 | 141 | [![peaceiris/actions-hugo - GitHub](https://gh-card.dev/repos/peaceiris/actions-hugo.svg?fullname)](https://github.com/peaceiris/actions-hugo) 142 | 143 | ```yaml 144 | name: GitHub Pages 145 | 146 | on: 147 | push: 148 | branches: 149 | - main # Set a branch name to trigger deployment 150 | pull_request: 151 | 152 | jobs: 153 | deploy: 154 | runs-on: ubuntu-22.04 155 | permissions: 156 | contents: write 157 | concurrency: 158 | group: ${{ github.workflow }}-${{ github.ref }} 159 | steps: 160 | - uses: actions/checkout@v4 161 | with: 162 | submodules: true # Fetch Hugo themes (true OR recursive) 163 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 164 | 165 | - name: Setup Hugo 166 | uses: peaceiris/actions-hugo@v2 167 | with: 168 | hugo-version: '0.110.0' 169 | 170 | - name: Build 171 | run: hugo --minify 172 | 173 | - name: Deploy 174 | uses: peaceiris/actions-gh-pages@v4 175 | # If you're changing the branch from main, 176 | # also change the `main` in `refs/heads/main` 177 | # below accordingly. 178 | if: github.ref == 'refs/heads/main' 179 | with: 180 | github_token: ${{ secrets.GITHUB_TOKEN }} 181 | publish_dir: ./public 182 | ``` 183 | 184 | | Actions log overview | GitHub Pages log | 185 | |---|---| 186 | | ![](./images/log_overview.jpg) | ![](./images/log_success.jpg) | 187 | 188 |
189 | Back to TOC ☝️ 190 |
191 | 192 | 193 | 194 | ## Options 195 | 196 | ### ⭐️ Set Runner's Access Token `github_token` 197 | 198 | **This option is for `GITHUB_TOKEN`, not a personal access token.** 199 | 200 | A GitHub Actions runner automatically creates a `GITHUB_TOKEN` secret to use in your workflow. You can use the `GITHUB_TOKEN` to authenticate in a workflow run. 201 | 202 | ```yaml 203 | - name: Deploy 204 | uses: peaceiris/actions-gh-pages@v4 205 | with: 206 | github_token: ${{ secrets.GITHUB_TOKEN }} 207 | publish_dir: ./public 208 | ``` 209 | 210 | For more details about `GITHUB_TOKEN`: [Automatic token authentication - GitHub Docs](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) 211 | 212 | ### ⭐️ Set SSH Private Key `deploy_key` 213 | 214 | Read [Create SSH Deploy Key](#%EF%B8%8F-create-ssh-deploy-key), create your SSH deploy key, and set the `deploy_key` option like the following. 215 | 216 | ```yaml 217 | - name: Deploy 218 | uses: peaceiris/actions-gh-pages@v4 219 | with: 220 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 221 | publish_dir: ./public 222 | ``` 223 | 224 | ### ⭐️ Set Personal Access Token `personal_token` 225 | 226 | [Generate a personal access token (`repo`)](https://github.com/settings/tokens) and add it to Secrets as `PERSONAL_TOKEN`, it works as well as `ACTIONS_DEPLOY_KEY`. 227 | 228 | ```yaml 229 | - name: Deploy 230 | uses: peaceiris/actions-gh-pages@v4 231 | with: 232 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 233 | publish_dir: ./public 234 | ``` 235 | 236 | ### ⭐️ Set Another GitHub Pages Branch `publish_branch` 237 | 238 | Set a branch name to use as GitHub Pages branch. 239 | The default is `gh-pages`. 240 | 241 | ```yaml 242 | - name: Deploy 243 | uses: peaceiris/actions-gh-pages@v4 244 | with: 245 | github_token: ${{ secrets.GITHUB_TOKEN }} 246 | publish_branch: your-branch # default: gh-pages 247 | ``` 248 | 249 | ### ⭐️ Source Directory `publish_dir` 250 | 251 | A source directory to deploy to GitHub Pages. The default is `public`. 252 | Only the contents of this dir are pushed to GitHub Pages branch, `gh-pages` by default. 253 | 254 | ```yaml 255 | - name: Deploy 256 | uses: peaceiris/actions-gh-pages@v4 257 | with: 258 | github_token: ${{ secrets.GITHUB_TOKEN }} 259 | publish_dir: ./out # default: public 260 | ``` 261 | 262 | ### ⭐️ Deploy to Subdirectory `destination_dir` 263 | 264 | *This feature is on beta.* 265 | *Any feedback is welcome at [Issue #324](https://github.com/peaceiris/actions-gh-pages/issues/324)* 266 | 267 | A destination subdirectory on a publishing branch. The default is empty. 268 | 269 | ```yaml 270 | - name: Deploy 271 | uses: peaceiris/actions-gh-pages@v4 272 | with: 273 | github_token: ${{ secrets.GITHUB_TOKEN }} 274 | destination_dir: subdir 275 | ``` 276 | 277 | ### ⭐️ Filter publishing assets `exclude_assets` 278 | 279 | *This feature is on beta.* 280 | *Any feedback is welcome at [Issue #163](https://github.com/peaceiris/actions-gh-pages/issues/163)* 281 | 282 | Set files or directories to exclude from publishing assets. 283 | The default is `.github`. 284 | Values should be split with a comma. 285 | 286 | ```yaml 287 | - name: Deploy 288 | uses: peaceiris/actions-gh-pages@v4 289 | with: 290 | github_token: ${{ secrets.GITHUB_TOKEN }} 291 | exclude_assets: '.github,exclude-file1,exclude-file2' 292 | ``` 293 | 294 | Set `exclude_assets` to empty for including the `.github` directory to deployment assets. 295 | 296 | ```yaml 297 | - name: Deploy 298 | uses: peaceiris/actions-gh-pages@v4 299 | with: 300 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # Recommended for this usage 301 | # personal_token: ${{ secrets.PERSONAL_TOKEN }} # An alternative 302 | # github_token: ${{ secrets.GITHUB_TOKEN }} # This does not work for this usage 303 | exclude_assets: '' 304 | ``` 305 | 306 | The `exclude_assets` option supports glob patterns. 307 | 308 | ```yaml 309 | - name: Deploy 310 | uses: peaceiris/actions-gh-pages@v4 311 | with: 312 | github_token: ${{ secrets.GITHUB_TOKEN }} 313 | exclude_assets: '.github,exclude-file.txt,exclude-dir/**.txt' 314 | ``` 315 | 316 | ### ⭐️ Add CNAME file `cname` 317 | 318 | To add the `CNAME` file, we can set the `cname` option. 319 | Alternatively, put your `CNAME` file into your `publish_dir`. (e.g. `public/CNAME`) 320 | 321 | For more details about the `CNAME` file, read the official documentation: [Managing a custom domain for your GitHub Pages site - GitHub Docs](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site) 322 | 323 | ```yaml 324 | - name: Deploy 325 | uses: peaceiris/actions-gh-pages@v4 326 | with: 327 | github_token: ${{ secrets.GITHUB_TOKEN }} 328 | publish_dir: ./public 329 | cname: github.com 330 | ``` 331 | 332 | ### ⭐️ Enable Built-in Jekyll `enable_jekyll` 333 | 334 | If you want GitHub Pages to process your site with the static site generator Jekyll, set `enable_jekyll` to true. 335 | 336 | By default, this action signals to GitHub Pages that the site shall not be processed with Jekyll. This is done by adding an empty `.nojekyll` file on your publishing branch. When that file already exists, this action does nothing. 337 | 338 | Bypassing Jekyll makes the deployment faster and is necessary if you are deploying files or directories that start with underscores, since Jekyll considers these to be special resources and does not copy them to the final site. You only need to set `enable_jekyll` to true when you want to deploy a Jekyll-powered website and let GitHub Pages do the Jekyll processing. 339 | 340 | 341 | ```yaml 342 | - name: Deploy 343 | uses: peaceiris/actions-gh-pages@v4 344 | with: 345 | github_token: ${{ secrets.GITHUB_TOKEN }} 346 | publish_dir: ./public 347 | enable_jekyll: true 348 | ``` 349 | 350 | For more details about `.nojekyll`: [Bypassing Jekyll on GitHub Pages - The GitHub Blog](https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/) 351 | 352 | ### ⭐️ Allow empty commits `allow_empty_commit` 353 | 354 | By default, a commit will not be generated when no file changes. If you want to allow an empty commit, set the optional parameter `allow_empty_commit` to `true`. 355 | 356 | For example: 357 | 358 | ```yaml 359 | - name: Deploy 360 | uses: peaceiris/actions-gh-pages@v4 361 | with: 362 | github_token: ${{ secrets.GITHUB_TOKEN }} 363 | publish_dir: ./public 364 | allow_empty_commit: true 365 | ``` 366 | 367 | ### ⭐️ Keeping existing files `keep_files` 368 | 369 | By default, existing files in the publish branch (or only in `destination_dir` if given) will be removed. If you want the action to add new files but leave existing ones untouched, set the optional parameter `keep_files` to `true`. 370 | 371 | Note that users who are using a Static Site Generator do not need this option in most cases. Please reconsider your project structure and building scripts, or use a built-in feature of a Static Site Generator before you enable this flag. 372 | 373 | - [Static Files | Hugo](https://gohugo.io/content-management/static-files/) 374 | - [Using the Static Folder | Gatsby](https://www.gatsbyjs.com/docs/how-to/images-and-media/static-folder/) 375 | 376 | For example: 377 | 378 | ```yaml 379 | - name: Deploy 380 | uses: peaceiris/actions-gh-pages@v4 381 | with: 382 | github_token: ${{ secrets.GITHUB_TOKEN }} 383 | publish_dir: ./public 384 | keep_files: true 385 | ``` 386 | 387 | With the v3, this option does not support working with the force_orphan option. The next major release (version 4) will support this. 388 | See [the issue #455](https://github.com/peaceiris/actions-gh-pages/issues/455) 389 | 390 | ### ⭐️ Deploy to external repository `external_repository` 391 | 392 | By default, your files are published to the repository which is running this action. 393 | If you want to publish to another repository on GitHub, set the environment variable `external_repository` to `/`. 394 | 395 | For example: 396 | 397 | ```yaml 398 | - name: Deploy 399 | uses: peaceiris/actions-gh-pages@v4 400 | with: 401 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 402 | external_repository: username/external-repository 403 | publish_branch: your-branch # default: gh-pages 404 | publish_dir: ./public 405 | ``` 406 | 407 | You can use `deploy_key` or `personal_token`. 408 | When you use `deploy_key`, set your private key to the repository which includes this action and set your public key to your external repository. 409 | 410 | **Note that `GITHUB_TOKEN` has no permission to access to external repositories. Please create a personal access token and set it to `personal_token` like `personal_token: ${{ secrets.PERSONAL_TOKEN }}`.** 411 | 412 | Use case: 413 | 414 | A GitHub Free Plan account cannot use the GitHub Pages in a private repository. To make your source contents private and deploy it with the GitHub Pages, you can deploy your site from a private repository to a public repository using this option. 415 | 416 | - `peaceiris/homepage`: A private repository running this action with `external_repository: peaceiris/peaceiris.github.io` 417 | - `peaceiris/peaceiris.github.io`: A public repository using GitHub Pages 418 | 419 | ### ⭐️ Force orphan `force_orphan` 420 | 421 | We can set the `force_orphan: true` option. 422 | This allows you to make your publish branch with only the latest commit. 423 | 424 | ```yaml 425 | - name: Deploy 426 | uses: peaceiris/actions-gh-pages@v4 427 | with: 428 | github_token: ${{ secrets.GITHUB_TOKEN }} 429 | publish_dir: ./public 430 | force_orphan: true 431 | ``` 432 | 433 | ### ⭐️ Set Git username and email 434 | 435 | Set custom `git config user.name` and `git config user.email`. 436 | A commit is always created with the same user. 437 | 438 | ```yaml 439 | - name: Deploy 440 | uses: peaceiris/actions-gh-pages@v4 441 | with: 442 | github_token: ${{ secrets.GITHUB_TOKEN }} 443 | publish_dir: ./public 444 | user_name: 'github-actions[bot]' 445 | user_email: 'github-actions[bot]@users.noreply.github.com' 446 | ``` 447 | 448 | Add GitHub Actions bot as a committer 449 | 450 | ### ⭐️ Set custom commit message 451 | 452 | Set a custom commit message. 453 | When we create a commit with a message `docs: Update some post`, a deployment commit will be generated with a message `docs: Update some post ${GITHUB_SHA}`. 454 | 455 | ```yaml 456 | - name: Deploy 457 | uses: peaceiris/actions-gh-pages@v4 458 | with: 459 | github_token: ${{ secrets.GITHUB_TOKEN }} 460 | publish_dir: ./public 461 | commit_message: ${{ github.event.head_commit.message }} 462 | ``` 463 | 464 | Set a custom commit message - GitHub Actions for GitHub Pages 465 | 466 | To set a full custom commit message without a triggered commit hash, 467 | use the `full_commit_message` option instead of the `commit_message` option. 468 | 469 | ```yaml 470 | - name: Deploy 471 | uses: peaceiris/actions-gh-pages@v4 472 | with: 473 | github_token: ${{ secrets.GITHUB_TOKEN }} 474 | publish_dir: ./public 475 | full_commit_message: ${{ github.event.head_commit.message }} 476 | ``` 477 | 478 | ### ⭐️ Create Git tag 479 | 480 | Here is an example workflow. 481 | 482 | ```yaml 483 | name: GitHub Pages 484 | 485 | on: 486 | push: 487 | branches: 488 | - main 489 | tags: 490 | - 'v*.*.*' 491 | 492 | jobs: 493 | deploy: 494 | runs-on: ubuntu-22.04 495 | permissions: 496 | contents: write 497 | concurrency: 498 | group: ${{ github.workflow }}-${{ github.ref }} 499 | steps: 500 | - uses: actions/checkout@v4 501 | 502 | - name: Some build 503 | 504 | - name: Prepare tag 505 | id: prepare_tag 506 | if: startsWith(github.ref, 'refs/tags/') 507 | run: | 508 | echo "DEPLOY_TAG_NAME=deploy-${TAG_NAME}" >> "${GITHUB_OUTPUT}" 509 | 510 | - name: Deploy 511 | uses: peaceiris/actions-gh-pages@v4 512 | with: 513 | github_token: ${{ secrets.GITHUB_TOKEN }} 514 | publish_dir: ./public 515 | tag_name: ${{ steps.prepare_tag.outputs.DEPLOY_TAG_NAME }} 516 | tag_message: 'Deployment ${{ github.ref_name }}' 517 | ``` 518 | 519 | Commands on a local machine. 520 | 521 | ```console 522 | $ # On a main branch 523 | $ git tag -a "v1.2.3" -m "Release v1.2.3" 524 | $ git push origin "v1.2.3" 525 | 526 | $ # After deployment 527 | $ git fetch origin 528 | $ git tag 529 | deploy-v1.2.3 # Tag on the gh-pages branch 530 | v1.2.3 # Tag on the main branch 531 | ``` 532 | 533 |
534 | Back to TOC ☝️ 535 |
536 | 537 | 538 | 539 | ## Tips and FAQ 540 | 541 | ### ⭐️ Create SSH Deploy Key 542 | 543 | Generate your deploy key with the following command. 544 | 545 | ```sh 546 | ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages -N "" 547 | ``` 548 | 549 | You will get 2 files: 550 | 551 | - `gh-pages.pub` is a public key 552 | - `gh-pages` is a private key 553 | 554 | Next, Go to **Repository Settings** 555 | 556 | - Go to **Deploy Keys** and add your public key with the **Allow write access** 557 | - Go to **Secrets** and add your private key as `ACTIONS_DEPLOY_KEY` 558 | 559 | | Add your public key | Success | 560 | |---|---| 561 | | ![](./images/deploy-keys-1.jpg) | ![](./images/deploy-keys-2.jpg) | 562 | 563 | | Add your private key | Success | 564 | |---|---| 565 | | ![](./images/secrets-1.jpg) | ![](./images/secrets-2.jpg) | 566 | 567 | ### ⭐️ First Deployment with `GITHUB_TOKEN` 568 | 569 | The `GITHUB_TOKEN` has limitations for the first deployment so we have to select the GitHub Pages branch on the repository settings tab. After that, do the second deployment like the following pictures. 570 | 571 | | First deployment failed | Go to the settings tab | 572 | |---|---| 573 | | ![](./images/log_first_deployment_failed_with_github_token.jpg) | ![](./images/settings_inactive.jpg) | 574 | 575 | | Select branch | Deploying again and succeed | 576 | |---|---| 577 | | ![](./images/settings_select.jpg) | ![](./images/log_success.jpg) | 578 | 579 | If the action fails to push the commit or tag with the following error: 580 | 581 | ```txt 582 | /usr/bin/git push origin gh-pages 583 | remote: Write access to repository not granted. 584 | fatal: unable to access 'https://github.com/username/repository.git/': The requested URL returned error: 403 585 | Error: Action failed with "The process '/usr/bin/git' failed with exit code 128" 586 | ``` 587 | 588 | Please add the write permission to the [`permissions.contents` in a workflow/job](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions). 589 | 590 | ```yaml 591 | permissions: 592 | contents: write 593 | ``` 594 | 595 | Alternatively, you can [configure the default `GITHUB_TOKEN` permissions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#configuring-the-default-github_token-permissions) by selecting read and write permissions. 596 | 597 | 598 | ### ⭐️ Use the latest and specific release 599 | 600 | We recommend you to use the latest and specific release of this action for stable CI/CD. 601 | It is useful to watch this repository (release only) to check the [latest release] of this action. 602 | 603 | [latest release]: https://github.com/peaceiris/actions-gh-pages/releases 604 | 605 | For continuous updating, we can use the GitHub native Dependabot. 606 | Here is an example configuration of the bot. The config file is located in `.github/dependabot.yml`. 607 | 608 | ```yaml 609 | version: 2 610 | updates: 611 | - package-ecosystem: "github-actions" 612 | directory: "/" 613 | schedule: 614 | interval: "daily" 615 | labels: 616 | - "CI/CD" 617 | commit-message: 618 | prefix: ci 619 | ``` 620 | 621 | See the official documentation for more details about the Dependabot: [Keeping your dependencies updated automatically - GitHub Docs](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically) 622 | 623 | ### ⭐️ Schedule and Manual Deployment 624 | 625 | For deploying regularly, we can set the `on.schedule` workflow trigger. 626 | See [Scheduled events | Events that trigger workflows - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#scheduled-events) 627 | 628 | For deploying manually, we can set the `on.workflow_dispatch` workflow trigger. 629 | See [Manual events `workflow_dispatch` | Events that trigger workflows - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#manual-events) 630 | 631 | ```yaml 632 | name: GitHub Pages 633 | 634 | on: 635 | push: 636 | branches: 637 | - main 638 | schedule: 639 | - cron: "22 22 * * *" 640 | workflow_dispatch: 641 | 642 | jobs: 643 | deploy: 644 | runs-on: ubuntu-22.04 645 | permissions: 646 | contents: write 647 | concurrency: 648 | group: ${{ github.workflow }}-${{ github.ref }} 649 | steps: 650 | ... 651 | ``` 652 | 653 | ### ⭐️ Release Strategy 654 | 655 | cf. [support: execution from hashref disabled/broken vs GitHub Actions Security Best Practice? · Issue #712 · peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages/issues/712) 656 | 657 | Our project builds and provides build assets only when creating a release. This is to prevent the user from executing this action with a specific branch (like main). For example, if we maintain build assets in the main branch and users use this action as follows, a major release including breaking changes will break the CI workflow of the users silently. 658 | 659 | ```yaml 660 | - uses: peaceiris/actions-gh-pages@main # Bad example! 661 | with: 662 | github_token: ${{ secrets.GITHUB_TOKEN }} 663 | publish_dir: ./public 664 | ``` 665 | 666 | In this project, a major tag (e.g. v3) is guaranteed to contain no breaking changes. But, we recommend using a tag or a commit hash for the stability of your workflows. 667 | 668 | ```yaml 669 | - uses: peaceiris/actions-gh-pages@v4.0.0 # tag: Better 670 | with: 671 | github_token: ${{ secrets.GITHUB_TOKEN }} 672 | publish_dir: ./public 673 | ``` 674 | 675 | ```yaml 676 | - uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # commit hash of v3.9.3: Best! 677 | with: 678 | github_token: ${{ secrets.GITHUB_TOKEN }} 679 | publish_dir: ./public 680 | ``` 681 | 682 | For verifying the release asset, we can use the following commands. 683 | 684 | ```sh 685 | git clone https://github.com/peaceiris/actions-gh-pages.git 686 | cd ./actions-gh-pages 687 | git checkout v3.9.3 688 | nvm install 689 | nvm use 690 | npm i -g npm 691 | npm ci 692 | npm run build 693 | git diff ./lib/index.js # We will get zero exit code 694 | ``` 695 | 696 |
697 | Back to TOC ☝️ 698 |
699 | 700 | 701 | 702 | ## Examples 703 | 704 | ### ⭐️ Static Site Generators with Node.js 705 | 706 | [hexo], [vuepress], [react-static], [gridsome], [create-react-app] and so on. 707 | Please check where your output directory is before pushing your workflow. 708 | e.g. `create-react-app` requires `publish_dir` to be set to `./build` 709 | 710 | [hexo]: https://github.com/hexojs/hexo 711 | [vuepress]: https://github.com/vuejs/vuepress 712 | [react-static]: https://github.com/react-static/react-static 713 | [gridsome]: https://github.com/gridsome/gridsome 714 | [create-react-app]: https://github.com/facebook/create-react-app 715 | 716 | Premise: Dependencies are managed by `package.json` and `package-lock.json` 717 | 718 | ```yaml 719 | name: GitHub Pages 720 | 721 | on: 722 | push: 723 | branches: 724 | - main 725 | pull_request: 726 | 727 | jobs: 728 | deploy: 729 | runs-on: ubuntu-22.04 730 | permissions: 731 | contents: write 732 | concurrency: 733 | group: ${{ github.workflow }}-${{ github.ref }} 734 | steps: 735 | - uses: actions/checkout@v4 736 | 737 | - name: Setup Node 738 | uses: actions/setup-node@v4 739 | with: 740 | node-version: '24' 741 | 742 | - name: Cache dependencies 743 | uses: actions/cache@v4 744 | with: 745 | path: ~/.npm 746 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 747 | restore-keys: | 748 | ${{ runner.os }}-node- 749 | 750 | - run: npm ci 751 | - run: npm run build 752 | 753 | - name: Deploy 754 | uses: peaceiris/actions-gh-pages@v4 755 | if: github.ref == 'refs/heads/main' 756 | with: 757 | github_token: ${{ secrets.GITHUB_TOKEN }} 758 | publish_dir: ./public 759 | ``` 760 | 761 | ### ⭐️ Gatsby 762 | 763 | An example for [Gatsby] (Gatsby.js) project with [gatsby-starter-blog] 764 | 765 | [Gatsby]: https://github.com/gatsbyjs/gatsby 766 | [gatsby-starter-blog]: https://github.com/gatsbyjs/gatsby-starter-blog 767 | 768 | ```yaml 769 | name: GitHub Pages 770 | 771 | on: 772 | push: 773 | branches: 774 | - main 775 | pull_request: 776 | 777 | jobs: 778 | deploy: 779 | runs-on: ubuntu-22.04 780 | permissions: 781 | contents: write 782 | concurrency: 783 | group: ${{ github.workflow }}-${{ github.ref }} 784 | steps: 785 | - uses: actions/checkout@v4 786 | 787 | - name: Setup Node 788 | uses: actions/setup-node@v4 789 | with: 790 | node-version: '24' 791 | 792 | - name: Cache dependencies 793 | uses: actions/cache@v4 794 | with: 795 | path: ~/.npm 796 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 797 | restore-keys: | 798 | ${{ runner.os }}-node- 799 | 800 | - run: npm ci 801 | - run: npm run format 802 | - run: npm run test 803 | - run: npm run build 804 | 805 | - name: Deploy 806 | uses: peaceiris/actions-gh-pages@v4 807 | if: github.ref == 'refs/heads/main' 808 | with: 809 | github_token: ${{ secrets.GITHUB_TOKEN }} 810 | publish_dir: ./public 811 | ``` 812 | 813 | ### ⭐️ React and Next 814 | 815 | An example for [Next.js] (React.js) project with [create-next-app] 816 | 817 | [Next.js]: https://github.com/vercel/next.js 818 | [create-next-app]: https://nextjs.org/docs 819 | 820 | ```yaml 821 | name: GitHub Pages 822 | 823 | on: 824 | push: 825 | branches: 826 | - main 827 | pull_request: 828 | 829 | jobs: 830 | deploy: 831 | runs-on: ubuntu-22.04 832 | permissions: 833 | contents: write 834 | concurrency: 835 | group: ${{ github.workflow }}-${{ github.ref }} 836 | steps: 837 | - uses: actions/checkout@v4 838 | 839 | - name: Setup Node 840 | uses: actions/setup-node@v4 841 | with: 842 | node-version: '24' 843 | 844 | - name: Get yarn cache 845 | id: yarn-cache 846 | run: echo "YARN_CACHE_DIR=$(yarn cache dir)" >> "${GITHUB_OUTPUT}" 847 | 848 | - name: Cache dependencies 849 | uses: actions/cache@v4 850 | with: 851 | path: ${{ steps.yarn-cache.outputs.YARN_CACHE_DIR }} 852 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 853 | restore-keys: | 854 | ${{ runner.os }}-yarn- 855 | 856 | - run: yarn install --frozen-lockfile 857 | - run: yarn build 858 | - run: yarn export 859 | 860 | - name: Deploy 861 | uses: peaceiris/actions-gh-pages@v4 862 | if: github.ref == 'refs/heads/main' 863 | with: 864 | github_token: ${{ secrets.GITHUB_TOKEN }} 865 | publish_dir: ./out 866 | ``` 867 | 868 | ### ⭐️ Vue and Nuxt 869 | 870 | An example for [Nuxt.js] (Vue.js) project with [create-nuxt-app] 871 | 872 | - cf. [Nuxt - GitHub Pages](https://nuxtjs.org/deployments/github-pages) 873 | 874 | [Nuxt.js]: https://github.com/nuxt/nuxt.js 875 | [create-nuxt-app]: https://github.com/nuxt/create-nuxt-app 876 | 877 | ```yaml 878 | name: GitHub Pages 879 | 880 | on: 881 | push: 882 | branches: 883 | - main 884 | pull_request: 885 | 886 | jobs: 887 | deploy: 888 | runs-on: ubuntu-22.04 889 | permissions: 890 | contents: write 891 | concurrency: 892 | group: ${{ github.workflow }}-${{ github.ref }} 893 | steps: 894 | - uses: actions/checkout@v4 895 | 896 | - name: Setup Node 897 | uses: actions/setup-node@v4 898 | with: 899 | node-version: '24' 900 | 901 | - name: Cache dependencies 902 | uses: actions/cache@v4 903 | with: 904 | path: ~/.npm 905 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 906 | restore-keys: | 907 | ${{ runner.os }}-node- 908 | 909 | - run: npm ci 910 | - run: npm test 911 | - run: npm run generate 912 | 913 | - name: deploy 914 | uses: peaceiris/actions-gh-pages@v4 915 | if: github.ref == 'refs/heads/main' 916 | with: 917 | github_token: ${{ secrets.GITHUB_TOKEN }} 918 | publish_dir: ./dist 919 | ``` 920 | 921 | ### ⭐️ Docusaurus 922 | 923 | An example workflow for [Docusaurus](https://docusaurus.io/). 924 | 925 | `npx @docusaurus/init@next init website classic` is useful to create a new Docusaurus project. 926 | 927 | ```yaml 928 | # .github/workflows/deploy.yml 929 | 930 | name: GitHub Pages 931 | 932 | on: 933 | push: 934 | branches: 935 | - main 936 | paths: 937 | - '.github/workflows/deploy.yml' 938 | - 'website/**' 939 | pull_request: 940 | 941 | jobs: 942 | deploy: 943 | runs-on: ubuntu-22.04 944 | permissions: 945 | contents: write 946 | concurrency: 947 | group: ${{ github.workflow }}-${{ github.ref }} 948 | defaults: 949 | run: 950 | working-directory: website 951 | steps: 952 | - uses: actions/checkout@v4 953 | 954 | - name: Setup Node 955 | uses: actions/setup-node@v4 956 | with: 957 | node-version: '24' 958 | 959 | - name: Get yarn cache 960 | id: yarn-cache 961 | run: echo "YARN_CACHE_DIR=$(yarn cache dir)" >> "${GITHUB_OUTPUT}" 962 | 963 | - name: Cache dependencies 964 | uses: actions/cache@v4 965 | with: 966 | path: ${{ steps.yarn-cache.outputs.YARN_CACHE_DIR }} 967 | key: ${{ runner.os }}-website-${{ hashFiles('**/yarn.lock') }} 968 | restore-keys: | 969 | ${{ runner.os }}-website- 970 | 971 | - run: yarn install --frozen-lockfile 972 | - run: yarn build 973 | 974 | - name: Deploy 975 | uses: peaceiris/actions-gh-pages@v4 976 | if: github.ref == 'refs/heads/main' 977 | with: 978 | github_token: ${{ secrets.GITHUB_TOKEN }} 979 | publish_dir: ./website/build 980 | ``` 981 | 982 | ### ⭐️ Static Site Generators with Python 983 | 984 | [pelican], [MkDocs], [sphinx], and so on. 985 | 986 | [pelican]: https://github.com/getpelican/pelican 987 | [MkDocs]: https://github.com/mkdocs/mkdocs 988 | [sphinx]: https://github.com/sphinx-doc/sphinx 989 | 990 | Premise: Dependencies are managed by `requirements.txt` 991 | 992 | ```yaml 993 | name: GitHub Pages 994 | 995 | on: 996 | push: 997 | branches: 998 | - main 999 | pull_request: 1000 | 1001 | jobs: 1002 | deploy: 1003 | runs-on: ubuntu-22.04 1004 | permissions: 1005 | contents: write 1006 | concurrency: 1007 | group: ${{ github.workflow }}-${{ github.ref }} 1008 | steps: 1009 | - uses: actions/checkout@v4 1010 | 1011 | - name: Setup Python 1012 | uses: actions/setup-python@v5 1013 | with: 1014 | python-version: '3.13' 1015 | 1016 | - name: Upgrade pip 1017 | run: | 1018 | # install pip=>20.1 to use "pip cache dir" 1019 | python3 -m pip install --upgrade pip 1020 | 1021 | - name: Get pip cache dir 1022 | id: pip-cache 1023 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 1024 | 1025 | - name: Cache dependencies 1026 | uses: actions/cache@v4 1027 | with: 1028 | path: ${{ steps.pip-cache.outputs.dir }} 1029 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 1030 | restore-keys: | 1031 | ${{ runner.os }}-pip- 1032 | 1033 | - name: Install dependencies 1034 | run: python3 -m pip install -r ./requirements.txt 1035 | 1036 | - run: mkdocs build 1037 | 1038 | - name: Deploy 1039 | uses: peaceiris/actions-gh-pages@v4 1040 | if: github.ref == 'refs/heads/main' 1041 | with: 1042 | github_token: ${{ secrets.GITHUB_TOKEN }} 1043 | publish_dir: ./site 1044 | ``` 1045 | 1046 | ### ⭐️ mdBook (Rust) 1047 | 1048 | An example GitHub Actions workflow to deploy [rust-lang/mdBook] site to GitHub Pages. 1049 | 1050 | [rust-lang/mdBook]: https://github.com/rust-lang/mdBook 1051 | 1052 | - [peaceiris/actions-mdbook: GitHub Actions for mdBook (rust-lang/mdBook)](https://github.com/peaceiris/actions-mdbook) 1053 | 1054 | ```yaml 1055 | name: GitHub Pages 1056 | 1057 | on: 1058 | push: 1059 | branches: 1060 | - main 1061 | pull_request: 1062 | 1063 | jobs: 1064 | deploy: 1065 | runs-on: ubuntu-22.04 1066 | permissions: 1067 | contents: write 1068 | concurrency: 1069 | group: ${{ github.workflow }}-${{ github.ref }} 1070 | steps: 1071 | - uses: actions/checkout@v4 1072 | 1073 | - name: Setup mdBook 1074 | uses: peaceiris/actions-mdbook@v1 1075 | with: 1076 | mdbook-version: '0.4.8' 1077 | # mdbook-version: 'latest' 1078 | 1079 | - run: mdbook build 1080 | 1081 | - name: Deploy 1082 | uses: peaceiris/actions-gh-pages@v4 1083 | if: github.ref == 'refs/heads/main' 1084 | with: 1085 | github_token: ${{ secrets.GITHUB_TOKEN }} 1086 | publish_dir: ./book 1087 | ``` 1088 | 1089 | Hint: you may want to publish your rustdocs. And use relative links to it from the md docs, and have them checked by `mdbook`. 1090 | Then, according to the [doc](https://rust-lang.github.io/mdBook/guide/creating.html#source-files), you may put `./target/doc/` 1091 | to your `./book/src` dir before you `mdbook build` and then it will end up in `./book/html/` and in your Github Pages. 1092 | 1093 | ### ⭐️ Flutter Web 1094 | 1095 | An example workflow for [Flutter web project]. 1096 | 1097 | [Flutter web project]: https://flutter.dev/docs/get-started/web 1098 | 1099 | ```yaml 1100 | name: GitHub Pages 1101 | 1102 | on: 1103 | push: 1104 | branches: 1105 | - main 1106 | pull_request: 1107 | 1108 | jobs: 1109 | deploy: 1110 | runs-on: ubuntu-22.04 1111 | permissions: 1112 | contents: write 1113 | concurrency: 1114 | group: ${{ github.workflow }}-${{ github.ref }} 1115 | steps: 1116 | - uses: actions/checkout@v4 1117 | 1118 | - name: Setup Flutter 1119 | run: | 1120 | git clone https://github.com/flutter/flutter.git --depth 1 -b beta _flutter 1121 | echo "${GITHUB_WORKSPACE}/_flutter/bin" >> ${GITHUB_PATH} 1122 | 1123 | - name: Install 1124 | run: | 1125 | flutter config --enable-web 1126 | flutter pub get 1127 | 1128 | - name: Build 1129 | run: flutter build web 1130 | 1131 | - name: Deploy 1132 | uses: peaceiris/actions-gh-pages@v4 1133 | if: github.ref == 'refs/heads/main' 1134 | with: 1135 | github_token: ${{ secrets.GITHUB_TOKEN }} 1136 | publish_dir: ./build/web 1137 | ``` 1138 | 1139 | ### ⭐️ Elm 1140 | 1141 | An example workflow for [Elm]. 1142 | 1143 | [Elm]: https://elm-lang.org 1144 | 1145 | ```yaml 1146 | name: GitHub Pages 1147 | 1148 | on: 1149 | push: 1150 | branches: 1151 | - main 1152 | pull_request: 1153 | 1154 | jobs: 1155 | deploy: 1156 | runs-on: ubuntu-22.04 1157 | permissions: 1158 | contents: write 1159 | concurrency: 1160 | group: ${{ github.workflow }}-${{ github.ref }} 1161 | steps: 1162 | - uses: actions/checkout@v4 1163 | 1164 | - name: Setup Node 1165 | uses: actions/setup-node@v4 1166 | with: 1167 | node-version: '24' 1168 | 1169 | - name: Setup Elm 1170 | run: npm install elm --global 1171 | 1172 | - name: Make 1173 | run: elm make --optimize src/Main.elm 1174 | 1175 | - name: Move files 1176 | run: | 1177 | mkdir ./public 1178 | mv ./index.html ./public/ 1179 | # If you have non-minimal setup with some assets and separate html/js files, 1180 | # provide --output= option for `elm make` and remove this step 1181 | 1182 | - name: Deploy 1183 | uses: peaceiris/actions-gh-pages@v4 1184 | if: github.ref == 'refs/heads/main' 1185 | with: 1186 | github_token: ${{ secrets.GITHUB_TOKEN }} 1187 | publish_dir: ./public 1188 | ``` 1189 | 1190 | ### ⭐️ Swift Publish 1191 | 1192 | An example workflow for [JohnSundell/Publish]. 1193 | 1194 | [JohnSundell/Publish]: https://github.com/JohnSundell/Publish 1195 | 1196 | ```yaml 1197 | name: GitHub Pages 1198 | 1199 | on: 1200 | push: 1201 | branches: 1202 | - main 1203 | pull_request: 1204 | 1205 | jobs: 1206 | deploy: 1207 | runs-on: macos-latest 1208 | concurrency: 1209 | group: ${{ github.workflow }}-${{ github.ref }} 1210 | steps: 1211 | - uses: actions/checkout@v4 1212 | 1213 | - uses: actions/cache@v4 1214 | with: 1215 | path: | 1216 | ~/Publish_build 1217 | .build 1218 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} 1219 | restore-keys: | 1220 | ${{ runner.os }}-spm- 1221 | 1222 | - name: Setup JohnSundell/Publish 1223 | run: | 1224 | cd ${HOME} 1225 | export PUBLISH_VERSION="0.7.0" 1226 | git clone https://github.com/JohnSundell/Publish.git 1227 | cd ./Publish && git checkout ${PUBLISH_VERSION} 1228 | mv ~/Publish_build .build || true 1229 | swift build -c release 1230 | cp -r .build ~/Publish_build || true 1231 | echo "${HOME}/Publish/.build/release" >> ${GITHUB_PATH} 1232 | 1233 | - run: publish-cli generate 1234 | 1235 | - name: Deploy to GitHub Pages 1236 | uses: peaceiris/actions-gh-pages@v4 1237 | if: github.ref == 'refs/heads/main' 1238 | with: 1239 | github_token: ${{ secrets.GITHUB_TOKEN }} 1240 | publish_dir: ./Output 1241 | ``` 1242 | 1243 |
1244 | Back to TOC ☝️ 1245 |
1246 | 1247 | 1248 | 1249 | ## License 1250 | 1251 | - [MIT License - peaceiris/actions-gh-pages] 1252 | 1253 | [MIT License - peaceiris/actions-gh-pages]: https://github.com/peaceiris/actions-gh-pages/blob/main/LICENSE 1254 | 1255 | 1256 | 1257 | ## Maintainer 1258 | 1259 | - [peaceiris homepage](https://peaceiris.com/) 1260 | - [GitHub Action Hero: Shohei Ueda - The GitHub Blog](https://github.blog/2020-03-22-github-action-hero-shohei-ueda/) 1261 | 1262 | 1263 | 1264 |
1265 | Back to TOC ☝️ 1266 |
1267 | --------------------------------------------------------------------------------