├── .node-version ├── .gitignore ├── .npmrc ├── aqua ├── imports │ └── ghalint.yaml ├── aqua.yaml └── aqua-checksums.json ├── renovate.json5 ├── src ├── index.ts └── run.ts ├── .github └── workflows │ ├── main.yaml │ ├── watch-star.yaml │ ├── wc-renovate-config-validator.yaml │ ├── actionlint.yaml │ ├── release.yaml │ ├── test.yaml │ ├── create-pr-branch.yaml │ ├── autofix.yaml │ ├── wc-ghalint.yaml │ ├── workflow_call_test.yaml │ └── wc-create-pr-branch.yaml ├── tsconfig.json ├── package.json ├── LICENSE ├── action.yaml └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | 20.19.6 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @jsr:registry=https://npm.jsr.io 2 | -------------------------------------------------------------------------------- /aqua/imports/ghalint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/ghalint@v1.5.1 3 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: [ 3 | "github>suzuki-shunsuke/renovate-config#3.3.1", 4 | "github>suzuki-shunsuke/renovate-config:nolimit#3.3.1", 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { main } from "./run"; 3 | 4 | try { 5 | await main(); 6 | } catch (error) { 7 | core.setFailed( 8 | error instanceof Error ? error.message : JSON.stringify(error), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Update the latest branch 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | uses: ./.github/workflows/wc-create-pr-branch.yaml 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | with: 13 | version: latest 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2023"], 4 | "module": "es2022", 5 | "moduleResolution": "bundler", 6 | "target": "ES2023", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /aqua/aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json 3 | # aqua - Declarative CLI Version Manager 4 | # https://aquaproj.github.io/ 5 | checksum: 6 | enabled: true 7 | require_checksum: true 8 | registries: 9 | - type: standard 10 | ref: v4.384.0 # renovate: depName=aquaproj/aqua-registry 11 | import_dir: imports 12 | -------------------------------------------------------------------------------- /.github/workflows/watch-star.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: watch-star 3 | on: 4 | watch: 5 | types: 6 | - started 7 | jobs: 8 | watch-star: 9 | timeout-minutes: 10 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - uses: suzuki-shunsuke/watch-star-action@2b3d259ce2ea06d53270dfe33a66d5642c8010ca # v0.1.1 15 | with: 16 | number: 2 17 | -------------------------------------------------------------------------------- /.github/workflows/wc-renovate-config-validator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: renovate-config-validator 3 | on: workflow_call 4 | jobs: 5 | renovate-config-validator: 6 | # Validate Renovate Configuration by renovate-config-validator. 7 | uses: suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@dcf025732ed76061838048f7f8b0099ae0e89876 # v0.2.5 8 | permissions: 9 | contents: read 10 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: actionlint 3 | on: pull_request 4 | jobs: 5 | actionlint: 6 | runs-on: ubuntu-24.04 7 | timeout-minutes: 10 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | steps: 12 | - uses: suzuki-shunsuke/actionlint-action@29e0b7cda52e51a495d15f22759745ef6e19583a # v0.1.1 13 | with: 14 | actionlint_options: "-ignore dist/index.js" 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | run-name: Release ${{inputs.tag}} 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | tag: 8 | description: version 9 | required: true 10 | pr: 11 | description: "pr number (pre-release)" 12 | required: false 13 | jobs: 14 | release: 15 | uses: ./.github/workflows/wc-create-pr-branch.yaml 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | with: 20 | version: ${{inputs.tag}} 21 | pr: ${{inputs.pr}} 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: pull_request 4 | permissions: {} 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | test: 10 | uses: ./.github/workflows/workflow_call_test.yaml 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | status-check: 15 | runs-on: ubuntu-24.04 16 | if: always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) 17 | timeout-minutes: 10 18 | permissions: {} 19 | needs: 20 | - test 21 | steps: 22 | - run: exit 1 23 | -------------------------------------------------------------------------------- /.github/workflows/create-pr-branch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Create Pull Request Branch 3 | run-name: Create Pull Request Branch (${{inputs.pr}}) 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | pr: 8 | description: "Pull Request Number" 9 | required: true 10 | is_comment: 11 | description: Whether a comment is posted 12 | type: boolean 13 | required: true 14 | jobs: 15 | create-pr-branch: 16 | uses: ./.github/workflows/wc-create-pr-branch.yaml 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | with: 21 | version: "pr/${{inputs.pr}}" 22 | pr: ${{inputs.pr}} 23 | is_comment: ${{inputs.is_comment}} 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "build": "ncc build src/index.ts --license LICENSE", 5 | "fmt": "prettier --write ." 6 | }, 7 | "dependencies": { 8 | "@actions/core": "2.0.1", 9 | "@actions/github": "6.0.1", 10 | "@octokit/rest": "^22.0.0", 11 | "@suzuki-shunsuke/commit-ts": "npm:@jsr/suzuki-shunsuke__commit-ts@^0.1.4", 12 | "@suzuki-shunsuke/github-app-token": "npm:@jsr/suzuki-shunsuke__github-app-token@^0.0.3" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "20.19.27", 16 | "@types/tmp": "0.2.6", 17 | "@vercel/ncc": "0.38.4", 18 | "prettier": "3.7.4", 19 | "ts-node": "10.9.2", 20 | "typescript": "5.9.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: autofix.ci 3 | on: pull_request 4 | permissions: {} 5 | jobs: 6 | autofix: 7 | runs-on: ubuntu-24.04 8 | permissions: {} 9 | timeout-minutes: 15 10 | steps: 11 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 12 | with: 13 | persist-credentials: false 14 | - run: npm ci 15 | - uses: aquaproj/aqua-installer@11dd79b4e498d471a9385aa9fb7f62bb5f52a73c # v4.0.4 16 | with: 17 | aqua_version: v2.55.1 18 | - run: aqua upc -prune 19 | env: 20 | AQUA_GITHUB_TOKEN: ${{github.token}} 21 | - run: npm run fmt 22 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 23 | -------------------------------------------------------------------------------- /.github/workflows/wc-ghalint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ghalint 3 | on: workflow_call 4 | env: 5 | AQUA_LOG_COLOR: always 6 | jobs: 7 | ghalint: 8 | # Validate GitHub Actions Workflows by ghalint. 9 | runs-on: ubuntu-24.04 10 | permissions: {} 11 | timeout-minutes: 15 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 14 | with: 15 | persist-credentials: false 16 | - uses: aquaproj/aqua-installer@11dd79b4e498d471a9385aa9fb7f62bb5f52a73c # v4.0.4 17 | with: 18 | aqua_version: v2.55.1 19 | env: 20 | AQUA_GITHUB_TOKEN: ${{github.token}} 21 | - run: ghalint run 22 | env: 23 | GHALINT_LOG_COLOR: always 24 | AQUA_GITHUB_TOKEN: ${{github.token}} 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Shunsuke Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/workflow_call_test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test (workflow_call) 3 | on: workflow_call 4 | permissions: {} 5 | jobs: 6 | path-filter: 7 | # Get changed files to filter jobs 8 | outputs: 9 | renovate-config-validator: ${{steps.changes.outputs.renovate-config-validator}} 10 | ghalint: ${{steps.changes.outputs.ghalint}} 11 | runs-on: ubuntu-24.04 12 | timeout-minutes: 10 13 | permissions: {} 14 | steps: 15 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 16 | id: changes 17 | with: 18 | filters: | 19 | renovate-config-validator: 20 | - renovate.json5 21 | - .github/workflows/test.yaml 22 | - .github/workflows/wc-renovate-config-validator.yaml 23 | ghalint: 24 | - .github/workflows/*.yaml 25 | - aqua/aqua.yaml 26 | - aqua/imports/ghalint.yaml 27 | - ghalint.yaml 28 | 29 | renovate-config-validator: 30 | uses: ./.github/workflows/wc-renovate-config-validator.yaml 31 | needs: path-filter 32 | if: needs.path-filter.outputs.renovate-config-validator == 'true' 33 | permissions: 34 | contents: read 35 | 36 | ghalint: 37 | needs: path-filter 38 | if: needs.path-filter.outputs.ghalint == 'true' 39 | uses: ./.github/workflows/wc-ghalint.yaml 40 | permissions: {} 41 | 42 | create-pr-branch: 43 | uses: ./.github/workflows/wc-create-pr-branch.yaml 44 | if: github.event.pull_request.user.login == 'suzuki-shunsuke' 45 | permissions: 46 | contents: write 47 | pull-requests: write 48 | with: 49 | version: pr/${{github.event.pull_request.number}} 50 | pr: ${{github.event.pull_request.number}} 51 | -------------------------------------------------------------------------------- /aqua/aqua-checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "checksums": [ 3 | { 4 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_darwin_amd64.tar.gz", 5 | "checksum": "112E4B35C5ABC3E92EC08A7F3FF583909F65BB5C9E649453B1FED139B43C4636", 6 | "algorithm": "sha256" 7 | }, 8 | { 9 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_darwin_arm64.tar.gz", 10 | "checksum": "F65636894E3E98B2413EA8BDCFF80FD7DDD661741000F6566EDBE157DE079BE4", 11 | "algorithm": "sha256" 12 | }, 13 | { 14 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_linux_amd64.tar.gz", 15 | "checksum": "E791009D9361C8F0F0D2E2B9B67D428FE2DDDC6694CBFEC9954E2502A8E0E0FF", 16 | "algorithm": "sha256" 17 | }, 18 | { 19 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_linux_arm64.tar.gz", 20 | "checksum": "67300713993EE6FC62AD72B6495D0A0B0002A6B3603E1BC49E4BF9F11C4B3E0E", 21 | "algorithm": "sha256" 22 | }, 23 | { 24 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_windows_amd64.zip", 25 | "checksum": "C020B03CC5785FDC0E170139EE171961E3A5F6A66A4D4B813DBD1C0DA48D74C5", 26 | "algorithm": "sha256" 27 | }, 28 | { 29 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.1/ghalint_1.5.1_windows_arm64.zip", 30 | "checksum": "86B971284006BBE357ACA6083E6B698CC0089410AED42A6FB5F7008D10A71530", 31 | "algorithm": "sha256" 32 | }, 33 | { 34 | "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.384.0/registry.yaml", 35 | "checksum": "48D1A8057BA09715E9F0AA4D3E110E9B5E19FFD8E83EC90F49E7623D8BF09430", 36 | "algorithm": "sha256" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/wc-create-pr-branch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release (workflow call) 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | description: Version 8 | required: true 9 | type: string 10 | pr: 11 | description: Pull Request Number 12 | required: false 13 | type: string 14 | is_comment: 15 | description: If the comment is posted 16 | required: false 17 | default: false 18 | type: boolean 19 | jobs: 20 | create-pr-branch: 21 | timeout-minutes: 20 22 | runs-on: ubuntu-24.04 23 | permissions: 24 | contents: write 25 | pull-requests: write 26 | steps: 27 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 28 | with: 29 | persist-credentials: false 30 | - run: gh pr checkout "$PR" 31 | if: inputs.pr != '' 32 | env: 33 | GITHUB_TOKEN: ${{github.token}} 34 | PR: ${{inputs.pr}} 35 | - run: npm ci 36 | - run: npm run build 37 | 38 | - uses: suzuki-shunsuke/release-js-action@23ab6d1545309c79664bc0e9aea74daf27339193 # v0.1.8-2 39 | with: 40 | version: ${{inputs.version}} 41 | pr: ${{inputs.pr}} 42 | is_comment: ${{inputs.is_comment}} 43 | 44 | # Test 45 | - if: github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork 46 | run: date > date.txt 47 | - if: github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork 48 | uses: ./ 49 | with: 50 | branch: test-pr-${{github.event.pull_request.number}} 51 | files: date.txt 52 | - if: github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork 53 | run: date > date.txt 54 | - if: github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork 55 | uses: ./ 56 | with: 57 | github_token: ${{github.token}} 58 | branch: test-pr-${{github.event.pull_request.number}} 59 | files: date.txt 60 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: Create Commit By GitHub API 2 | description: Commit changes by GitHub API 3 | author: Shunsuke Suzuki 4 | branding: 5 | icon: git-commit 6 | color: blue 7 | inputs: 8 | github_token: 9 | description: | 10 | GitHub Access Token 11 | contents:write - Push commits 12 | required: false 13 | default_github_token: 14 | description: | 15 | A default GitHub Access Token. 16 | This is used if `github_token` and `app_id`/`app_private_key` are not specified. 17 | contents:write - Push commits 18 | required: false 19 | default: ${{ github.token }} 20 | app_id: 21 | description: | 22 | GitHub App ID 23 | required: false 24 | app_private_key: 25 | description: | 26 | GitHub App Private Key 27 | required: false 28 | commit_message: 29 | description: | 30 | Commit message 31 | required: false 32 | default: "commit changes" 33 | branch: 34 | description: | 35 | A Pushed branch. 36 | By default, GITHUB_HEAD_REF or GITHUB_REF_NAME is used. 37 | required: false 38 | parent_branch: 39 | description: | 40 | If a branch is newly created, the parent branch is used as the base branch. 41 | By default, the default branch is used. 42 | required: false 43 | repository: 44 | description: | 45 | A Pushed repository. 46 | By default, GITHUB_REPOSITORY is used. 47 | required: false 48 | root_dir: 49 | description: | 50 | The Git root directory for the files to commit. 51 | required: false 52 | files: 53 | description: | 54 | Files to commit. Unchanged files are ignored. 55 | To specify multiple files, separate them with a newline. 56 | By default, all modified and untracked files are committed. 57 | git ls-files --modified --others --exclude-standard 58 | required: false 59 | workflow: 60 | description: | 61 | How to handle changed workflow files. 62 | This input is used if `app_id` and `app_private_key` are specified. 63 | To commit workflow files, the permission `workflows:write` is required. 64 | The following values are available: 65 | 1. allow - Grant `workflows:write` permission when creating an access token 66 | 2. deny - Fail if workflow files are changed 67 | 3. ignore - Ignore workflow files 68 | required: false 69 | default: allow 70 | fail_on_self_push: 71 | description: | 72 | If true (default), the action fails when it pushes a commit to the same repo/branch as the current workflow run. 73 | Set to false to avoid failing and check outputs instead. 74 | required: false 75 | default: "true" 76 | runs: 77 | using: "node20" 78 | main: "dist/index.js" 79 | post: "dist/index.js" 80 | -------------------------------------------------------------------------------- /src/run.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 commit from "@suzuki-shunsuke/commit-ts"; 5 | import * as githubAppToken from "@suzuki-shunsuke/github-app-token"; 6 | 7 | export const main = async () => { 8 | if (core.getState("post")) { 9 | const token = core.getState("token"); 10 | const expiresAt = core.getState("expires_at"); 11 | if (token) { 12 | if (expiresAt && githubAppToken.hasExpired(expiresAt)) { 13 | core.info("GitHub App token has already expired"); 14 | return; 15 | } 16 | // This is post-cleanup: revoke the token created during main execution 17 | core.info("Revoking GitHub App token"); 18 | return githubAppToken.revoke(token); 19 | } 20 | return; 21 | } 22 | core.saveState("post", "true"); 23 | 24 | const permissions: githubAppToken.Permissions = { 25 | contents: "write", 26 | }; 27 | 28 | let files = await getFiles(); 29 | if (files.length === 0) { 30 | core.notice("No changes"); 31 | return; 32 | } 33 | const workflowOption = core.getInput("workflow"); 34 | if (workflowOption === "ignore") { 35 | core.notice("Ignore workflow files"); 36 | files = files.filter((f) => !f.startsWith(".github/workflows/")); 37 | if (files.length === 0) { 38 | core.notice("No changes after ignoring workflow files"); 39 | return; 40 | } 41 | } else if (workflowOption === "allow") { 42 | if (files.some((f) => f.startsWith(".github/workflows/"))) { 43 | core.notice("Grant workflows:write permission"); 44 | permissions.workflows = "write"; 45 | } 46 | } 47 | 48 | // This is main execution: continue with normal processing 49 | const defaultBranch = 50 | process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME; 51 | const branch = core.getInput("branch") || defaultBranch; 52 | const failOnSelfPush = core.getBooleanInput("fail_on_self_push"); 53 | if (!branch) { 54 | core.setFailed("Branch input is required."); 55 | return; 56 | } 57 | const repoFullName = core.getInput("repository"); 58 | let owner = github.context.repo.owner; 59 | let repo = github.context.repo.repo; 60 | if (repoFullName) { 61 | const [o, r] = repoFullName.split("/"); 62 | if (!o || !r) { 63 | core.setFailed("Invalid repository format. Use 'owner/repo'."); 64 | return; 65 | } 66 | owner = o; 67 | repo = r; 68 | } 69 | const token = await getToken(owner, repo, permissions); 70 | core.info( 71 | `creating a commit: ${JSON.stringify({ 72 | owner: owner, 73 | repo: repo, 74 | branch: branch, 75 | message: core.getInput("commit_message") || "Commit changes", 76 | files: files, 77 | rootDir: core.getInput("root_dir"), 78 | baseBranch: core.getInput("parent_branch"), 79 | deleteIfNotExist: true, 80 | })}`, 81 | ); 82 | const octokit = github.getOctokit(token); 83 | const result = await commit.createCommit(octokit, { 84 | owner: owner, 85 | repo: repo, 86 | branch: branch, 87 | message: core.getInput("commit_message") || "Commit changes", 88 | files: files, 89 | rootDir: core.getInput("root_dir"), 90 | baseBranch: core.getInput("parent_branch"), 91 | deleteIfNotExist: true, 92 | logger: { 93 | info: core.info, 94 | }, 95 | }); 96 | const pushed = result?.commit.sha !== undefined && result?.commit.sha !== ""; 97 | core.setOutput("sha", result?.commit.sha || ""); 98 | core.setOutput("pushed", pushed); 99 | const isSameTarget = 100 | `${owner}/${repo}` === 101 | `${github.context.repo.owner}/${github.context.repo.repo}` && 102 | branch === defaultBranch; 103 | const selfPush = pushed && isSameTarget; 104 | core.setOutput("self_push", selfPush); 105 | if (selfPush) { 106 | if (failOnSelfPush) { 107 | core.setFailed("a commit was pushed"); 108 | return; 109 | } 110 | core.notice("a commit was pushed"); 111 | } 112 | }; 113 | 114 | const getToken = async ( 115 | owner: string, 116 | repo: string, 117 | permissions: githubAppToken.Permissions, 118 | ): Promise => { 119 | const token = core.getInput("github_token"); 120 | if (token) { 121 | return token; 122 | } 123 | const appId = core.getInput("app_id"); 124 | const appPrivateKey = core.getInput("app_private_key"); 125 | if (appId) { 126 | if (!appPrivateKey) { 127 | throw new Error("app_private_key is required when app_id is provided"); 128 | } 129 | core.info( 130 | `creating a GitHub App token: ${JSON.stringify({ 131 | owner: owner, 132 | repositories: [repo], 133 | permissions: permissions, 134 | })}`, 135 | ); 136 | const appToken = await githubAppToken.create({ 137 | appId: appId, 138 | privateKey: appPrivateKey, 139 | owner: owner, 140 | repositories: [repo], 141 | permissions: permissions, 142 | }); 143 | core.saveState("token", appToken.token); 144 | core.saveState("expires_at", appToken.expiresAt); 145 | return appToken.token; 146 | } 147 | if (appPrivateKey) { 148 | throw new Error("app_id is required when app_private_key is provided"); 149 | } 150 | return core.getInput("default_github_token"); 151 | }; 152 | 153 | const getFiles = async (): Promise => { 154 | const files = core.getMultilineInput("files"); 155 | if (files.length > 0) { 156 | return files; 157 | } 158 | const gitLsFilesOutput: string[] = []; 159 | const out = await exec.getExecOutput( 160 | "git", 161 | ["ls-files", "--modified", "--others", "--exclude-standard"], 162 | { 163 | cwd: core.getInput("root_dir") || undefined, 164 | }, 165 | ); 166 | out.stdout.split("\n").forEach((line) => { 167 | const trimmed = line.trim(); 168 | if (trimmed) { 169 | gitLsFilesOutput.push(trimmed); 170 | } 171 | }); 172 | return gitLsFilesOutput; 173 | }; 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # commit-action 2 | 3 | [![DeepWiki](https://img.shields.io/badge/DeepWiki-suzuki--shunsuke%2Fcommit--action-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/suzuki-shunsuke/commit-action) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/commit-action/main/LICENSE) | [action.yaml](action.yaml) 4 | 5 | commit-action is a GitHub Action to push changes to remote branches by GitHub API. 6 | You can create **verified** commits using GitHub App. 7 | 8 | image 9 | 10 | ## Blog posts 11 | 12 | - [2025-02-15 GitHub Actions で Verified Commit でコードを自動修正](https://zenn.dev/shunsuke_suzuki/articles/commit-action) 13 | - [2025-02-15 Fix Code Via GitHub Actions By Verified Commits](https://dev.to/suzukishunsuke/fix-code-via-github-actions-by-verified-commits-3o1d) 14 | 15 | ## Why Use commit-action? 16 | 17 | Unlike similar actions, **commit-action creates and pushes commits by GitHub API instead of Git commands**. 18 | So you can create **verified commits** using GitHub Actions token `${{github.token}}` or a GitHub App installation access token. 19 | 20 | Commit signing is so important for security. 21 | 22 | https://docs.github.com/en/authentication/managing-commit-signature-verification 23 | 24 | To create verified commits using Git, a GPG key or SSH key is required. 25 | It's bothersome to manage GPG keys and SSH keys properly for automation, so it's awesome that commit-action can create verified commits without them. 26 | 27 | ## GitHub Access Token 28 | 29 | You can use the following things: 30 | 31 | - :thumbsup: GitHub App Installation access token: We recommend this 32 | - :thumbsdown: GitHub Personal Access Token: This can't create verified commits 33 | - :thumbsdown: `${{secrets.GITHUB_TOKEN}}`: This can't trigger new workflow runs. 34 | 35 | https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow 36 | 37 | > When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN, with the exception of workflow_dispatch and repository_dispatch, will not create a new workflow run. 38 | 39 | ### Required permissions 40 | 41 | `contents:write` is required. 42 | Furthermore, if you want to fix workflow files, `workflows:write` is also required. 43 | 44 | ## How To Use 45 | 46 | commit-action is so easy to use. 47 | All inputs are optional. 48 | 49 | You only need to run commit-action after fixing code in workflows. 50 | Then it creates and pushes a commit to a remote branch. 51 | 52 | ```yaml 53 | name: Example 54 | on: 55 | pull_request: {} 56 | jobs: 57 | example: 58 | runs-on: ubuntu-24.04 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 62 | with: 63 | persist-credentials: false 64 | 65 | # Fix files 66 | # ... 67 | 68 | - name: Push changes to the remote branch 69 | uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 70 | ``` 71 | 72 | commit-action fails if it pushes a commit to `${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}` in `$GITHUB_REPOSITORY`. 73 | If you want to continue without failing, set `fail_on_self_push: false` and check outputs instead (see `self_push`). 74 | If no change is pushed, commit-action does nothing and exits successfully. 75 | 76 | ### branch, repository 77 | 78 | By default, commit-action pushes a commit to `${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}` in `$GITHUB_REPOSITORY`, but you can change them. 79 | 80 | ```yaml 81 | - uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 82 | with: 83 | branch: foo 84 | repository: suzuki-shunsuke/tfcmt 85 | ``` 86 | 87 | ### parent branch 88 | 89 | If a new branch is created, the parent branch is the default branch by default. 90 | You can specify the paretn branch. 91 | 92 | ```yaml 93 | - uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 94 | with: 95 | branch: foo-2 96 | parent_branch: foo 97 | ``` 98 | 99 | ### GitHub Access token 100 | 101 | `${{github.token}}` is used by default, but we don't recommend it because `${{github.token}}` doesn't trigger a new workflow run. 102 | We recommend GitHub App installation access tokens. 103 | You can create a GitHub App installation access token and pass it to commit-action yourself, but you can also pass a pair of GitHub App ID and private key. 104 | Then commit-action creates a GitHub App installation access token with minimum `repositories` and `permissions`. 105 | 106 | ```yaml 107 | - uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 108 | with: 109 | app_id: ${{secrets.APP_ID}} 110 | app_private_key: ${{secrets.APP_PRIVATE_KEY}} 111 | ``` 112 | 113 | ### files 114 | 115 | commit-action commits all created, updated, and deleted files by default, but you can also commit only specific files. 116 | And you can also change the commit message. 117 | 118 | ```yaml 119 | - uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 120 | with: 121 | commit_message: "style: format code" 122 | files: | 123 | README.md 124 | package-lock.json 125 | fail_on_self_push: false # continue without failing when self-pushing 126 | 127 | ### Outputs 128 | 129 | - `pushed`: true if a commit was pushed. 130 | - `sha`: the pushed commit SHA (empty if none). 131 | - `self_push`: true if a commit was pushed to the same repo/branch as the current workflow run. 132 | ``` 133 | 134 | ### Fix workflow files 135 | 136 | If you want to fix workflow files, the permission `workflows:write` is required. 137 | The input `workflow` changes the behaviour when workflow files are changed. 138 | The input is used if `app_id` and `app_private_key` are passed. 139 | The following values are available: 140 | 141 | 1. `allow` (default) - Grant workflows:write permission when issuing an access token 142 | 1. `deny` - Fail if workflow files are changed 143 | 1. `ignore` - Ignore workflow files 144 | 145 | ```yaml 146 | - uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 147 | with: 148 | workflow: ignore # allow (default), deny 149 | ``` 150 | 151 | ## Available versions 152 | 153 | commit-action's main branch and feature branches don't work. 154 | [Please see the document](https://github.com/suzuki-shunsuke/release-js-action/blob/main/docs/available_versions.md). 155 | --------------------------------------------------------------------------------