├── .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 |
7 |
8 | [](https://github.com/peaceiris/actions-gh-pages/blob/main/LICENSE)
9 | [](https://github.com/peaceiris/actions-gh-pages/releases/latest)
10 | [](https://github.com/peaceiris/actions-gh-pages/releases)
11 | 
12 | 
13 | [](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 | [](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 | |  |  |
187 |
188 |
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 |
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 |
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 |
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 | |  |  |
562 |
563 | | Add your private key | Success |
564 | |---|---|
565 | |  |  |
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 | |  |  |
574 |
575 | | Select branch | Deploying again and succeed |
576 | |---|---|
577 | |  |  |
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 |
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 |
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 |
1267 |
--------------------------------------------------------------------------------