├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── feature-request.yml │ ├── general.yml │ ├── question.yml │ └── support-request.yml ├── pull_request_template.md └── workflows │ ├── actionlint.yaml │ ├── autofix.yaml │ ├── check-commit-signing.yaml │ ├── renovate-config-validator.yaml │ ├── test-action.yaml │ ├── test.yaml │ ├── typos.yaml │ ├── watch-star.yaml │ └── workflow_call_test.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _typos.toml ├── action.yaml ├── aqua ├── aqua-checksums.json ├── aqua.yaml └── imports │ ├── nllint.yaml │ ├── pinact.yaml │ └── typos.yaml ├── docs └── client.md ├── renovate.json5 └── server ├── commit ├── README.md └── action.yaml ├── notify ├── README.md └── action.yaml └── prepare ├── README.md └── action.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository 2 | github: 3 | - suzuki-shunsuke 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: | 3 | Please report the bug of Server Action. 4 | If you're not sure if it's a bug or not, please use the template `Support Request` instead. 5 | labels: 6 | - bug 7 | body: 8 | - type: textarea 9 | id: version 10 | attributes: 11 | label: version 12 | value: | 13 | Versions that the issue occurs: 14 | 15 | (Opitonal) Versions that the issue doesn't occur: 16 | 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: overview 21 | attributes: 22 | label: Overview 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: how-to-reproduce 27 | attributes: 28 | label: How to reproduce 29 | description: | 30 | Please see [the guide](https://github.com/suzuki-shunsuke/oss-contribution-guide#write-good-how-to-reproduce) too. 31 | workflow should be not partial but complete configuration. 32 | Please remove unnecessary code to reproduce the issue. 33 | value: | 34 | Workflow: 35 | 36 | ```yaml 37 | 38 | ``` 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: expected-behaviour 43 | attributes: 44 | label: Expected behaviour 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: actual-behaviour 49 | attributes: 50 | label: Actual behaviour 51 | validations: 52 | required: true 53 | - type: textarea 54 | id: note 55 | attributes: 56 | label: Note 57 | validations: 58 | required: false 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: If you want to add a feature. 3 | labels: 4 | - enhancement 5 | body: 6 | - type: textarea 7 | id: feature-overview 8 | attributes: 9 | label: Feature Overview 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: why 14 | attributes: 15 | label: Why is the feature needed? 16 | description: Please explain the problem you want to solve. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: example-code 21 | attributes: 22 | label: Example Code 23 | description: | 24 | Please explain the feature with code. For example, if you want a new input, please explain the usage of it. 25 | value: | 26 | Workflow: 27 | 28 | ```yaml 29 | 30 | ``` 31 | validations: 32 | required: false 33 | - type: textarea 34 | id: note 35 | attributes: 36 | label: Note 37 | validations: 38 | required: false 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general.yml: -------------------------------------------------------------------------------- 1 | name: General 2 | description: Please use this template only when other templates don't meet your requirement 3 | labels: 4 | - general 5 | body: 6 | - type: textarea 7 | id: what 8 | attributes: 9 | label: What 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: why 14 | attributes: 15 | label: Why 16 | description: Please explain the background and the reason of this issue 17 | validations: 18 | required: false 19 | - type: textarea 20 | id: note 21 | attributes: 22 | label: Note 23 | description: Please write any additional information 24 | validations: 25 | required: false 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: | 3 | Please use this template when you have any questions. 4 | Please don't hesitate to ask any questions via Issues. 5 | labels: 6 | - question 7 | body: 8 | - type: textarea 9 | id: question 10 | attributes: 11 | label: Question 12 | description: | 13 | Please explain your question in details. 14 | If example code is useful to explain your question, please write it. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: background 19 | attributes: 20 | label: Background 21 | description: Please explain the background and why you have the question 22 | validations: 23 | required: false 24 | - type: textarea 25 | id: note 26 | attributes: 27 | label: Note 28 | description: Please write any additional information 29 | validations: 30 | required: false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-request.yml: -------------------------------------------------------------------------------- 1 | name: Support Request 2 | description: | 3 | Please use this template when you face any problem (not bug) and need our help. 4 | If you're not sure if it's a bug or not, please use this template. 5 | labels: 6 | - support-request 7 | body: 8 | - type: textarea 9 | id: version 10 | attributes: 11 | label: version 12 | value: | 13 | Versions that the issue occurs: 14 | 15 | (Opitonal) Versions that the issue doesn't occur: 16 | 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: overview 21 | attributes: 22 | label: Overview 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: how-to-reproduce 27 | attributes: 28 | label: How to reproduce 29 | description: | 30 | Please see [the guide](https://github.com/suzuki-shunsuke/oss-contribution-guide#write-good-how-to-reproduce) too. 31 | workflow should be not partial but complete configuration. 32 | Please remove unnecessary code to reproduce the issue. 33 | value: | 34 | Workflow: 35 | 36 | ```yaml 37 | 38 | ``` 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: expected-behaviour 43 | attributes: 44 | label: Expected behaviour 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: actual-behaviour 49 | attributes: 50 | label: Actual behaviour 51 | validations: 52 | required: true 53 | - type: textarea 54 | id: note 55 | attributes: 56 | label: Note 57 | validations: 58 | required: false 59 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Check List 2 | 3 | 4 | 5 | - [ ] Read [CONTRIBUTING.md](https://github.com/csm-actions/securefix-action/blob/main/CONTRIBUTING.md) 6 | - [ ] [Write a GitHub Issue before creating a Pull Request](https://github.com/suzuki-shunsuke/oss-contribution-guide/blob/main/README.md#create-an-issue-before-creating-a-pull-request) 7 | - Link to the issue: 8 | - [Avoid force push](https://github.com/suzuki-shunsuke/oss-contribution-guide?tab=readme-ov-file#dont-do-force-pushes-after-opening-pull-requests) 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: actionlint 3 | on: pull_request 4 | permissions: {} 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-24.04 8 | if: failure() 9 | timeout-minutes: 10 10 | permissions: {} 11 | needs: 12 | - main 13 | steps: 14 | - run: exit 1 15 | main: 16 | uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@1af1919efef445ad960f5ab4cf370e47e961d721 # v2.0.0 17 | permissions: 18 | pull-requests: write 19 | contents: read 20 | -------------------------------------------------------------------------------- /.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | with: 13 | persist-credentials: false 14 | - uses: aquaproj/aqua-installer@5e54e5cee8a95ee2ce7c04cb993da6dfad13e59c # v3.1.2 15 | with: 16 | aqua_version: v2.50.1 17 | env: 18 | GITHUB_TOKEN: ${{github.token}} 19 | - run: aqua upc -prune 20 | - run: git ls-files | xargs nllint -f -s 21 | - run: | 22 | git ls-files | grep -E '\.md$' | xargs pinact run -u 23 | pinact run -u 24 | - uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944 # v1.3.1 25 | -------------------------------------------------------------------------------- /.github/workflows/check-commit-signing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Check if all commits are signed 3 | on: 4 | pull_request_target: 5 | branches: [main] 6 | concurrency: 7 | group: ${{ github.workflow }}--${{ github.head_ref }} # github.ref is unavailable in case of pull_request_target 8 | cancel-in-progress: true 9 | jobs: 10 | check-commit-signing: 11 | uses: suzuki-shunsuke/check-commit-signing-workflow/.github/workflows/check.yaml@547eee345f56310a656f271ec5eaa900af46b0fb # v0.1.0 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | -------------------------------------------------------------------------------- /.github/workflows/renovate-config-validator.yaml: -------------------------------------------------------------------------------- 1 | name: renovate-config-validator 2 | permissions: {} 3 | on: workflow_call 4 | jobs: 5 | renovate-config-validator: 6 | uses: suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@e8effbd185cbe3874cddef63f48b8bdcfc9ada55 # v0.2.4 7 | permissions: 8 | contents: read 9 | -------------------------------------------------------------------------------- /.github/workflows/test-action.yaml: -------------------------------------------------------------------------------- 1 | # This is a dummy workflow file to test actions using actionlint. 2 | # This workflow is never run. 3 | name: test action (actionlint) 4 | on: workflow_call 5 | jobs: 6 | actionlint-action: 7 | timeout-minutes: 10 8 | runs-on: ubuntu-24.04 9 | permissions: {} 10 | steps: 11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | with: 13 | persist-credentials: false 14 | - uses: ./ 15 | with: 16 | app_id: "dummy" 17 | app_private_key: "dummy" 18 | - uses: ./server/prepare 19 | id: prepare 20 | with: 21 | app_id: "dummy" 22 | app_private_key: "dummy" 23 | - uses: ./server/commit 24 | with: 25 | outputs: ${{ toJson(steps.prepare.outputs) }} 26 | - uses: ./server/notify 27 | with: 28 | outputs: ${{ toJson(steps.prepare.outputs) }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | permissions: {} 3 | on: pull_request 4 | jobs: 5 | status-check: 6 | runs-on: ubuntu-24.04 7 | if: failure() 8 | timeout-minutes: 10 9 | permissions: {} 10 | needs: 11 | - test 12 | steps: 13 | - run: exit 1 14 | test: 15 | uses: ./.github/workflows/workflow_call_test.yaml 16 | permissions: 17 | contents: read 18 | -------------------------------------------------------------------------------- /.github/workflows/typos.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: typos 3 | on: workflow_call 4 | env: 5 | AQUA_LOG_COLOR: always 6 | jobs: 7 | typos: 8 | timeout-minutes: 15 9 | runs-on: ubuntu-latest 10 | permissions: {} 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | with: 14 | persist-credentials: false 15 | - uses: aquaproj/aqua-installer@5e54e5cee8a95ee2ce7c04cb993da6dfad13e59c # v3.1.2 16 | with: 17 | aqua_version: v2.50.1 18 | env: 19 | AQUA_GITHUB_TOKEN: ${{github.token}} 20 | - run: typos 21 | -------------------------------------------------------------------------------- /.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-24.04 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/workflow_call_test.yaml: -------------------------------------------------------------------------------- 1 | name: test (workflow_call) 2 | permissions: {} 3 | on: workflow_call 4 | jobs: 5 | path-filter: 6 | timeout-minutes: 10 7 | outputs: 8 | renovate-config-validator: ${{steps.changes.outputs.renovate-config-validator}} 9 | runs-on: ubuntu-24.04 10 | permissions: {} 11 | steps: 12 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 13 | id: changes 14 | with: 15 | filters: | 16 | renovate-config-validator: 17 | - renovate.json5 18 | - .github/workflows/workflow_call_test.yaml 19 | - .github/workflows/renovate-config-vdalidator.yaml 20 | 21 | renovate-config-validator: 22 | uses: ./.github/workflows/renovate-config-validator.yaml 23 | needs: path-filter 24 | if: needs.path-filter.outputs.renovate-config-validator == 'true' 25 | permissions: 26 | contents: read 27 | 28 | typos: 29 | uses: ./.github/workflows/typos.yaml 30 | permissions: {} 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the following document. 4 | 5 | - https://github.com/suzuki-shunsuke/oss-contribution-guide 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 csm-actions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Securefix Action 2 | 3 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/csm-actions/securefix-action/main/LICENSE) | [Versioning Policy](https://github.com/suzuki-shunsuke/versioning-policy/blob/main/POLICY.md) 4 | 5 | Securefix Action is GitHub Actions to fix code securely. 6 | 7 | ![image](https://github.com/user-attachments/assets/21ec46f9-3c9b-4314-8609-0ef1b8c25791) 8 | 9 | ![image](https://github.com/user-attachments/assets/5d854cf1-cff1-4af2-ab71-81cba3d8eb1d) 10 | 11 | Securefix Action allows you to fix code securely without sharing a GitHub App private key with strong permissions such as `contents:write` across GitHub Actions workflows. 12 | You don't need to allow external services to access your code. 13 | It elevates the security of your workflows to the next level. 14 | 15 | Furthermore, it's easy to use. 16 | You don't need to host a server application. 17 | It achieves a server/client architecture using GitHub Actions by unique approach. 18 | 19 | ## Features 20 | 21 | - 💪 Increase the developer productivity by fixing code in CI 22 | - 🛡 Secure 23 | - You don't need to pass a GitHub App private key with strong permissions to GitHub Actions workflows on the client side 24 | - You don't need to allow external services to access your code 25 | - You can define custom validation before creating a commit 26 | - Commits are verified (signed) 27 | - 😊 Easy to use 28 | - You can create a commit by one action on the client side 29 | - You don't need to host a server application 30 | - 😉 [OSS (MIT License)](LICENSE) 31 | 32 | ## Overview 33 | 34 | Sometimes you want to fix code in CI: 35 | 36 | - Format code 37 | - Generate document from code 38 | - etc 39 | 40 | In case of public repositories, we strongly recommend [autofix.ci](https://autofix.ci). 41 | autofix.ci allows you to fix code in CI of pull requests including pull requests from fork repositories securely. 42 | autofix.ci is easy to use, and it's free in public repositories. We love it. 43 | 44 | autofix.ci is also available in private repositories, but perhaps it's a bit hard to use it in your private repositories for your business. 45 | 46 | - It's not free, so you may have to submit a request to your company 47 | - You need to allow the external server of autofix.ci to access your code. Perhaps you can't accept it 48 | - If you don't receive pull requests from fork repositories, the reason to use autofix.ci might not be so string because you can fix code using actions such as [commit-action](https://github.com/suzuki-shunsuke/commit-action) 49 | 50 | We think autofix.ci is valuable in private repositories too, but perhaps you can't use it. 51 | 52 | If you use fix code in CI, you need to use an access token with `contents:write` permission. 53 | But if the token is abused, malicious code can be committed. 54 | For instance, an attacker can create a malicious commit to a pull request, and he may be able to approve and merge the pull request. 55 | It's so dangerous. 56 | 57 | To prevent such a threat, you should protect personal access tokens and GitHub Apps having strong permissions securely. 58 | 59 | One solution is [the Client/Server Model](https://github.com/securefix-action/client-server-model-docs). 60 | Clients are GitHub Actions workflows that want to fix code. 61 | They send a request to the server, then the server fix code. 62 | You don't need to pass an access token with strong permissions to clients (GitHub Actions Workflows). 63 | 64 | Then how do you build the server? 65 | For instance, you would be able to build the server using AWS Lambda, Google Cloud Function, or k8s, and so on. 66 | But we don't want to host such a server application. 67 | 68 | So we build a server using GitHub Actions workflow by unique approach. 69 | You don't need to host a server application. 70 | 71 | ## Example 72 | 73 | - [demo-server](https://github.com/securefix-action/demo-server): [workflow](https://github.com/securefix-action/demo-server/blob/main/.github/workflows/securefix.yaml) 74 | - [demo-client](https://github.com/securefix-action/demo-client): [workflow](https://github.com/securefix-action/demo-client/blob/main/.github/workflows/securefix.yaml) 75 | 76 | ## Architecture 77 | 78 | Securefix Action adopts [the Client/Server Model](https://github.com/securefix-action/client-server-model-docs). 79 | It uses following GitHub Apps, repositories, and workflows: 80 | 81 | - two GitHub Apps 82 | - a Server GitHub App: a GitHub App to create commits 83 | - a Client GitHub App: a GitHub App to send requests to a server workflow 84 | - Repositories 85 | - a Server repository: a repository where a server workflow works 86 | - Client repositories: repositories where client workflows work 87 | - Workflows 88 | - a Server Workflow: Receive requests from client workflows and create commits 89 | - a Client Workflow: Request to fix code to the Server Workflow 90 | 91 | ![Image](https://github.com/user-attachments/assets/94781831-0aad-4513-ac92-fb5cfa859e19) 92 | 93 | - Server: 1 GitHub App, 1 Repository 94 | - Client: 1 GitHub App, N Repositories 95 | 96 | ![Image](https://github.com/user-attachments/assets/383de1da-a267-4f96-a86c-9151d66cebc5) 97 | 98 | 1. The client workflow uploads fixed files and metadata to GitHub Actions Artifacts 99 | 2. The client workflow creates an issue label to the server repository (The label is deleted immediately) 100 | 3. The server workflow is triggered by `label:created` event 101 | 4. The server workflow downloads fixed files and metadata from GitHub Actions Artifacts 102 | 5. The server workflow validates the request 103 | 6. The server workflow pushes a commit to the client repository 104 | 105 | ### :bulb: Why are labels used? 106 | 107 | Securefix Action uses `label` event to trigger a server workflow. 108 | Generally `repository_dispatch` events are used to trigger workflows by API, but they require the permission `contents:write`. 109 | If we grant `contents:write` permissions to client workflows, this action has no sense because clients can create commits without this action. 110 | So we looked for alternative events from [all events](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows), and we found `label` event. 111 | Even if the permission is abused, the risk is low. 112 | 113 | ## Getting Started 114 | 115 | 1. Create two repositories from templates [demo-server](https://github.com/new?template_name=demo-server&template_owner=securefix-action) and [demo-client](https://github.com/new?template_name=demo-client&template_owner=securefix-action) 116 | 1. [Create a GitHub App for server](#github-app-for-server) 117 | 1. [Create a GitHub App for client](#github-app-for-client) 118 | 1. Create GitHub App private keys 119 | 1. [Add GitHub App's id and private keys to GitHub Secrets and Variables](#add-github-apps-id-and-private-keys-to-github-secrets-and-variables) 120 | 1. [Fix the server workflow if necessary](#fix-the-server-workflow-if-necessary) 121 | 1. [Fix the client workflow if necessary](#fix-the-client-workflow-if-necessary) 122 | 1. [Add a new file `bar.yaml` to client' repository and create a pull request](#add-a-new-file-baryaml-to-client-repository-and-create-a-pull-request) 123 | 124 | ### GitHub App for server 125 | 126 | Deactivate Webhook. 127 | 128 | Permissions: 129 | 130 | - `contents:write`: To create commits 131 | - `actions:read`: To download GitHub Actions Artifacts to fix code 132 | - `workflows:write`: Optional. This is required if you want to fix GitHub Actions workflows 133 | - `pull_requests:write`: To notify problems on the server side to pull requests 134 | 135 | Installed Repositories: Install the app into the server repository and client repositories. 136 | 137 | ### GitHub App for client 138 | 139 | Deactivate Webhook. 140 | 141 | Permissions: 142 | 143 | - `issues:write`: To create labels 144 | 145 | Installed Repositories: Install the app into the server repository and client repositories. 146 | 147 | ### Add GitHub App's id and private keys to GitHub Secrets and Variables 148 | 149 | Add GitHub App's private keys and ID to Repository Secrets and Variables 150 | 151 | - client 152 | - id: client repository's variable `DEMO_CLIENT_APP_ID` 153 | - private key: client repository's Repository Secret `DEMO_CLIENT_PRIVATE_KEY` 154 | - server 155 | - id: server repository's variable `DEMO_SERVER_APP_ID` 156 | - private key: server repository's Repository Secret `DEMO_SERVER_PRIVATE_KEY` 157 | 158 | > [!WARNING] 159 | > In the getting started, we add private keys to Repository Secrets simply. 160 | > But when you use Securefix Action actually, you must manage the Server GitHub App's private key and the server workflow securely. 161 | > Only the server workflow must be able to access the server app's private key. 162 | > [See also `How to manage a server GitHub App and a server workflow`](#how-to-manage-a-server-github-app-and-a-server-workflow). 163 | 164 | ### Fix the server workflow if necessary 165 | 166 | [Workflow](https://github.com/securefix-action/demo-server/blob/main/.github/workflows/securefix.yaml) 167 | 168 | If you change a variable name and a secret name, please fix the workflow. 169 | 170 | ### Fix the client workflow if necessary 171 | 172 | [Workflow](https://github.com/securefix-action/demo-client/blob/main/.github/workflows/securefix.yaml) 173 | 174 | - If you change a variable name and a secret name, please fix the workflow 175 | - If you change the server repository name, please fix the input `server_repository` 176 | 177 | ### Add a new file `bar.yaml` to client' repository and create a pull request 178 | 179 | 1. Add `bar.yaml` to client repository ([Example](https://github.com/securefix-action/demo-client/pull/8/commits/0a0103a3c319f08739632b72aef6b539377da11b)) 180 | 181 | bar.yaml: 182 | 183 | ```yaml 184 | names: 185 | - bar 186 | ``` 187 | 188 | 2. Create a pull request ([Example](https://github.com/securefix-action/demo-client/pull/8)): 189 | 190 | Then workflows are run and `bar.yaml` is fixed automatically: 191 | 192 | ![image](https://github.com/user-attachments/assets/808b7348-f1a6-41ff-97fb-c4125f31ed14) 193 | 194 | ![image](https://github.com/user-attachments/assets/610668c2-a6e9-4c9b-a02a-381f9f1cd56a) 195 | 196 | [commit](https://github.com/securefix-action/demo-client/pull/8/commits/e8b1f71602ecacd7948351fd197a55370bdc38dd) 197 | 198 | ```diff 199 | --- a/bar.yaml 200 | +++ b/bar.yaml 201 | @@ -1,2 +1,2 @@ 202 | names: 203 | -- bar 204 | + - bar 205 | ``` 206 | 207 | ## Actions 208 | 209 | Securefix Action composes of four actions: 210 | 211 | - [csm-actions/securefix-action](docs/client.md) ([action.yaml](action.yaml)): Client action 212 | - [csm-actions/securefix-action/server/prepare](server/prepare) ([action.yaml](server/prepare/action.yaml)): Server action to prepare for creating commits 213 | - [csm-actions/securefix-action/server/commit](server/commit) ([action.yaml](server/commit/action.yaml)): Server action to create commits 214 | - [csm-actions/securefix-action/server/notify](server/notify) ([action.yaml](server/notify/action.yaml)): Server action to notify the server failure 215 | 216 | ## Security 217 | 218 | > - You don't need to pass a GitHub App private key having strong permissions to GitHub Actions workflows on the client side 219 | > - You don't need to allow external services to access your code 220 | > - You can define custom validation before creating a commit 221 | > - Commits are verified (signed) 222 | 223 | Client workflows can use a Client GitHub App, but it has only `issues:write` permission. 224 | Even if the app is abused, the risk is low. 225 | Server action creates a commit to the same repository and branch with the GitHub Actions Artifact. 226 | So it doesn't allow attackers to create a malicious commit to a different repository or a different branch. 227 | 228 | ### How to manage a server GitHub App and a server workflow 229 | 230 | You must protect a server GitHub App and a server workflow from attacks securely. 231 | There are several ideas: 232 | 233 | - GitHub App Private Key: 234 | - [Use GitHub Environment Secret](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#deployment-protection-rules) 235 | - Restrict the branch 236 | - Use a secret manager such as AWS Secrets Manager and [restrict the access by OIDC claims (repository, event, branch, workflow, etc)](https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect) 237 | - Server Workflow 238 | - Restrict members having the write permission of the server repository 239 | - For instance, grant the write permission to only system administrators 240 | 241 | ### Custom Validation 242 | 243 | You can insert custom validation between `server/prepare` action and `server/commit` action. 244 | You can use [`server/prepare` action's outputs](server/prepare#outputs). 245 | 246 | ```yaml 247 | - uses: csm-actions/securefix-action/server/prepare@c5696e3325f3906c5054ad80f3b9cdd92d65173b # v0.1.0 248 | id: prepare 249 | with: 250 | app_id: ${{ vars.DEMO_SERVER_APP_ID }} 251 | app_private_key: ${{ secrets.DEMO_SERVER_PRIVATE_KEY }} 252 | # Custom Validation 253 | - if: fromJson(steps.prepare.outputs.pull_request).user.login != 'suzuki-shunsuke' 254 | run: | 255 | exit 1 256 | - uses: csm-actions/securefix-action/server/commit@c5696e3325f3906c5054ad80f3b9cdd92d65173b # v0.1.0 257 | with: 258 | outputs: ${{ toJson(steps.prepare.outputs) }} 259 | - uses: csm-actions/securefix-action/server/notify@c5696e3325f3906c5054ad80f3b9cdd92d65173b # v0.1.0 260 | failure() 261 | with: 262 | outputs: ${{ toJson(steps.prepare.outputs) }} 263 | ``` 264 | 265 | ## Troubleshooting 266 | 267 | ### Client Workflow Name 268 | 269 | By default, the client workflow name must be `securefix` for security. 270 | Otherwise, the server/prepare action fails. 271 | [You can change the workflow name or remove the restriction using server/prepare action's `workflow_name` input.](server/prepare#optional-inputs) 272 | 273 | ### How To Fix Workflow Files 274 | 275 | By default, Serverfix Action doesn't allow you to fix workflow files for security. 276 | By default, the server action fails if fixed files include workflow files. 277 | [You can allow it by setting server/prepare action's `allow_workflow_fix` to `true`.](server/prepare#optional-inputs) 278 | 279 | ### GitHub API Rate Limiting 280 | 281 | If you use Server Action in many client repositories and face GitHub API limiting, you can avoid the rate limiting by creating new GitHub Apps and a server repository and splitting clients: 282 | 283 | ![Image](https://github.com/user-attachments/assets/34551fb7-6471-4bf2-bea1-c6056c005330) 284 | 285 | Reference: 286 | 287 | - [Rate Limit of REST API](https://docs.github.com/en/enterprise-cloud@latest/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28) 288 | - [Rate Limit of GraphQL API](https://docs.github.com/en/enterprise-cloud@latest/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api) 289 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | repositorys = "repositorys" 3 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: Fix Code securely 2 | description: Fix Code securely 3 | author: Shunsuke Suzuki 4 | branding: 5 | icon: git-commit 6 | color: green 7 | inputs: 8 | app_id: 9 | description: | 10 | GitHub App ID 11 | required: true 12 | app_private_key: 13 | description: | 14 | GitHub App Private Key 15 | required: true 16 | server_repository: 17 | description: | 18 | Server repository name 19 | required: false 20 | default: securefix 21 | commit_message: 22 | description: | 23 | Commit message 24 | required: false 25 | runs: 26 | using: composite 27 | steps: 28 | - id: artifact-name 29 | shell: bash 30 | run: | 31 | value=$(tr -dc A-Za-z0-9 > "$GITHUB_OUTPUT" 33 | # List fixed file paths 34 | - id: files 35 | shell: bash 36 | env: 37 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 38 | run: | 39 | FILES=$(git ls-files --modified --others --exclude-standard) 40 | if [ -z "$FILES" ]; then 41 | echo "::notice:: No changes" >&2 42 | exit 0 43 | fi 44 | { 45 | echo 'value<> "$GITHUB_OUTPUT" 49 | # Create a GitHub App token to create and delete a label 50 | - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 51 | if: steps.files.outputs.value != '' 52 | id: token 53 | with: 54 | app_id: ${{inputs.app_id}} 55 | private_key: ${{inputs.app_private_key}} 56 | repositories: >- 57 | ["${{inputs.server_repository}}"] 58 | permissions: >- 59 | { 60 | "issues": "write" 61 | } 62 | 63 | # Create metadata 64 | - if: steps.files.outputs.value != '' 65 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 66 | id: metadata 67 | env: 68 | COMMIT_MESSAGE: ${{inputs.commit_message}} 69 | with: 70 | script: | 71 | return { 72 | context: context, 73 | inputs: { 74 | commit_message: process.env.COMMIT_MESSAGE || '', 75 | }, 76 | }; 77 | 78 | # Create a metadata file 79 | - if: steps.files.outputs.value != '' 80 | shell: bash 81 | run: echo "$EVENT" > "${ARTIFACT_NAME}.json" 82 | env: 83 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 84 | EVENT: ${{steps.metadata.outputs.result}} 85 | 86 | # Create a file where fixed file paths are listed 87 | - if: steps.files.outputs.value != '' 88 | shell: bash 89 | run: echo "$FILES" > "${ARTIFACT_NAME}_files.txt" 90 | env: 91 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 92 | FILES: ${{ steps.files.outputs.value }} 93 | # Upload files to GitHub Actions Artifact 94 | - if: steps.files.outputs.value != '' 95 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 96 | with: 97 | name: ${{steps.artifact-name.outputs.value}} 98 | path: | 99 | ${{ steps.files.outputs.value }} 100 | ${{steps.artifact-name.outputs.value}}.json 101 | ${{steps.artifact-name.outputs.value}}_files.txt 102 | # Delete metadata files 103 | - if: steps.files.outputs.value != '' 104 | shell: bash 105 | run: rm "${ARTIFACT_NAME}.json" "${ARTIFACT_NAME}_files.txt" 106 | env: 107 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 108 | # Create a label 109 | - if: steps.files.outputs.value != '' 110 | shell: bash 111 | env: 112 | GH_TOKEN: ${{ steps.token.outputs.token }} 113 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 114 | REPO: ${{inputs.server_repository}} 115 | run: | 116 | gh label create \ 117 | -R "${GITHUB_REPOSITORY_OWNER}/${REPO}" \ 118 | "$ARTIFACT_NAME" \ 119 | --description "$GITHUB_REPOSITORY/${GITHUB_RUN_ID}" 120 | # Sleep for a while 121 | - if: steps.files.outputs.value != '' 122 | shell: bash 123 | run: sleep 1 124 | # Delete a label 125 | - if: steps.files.outputs.value != '' 126 | shell: bash 127 | env: 128 | GH_TOKEN: ${{ steps.token.outputs.token }} 129 | ARTIFACT_NAME: ${{ steps.artifact-name.outputs.value }} 130 | REPO: ${{inputs.server_repository}} 131 | run: | 132 | gh label delete \ 133 | -R "${GITHUB_REPOSITORY_OWNER}/${REPO}" \ 134 | --yes \ 135 | "$ARTIFACT_NAME" 136 | # Fail if changes are detected 137 | - if: steps.files.outputs.value != '' 138 | shell: bash 139 | env: 140 | FILES: ${{ steps.files.outputs.value }} 141 | run: | 142 | echo "::error:: Changes detected. A commit will be pushed" 143 | git ls-files --modified --others --exclude-standard 144 | exit 1 145 | -------------------------------------------------------------------------------- /aqua/aqua-checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "checksums": [ 3 | { 4 | "id": "github_release/github.com/crate-ci/typos/v1.31.2/typos-v1.31.2-aarch64-apple-darwin.tar.gz", 5 | "checksum": "F9D75D8CBBD479DB5D060E9B666422F03FA20F31D213443F7A73E15A8875AA5D", 6 | "algorithm": "sha256" 7 | }, 8 | { 9 | "id": "github_release/github.com/crate-ci/typos/v1.31.2/typos-v1.31.2-aarch64-unknown-linux-musl.tar.gz", 10 | "checksum": "87E53661EC8BD6F466191AC76D543482E86E1E55A3611D85CC110A3E7C0E6D0F", 11 | "algorithm": "sha256" 12 | }, 13 | { 14 | "id": "github_release/github.com/crate-ci/typos/v1.31.2/typos-v1.31.2-x86_64-apple-darwin.tar.gz", 15 | "checksum": "91E29C5ABE6FCD94AE200D2E6DCEE47946778D6C36450A7D08E53738B0747557", 16 | "algorithm": "sha256" 17 | }, 18 | { 19 | "id": "github_release/github.com/crate-ci/typos/v1.31.2/typos-v1.31.2-x86_64-pc-windows-msvc.zip", 20 | "checksum": "4C48D861ECA575BE03D9433BE937C4DAA4C6264934F92A56298E7AF474457F95", 21 | "algorithm": "sha256" 22 | }, 23 | { 24 | "id": "github_release/github.com/crate-ci/typos/v1.31.2/typos-v1.31.2-x86_64-unknown-linux-musl.tar.gz", 25 | "checksum": "DB179D95B35B5E84E7E85E85AEF6302C93FFC26E849FE966E1BB7F121D97976E", 26 | "algorithm": "sha256" 27 | }, 28 | { 29 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_darwin_amd64.tar.gz", 30 | "checksum": "C576EC52A5777255C074B0C20CDCD83060E4E502BC1FEA05EE8B149B35A0867F", 31 | "algorithm": "sha256" 32 | }, 33 | { 34 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_darwin_arm64.tar.gz", 35 | "checksum": "336ACCC20169D49EBA7DDBCF030758BE2DC6655BC3EC2F2AC03DE833CE35492E", 36 | "algorithm": "sha256" 37 | }, 38 | { 39 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_linux_amd64.tar.gz", 40 | "checksum": "8AB2CB63A60CCD8FDC9E0F71B7FD2D9B7374054DEA5F8AE63DD2E3E7D233686D", 41 | "algorithm": "sha256" 42 | }, 43 | { 44 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_linux_arm64.tar.gz", 45 | "checksum": "F51B84DCA69C8AECD992C606D23F735EC37BE692CEF2E895270B8E72B84183C4", 46 | "algorithm": "sha256" 47 | }, 48 | { 49 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_windows_amd64.zip", 50 | "checksum": "F603D6EDD76AB44C1C710A2B1E50905D444C9EFFA848BFB537CA4A1ABD93A20D", 51 | "algorithm": "sha256" 52 | }, 53 | { 54 | "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.0/nllint_windows_arm64.zip", 55 | "checksum": "4AEB83825367044C345128D34E38470AB53BED2D76491FB6F8A6737EC619DB90", 56 | "algorithm": "sha256" 57 | }, 58 | { 59 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_darwin_amd64.tar.gz", 60 | "checksum": "7546BF811AD263AF402549C3AE1C52156BCE864D12EF369D03F0CE564625463C", 61 | "algorithm": "sha256" 62 | }, 63 | { 64 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_darwin_arm64.tar.gz", 65 | "checksum": "460BBFE3CB7E56634E193CA9D321C98CCD99DE1D7D433954B2E8B31A626F79B3", 66 | "algorithm": "sha256" 67 | }, 68 | { 69 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_linux_amd64.tar.gz", 70 | "checksum": "FE009D86B9405BDC3DAAC7C53D07334C2014DA6B2B4A3DD0FB837C4F2146B447", 71 | "algorithm": "sha256" 72 | }, 73 | { 74 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_linux_arm64.tar.gz", 75 | "checksum": "BE01A1D9C0D4311C88465D036246D65CDCAAED88888F3BCE32849A850EB043F3", 76 | "algorithm": "sha256" 77 | }, 78 | { 79 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_windows_amd64.zip", 80 | "checksum": "A5318C528943C78C036DF98084B6120CDAC63147C1ED00442250B0E01F7A72C7", 81 | "algorithm": "sha256" 82 | }, 83 | { 84 | "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.0.5/pinact_windows_arm64.zip", 85 | "checksum": "EF542802CD58EC89E91F91A300EA9250956010BC253F5AA5491022EAF79BC7BA", 86 | "algorithm": "sha256" 87 | }, 88 | { 89 | "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.360.0/registry.yaml", 90 | "checksum": "55B374C40E680542951FA9504F20CC30548DFBA1E98CABDE3D6A5498447F2AD21958490546CD9B01143B0EBDB8CB3658D4F283D1E29CD29D0128E142B088F15A", 91 | "algorithm": "sha512" 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /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.360.0 # renovate: depName=aquaproj/aqua-registry 11 | import_dir: imports 12 | -------------------------------------------------------------------------------- /aqua/imports/nllint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/nllint@v0.1.0 3 | -------------------------------------------------------------------------------- /aqua/imports/pinact.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/pinact@v3.0.5 3 | -------------------------------------------------------------------------------- /aqua/imports/typos.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: crate-ci/typos@v1.31.2 3 | -------------------------------------------------------------------------------- /docs/client.md: -------------------------------------------------------------------------------- 1 | # Client Action 2 | 3 | [action.yaml](../action.yaml) 4 | 5 | Client Action uploads fixed files and metadata to GitHub Actions Artifacts and creates and deletes a GitHub Issue label to request fixing code to a server workflow. 6 | 7 | ## Example 8 | 9 | [Workflow](https://github.com/securefix-action/demo-client/blob/main/.github/workflows/securefix.yaml) 10 | 11 | ```yaml 12 | name: securefix 13 | on: pull_request 14 | jobs: 15 | test: 16 | runs-on: ubuntu-24.04 17 | timeout-minutes: 10 18 | permissions: 19 | contents: read 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | # Fix code as you like 26 | - run: npm i -g prettier 27 | - run: prettier -w . 28 | 29 | # Send a request to a server workflow 30 | - uses: csm-actions/securefix-action@c5696e3325f3906c5054ad80f3b9cdd92d65173b # v0.1.0 31 | with: 32 | app_id: ${{vars.DEMO_CLIENT_APP_ID}} 33 | app_private_key: ${{secrets.DEMO_CLIENT_PRIVATE_KEY}} 34 | server_repository: demo-server 35 | ``` 36 | 37 | ## Inputs 38 | 39 | ### Required Inputs 40 | 41 | - `app_id`: A GitHub App ID 42 | - `app_private_key`: A GitHub App Private Key 43 | - `server_repository`: A GitHub Repository name where a server workflow works 44 | 45 | ### Optional Inputs 46 | 47 | - `commit_message`: A commit message 48 | 49 | ## Outputs 50 | 51 | Nothing. 52 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: [ 3 | "github>suzuki-shunsuke/renovate-config#3.2.1", 4 | "github>suzuki-shunsuke/renovate-config:nolimit#3.2.1", 5 | "github>aquaproj/aqua-renovate-config#2.7.5", 6 | "github>aquaproj/aqua-renovate-config:file#2.7.5(aqua/imports/.*\\.yaml)", 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /server/commit/README.md: -------------------------------------------------------------------------------- 1 | # Server Commit Action 2 | 3 | [action.yaml](action.yaml) | [Example](https://github.com/securefix-action/demo-server/blob/main/.github/workflows/securefix.yaml) 4 | 5 | Server Commit Action creates a commit. 6 | 7 | ## Inputs 8 | 9 | ### Required Inputs 10 | 11 | - `outputs`: Server Prepare Action's outputs 12 | 13 | ```yaml 14 | outputs: ${{ toJson(steps.prepare.outputs) }} 15 | ``` 16 | 17 | ### Optional Inputs 18 | 19 | - `commit_message`: A commit message 20 | 21 | The priority of `commit_message`: 22 | 23 | 1. client action's input 24 | 1. server action's input 25 | 1. default value 26 | 27 | ## Outputs 28 | 29 | Nothing. 30 | -------------------------------------------------------------------------------- /server/commit/action.yaml: -------------------------------------------------------------------------------- 1 | name: Create a commit 2 | description: Create a commit 3 | inputs: 4 | outputs: 5 | description: | 6 | Prepare action's outputs 7 | required: true 8 | commit_message: 9 | description: | 10 | Commit message 11 | required: false 12 | default: Securefix 13 | runs: 14 | using: composite 15 | steps: 16 | # Create and push a commit 17 | - if: fromJson(inputs.outputs).fixed_files != '' 18 | uses: suzuki-shunsuke/commit-action@cc96d3a3fd959d05e9b79ca395eb30b835aeba24 # v0.0.7 19 | with: 20 | github_token: ${{fromJson(inputs.outputs).github_token}} 21 | repository: ${{ fromJson(inputs.outputs).repository }} 22 | branch: ${{ fromJson(fromJson(inputs.outputs).workflow_run).head_branch }} 23 | files: ${{ fromJson(inputs.outputs).fixed_files }} 24 | commit_message: | 25 | ${{fromJson(fromJson(inputs.outputs).metadata).inputs.commit_message && fromJson(fromJson(inputs.outputs).metadata).inputs.commit_message || inputs.commit_message}} 26 | ${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}} 27 | -------------------------------------------------------------------------------- /server/notify/README.md: -------------------------------------------------------------------------------- 1 | # Server Notify Action 2 | 3 | [action.yaml](action.yaml) | [Example](https://github.com/securefix-action/demo-server/blob/main/.github/workflows/securefix.yaml) 4 | 5 | Server Notify Action notifies the failure. 6 | 7 | ![image](https://github.com/user-attachments/assets/3a06184f-2566-4863-9b2f-20548b8bc990) 8 | 9 | ## Inputs 10 | 11 | ### Required Inputs 12 | 13 | - `outputs`: Server Prepare Action's outputs 14 | 15 | ```yaml 16 | outputs: ${{ toJson(steps.prepare.outputs) }} 17 | ``` 18 | 19 | ### Optional Inputs 20 | 21 | - `pull_request_comment`: A pull request comment template. A comment is posted if server actions fail to create a commit. The default value is `## :x: Securefix failed.` 22 | 23 | ## Outputs 24 | 25 | Nothing. 26 | -------------------------------------------------------------------------------- /server/notify/action.yaml: -------------------------------------------------------------------------------- 1 | name: Notify failure 2 | description: Notify failure 3 | inputs: 4 | outputs: 5 | description: | 6 | Prepare action's outputs 7 | required: true 8 | pull_request_comment: 9 | description: | 10 | Pull Request comment 11 | required: false 12 | default: | 13 | ## :x: Securefix Action failed. 14 | runs: 15 | using: composite 16 | steps: 17 | # Create a pull request comment to report an error 18 | - if: fromJson(inputs.outputs).pull_request_number != '' 19 | shell: bash 20 | env: 21 | GH_TOKEN: ${{fromJson(inputs.outputs).github_token}} 22 | REPO: ${{ fromJson(inputs.outputs).repository }} 23 | PR_NUMBER: ${{ fromJson(inputs.outputs).pull_request_number }} 24 | COMMENT: | 25 | ${{ inputs.pull_request_comment }} 26 | [Workflow](${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}) 27 | The comment was created by [Securefix Action](https://github.com/csm-actions/securefix-action). 28 | run: | 29 | gh pr comment \ 30 | -R "$REPO" \ 31 | -b "${COMMENT}" \ 32 | "$PR_NUMBER" 33 | -------------------------------------------------------------------------------- /server/prepare/README.md: -------------------------------------------------------------------------------- 1 | # Server Prepare Action 2 | 3 | [action.yaml](action.yaml) | [Workflow](https://github.com/securefix-action/demo-server/blob/main/.github/workflows/securefix.yaml) 4 | 5 | Server Prepare Action prepares for creating a commit. 6 | 7 | 1. Download fixed files and metadata from GitHub Actions Artifacts 8 | 1. Get data about associated Workflow Run, Pull Request, and Branch by GitHub API 9 | 1. Validate the request 10 | 1. Output data for custom validation and creating a commit 11 | 12 | ## Inputs 13 | 14 | ### Required Inputs 15 | 16 | - `app_id`: A GitHub App ID 17 | - `app_private_key`: A GitHub App Private Key 18 | 19 | ### Optional Inputs 20 | 21 | - `allow_workflow_fix`: Either `true` or `false`. The default is `false`. If `true`, the action can fix workflow files. You need to grant `workflows:write` permission to the Server GitHub App 22 | - `workflow_name`: An expected client workflow name. If the actual client workflow name is different from this input, the request is denied. The default value is `securefix`. If this is empty, the workflow name is free 23 | - `pull_request_comment`: A pull request comment template. A comment is posted if server actions fail to create a commit. The default value is `:x: Securefix failed.` 24 | 25 | ## Outputs 26 | 27 | - `pull_request`: A Pull Request Payload ([ref](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request)) 28 | - `pull_request_number`: A Pull Request Number 29 | - `workflow_run`: A Workflow Run Payload ([ref](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#get-a-workflow-run)) 30 | - `repository`: A client repository's full name 31 | - `repository_name`: A client repository's name 32 | - `workflow_run_id`: A client workflow run id 33 | - `metadata`: A request's metadata. It's a JSON string. 34 | 35 | ```json 36 | { 37 | "context": { 38 | // github-script's context object 39 | }, 40 | "inputs": { 41 | "commit_message": "commit message", 42 | }, 43 | } 44 | ``` 45 | 46 | `context` is a [github-script](https://github.com/actions/github-script)'s context object. 47 | 48 | - `fixed_files`: Fixed file paths. Paths are separated with newlines 49 | - `pull_request_comment`: A pull request comment template. 50 | -------------------------------------------------------------------------------- /server/prepare/action.yaml: -------------------------------------------------------------------------------- 1 | name: Prepare for fixing code 2 | description: Prepare for fixing code 3 | inputs: 4 | app_id: 5 | description: | 6 | GitHub App ID 7 | required: true 8 | app_private_key: 9 | description: | 10 | GitHub App Private Key 11 | required: true 12 | workflow_name: 13 | description: | 14 | Client Workflow name. If this is empty, the action does not check the workflow name. 15 | required: false 16 | default: securefix 17 | allow_workflow_fix: 18 | description: | 19 | Either true or false. If true, the action allows the workflow to fix the code. 20 | required: false 21 | default: "false" 22 | outputs: 23 | pull_request: 24 | description: | 25 | Pull Request 26 | value: ${{ steps.pr.outputs.value }} 27 | pull_request_number: 28 | description: | 29 | Pull Request number 30 | value: ${{ steps.metadata.outputs.value && (fromJson(steps.metadata.outputs.value).context.payload.pull_request && fromJson(steps.metadata.outputs.value).context.payload.pull_request.number || '') || '' }} 31 | workflow_run: 32 | description: | 33 | Workflow Run 34 | value: ${{ steps.workflow_run.outputs.value }} 35 | repository: 36 | description: | 37 | Repository Full Name 38 | value: ${{ steps.info.outputs.repo_full_name }} 39 | repository_name: 40 | description: | 41 | Repository Name 42 | value: ${{ steps.info.outputs.repo }} 43 | workflow_run_id: 44 | description: | 45 | Workflow Run ID 46 | value: ${{ steps.info.outputs.run_id }} 47 | metadata: 48 | description: | 49 | Metadata 50 | value: ${{ steps.metadata.outputs.value }} 51 | fixed_files: 52 | description: | 53 | Fixed file paths 54 | value: ${{ steps.files.outputs.value }} 55 | github_token: 56 | description: | 57 | GitHub App token 58 | value: ${{ steps.token.outputs.token }} 59 | runs: 60 | using: composite 61 | steps: 62 | - id: info 63 | shell: bash 64 | env: 65 | LABEL_NAME: ${{ github.event.label.name }} 66 | # $GITHUB_REPOSITORY/${GITHUB_RUN_ID} 67 | LABEL_DESCRIPTION: ${{ github.event.label.description }} 68 | run: | 69 | repo_full_name=${LABEL_DESCRIPTION%/*} 70 | { 71 | echo "repo_full_name=${repo_full_name}" 72 | echo "repo=${repo_full_name#*/}" 73 | echo "run_id=${LABEL_DESCRIPTION##*/}" 74 | } >> "$GITHUB_OUTPUT" 75 | 76 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 77 | id: permissions 78 | env: 79 | ALLOW_WORKFLOW_FIX: ${{inputs.allow_workflow_fix}} 80 | with: 81 | script: | 82 | let permissions = { 83 | actions: "read", 84 | contents: "write", 85 | pull_requests: "write", 86 | }; 87 | if (process.env.ALLOW_WORKFLOW_FIX === 'true') { 88 | permissions.workflows = "write"; 89 | } 90 | return permissions; 91 | 92 | # Create a GitHub App token to download a GitHub Actions artifact 93 | - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 94 | id: token 95 | with: 96 | app_id: ${{inputs.app_id}} 97 | private_key: ${{inputs.app_private_key}} 98 | repositories: >- 99 | ["${{ steps.info.outputs.repo }}"] 100 | permissions: ${{ steps.permissions.outputs.result }} 101 | 102 | # Download a GitHub Actions Artifact 103 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 104 | with: 105 | github-token: ${{ steps.token.outputs.token }} 106 | name: ${{ github.event.label.name }} 107 | repository: ${{ steps.info.outputs.repo_full_name }} 108 | run-id: ${{ steps.info.outputs.run_id }} 109 | 110 | # Read a metadata file 111 | - id: metadata 112 | shell: bash 113 | env: 114 | METADATA_FILE: ${{ github.event.label.name}}.json 115 | run: | 116 | { 117 | echo 'value<> "$GITHUB_OUTPUT" 121 | 122 | # Get a pull request 123 | - if: fromJson(steps.metadata.outputs.value).context.payload.pull_request 124 | id: pr 125 | shell: bash 126 | env: 127 | NUMBER: ${{ fromJson(steps.metadata.outputs.value).context.payload.pull_request.number }} 128 | GH_TOKEN: ${{ steps.token.outputs.token }} 129 | REPO: ${{ steps.info.outputs.repo_full_name }} 130 | run: | 131 | data=$(gh api \ 132 | -H "Accept: application/vnd.github+json" \ 133 | -H "X-GitHub-Api-Version: 2022-11-28" \ 134 | "/repos/$REPO/pulls/$NUMBER") 135 | { 136 | echo 'value<> "$GITHUB_OUTPUT" 140 | 141 | # Get GitHub Actions Workflow Run 142 | - id: workflow_run 143 | shell: bash 144 | env: 145 | GH_TOKEN: ${{ steps.token.outputs.token }} 146 | REPO: ${{ steps.info.outputs.repo_full_name }} 147 | RUN_ID: ${{ steps.info.outputs.run_id }} 148 | WORKFLOW: ${{ inputs.workflow_name }} 149 | run: | 150 | data=$(gh api \ 151 | -H "Accept: application/vnd.github+json" \ 152 | -H "X-GitHub-Api-Version: 2022-11-28" \ 153 | "/repos/$REPO/actions/runs/$RUN_ID") 154 | { 155 | echo 'value<> "$GITHUB_OUTPUT" 159 | 160 | # Validate workflow name 161 | - if: inputs.workflow_name != '' && fromJson(steps.workflow_run.outputs.value).name != inputs.workflow_name 162 | shell: bash 163 | env: 164 | WORKFLOW: ${{ inputs.workflow_name }} 165 | run: | 166 | echo "::error:: The client workflow name must be $WORKFLOW" 167 | exit 1 168 | 169 | # Get a branch 170 | - if: | 171 | !fromJson(steps.metadata.outputs.value).context.payload.pull_request 172 | id: branch 173 | shell: bash 174 | env: 175 | GH_TOKEN: ${{ steps.token.outputs.token }} 176 | REPO: ${{ steps.info.outputs.repo_full_name }} 177 | BRANCH: ${{ fromJson(steps.workflow_run.outputs.value).head_branch }} 178 | run: | 179 | data=$(gh api \ 180 | -H "Accept: application/vnd.github+json" \ 181 | -H "X-GitHub-Api-Version: 2022-11-28" \ 182 | "/repos/$REPO/branches/$BRANCH") 183 | { 184 | echo 'value<> "$GITHUB_OUTPUT" 188 | 189 | # Validate if the workflow commit sha is latest 190 | - shell: bash 191 | if: fromJson(steps.workflow_run.outputs.value).head_sha != (fromJson(steps.metadata.outputs.value).context.payload.pull_request && fromJson(steps.pr.outputs.value).head.sha || fromJson(steps.branch.outputs.value).commit.sha) 192 | env: 193 | EXPECTED: ${{ fromJson(steps.workflow_run.outputs.value).head_sha }} 194 | ACTUAL: ${{ fromJson(steps.metadata.outputs.value).context.payload.pull_request && fromJson(steps.pr.outputs.value).head.sha || fromJson(steps.branch.outputs.value).commit.sha }} 195 | run: | 196 | echo "::error:: The workflow commit sha ($ACTUAL) is not the latest ($EXPECTED)" 197 | exit 1 198 | 199 | # Read changed file paths 200 | - id: files 201 | shell: bash 202 | env: 203 | FILES: ${{ github.event.label.name}}_files.txt 204 | run: | 205 | { 206 | echo 'value<> "$GITHUB_OUTPUT" 210 | rm "$FILES" 211 | --------------------------------------------------------------------------------