Repository: suzuki-shunsuke/ghalint
Branch: main
Commit: d507057d2a3d
Files: 113
Total size: 161.4 KB
Directory structure:
gitextract_p8kwv86v/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── actionlint.yaml
│ ├── autofix.yaml
│ ├── check-commit-signing.yaml
│ ├── release.yaml
│ ├── test.yaml
│ └── workflow_call_test.yaml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── _typos.toml
├── aqua/
│ ├── aqua-checksums.json
│ ├── aqua.yaml
│ └── imports/
│ ├── cmdx.yaml
│ ├── cosign.yaml
│ ├── ghalint.yaml
│ ├── go-licenses.yaml
│ ├── golangci-lint.yaml
│ ├── goreleaser.yaml
│ ├── reviewdog.yaml
│ ├── syft.yaml
│ └── typos.yaml
├── cmd/
│ ├── gen-jsonschema/
│ │ └── main.go
│ └── ghalint/
│ └── main.go
├── cmdx.yaml
├── docs/
│ ├── codes/
│ │ ├── 001.md
│ │ └── 002.md
│ ├── install.md
│ ├── policies/
│ │ ├── 001.md
│ │ ├── 002.md
│ │ ├── 003.md
│ │ ├── 004.md
│ │ ├── 005.md
│ │ ├── 006.md
│ │ ├── 007.md
│ │ ├── 008.md
│ │ ├── 009.md
│ │ ├── 010.md
│ │ ├── 011.md
│ │ ├── 012.md
│ │ └── 013.md
│ └── usage.md
├── go.mod
├── go.sum
├── json-schema/
│ └── ghalint.json
├── pkg/
│ ├── action/
│ │ └── find.go
│ ├── cli/
│ │ ├── app.go
│ │ ├── experiment/
│ │ │ ├── command.go
│ │ │ └── validateinput/
│ │ │ └── command.go
│ │ ├── gflags/
│ │ │ └── gflags.go
│ │ ├── run.go
│ │ └── run_action.go
│ ├── config/
│ │ ├── config.go
│ │ └── config_test.go
│ ├── controller/
│ │ ├── act/
│ │ │ ├── controller.go
│ │ │ └── run.go
│ │ ├── controller.go
│ │ ├── run.go
│ │ └── schema/
│ │ ├── action.go
│ │ ├── controller.go
│ │ ├── job.go
│ │ ├── reusable_workflow.go
│ │ ├── run.go
│ │ ├── step.go
│ │ └── workflow.go
│ ├── github/
│ │ ├── github.go
│ │ └── keyring.go
│ ├── policy/
│ │ ├── action_ref_should_be_full_length_commit_sha_policy.go
│ │ ├── action_ref_should_be_full_length_commit_sha_policy_test.go
│ │ ├── action_shell_is_required.go
│ │ ├── action_shell_is_required_test.go
│ │ ├── checkout_persist_credentials_should_be_false.go
│ │ ├── checkout_persist_credentials_should_be_false_test.go
│ │ ├── context.go
│ │ ├── deny_inherit_secrets.go
│ │ ├── deny_inherit_secrets_test.go
│ │ ├── deny_job_container_latest_image.go
│ │ ├── deny_job_container_latest_image_test.go
│ │ ├── deny_read_all_policy.go
│ │ ├── deny_read_all_policy_test.go
│ │ ├── deny_write_all_policy.go
│ │ ├── deny_write_all_policy_test.go
│ │ ├── error.go
│ │ ├── github_app_should_limit_permissions.go
│ │ ├── github_app_should_limit_permissions_test.go
│ │ ├── github_app_should_limit_repositories.go
│ │ ├── github_app_should_limit_repositories_test.go
│ │ ├── job_permissions_policy.go
│ │ ├── job_permissions_policy_test.go
│ │ ├── job_secrets_policy.go
│ │ ├── job_secrets_policy_test.go
│ │ ├── job_timeout_minutes_is_required.go
│ │ ├── job_timeout_minutes_is_required_test.go
│ │ ├── workflow_secrets_policy.go
│ │ └── workflow_secrets_policy_test.go
│ └── workflow/
│ ├── container.go
│ ├── container_test.go
│ ├── job_secrets.go
│ ├── job_secrets_test.go
│ ├── list_workflows.go
│ ├── permissions.go
│ ├── permissions_test.go
│ ├── read_action.go
│ ├── read_workflow.go
│ └── workflow.go
├── renovate.json5
├── scripts/
│ ├── coverage.sh
│ └── generate-usage.sh
├── test-action.yaml
└── test-workflow.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github:
- suzuki-shunsuke
================================================
FILE: .github/workflows/actionlint.yaml
================================================
---
name: actionlint
on: pull_request
jobs:
actionlint:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: suzuki-shunsuke/actionlint-action@8297c48141939cdcf80a8341c3cd525b300e36db # v0.1.2
================================================
FILE: .github/workflows/autofix.yaml
================================================
---
name: autofix.ci
on: pull_request
permissions: {}
jobs:
autofix:
runs-on: ubuntu-24.04
permissions: {}
timeout-minutes: 15
steps:
- uses: suzuki-shunsuke/go-autofix-action@ba716cebb7767055bdde0e62bb91d715bde39ab2 # v0.1.12
with:
aqua_version: v2.59.0
================================================
FILE: .github/workflows/check-commit-signing.yaml
================================================
---
name: Check if all commits are signed
on:
pull_request_target:
branches: [main]
concurrency:
group: ${{ github.workflow }}--${{ github.head_ref }} # github.ref is unavailable in case of pull_request_target
cancel-in-progress: true
jobs:
check-commit-signing:
uses: suzuki-shunsuke/check-commit-signing-workflow/.github/workflows/check.yaml@547eee345f56310a656f271ec5eaa900af46b0fb # v0.1.0
permissions:
contents: read
pull-requests: write
================================================
FILE: .github/workflows/release.yaml
================================================
---
name: Release
on:
push:
tags: [v*]
jobs:
release:
uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@b2ecf54e35aca9e9689e761f5bd6d1ad9542a8cf # v8.0.0
with:
aqua_version: v2.59.0
go-version-file: go.mod
permissions:
contents: write
id-token: write
actions: read
attestations: write
================================================
FILE: .github/workflows/test.yaml
================================================
---
name: test
on: pull_request
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test:
uses: ./.github/workflows/workflow_call_test.yaml
permissions:
pull-requests: write
contents: read
status-check:
runs-on: ubuntu-24.04
if: always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled'))
timeout-minutes: 10
permissions: {}
needs:
- test
steps:
- run: exit 1
================================================
FILE: .github/workflows/workflow_call_test.yaml
================================================
---
name: test (workflow_call)
on: workflow_call
permissions: {}
jobs:
test:
uses: suzuki-shunsuke/go-test-full-workflow/.github/workflows/test.yaml@e442a17816baa8a940edc1544cc6e739d870e165 # v5.0.2
with:
aqua_version: v2.59.0
golangci-lint-timeout: 120s
permissions:
pull-requests: write
contents: read
================================================
FILE: .gitignore
================================================
dist
.coverage
third_party_licenses
================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
default: all
disable:
- depguard
- err113
- exhaustruct
- godot
- ireturn
- lll
- musttag
- nlreturn
- tagalign
- tagliatelle
- varnamelen
- wsl
- wsl_v5
- noinlineerr
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
rules:
- path: _test\.go
linters:
- goconst
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yml
================================================
version: 2
project_name: ghalint
archives:
- format_overrides:
- goos: windows
formats: [zip]
files:
- LICENSE
- README.md
- third_party_licenses/**/*
env:
- GO111MODULE=on
before:
hooks:
- go mod tidy
sboms:
- id: default
disable: false
builds:
- main: ./cmd/ghalint
binary: ghalint
env:
- CGO_ENABLED=0
goos:
- windows
- darwin
- linux
goarch:
- amd64
- arm64
release:
prerelease: "true" # we update release note manually before releasing
header: |
[Pull Requests](https://github.com/suzuki-shunsuke/ghalint/pulls?q=is%3Apr+milestone%3A{{.Tag}}) | [Issues](https://github.com/suzuki-shunsuke/ghalint/issues?q=is%3Aissue+milestone%3A{{.Tag}}) | https://github.com/suzuki-shunsuke/ghalint/compare/{{.PreviousTag}}...{{.Tag}}
homebrew_casks:
-
# NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the
# same kind. We will probably unify this in the next major version like it is done with scoop.
repository:
owner: suzuki-shunsuke
name: homebrew-ghalint
# The project name and current git tag are used in the format string.
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
homepage: https://github.com/suzuki-shunsuke/ghalint
description: GitHub Actions linter
license: MIT
skip_upload: true
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/ghalint"]
end
scoops:
- description: GitHub Actions linter for security best practices.
license: MIT
skip_upload: true
repository:
owner: suzuki-shunsuke
name: scoop-bucket
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Please read the following document.
- https://github.com/suzuki-shunsuke/oss-contribution-guide
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Shunsuke Suzuki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# ghalint
[](https://deepwiki.com/suzuki-shunsuke/ghalint)
[Install](docs/install.md) | [Policies](#policies) | [How to use](#how-to-use) | [Configuration](#configuration)
GitHub Actions linter for security best practices.
```console
$ ghalint run
ERRO[0000] read a workflow file error="parse a workflow file as YAML: yaml: line 10: could not find expected ':'" program=ghalint version= workflow_file_path=.github/workflows/release.yaml
ERRO[0000] github.token should not be set to workflow's env env_name=GITHUB_TOKEN policy_name=workflow_secrets program=ghalint version= workflow_file_path=.github/workflows/test.yaml
ERRO[0000] secret should not be set to workflow's env env_name=DATADOG_API_KEY policy_name=workflow_secrets program=ghalint version= workflow_file_path=.github/workflows/test.yaml
```
ghalint is a command line tool to check GitHub Actions Workflows and action.yaml for security policy compliance.
## :bulb: We've ported ghalint to lintnet module
- https://lintnet.github.io/
- https://github.com/lintnet-modules/ghalint
lintnet is a general purpose linter powered by Jsonnet.
We've ported ghalint to [the lintnet module](https://github.com/lintnet-modules/ghalint), so you can migrate ghalint to lintnet!
## Policies
### 1. Workflow Policies
1. [job_permissions](docs/policies/001.md): All jobs should have `permissions`
1. [deny_read_all_permission](docs/policies/002.md): `read-all` permission should not be used
1. [deny_write_all_permission](docs/policies/003.md): `write-all` permission should not be used
1. [deny_inherit_secrets](docs/policies/004.md): `secrets: inherit` should not be used
1. [workflow_secrets](docs/policies/005.md): Workflow should not set secrets to environment variables
1. [job_secrets](docs/policies/006.md): Job should not set secrets to environment variables
1. [deny_job_container_latest_image](docs/policies/007.md): Job's container image tag should not be `latest`
1. [action_ref_should_be_full_length_commit_sha](docs/policies/008.md): action's ref should be full length commit SHA
1. [github_app_should_limit_repositories](docs/policies/009.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories
1. [github_app_should_limit_permissions](docs/policies/010.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions
1. [job_timeout_minutes_is_required](docs/policies/012.md): All jobs should set [timeout-minutes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes)
1. [checkout_persist_credentials_should_be_false](docs/policies/013.md): [actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`
### 2. Action Policies
1. [action_ref_should_be_full_length_commit_sha](docs/policies/008.md): action's ref should be full length commit SHA
1. [github_app_should_limit_repositories](docs/policies/009.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories
1. [github_app_should_limit_permissions](docs/policies/010.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions
1. [action_shell_is_required](docs/policies/011.md): `shell` is required if `run` is set
1. [checkout_persist_credentials_should_be_false](docs/policies/013.md): [actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`
## How to use
### 1. Validate workflows
Run the command `ghalint run` on the repository root directory.
```sh
ghalint run
```
Then ghalint validates workflow files `^\.github/workflows/.*\.ya?ml$`.
### 2. Validate action.yaml
Run the command `ghalint run-action`.
```sh
ghalint run-action
```
The alias `act` is available.
```sh
ghalint act
```
Then ghalint validates action files `^([^/]+/){0,3}action\.ya?ml$` on the current directory.
You can also specify file paths.
```sh
ghalint act foo/action.yaml bar/action.yml
```
## Configuration file
Configuration file path: `^(\.|\.github/)?ghalint\.ya?ml$`
You can specify the configuration file with the command line option `-config (-c)` or the environment variable `GHALINT_CONFIG`.
```sh
ghalint -c foo.yaml run
```
### JSON Schema
- [ghalint.json](json-schema/ghalint.json)
- https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/refs/heads/main/json-schema/ghalint.json
If you look for a CLI tool to validate configuration with JSON Schema, [ajv-cli](https://ajv.js.org/packages/ajv-cli.html) is useful.
```sh
ajv --spec=draft2020 -s json-schema/ghalint.json -d ghalint.yaml
```
#### Input Complementation by YAML Language Server
[Please see the comment too.](https://github.com/szksh-lab/.github/issues/67#issuecomment-2564960491)
Version: `main`
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/main/json-schema/ghalint.json
```
Or pinning version:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/v1.2.1/json-schema/ghalint.json
```
### Disable policies
You can disable the following policies.
- [deny_inherit_secrets](docs/policies/004.md)
- [job_secrets](docs/policies/006.md)
- [action_ref_should_be_full_length_commit_sha](docs/policies/008.md)
- [github_app_should_limit_repositories](docs/policies/009.md)
e.g.
```yaml
excludes:
- policy_name: deny_inherit_secrets
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
- policy_name: job_secrets
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
- policy_name: action_ref_should_be_full_length_commit_sha
action_name: slsa-framework/slsa-github-generator
- policy_name: github_app_should_limit_repositories
workflow_file_path: .github/workflows/test.yaml
job_name: test
step_id: create_token
```
## Environment variables
- `GHALINT_CONFIG`: Configuration file path
- `GHALINT_LOG_LEVEL`: Log level One of `error`, `warn`, `info` (default), `debug`
- `GHALINT_LOG_COLOR`: Configure log color. One of `auto` (default), `always`, and `never`.
💡 If you want to enable log color in GitHub Actions, please try `GHALINT_LOG_COLOR=always`
```yaml
env:
GHALINT_LOG_COLOR: always
```
AS IS
<img width="986" alt="image" src="https://user-images.githubusercontent.com/13323303/216190768-cb09597f-5669-4907-b443-78d96b4491ab.png">
TO BE
<img width="1023" alt="image" src="https://user-images.githubusercontent.com/13323303/216190842-0c015088-dda2-4e6f-8dbe-2db89cfbf438.png">
## How does it works?
ghalint reads GitHub Actions Workflows `^\.github/workflows/.*\.ya?ml$` and validates them.
If there are violatation ghalint outputs error logs and fails.
If there is no violation ghalint succeeds.
## Experimental Features
> [!WARNING]
> These features are experimental, meaning they are unstable and may be changed or removed at minor or patch versions.
### Validate inputs of actions and reusable workflows
[#904](https://github.com/suzuki-shunsuke/ghalint/pull/904)
```console
$ ghalint exp validate-input
ERRO[0000] invalid input key action=suzuki-shunsuke/actionlint-action@c8d3c0dcc9152f1d1c7d4a38cbf4953c3a55953d input_key=actionlint_option job_key=actionlint program=ghalint required_inputs= valid_inputs="sparse-checkout, actionlint_options" version=v1.0.0-local workflow_file_path=.github/workflows/actionlint.yaml
```
`ghalint exp validate-input` command validates inputs of actions and reusable workflows.
It fails if required inputs aren't given or unknown inputs are passed.
> [!WARNING]
> [Actions using `required: true` will not automatically return an error if the input is not specified.](https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#inputs)
> This means if `ghalint exp validate-input` fails as required inputs aren't given, the action may work without any problem.
> Now `ghalint exp validate-input` can't ignore those errors.
> Ideally, actions should be fixed.
By default, the following files are validated.
```
.github/workflows/*.yaml
.github/workflows/*.yml
action.yaml
action.yml
*/action.yaml
*/action.yml
*/*/action.yaml
*/*/action.yml
*/*/*/action.yaml
*/*/*/action.yml
```
This command uses a GitHub access token with `contents:read` permission to download actions and reusable workflows.
It downloads them into `XDG_DATA_HOME/ghalint`.
You can pass a GitHub access token by environment variables `GITHUB_TOKEN` or `GHALINT_GITHUB_TOKEN`.
You can also manage it by secret stores such as GNOME Keyring, Windows Credential Manager, and macOS Keychain.
```sh
ghalint exp token set [-stdin]
```
```sh
ghalint exp token rm # Remove a token from secret store
```
## LICENSE
[MIT](LICENSE)
================================================
FILE: _typos.toml
================================================
[default.extend-words]
ERRO = "ERRO"
intoto = "intoto"
================================================
FILE: aqua/aqua-checksums.json
================================================
{
"checksums": [
{
"id": "github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_darwin_amd64.tar.gz",
"checksum": "C40ECE5407927327F94F35901727DBC604B46857E04F04EC94A310845FB71BDE",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_darwin_arm64.tar.gz",
"checksum": "24E4D34078AE81DA7C82539616F0CCAC3E226CF4F74A38CE6FB3463619E50A55",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_linux_amd64.tar.gz",
"checksum": "0E91737AEE2B5BAF1D255B959630194A302335D848FF97BB07921EB6205B5F5A",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_linux_arm64.tar.gz",
"checksum": "6F6CDCDC695721D91CE756E3B5BC3E3416599C464101F5E32E9C3F33054EE6D9",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_windows_amd64.zip",
"checksum": "195E786EB84EC145854F20528992E86637C77D1968731DFE6CE850C90E28F47A",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-aarch64-apple-darwin.tar.gz",
"checksum": "4B15EE9548CD68CF22D6E67AF8A12CEB608EA4DBC34E0346792D09994222D694",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-aarch64-unknown-linux-musl.tar.gz",
"checksum": "311F2A15E8433C895CD9EE3198530BBFF552F59609EBA739F5BD9CEB2A2C0887",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-apple-darwin.tar.gz",
"checksum": "3652F90D82D38F64E40C1791D2D82209979048EF3ABD715B0EB1488CF483CE1D",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-pc-windows-msvc.zip",
"checksum": "DDC4AE26822E806CE84BC410643D02A3DAC53AAC9AB2A5F389624418C5654A17",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-unknown-linux-musl.tar.gz",
"checksum": "D68C1A9C5ABD8DE11F7749EDFA414087C8BC828E89064714487D23C89F36B06E",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-amd64.tar.gz",
"checksum": "F6F06D94B6241521C53D15450C5209B028270BF966F842AFB11C030C79F5BC16",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-arm64.tar.gz",
"checksum": "A9C54498731B3128F79E090BE6110F3E5FFFCCC617B08142ED244D4126C73F29",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-amd64.tar.gz",
"checksum": "8DF580D2670FED8FA984AAC0507099AF8DF275E665215F5C7A2AE3943893A553",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-arm64.tar.gz",
"checksum": "44CD40A8C76C86755375ADFEEA52CFD3533CB43D7BD647771E0AE065E166DF3A",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-amd64.zip",
"checksum": "BD42E3EBC8CB4ECECB86941983BAAF1DC221BBB04D838E94CE63B49CC91E02BB",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-arm64.zip",
"checksum": "947B9A5BF762D465710B376C156F0184ABB2168378B0826AF1899E0EE7183742",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Darwin_all.tar.gz",
"checksum": "82D730F3366350C90D7E5DF3CF9E8E425FD1C84BF7D7E3E564F92D97C5EA9EA4",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Linux_arm64.tar.gz",
"checksum": "DE01CA1497571E9B348413CD2E7F74BE49B8D57696AE386F7EEDD06176544A88",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Linux_x86_64.tar.gz",
"checksum": "AAE00C71A4A6D55E08CCE9273A1516BDCE33C1E07CFFB7E502FA6FEC4377DEDE",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Windows_arm64.zip",
"checksum": "10227D9DE3EB846F0E58529C22E75DCBD713B67879A7F83912DE7ABE658C5FD7",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Windows_x86_64.zip",
"checksum": "146695F49717DFD79D64D5D6F4B1D25E2B56D73E723BBF68A8DC13CE5CF69693",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Darwin_arm64.tar.gz",
"checksum": "C28DEF83AF6C5AA8728D6D18160546AFD3E5A219117715A2C6C023BD16F14D10",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Darwin_x86_64.tar.gz",
"checksum": "9BAADB110C87F22C55688CF4A966ACCE3006C7A4A962732D6C8B45234C454C6E",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Linux_arm64.tar.gz",
"checksum": "B6AFF657B39E9267A258E8FA66D616F7221AEC5975D0251DAC76043BAD0FA177",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Linux_x86_64.tar.gz",
"checksum": "AD5CE7D5FFA52AAA7EC8710A8FA764181B6CECAAB843CC791E1CCE1680381569",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Windows_arm64.tar.gz",
"checksum": "72ABE9907454C5697777CFFF1D0D03DB8F5A9FD6950C609CA397A90D41AB65D7",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Windows_x86_64.tar.gz",
"checksum": "97C733E492DEC1FD83B9342C25A384D5AB6EBFA72B6978346E9A436CAD1853F6",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/sigstore/cosign/v3.0.6/cosign-darwin-amd64",
"checksum": "4C3E7AF8372D3CA3296E62FA56F23FCBB5721CC6AC1827900D398F110D7CD280",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/sigstore/cosign/v3.0.6/cosign-darwin-arm64",
"checksum": "5FADD012AE6381A6A29FF86A7D39AA873878852F1073FC90B15995961ECFB084",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/sigstore/cosign/v3.0.6/cosign-linux-amd64",
"checksum": "C956E5DFCAC53D52BCF058360D579472F0C1D2D9B69F55209E256FE7783F4C74",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/sigstore/cosign/v3.0.6/cosign-linux-arm64",
"checksum": "BEDAC92E8C3729864E13D4A17048007CFAFA79D5DECA993A43A90FFE018EF2B8",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/sigstore/cosign/v3.0.6/cosign-windows-amd64.exe",
"checksum": "9B85A88EBFF2D9DD30FF4984A6F61F2CEDC232DD87D81FA7F2FF3C0ED96C241C",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_darwin_amd64.tar.gz",
"checksum": "768B8517666A15D25A6870307231416016FC1165F8A1C1743B6AACDBAC7A5FAC",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_darwin_arm64.tar.gz",
"checksum": "FBD7DADDBB65ABD0DE5C6B898F2219588C7D1A71DF6808137D0A628858E7777B",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_linux_amd64.tar.gz",
"checksum": "40BC7B5F472211B22C4786D55F6859FA8093F1A373FF40A2DCCD29BD3D11CF96",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_linux_arm64.tar.gz",
"checksum": "691EB4CC3929A5E065F7C2F977CEE8306D817CB0F8DE9D5B4B4ED38C027CEC41",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_windows_amd64.zip",
"checksum": "4452010897556935E3F94A11AF2B2889563E05073A6DEA72FCF40B83B7F4AE5B",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_windows_arm64.zip",
"checksum": "156D02F4E784E237B0661464D6FF76D6C4EFC4E01F858F8A9734364CD41BC98E",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_darwin_amd64.tar.gz",
"checksum": "D2A0E8605333068065DCF4C9B7B7A24891EDA1750AC01FB755DFBA426A390883",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_darwin_arm64.tar.gz",
"checksum": "1262CAC411E27B4653E6B66B7B06580EBCC2026FDD903E12E6CB0E4591639DE6",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_linux_amd64.tar.gz",
"checksum": "98EE0E3330DE7286F470D1E89C03FF7CE70D7A5998BA0F15969C400447BE579C",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_linux_arm64.tar.gz",
"checksum": "203A22C70B40BB161626973AD2A8DD06AEB736699FC8E03DD425DEE8FF3406E6",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_windows_amd64.zip",
"checksum": "109EA9B39C8E263CEF924BD3B4FE5505964204F934CA60D986F4090D01A99BA5",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_windows_arm64.zip",
"checksum": "C1219CAE104EA418A1CB4E7A02526A3FAD384C0788FA3540A86B316BB074D0D8",
"algorithm": "sha256"
},
{
"id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.513.1/registry.yaml",
"checksum": "2F2D35FCFD79012DD744CD867EDFEDED4954A9382191C35BB25D776C35DECF3A",
"algorithm": "sha256"
}
]
}
================================================
FILE: aqua/aqua.yaml
================================================
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
checksum:
enabled: true
require_checksum: true
registries:
- type: standard
ref: v4.513.1 # renovate: depName=aquaproj/aqua-registry
import_dir: imports
================================================
FILE: aqua/imports/cmdx.yaml
================================================
packages:
- name: suzuki-shunsuke/cmdx@v2.0.2
================================================
FILE: aqua/imports/cosign.yaml
================================================
packages:
- name: sigstore/cosign@v3.0.6
================================================
FILE: aqua/imports/ghalint.yaml
================================================
packages:
- name: suzuki-shunsuke/ghalint@v1.5.6
================================================
FILE: aqua/imports/go-licenses.yaml
================================================
packages:
- name: google/go-licenses@v2.0.1
================================================
FILE: aqua/imports/golangci-lint.yaml
================================================
packages:
- name: golangci/golangci-lint@v2.12.2
================================================
FILE: aqua/imports/goreleaser.yaml
================================================
packages:
- name: goreleaser/goreleaser@v2.15.4
================================================
FILE: aqua/imports/reviewdog.yaml
================================================
packages:
- name: reviewdog/reviewdog@v0.21.0
================================================
FILE: aqua/imports/syft.yaml
================================================
packages:
- name: anchore/syft@v1.44.0
================================================
FILE: aqua/imports/typos.yaml
================================================
packages:
- name: crate-ci/typos@v1.46.2
================================================
FILE: cmd/gen-jsonschema/main.go
================================================
package main
import (
"fmt"
"log"
"github.com/suzuki-shunsuke/gen-go-jsonschema/jsonschema"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
)
func main() {
if err := core(); err != nil {
log.Fatal(err)
}
}
func core() error {
if err := jsonschema.Write(&config.Config{}, "json-schema/ghalint.json"); err != nil {
return fmt.Errorf("create or update a JSON Schema: %w", err)
}
return nil
}
================================================
FILE: cmd/ghalint/main.go
================================================
package main
import (
"github.com/suzuki-shunsuke/ghalint/pkg/cli"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
var version = ""
func main() {
urfave.Main("ghalint", version, cli.Run)
}
================================================
FILE: cmdx.yaml
================================================
---
# cmdx - task runner
# https://github.com/suzuki-shunsuke/cmdx
tasks:
- name: test
short: t
description: test
usage: test
script: go test ./... -race -covermode=atomic
- name: coverage
short: c
description: coverage test
usage: coverage test
script: "bash scripts/coverage.sh {{.target}}"
args:
- name: target
- name: vet
short: v
description: go vet
usage: go vet
script: go vet ./...
- name: lint
short: l
description: lint the go code
usage: lint the go code
script: golangci-lint run
- name: install
short: i
description: go install
usage: go install
script: |
sha=""
if git diff --quiet; then
sha=$(git rev-parse HEAD)
fi
go install \
-ldflags "-X main.version=v1.0.0-local -X main.commit=$sha -X main.date=$(date +"%Y-%m-%dT%H:%M:%SZ%:z" | tr -d '+')" \
./cmd/ghalint
- name: usage
description: Update usage.md
usage: Update usage.md
script: bash scripts/generate-usage.sh
- name: js
description: Generate JSON Schema
usage: Generate JSON Schema
script: "go run ./cmd/gen-jsonschema"
================================================
FILE: docs/codes/001.md
================================================
# parse a workflow file as YAML: EOF
```console
$ ghalint run
ERRO[0000] read a workflow file error="parse a workflow file as YAML: EOF" program=ghalint version=0.2.6 workflow_file_path=.github/workflows/test.yaml
```
This error occurs if the workflow file has no YAML node.
Probably this means the YAML file is empty or all codes are empty lines or commented out.
## How to solve
1. Fix the workflow file
1. Move or rename the workflow file to exclude it from targets of ghalint
If this error occurs, probably the YAML file is invalid as a GitHub Actions Workflow.
So this isn't a bug of ghalint.
Please fix the workflow file.
ref. https://github.com/suzuki-shunsuke/ghalint/issues/197#issuecomment-1782032909
<img width="1095" alt="image" src="https://github.com/suzuki-shunsuke/ghalint/assets/13323303/f471466c-6b87-415e-853c-115c3e76fded">
> [Error: .github#L1](https://github.com/suzuki-shunsuke/test-github-action/commit/52b75ce5cf55aeff15394fb0cabdbaaa28fab847#annotation_15218437727)
> No event triggers defined in `on`
================================================
FILE: docs/codes/002.md
================================================
# read a configuration file: parse configuration file as YAML: EOF
```console
$ ghalint run
FATA[0000] ghalint failed config_file=ghalint.yaml error="read a configuration file: parse configuration file as YAML: EOF"
```
This error occurs if the configuration file has no YAML node.
Probably this means the YAML file is empty or all codes are empty lines or commented out.
## How to solve
Please fix the configuration file.
================================================
FILE: docs/install.md
================================================
# Install
ghalint is written in Go. So you only have to install a binary in your `PATH`.
There are some ways to install ghalint.
1. [Homebrew](#homebrew)
1. [Scoop](#scoop)
1. [aqua](#aqua)
1. [mise](#mise)
1. [GitHub Releases](#github-releases)
1. [Build an executable binary from source code yourself using Go](#build-an-executable-binary-from-source-code-yourself-using-go)
## Homebrew
You can install ghalint using [Homebrew](https://brew.sh/).
```sh
brew install ghalint
```
Or
```sh
brew install suzuki-shunsuke/ghalint/ghalint
```
## Scoop
You can install ghalint using [Scoop](https://scoop.sh/).
```sh
scoop bucket add suzuki-shunsuke https://github.com/suzuki-shunsuke/scoop-bucket
scoop install ghalint
```
## aqua
You can install ghalint using [aqua](https://aquaproj.github.io/).
```sh
aqua g -i suzuki-shunsuke/ghalint
```
## mise
You can install ghalint using [mise](https://github.com/jdx/mise).
```sh
mise use -g ghalint@latest
```
## Build an executable binary from source code yourself using Go
```sh
go install github.com/suzuki-shunsuke/ghalint/cmd/ghalint@latest
```
## GitHub Releases
You can download an asset from [GitHub Releases](https://github.com/suzuki-shunsuke/ghalint/releases).
Please unarchive it and install a pre built binary into `$PATH`.
### Verify downloaded assets from GitHub Releases
You can verify downloaded assets using some tools.
1. [GitHub CLI](https://cli.github.com/)
1. [slsa-verifier](https://github.com/slsa-framework/slsa-verifier)
1. [Cosign](https://github.com/sigstore/cosign)
### 1. GitHub CLI
You can install GitHub CLI by aqua.
```sh
aqua g -i cli/cli
```
```sh
version=v1.2.0
asset=ghalint_darwin_arm64.tar.gz
gh release download -R suzuki-shunsuke/ghalint "$version" -p "$asset"
gh attestation verify "$asset" \
-R suzuki-shunsuke/ghalint \
--signer-workflow suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml
```
### 2. slsa-verifier
You can install slsa-verifier by aqua.
```sh
aqua g -i slsa-framework/slsa-verifier
```
```sh
version=v1.2.0
asset=ghalint_darwin_arm64.tar.gz
gh release download -R suzuki-shunsuke/ghalint "$version" -p "$asset" -p multiple.intoto.jsonl
slsa-verifier verify-artifact "$asset" \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/suzuki-shunsuke/ghalint \
--source-tag "$version"
```
### 3. Cosign
You can install Cosign by aqua.
```sh
aqua g -i sigstore/cosign
```
```sh
version=v1.2.0
checksum_file="ghalint_${version#v}_checksums.txt"
asset=ghalint_darwin_arm64.tar.gz
gh release download "$version" \
-R suzuki-shunsuke/ghalint \
-p "$asset" \
-p "$checksum_file" \
-p "${checksum_file}.pem" \
-p "${checksum_file}.sig"
cosign verify-blob \
--signature "${checksum_file}.sig" \
--certificate "${checksum_file}.pem" \
--certificate-identity-regexp 'https://github\.com/suzuki-shunsuke/go-release-workflow/\.github/workflows/release\.yaml@.*' \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$checksum_file"
cat "$checksum_file" | sha256sum -c --ignore-missing
```
================================================
FILE: docs/policies/001.md
================================================
# job_permissions
All jobs should have the field [permissions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions).
## Examples
:x:
```yaml
jobs:
foo: # The job doesn't have `permissions`
runs-on: ubuntu-latest
steps:
- run: echo hello
```
:o:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
permissions: {} # Set permissions
steps:
- run: echo hello
```
## Why?
For least privilege.
## Exceptions
1. workflow's `permissions` is empty `{}`
```yaml
permissions: {} # empty permissions
jobs:
foo: # The job is missing `permissions`, but it's okay because the workflow's `permissions` is empty
runs-on: ubuntu-latest
steps:
- run: echo hello
```
2. workflow has only one job and the workflow has `permissions`
```yaml
permissions:
contents: read
jobs:
foo: # The job is missing `permissions`, but it's okay because the workflow has permissions and the workflow has only one job.
runs-on: ubuntu-latest
steps:
- run: echo hello
```
================================================
FILE: docs/policies/002.md
================================================
# deny_read_all_permission
[`read-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defining-access-for-the-github_token-scopes) should not be used.
## Examples
:x:
```yaml
name: test
jobs:
foo:
runs-on: ubuntu-latest
permissions: read-all # Don't use read-all
steps:
- run: echo foo
```
:o:
```yaml
name: test
jobs:
foo:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- run: echo foo
```
## Why?
For least privilege.
You should grant only necessary permissions.
================================================
FILE: docs/policies/003.md
================================================
# deny_write_all_permission
[`write-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defining-access-for-the-github_token-scopes) should not be used.
## Examples
:x:
```yaml
name: test
jobs:
foo:
runs-on: ubuntu-latest
permissions: write-all # Don't use write-all
steps:
- run: echo foo
```
:o:
```yaml
name: test
jobs:
foo:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- run: echo foo
```
## Why?
For least privilege.
You should grant only necessary permissions.
================================================
FILE: docs/policies/004.md
================================================
# deny_inherit_secrets
[`secrets: inherit`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsecretsinherit) should not be used
## Examples
:x:
```yaml
jobs:
release:
uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4
secrets: inherit # `inherit` should not be used
```
:o:
```yaml
jobs:
release:
uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4
secrets: # Only required secrets should be passed
gh_app_id: ${{ secrets.APP_ID }}
gh_app_private_key: ${{ secrets.APP_PRIVATE_KEY }}
```
## Why?
Secrets should be exposed to only required jobs.
## How to ignore the violation
We don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).
e.g.
ghalint.yaml
```yaml
excludes:
- policy_name: deny_inherit_secrets
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
```
`policy_name`, `workflow_file_path`, and `job_name` are required.
================================================
FILE: docs/policies/005.md
================================================
# workflow_secrets
Workflows should not set secrets to environment variables.
## Examples
:x:
```yaml
name: test
env:
GITHUB_TOKEN: ${{github.token}} # The secret should not be set to workflow's environment variables
DATADOG_API_KEY: ${{secrets.DATADOG_API_KEY}} # The secret should not be set to workflow's environment variables
jobs:
foo:
runs-on: ubuntu-latest
permissions: {}
steps:
- run: echo foo
bar:
runs-on: ubuntu-latest
permissions: {}
steps:
- run: echo bar
```
:o:
```yaml
name: test
jobs:
foo:
runs-on: ubuntu-latest
permissions: {}
env:
GITHUB_TOKEN: ${{github.token}}
steps:
- run: echo foo
bar:
runs-on: ubuntu-latest
permissions: {}
env:
DATADOG_API_KEY: ${{secrets.DATADOG_API_KEY}}
steps:
- run: echo bar
```
## How to fix
Set secrets to jobs or steps.
## Why?
Secrets should be exposed to only necessary jobs or steps.
## Exceptions
Workflow has only one job.
================================================
FILE: docs/policies/006.md
================================================
# job_secrets
Job should not set secrets to environment variables.
## Examples
:x:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
permissions:
issues: write
env:
GITHUB_TOKEN: ${{github.token}} # secret is set in job
steps:
- run: echo foo
- run: gh label create bug
```
:o:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- run: echo foo
- run: gh label create bug
env:
GITHUB_TOKEN: ${{github.token}} # secret is set in step
```
## How to fix
Set secrets to steps.
## Why?
Secrets should be exposed to only necessary steps.
## Exceptions
Job has only one step.
## How to ignore the violation
We don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).
e.g.
ghalint.yaml
```yaml
excludes:
- policy_name: job_secrets
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
```
`policy_name`, `workflow_file_path`, and `job_name` are required.
================================================
FILE: docs/policies/007.md
================================================
# deny_job_container_latest_image
Job's container image tag should not be `latest`.
## Examples
:x:
```yaml
jobs:
container-test-job:
runs-on: ubuntu-latest
container:
image: node:latest # latest tags should not be used
```
⭕
```yaml
jobs:
container-test-job:
runs-on: ubuntu-latest
container:
image: node:10 # Ideally, hash is best
```
## Why?
Image tags should be pinned with tag or hash.
================================================
FILE: docs/policies/008.md
================================================
# action_ref_should_be_full_length_commit_sha
action's ref should be full length commit SHA
## Examples
:x:
```
actions/checkout@v3
```
⭕
```
actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
```
## Why?
https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions
> Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release.
> Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload
## Exclude
Some actions and reusable workflows don't support pinning version.
You can exclude those actions and reusable workflows.
ghalint.yaml
```yaml
excludes:
# slsa-framework/slsa-github-generator doesn't support pinning version
# > Invalid ref: 68bad40844440577b33778c9f29077a3388838e9. Expected ref of the form refs/tags/vX.Y.Z
# https://github.com/slsa-framework/slsa-github-generator/issues/722
- policy_name: action_ref_should_be_full_length_commit_sha
action_name: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml
```
[#650](https://github.com/suzuki-shunsuke/ghalint/pull/650) As of v1.1.0, `action_name` supports a glob pattern.
https://pkg.go.dev/path#Match
```yaml
excludes:
- policy_name: action_ref_should_be_full_length_commit_sha
action_name: suzuki-shunsuke/tfaction/* # glob pattern
```
`policy_name` and `action_name` are mandatory.
## pinact
https://github.com/suzuki-shunsuke/pinact
[pinact](https://github.com/suzuki-shunsuke/pinact) is useful to convert tags to full length commit SHA.
================================================
FILE: docs/policies/009.md
================================================
# github_app_should_limit_repositories
GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories.
This policy supports the following actions.
1. https://github.com/tibdex/github-app-token
1. https://github.com/actions/create-github-app-token
## Examples
### tibdex/github-app-token
https://github.com/tibdex/github-app-token
:x:
```yaml
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
```
⭕
```yaml
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
repositories: >-
["${{github.event.repository.name}}"]
```
### actions/create-github-app-token
https://github.com/actions/create-github-app-token
:x:
```yaml
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
permission-issues: write
```
⭕
```yaml
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
repositories: "repo1,repo2"
permission-issues: write
```
Or
> If owner and repositories are empty, access will be scoped to only the current repository.
```yaml
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
permission-issues: write
```
## Why?
The scope of access tokens should be limited.
## How to ignore the violation
We don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).
e.g.
ghalint.yaml
```yaml
excludes:
- policy_name: github_app_should_limit_repositories
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
step_id: create_token
```
- workflow: `policy_name`, `workflow_file_path`, `job_name`, `step_id` are required.
- action: `policy_name`, `action_file_path`, `step_id` are required.
================================================
FILE: docs/policies/010.md
================================================
# github_app_should_limit_permissions
GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions.
This policy supports the following actions.
1. https://github.com/tibdex/github-app-token
1. https://github.com/actions/create-github-app-token
> [!NOTE]
> This policy has supported [actions/create-github-app-token](https://github.com/actions/create-github-app-token) since ghalint v1.3.0.
> [actions/create-github-app-token](https://github.com/actions/create-github-app-token) has supported custom permissions since [v1.12.0](https://github.com/actions/create-github-app-token/releases/tag/v1.12.0).
> If you use old create-github-app-token, please update it to v1.12.0 or later.
## Examples
### tibdex/github-app-token
https://github.com/tibdex/github-app-token
:x:
```yaml
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
repositories: >-
["${{github.event.repository.name}}"]
```
⭕
```yaml
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
repositories: >-
["${{github.event.repository.name}}"]
permissions: >-
{
"contents": "read"
}
```
### actions/create-github-app-token
:x:
```yaml
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
```
⭕
```yaml
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
permission-issues: write
```
## Why?
The scope of access tokens should be limited.
================================================
FILE: docs/policies/011.md
================================================
# action_shell_is_required
`shell` is required if `run` is set
## Examples
:x:
```yaml
- run: echo hello
```
⭕
```yaml
- run: echo hello
shell: bash
```
## Why?
> Required if run is set.
https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsshell
================================================
FILE: docs/policies/012.md
================================================
# job_timeout_minutes_is_required
All jobs should set [timeout-minutes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes).
## Examples
:x:
```yaml
jobs:
foo: # The job doesn't have `timeout-minutes`
runs-on: ubuntu-latest
steps:
- run: echo hello
```
:o:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- run: echo hello
```
## :bulb: Set `timeout-minutes` by `ghatm`
https://github.com/suzuki-shunsuke/ghatm
It's so bothersome to fix a lot of workflow files by hand.
[ghatm](https://github.com/suzuki-shunsuke/ghatm) is a command line tool to fix them automatically.
## Why?
https://exercism.org/docs/building/github/gha-best-practices#h-set-timeouts-for-workflows
> By default, GitHub Actions kills workflows after 6 hours if they have not finished by then. Many workflows don't need nearly as much time to finish, but sometimes unexpected errors occur or a job hangs until the workflow run is killed 6 hours after starting it. Therefore it's recommended to specify a shorter timeout.
>
> The ideal timeout depends on the individual workflow but 30 minutes is typically more than enough for the workflows used in Exercism repos.
>
> This has the following advantages:
>
> PRs won't be pending CI for half the day, issues can be caught early or workflow runs can be restarted.
> The number of overall parallel builds is limited, hanging jobs will not cause issues for other PRs if they are cancelled early.
## Exceptions
1. All steps set `timeout-minutes`
```yaml
jobs:
foo: # The job is missing `timeout-minutes`, but it's okay because all steps set timeout-minutes
runs-on: ubuntu-latest
steps:
- run: echo hello
timeout-minutes: 5
- run: echo bar
timeout-minutes: 5
```
2. A job uses a reusable workflow
When a reusable workflow is called with `uses`, `timeout-minutes` is not available.
```yaml
jobs:
foo:
uses: suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@v0.2.3
```
================================================
FILE: docs/policies/013.md
================================================
# checkout_persist_credentials_should_be_false
[actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`.
## Examples
:x:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
steps:
# persist-credentials is not set
- uses: actions/checkout@v4
bar:
runs-on: ubuntu-latest
steps:
# persist-credentials is true
- uses: actions/checkout@v4
with:
persist-credentials: "true"
```
:o:
```yaml
jobs:
foo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: "false"
```
## Why?
https://github.com/actions/checkout/issues/485
Persisting token allows every step after `actions/checkout` to access token.
This is a security risk.
## :bulb: Fix using suzuki-shunsuke/disable-checkout-persist-credentials
Adding `persist-credentials: false` by hand is bothersome.
You can do this automatically using suzuki-shunsuke/disable-checkout-persist-credentials.
https://github.com/suzuki-shunsuke/disable-checkout-persist-credentials
## How to ignore the violation
If you need to persist token in a specific job, please configure it with [the configuration file](../../README.md#configuration-file).
e.g.
ghalint.yaml
```yaml
excludes:
- policy_name: checkout_persist_credentials_should_be_false
workflow_file_path: .github/workflows/actionlint.yaml
job_name: actionlint
```
- workflow: `policy_name`, `workflow_file_path`, `job_name` are required
- action: `policy_name` and `action_file_path` are required
================================================
FILE: docs/usage.md
================================================
# Usage
<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->
```console
$ ghalint --help
NAME:
ghalint - GitHub Actions linter
USAGE:
ghalint [global options] [command [command options]]
VERSION:
1.5.6
COMMANDS:
run lint GitHub Actions Workflows
run-action, act lint actions
experiment, exp experimental commands
version Show version
help, h Shows a list of commands or help for one command
completion Output shell completion script for bash, zsh, fish, or Powershell
GLOBAL OPTIONS:
--log-color string log color [$GHALINT_LOG_COLOR]
--log-level string log level [$GHALINT_LOG_LEVEL]
--config string, -c string configuration file path [$GHALINT_CONFIG]
--help, -h show help
--version, -v print the version
```
## ghalint run
```console
$ ghalint run --help
NAME:
ghalint run - lint GitHub Actions Workflows
USAGE:
ghalint run
OPTIONS:
--help, -h show help
```
## ghalint run-action
```console
$ ghalint run-action --help
NAME:
ghalint run-action - lint actions
USAGE:
ghalint run-action [arguments...]
OPTIONS:
--help, -h show help
```
## ghalint experiment
```console
$ ghalint experiment --help
NAME:
ghalint experiment - experimental commands
USAGE:
ghalint experiment [command [command options]]
DESCRIPTION:
experimental commands. These commands are not stable and may change in the future without major updates.
COMMANDS:
validate-input validate action inputs
OPTIONS:
--help, -h show help
```
### experiment validate-input
```console
$ experiment validate-input --help
NAME:
ghalint experiment validate-input - validate action inputs
USAGE:
ghalint experiment validate-input
DESCRIPTION:
validate action inputs
OPTIONS:
--help, -h show help
```
## ghalint version
```console
$ ghalint version --help
NAME:
ghalint version - Show version
USAGE:
ghalint version
OPTIONS:
--json, -j Output version in JSON format
--help, -h show help
```
## ghalint completion
```console
$ ghalint completion --help
NAME:
ghalint completion - Output shell completion script for bash, zsh, fish, or Powershell
USAGE:
ghalint completion
DESCRIPTION:
Output shell completion script for bash, zsh, fish, or Powershell.
Source the output to enable completion.
# .bashrc
source <(ghalint completion bash)
# .zshrc
source <(ghalint completion zsh)
# fish
ghalint completion fish > ~/.config/fish/completions/ghalint.fish
# Powershell
Output the script to path/to/autocomplete/ghalint.ps1 an run it.
OPTIONS:
--help, -h show help
```
================================================
FILE: go.mod
================================================
module github.com/suzuki-shunsuke/ghalint
go 1.26.3
require (
github.com/adrg/xdg v0.5.3
github.com/google/go-github/v86 v86.0.0
github.com/spf13/afero v1.15.0
github.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0
github.com/suzuki-shunsuke/slog-error v0.2.2
github.com/suzuki-shunsuke/slog-util v0.3.2
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3
github.com/urfave/cli/v3 v3.9.0
golang.org/x/oauth2 v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lmittmann/tint v1.1.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
================================================
FILE: go.sum
================================================
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v86 v86.0.0 h1:S/6aANJhwRm8EQmGKVML3j41yq0h2BsTP8FnDkO7kcA=
github.com/google/go-github/v86 v86.0.0/go.mod h1:zKv1l4SwDXNFMGByi2FWkq71KwSXqj/eQRZuqtmcot8=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0 h1:g7askc+nskCkKRWTVOdsAT8nMhwiaVT6Dmlnh6uvITM=
github.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0/go.mod h1:yFO7h5wwFejxi6jbtazqmk7b/JSBxHcit8DGwb1bhg0=
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 h1:oVXrrYNGBq4POyITQNWKzwsYz7B2nUcqtDbeX4BfeEc=
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0/go.mod h1:kDFtLeftDiIUUHXGI3xq5eJ+uAOi50FPrxPENTHktJ0=
github.com/suzuki-shunsuke/slog-error v0.2.2 h1:z8rymlIlZcMA+ERnnhVigQ0Q+X0pxKqBfDzSIyGh6vU=
github.com/suzuki-shunsuke/slog-error v0.2.2/go.mod h1:w45QyO2G0uiEuo9hhrcLqqRl3hmYon9jGgq9CrCxxOY=
github.com/suzuki-shunsuke/slog-util v0.3.2 h1:P4sc/swT8rwmmKDfMrh9GR+AzYJhJdW3BSxZXYBURuY=
github.com/suzuki-shunsuke/slog-util v0.3.2/go.mod h1:fHyN2kPkinXSgo6GMR0QBj0gd/CpSer0j8bc5C4Pqks=
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3 h1:28ZzFUyh118PFMBeHuKYPkIwaxHo+/mJYmljlr9DBRU=
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3/go.mod h1:pfMAEENW39YADk1hW/bfHfO4rMu8GKgO4Psh6YY9nyM=
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: json-schema/ghalint.json
================================================
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/suzuki-shunsuke/ghalint/pkg/config/config",
"$ref": "#/$defs/Config",
"$defs": {
"Config": {
"properties": {
"excludes": {
"items": {
"$ref": "#/$defs/Exclude"
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object"
},
"Exclude": {
"properties": {
"policy_name": {
"type": "string"
},
"workflow_file_path": {
"type": "string"
},
"action_file_path": {
"type": "string"
},
"job_name": {
"type": "string"
},
"action_name": {
"type": "string"
},
"step_id": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"policy_name"
]
}
}
}
================================================
FILE: pkg/action/find.go
================================================
package action
import (
"fmt"
"github.com/spf13/afero"
)
func Find(fs afero.Fs) ([]string, error) {
patterns := []string{
"action.yaml",
"action.yml",
"*/action.yaml",
"*/action.yml",
"*/*/action.yaml",
"*/*/action.yml",
"*/*/*/action.yaml",
"*/*/*/action.yml",
}
files := []string{}
for _, pattern := range patterns {
matches, err := afero.Glob(fs, pattern)
if err != nil {
return nil, fmt.Errorf("check if the action file exists: %w", err)
}
files = append(files, matches...)
}
return files, nil
}
================================================
FILE: pkg/cli/app.go
================================================
package cli
import (
"context"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment"
"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment/validateinput"
"github.com/suzuki-shunsuke/ghalint/pkg/cli/gflags"
"github.com/suzuki-shunsuke/slog-util/slogutil"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
"github.com/urfave/cli/v3"
)
type RunArgs struct {
*gflags.GlobalFlags
}
type RunActionArgs struct {
*gflags.GlobalFlags
Files []string
}
func Run(ctx context.Context, logger *slogutil.Logger, env *urfave.Env) error { //nolint:funlen
fs := afero.NewOsFs()
runner := &Runner{
fs: fs,
}
gf := &gflags.GlobalFlags{}
runArgs := &RunArgs{
GlobalFlags: gf,
}
runActionArgs := &RunActionArgs{
GlobalFlags: gf,
}
validateInputArgs := &validateinput.Args{
GlobalFlags: gf,
}
return urfave.Command(env, &cli.Command{ //nolint:wrapcheck
Name: "ghalint",
Usage: "GitHub Actions linter",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "log-color",
Usage: "log color",
Sources: cli.EnvVars(
"GHALINT_LOG_COLOR",
),
Destination: &gf.LogColor,
},
&cli.StringFlag{
Name: "log-level",
Usage: "log level",
Sources: cli.EnvVars(
"GHALINT_LOG_LEVEL",
),
Destination: &gf.LogLevel,
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "configuration file path",
Sources: cli.EnvVars(
"GHALINT_CONFIG",
),
Destination: &gf.Config,
},
},
Commands: []*cli.Command{
{
Name: "run",
Usage: "lint GitHub Actions Workflows",
Action: func(ctx context.Context, _ *cli.Command) error {
return runner.Run(ctx, logger, runArgs)
},
},
{
Name: "run-action",
Aliases: []string{
"act",
},
Usage: "lint actions",
Action: func(ctx context.Context, _ *cli.Command) error {
return runner.RunAction(ctx, logger, runActionArgs)
},
Arguments: []cli.Argument{
&cli.StringArgs{
Name: "files",
Destination: &runActionArgs.Files,
Max: -1,
},
},
},
experiment.New(logger, fs, validateInputArgs),
},
}).Run(ctx, env.Args)
}
type Runner struct {
fs afero.Fs
}
================================================
FILE: pkg/cli/experiment/command.go
================================================
package experiment
import (
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment/validateinput"
"github.com/suzuki-shunsuke/slog-util/slogutil"
"github.com/urfave/cli/v3"
)
func New(logger *slogutil.Logger, fs afero.Fs, validateInputArgs *validateinput.Args) *cli.Command {
return &cli.Command{
Name: "experiment",
Aliases: []string{"exp"},
Usage: "experimental commands",
Description: "experimental commands. These commands are not stable and may change in the future without major updates.",
Commands: []*cli.Command{
validateinput.New(logger, fs, validateInputArgs),
},
}
}
================================================
FILE: pkg/cli/experiment/validateinput/command.go
================================================
package validateinput
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/cli/gflags"
"github.com/suzuki-shunsuke/ghalint/pkg/controller/schema"
"github.com/suzuki-shunsuke/ghalint/pkg/github"
"github.com/suzuki-shunsuke/slog-util/slogutil"
"github.com/urfave/cli/v3"
)
type Args struct {
*gflags.GlobalFlags
}
func New(logger *slogutil.Logger, fs afero.Fs, args *Args) *cli.Command {
runner := &Runner{
fs: fs,
}
return &cli.Command{
Name: "validate-input",
Usage: "validate action inputs",
Description: "validate action inputs",
Action: func(ctx context.Context, _ *cli.Command) error {
return runner.Action(ctx, logger, args)
},
}
}
type Runner struct {
fs afero.Fs
}
func (r *Runner) Action(ctx context.Context, logger *slogutil.Logger, args *Args) error {
if err := logger.SetLevel(args.LogLevel); err != nil {
return fmt.Errorf("set log level: %w", err)
}
if err := logger.SetColor(args.LogColor); err != nil {
return fmt.Errorf("set log color: %w", err)
}
rootDir, err := GetRootDir()
if err != nil {
return fmt.Errorf("get the root directory: %w", err)
}
gh := github.New(ctx, logger.Logger)
ctrl := schema.New(r.fs, logger.Logger, gh.Repositories, rootDir)
return ctrl.Run(ctx) //nolint:wrapcheck
}
func GetRootDir() (string, error) {
// ${GHALINT_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/ghalint}
rootDir := os.Getenv("GHALINT_ROOT_DIR")
if rootDir != "" {
return rootDir, nil
}
xdgDataHome := xdg.DataHome
if xdgDataHome == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get the current user home directory: %w", err)
}
xdgDataHome = filepath.Join(home, ".local", "share")
}
return filepath.Join(xdgDataHome, "ghalint"), nil
}
================================================
FILE: pkg/cli/gflags/gflags.go
================================================
package gflags
type GlobalFlags struct {
LogColor string
LogLevel string
Config string
}
================================================
FILE: pkg/cli/run.go
================================================
package cli
import (
"context"
"fmt"
"github.com/suzuki-shunsuke/ghalint/pkg/controller"
"github.com/suzuki-shunsuke/slog-util/slogutil"
)
func (r *Runner) Run(ctx context.Context, logger *slogutil.Logger, args *RunArgs) error {
if err := logger.SetLevel(args.LogLevel); err != nil {
return fmt.Errorf("set log level: %w", err)
}
if err := logger.SetColor(args.LogColor); err != nil {
return fmt.Errorf("set log color: %w", err)
}
ctrl := controller.New(r.fs)
return ctrl.Run(ctx, logger.Logger, args.Config) //nolint:wrapcheck
}
================================================
FILE: pkg/cli/run_action.go
================================================
package cli
import (
"context"
"fmt"
"github.com/suzuki-shunsuke/ghalint/pkg/controller/act"
"github.com/suzuki-shunsuke/slog-util/slogutil"
)
func (r *Runner) RunAction(ctx context.Context, logger *slogutil.Logger, args *RunActionArgs) error {
if err := logger.SetColor(args.LogColor); err != nil {
return fmt.Errorf("set log color: %w", err)
}
if err := logger.SetLevel(args.LogLevel); err != nil {
return fmt.Errorf("set log level: %w", err)
}
ctrl := act.New(r.fs)
return ctrl.Run(ctx, logger.Logger, args.Config, args.Files...) //nolint:wrapcheck
}
================================================
FILE: pkg/config/config.go
================================================
package config
import (
"errors"
"fmt"
"io"
"path"
"path/filepath"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"gopkg.in/yaml.v3"
)
type Config struct {
Excludes []*Exclude `json:"excludes,omitempty"`
}
type Exclude struct {
PolicyName string `json:"policy_name" yaml:"policy_name"`
WorkflowFilePath string `json:"workflow_file_path,omitempty" yaml:"workflow_file_path"`
ActionFilePath string `json:"action_file_path,omitempty" yaml:"action_file_path"`
JobName string `json:"job_name,omitempty" yaml:"job_name"`
ActionName string `json:"action_name,omitempty" yaml:"action_name"`
StepID string `json:"step_id,omitempty" yaml:"step_id"`
}
func (e *Exclude) FilePath() string {
if e.WorkflowFilePath != "" {
return e.WorkflowFilePath
}
return e.ActionFilePath
}
func Find(fs afero.Fs) string {
filePaths := []string{
"ghalint.yaml",
".ghalint.yaml",
".github/ghalint.yaml",
"ghalint.yml",
".ghalint.yml",
".github/ghalint.yml",
}
for _, filePath := range filePaths {
if _, err := fs.Stat(filePath); err == nil {
return filePath
}
}
return ""
}
func Read(fs afero.Fs, cfg *Config, filePath string) error {
f, err := fs.Open(filePath)
if err != nil {
return fmt.Errorf("open a configuration file: %w", err)
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(cfg); err != nil {
err := fmt.Errorf("parse configuration file as YAML: %w", err)
if errors.Is(err, io.EOF) {
return slogerr.With(err, "reference", "https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/002.md") //nolint:wrapcheck
}
return err
}
return nil
}
func Validate(cfg *Config) error {
for _, exclude := range cfg.Excludes {
if err := validate(exclude); err != nil {
return err
}
}
return nil
}
func ConvertPath(cfg *Config) {
for _, exclude := range cfg.Excludes {
convertPath(exclude)
}
}
func convertPath(exclude *Exclude) {
exclude.WorkflowFilePath = filepath.FromSlash(exclude.WorkflowFilePath)
exclude.ActionFilePath = filepath.FromSlash(exclude.ActionFilePath)
}
func validate(exclude *Exclude) error { //nolint:cyclop
if exclude.PolicyName == "" {
return errors.New(`policy_name is required`)
}
switch exclude.PolicyName {
case "action_ref_should_be_full_length_commit_sha":
if exclude.ActionName == "" {
return errors.New(`action_name is required to exclude action_ref_should_be_full_length_commit_sha`)
}
if _, err := path.Match(exclude.ActionName, ""); err != nil {
return fmt.Errorf("action_name must be a glob pattern: %w", slogerr.With(err, "pattern_reference", "https://pkg.go.dev/path#Match"))
}
case "job_secrets":
if exclude.WorkflowFilePath == "" {
return errors.New(`workflow_file_path is required to exclude job_secrets`)
}
if exclude.JobName == "" {
return errors.New(`job_name is required to exclude job_secrets`)
}
case "deny_inherit_secrets":
if exclude.WorkflowFilePath == "" {
return errors.New(`workflow_file_path is required to exclude deny_inherit_secrets`)
}
if exclude.JobName == "" {
return errors.New(`job_name is required to exclude deny_inherit_secrets`)
}
case "github_app_should_limit_repositories":
if exclude.WorkflowFilePath == "" && exclude.ActionFilePath == "" {
return errors.New(`workflow_file_path or action_file_path is required to exclude github_app_should_limit_repositories`)
}
if exclude.WorkflowFilePath != "" && exclude.JobName == "" {
return errors.New(`job_name is required to exclude github_app_should_limit_repositories`)
}
if exclude.StepID == "" {
return errors.New(`step_id is required to exclude github_app_should_limit_repositories`)
}
case "checkout_persist_credentials_should_be_false":
if exclude.WorkflowFilePath == "" && exclude.ActionFilePath == "" {
return errors.New(`workflow_file_path or action_file_path is required to exclude checkout_persist_credentials_should_be_false`)
}
if exclude.WorkflowFilePath != "" && exclude.JobName == "" {
return errors.New(`job_name is required to exclude checkout_persist_credentials_should_be_false`)
}
default:
return slogerr.With(errors.New(`the policy can't be excluded`), "policy_name", exclude.PolicyName) //nolint:wrapcheck
}
return nil
}
================================================
FILE: pkg/config/config_test.go
================================================
package config_test
import (
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
)
func TestValidate(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
isErr bool
}{
{
name: "policy_name is required",
cfg: &config.Config{
Excludes: []*config.Exclude{
{},
},
},
isErr: true,
},
{
name: "action_name is required",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
},
},
},
isErr: true,
},
{
name: "workflow_file_path is required",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "job_secrets",
},
},
},
isErr: true,
},
{
name: "job_name is required",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "job_secrets",
WorkflowFilePath: ".github/workflows/foo.yaml",
},
},
},
isErr: true,
},
{
name: "disallowed policy",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "deny_read_all_permission",
WorkflowFilePath: ".github/workflows/foo.yaml",
JobName: "foo",
},
},
},
isErr: true,
},
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := config.Validate(d.cfg); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/controller/act/controller.go
================================================
package act
import (
"github.com/spf13/afero"
)
type Controller struct {
fs afero.Fs
}
func New(fs afero.Fs) *Controller {
return &Controller{
fs: fs,
}
}
================================================
FILE: pkg/controller/act/run.go
================================================
package act
import (
"context"
"fmt"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/action"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/controller"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
func (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFilePath string, args ...string) error {
cfg := &config.Config{}
if err := c.readConfig(cfg, cfgFilePath); err != nil {
return err
}
filePaths, err := c.listFiles(args...)
if err != nil {
return fmt.Errorf("find action files: %w", err)
}
stepPolicies := []controller.StepPolicy{
&policy.GitHubAppShouldLimitRepositoriesPolicy{},
&policy.GitHubAppShouldLimitPermissionsPolicy{},
&policy.ActionShellIsRequiredPolicy{},
policy.NewActionRefShouldBeSHAPolicy(),
&policy.CheckoutPersistCredentialShouldBeFalsePolicy{},
}
failed := false
for _, filePath := range filePaths {
logger := logger.With("action_file_path", filePath)
if c.validateAction(logger, cfg, stepPolicies, filePath) {
failed = true
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
func (c *Controller) listFiles(args ...string) ([]string, error) {
if len(args) != 0 {
return args, nil
}
return action.Find(c.fs) //nolint:wrapcheck
}
func (c *Controller) validateAction(logger *slog.Logger, cfg *config.Config, stepPolicies []controller.StepPolicy, filePath string) bool {
action := &workflow.Action{}
if err := workflow.ReadAction(c.fs, filePath, action); err != nil {
slogerr.WithError(logger, err).Error("read an action file")
return true
}
stepCtx := &policy.StepContext{
FilePath: filePath,
Action: action,
}
return c.applyStepPolicies(logger, cfg, stepCtx, action, stepPolicies)
}
type Policy interface {
Name() string
ID() string
}
func withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {
return logger.With(
"policy_name", p.Name(),
"reference", fmt.Sprintf("https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/%s.md", p.ID()),
)
}
func (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, action *workflow.Action, stepPolicies []controller.StepPolicy) bool {
failed := false
for _, stepPolicy := range stepPolicies {
logger := withPolicyReference(logger, stepPolicy)
if c.applyStepPolicy(logger, cfg, stepCtx, action, stepPolicy) {
failed = true
}
}
return failed
}
func (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, action *workflow.Action, stepPolicy controller.StepPolicy) bool {
failed := false
for _, step := range action.Runs.Steps {
logger := logger
if step.ID != "" {
logger = logger.With("step_id", step.ID)
}
if step.Name != "" {
logger = logger.With("step_name", step.Name)
}
if err := stepPolicy.ApplyStep(logger, cfg, stepCtx, step); err != nil {
if err.Error() != "" {
slogerr.WithError(logger, err).Error("the step violates policies")
}
failed = true
}
}
return failed
}
func (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) error {
if cfgFilePath == "" {
if c := config.Find(c.fs); c != "" {
cfgFilePath = c
}
}
if cfgFilePath != "" {
if err := config.Read(c.fs, cfg, cfgFilePath); err != nil {
return fmt.Errorf("read a configuration file: %w", slogerr.With(err,
"config_file", cfgFilePath,
))
}
if err := config.Validate(cfg); err != nil {
return fmt.Errorf("validate a configuration file: %w", slogerr.With(err,
"config_file", cfgFilePath,
))
}
config.ConvertPath(cfg)
}
return nil
}
================================================
FILE: pkg/controller/controller.go
================================================
package controller
import (
"log/slog"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type Controller struct {
fs afero.Fs
}
func New(fs afero.Fs) *Controller {
return &Controller{
fs: fs,
}
}
type WorkflowPolicy interface {
Name() string
ID() string
ApplyWorkflow(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, wf *workflow.Workflow) error
}
type JobPolicy interface {
Name() string
ID() string
ApplyJob(logger *slog.Logger, cfg *config.Config, jobCtx *policy.JobContext, job *workflow.Job) error
}
type StepPolicy interface {
Name() string
ID() string
ApplyStep(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, step *workflow.Step) error
}
================================================
FILE: pkg/controller/run.go
================================================
package controller
import (
"context"
"fmt"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
func (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFilePath string) error {
cfg := &config.Config{}
if err := c.readConfig(cfg, cfgFilePath); err != nil {
return err
}
filePaths, err := workflow.List(c.fs)
if err != nil {
return fmt.Errorf("find workflow files: %w", err)
}
wfPolicies := []WorkflowPolicy{
policy.NewWorkflowSecretsPolicy(),
}
jobPolicies := []JobPolicy{
&policy.JobPermissionsPolicy{},
&policy.JobTimeoutMinutesIsRequiredPolicy{},
policy.NewJobSecretsPolicy(),
&policy.DenyInheritSecretsPolicy{},
&policy.DenyJobContainerLatestImagePolicy{},
policy.NewActionRefShouldBeSHAPolicy(),
&policy.DenyReadAllPermissionPolicy{},
&policy.DenyWriteAllPermissionPolicy{},
}
stepPolicies := []StepPolicy{
&policy.GitHubAppShouldLimitRepositoriesPolicy{},
&policy.GitHubAppShouldLimitPermissionsPolicy{},
policy.NewActionRefShouldBeSHAPolicy(),
&policy.CheckoutPersistCredentialShouldBeFalsePolicy{},
}
failed := false
for _, filePath := range filePaths {
logger := logger.With("workflow_file_path", filePath)
if c.validateWorkflow(logger, cfg, wfPolicies, jobPolicies, stepPolicies, filePath) {
failed = true
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
func (c *Controller) validateWorkflow(logger *slog.Logger, cfg *config.Config, wfPolicies []WorkflowPolicy, jobPolicies []JobPolicy, stepPolicies []StepPolicy, filePath string) bool {
wf := &workflow.Workflow{
FilePath: filePath,
}
if err := workflow.Read(c.fs, filePath, wf); err != nil {
slogerr.WithError(logger, err).Error("read a workflow file")
return true
}
wfCtx := &policy.WorkflowContext{
FilePath: filePath,
Workflow: wf,
}
failed := false
for _, wfPolicy := range wfPolicies {
logger := withPolicyReference(logger, wfPolicy)
if err := wfPolicy.ApplyWorkflow(logger, cfg, wfCtx, wf); err != nil {
if err.Error() != "" {
slogerr.WithError(logger, err).Error("the workflow violates policies")
}
failed = true
continue
}
}
if c.applyJobPolicies(logger, cfg, wfCtx, jobPolicies) {
failed = true
}
if c.applyStepPolicies(logger, cfg, wfCtx, wf.Jobs, stepPolicies) {
failed = true
}
return failed
}
type Policy interface {
Name() string
ID() string
}
func withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {
return logger.With(
"policy_name", p.Name(),
"reference", fmt.Sprintf("https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/%s.md", p.ID()),
)
}
func (c *Controller) applyJobPolicies(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobPolicies []JobPolicy) bool {
failed := false
for _, jobPolicy := range jobPolicies {
logger := withPolicyReference(logger, jobPolicy)
if c.applyJobPolicy(logger, cfg, wfCtx, jobPolicy) {
failed = true
}
}
return failed
}
func (c *Controller) applyJobPolicy(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobPolicy JobPolicy) bool {
failed := false
for jobName, job := range wfCtx.Workflow.Jobs {
jobCtx := &policy.JobContext{
Workflow: wfCtx,
Name: jobName,
}
logger := logger.With("job_name", jobName)
if err := jobPolicy.ApplyJob(logger, cfg, jobCtx, job); err != nil {
failed = true
if err.Error() != "" {
slogerr.WithError(logger, err).Error("the job violates policies")
}
}
}
return failed
}
func (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobs map[string]*workflow.Job, stepPolicies []StepPolicy) bool {
failed := false
for _, stepPolicy := range stepPolicies {
logger := withPolicyReference(logger, stepPolicy)
if c.applyStepPolicy(logger, cfg, wfCtx, jobs, stepPolicy) {
failed = true
}
}
return failed
}
func (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobs map[string]*workflow.Job, stepPolicy StepPolicy) bool {
failed := false
for jobName, job := range jobs {
stepCtx := &policy.StepContext{
FilePath: wfCtx.FilePath,
Job: &policy.JobContext{
Name: jobName,
Workflow: wfCtx,
Job: job,
},
}
logger := logger.With("job_name", jobName)
for _, step := range job.Steps {
logger := logger
if step.ID != "" {
logger = logger.With("step_id", step.ID)
}
if step.Name != "" {
logger = logger.With("step_name", step.Name)
}
if err := stepPolicy.ApplyStep(logger, cfg, stepCtx, step); err != nil {
if err.Error() != "" {
slogerr.WithError(logger, err).Error("the step violates policies")
}
failed = true
}
}
}
return failed
}
func (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) error {
if cfgFilePath == "" {
if c := config.Find(c.fs); c != "" {
cfgFilePath = c
}
}
if cfgFilePath != "" {
if err := config.Read(c.fs, cfg, cfgFilePath); err != nil {
return fmt.Errorf("read a configuration file: %w", slogerr.With(err,
"config_file", cfgFilePath,
))
}
if err := config.Validate(cfg); err != nil {
return fmt.Errorf("validate a configuration file: %w", slogerr.With(err,
"config_file", cfgFilePath,
))
}
config.ConvertPath(cfg)
}
return nil
}
================================================
FILE: pkg/controller/schema/action.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/action"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
func (c *Controller) runActions(ctx context.Context) error {
filePaths, err := action.Find(c.fs)
if err != nil {
return fmt.Errorf("find action files: %w", err)
}
failed := false
for _, filePath := range filePaths {
logger := c.logger.With("action_file_path", filePath)
vw := &validateAction{
action: filePath,
logger: logger,
fs: c.fs,
gh: c.gh,
rootDir: c.rootDir,
}
if err := vw.validate(ctx); err != nil {
slogerr.WithError(logger, err).Error("validate action")
failed = true
}
}
if failed {
return errors.New("some action files are invalid")
}
return nil
}
type validateAction struct {
action string
logger *slog.Logger
fs afero.Fs
gh GitHub
rootDir string
}
func (v *validateAction) validate(ctx context.Context) error {
act := &workflow.Action{}
if err := workflow.ReadAction(v.fs, v.action, act); err != nil {
return fmt.Errorf("read an action file: %w", err)
}
failed := false
for _, step := range act.Runs.Steps {
vs := &validateStep{
step: step,
logger: v.logger,
fs: v.fs,
gh: v.gh,
rootDir: v.rootDir,
}
if err := vs.validate(ctx); err != nil {
slogerr.WithError(v.logger, err).Error("validate a step")
failed = true
}
}
if failed {
return errors.New("some steps are invalid")
}
return nil
}
================================================
FILE: pkg/controller/schema/controller.go
================================================
package schema
import (
"context"
"log/slog"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/github"
)
type Controller struct {
fs afero.Fs
logger *slog.Logger
gh GitHub
rootDir string
}
func New(fs afero.Fs, logger *slog.Logger, gh GitHub, rootDir string) *Controller {
return &Controller{
fs: fs,
logger: logger,
gh: gh,
rootDir: rootDir,
}
}
type GitHub interface {
GetCommitSHA1(ctx context.Context, owner, repo, ref, lastSHA string) (string, *github.Response, error)
GetContents(ctx context.Context, owner, repo, path string, opts *github.RepositoryContentGetOptions) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, resp *github.Response, err error)
}
================================================
FILE: pkg/controller/schema/job.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
type validateJob struct {
job *workflow.Job
logger *slog.Logger
fs afero.Fs
gh GitHub
rootDir string
}
func (v *validateJob) validate(ctx context.Context) error {
// Get actions
if v.job.Uses != "" {
v.logger = v.logger.With("reusable_workflow", v.job.Uses)
if err := v.validateReusableWorkflow(ctx); err != nil {
return fmt.Errorf("validate a reusable workflow: %w", err)
}
return nil
}
failed := false
for _, step := range v.job.Steps {
vs := &validateStep{
step: step,
fs: v.fs,
logger: v.logger,
gh: v.gh,
rootDir: v.rootDir,
}
if err := vs.validate(ctx); err != nil {
failed = true
if !errors.Is(err, urfave.ErrSilent) {
slogerr.WithError(v.logger, err).Error("validate a step")
}
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
================================================
FILE: pkg/controller/schema/reusable_workflow.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"io"
"maps"
"path/filepath"
"slices"
"strings"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/github"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
"gopkg.in/yaml.v3"
)
func (v *validateJob) validateReusableWorkflow(ctx context.Context) error {
// read workflow
wf := &ReusableWorkflow{}
if err := v.read(ctx, wf); err != nil {
return fmt.Errorf("read a reusable workflow: %w", err)
}
if err := v.validateWorkflow(wf); err != nil {
return fmt.Errorf("validate a reusable workflow: %w", err)
}
return nil
}
/*
on:
workflow_call:
inputs:
aqua_policy_config:
required: false
type: string
*/
type ReusableWorkflow struct {
On *On
}
type On struct {
WorkflowCall *WorkflowCall `yaml:"workflow_call"`
}
func (o *On) UnmarshalYAML(unmarshal func(any) error) error { //nolint:cyclop
var onAny any
if err := unmarshal(&onAny); err != nil {
return fmt.Errorf("unmarshal a workflow to any: %w", err)
}
if s, ok := onAny.(string); ok {
if s != "workflow_call" {
return nil
}
o.WorkflowCall = &WorkflowCall{}
return nil
}
onMap, ok := onAny.(map[string]any)
if !ok {
return errors.New("failed to convert workflow on into map")
}
workflowCallAny, ok := onMap["workflow_call"]
if !ok {
return nil
}
o.WorkflowCall = &WorkflowCall{}
workflowCallMap, ok := workflowCallAny.(map[string]any)
if !ok {
return nil
}
inputsAny, ok := workflowCallMap["inputs"]
if !ok {
return nil
}
inputsMap, ok := inputsAny.(map[string]any)
if !ok {
return nil
}
o.WorkflowCall.Inputs = map[string]*workflow.Input{}
for inputKey, v := range inputsMap {
o.WorkflowCall.Inputs[inputKey] = &workflow.Input{}
inputValueMap, ok := v.(map[string]any)
if !ok {
continue
}
requiredAny, ok := inputValueMap["required"]
if !ok {
continue
}
required, ok := requiredAny.(bool)
if !ok {
continue
}
o.WorkflowCall.Inputs[inputKey] = &workflow.Input{
Required: required,
}
}
return nil
}
type WorkflowCall struct {
Inputs map[string]*workflow.Input
}
func (v *validateJob) validateWorkflow(wf *ReusableWorkflow) error {
if wf.On == nil {
return errors.New("the reusable workflow is invalid. on is not set")
}
if wf.On.WorkflowCall == nil {
return errors.New("the reusable workflow is invalid. workflow_call is not set")
}
inputs := wf.On.WorkflowCall.Inputs
requiredKeys := map[string]struct{}{}
for key, input := range inputs {
if input.Required {
requiredKeys[key] = struct{}{}
}
}
v.logger = v.logger.With(
"valid_inputs", strings.Join(slices.Collect(maps.Keys(inputs)), ", "),
"required_inputs", strings.Join(slices.Collect(maps.Keys(requiredKeys)), ", "),
)
failed := false
// Check if the input is valid
for key := range v.job.With {
if _, ok := inputs[key]; !ok {
v.logger.Error("invalid input key", "input_key", key)
failed = true
}
}
// Check if required keys are set
for key := range requiredKeys {
if _, ok := v.job.With[key]; !ok {
v.logger.Error("required key is not set", "input_key", key)
failed = true
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
func readReusableWorkflow(fs afero.Fs, p string, wf *ReusableWorkflow) error {
f, err := fs.Open(p)
if err != nil {
return fmt.Errorf("open a workflow file: %w", err)
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(wf); err != nil {
err := fmt.Errorf("parse a workflow file as YAML: %w", err)
if errors.Is(err, io.EOF) {
return slogerr.With(err, //nolint:wrapcheck
"reference", "https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md",
)
}
return err
}
return nil
}
func (v *validateJob) read(ctx context.Context, wf *ReusableWorkflow) error { //nolint:cyclop
if strings.HasPrefix(v.job.Uses, "./") {
// local workflow
if err := readReusableWorkflow(v.fs, v.job.Uses, wf); err != nil {
return fmt.Errorf("read a local workflow file: %w", err)
}
return nil
}
// <owner>/<repo>[/<path>]@<ref>
fullPath, ref, ok := strings.Cut(v.job.Uses, "@")
if !ok {
return fmt.Errorf("invalid job.uses format: %s", v.job.Uses)
}
elems := strings.Split(fullPath, "/")
owner := elems[0]
repo := elems[1]
path := strings.Join(elems[2:], "/")
sha := ref
if !fullCommitSHAPattern.MatchString(ref) {
// Get SHA of actions
s, _, err := v.gh.GetCommitSHA1(ctx, owner, repo, ref, "")
if err != nil {
return fmt.Errorf("get commit SHA1: %w", err)
}
sha = s
}
// Download actions and store them in $GHALINT_ROOT_DIR/actions
// Check if the action file exists
cachePath := filepath.Join(v.rootDir, "actions", owner, repo, sha, path)
if f, err := afero.Exists(v.fs, cachePath); err != nil {
return fmt.Errorf("check if the workflow file exists: %w", err)
} else if f {
if err := readReusableWorkflow(v.fs, cachePath, wf); err != nil {
return fmt.Errorf("read a cached workflow file: %w", err)
}
return nil
}
// Download a wofklow file
content, _, _, err := v.gh.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{
Ref: sha,
})
if err != nil {
return fmt.Errorf("download workflow file: %w", err)
}
// write workflow to the cache dir
if err := v.fs.MkdirAll(filepath.Dir(cachePath), dirPermission); err != nil {
return fmt.Errorf("create workflow directory: %w", err)
}
c, err := content.GetContent()
if err != nil {
return fmt.Errorf("get content: %w", err)
}
b := []byte(c)
if err := afero.WriteFile(v.fs, cachePath, b, filePermission); err != nil {
return fmt.Errorf("write workflow file: %w", err)
}
if err := yaml.Unmarshal(b, wf); err != nil {
return fmt.Errorf("unmarshal workflow file: %w", err)
}
return nil
}
================================================
FILE: pkg/controller/schema/run.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
func (c *Controller) Run(ctx context.Context) error {
// Find action.yaml and workflow files
failed := false
if err := c.runWorkflow(ctx); err != nil {
failed = true
if !errors.Is(err, urfave.ErrSilent) {
slogerr.WithError(c.logger, err).Error("validate workflows")
}
}
if err := c.runActions(ctx); err != nil {
if !errors.Is(err, urfave.ErrSilent) {
return fmt.Errorf("validate actions: %w", err)
}
return urfave.ErrSilent
}
if failed {
return urfave.ErrSilent
}
return nil
}
================================================
FILE: pkg/controller/schema/step.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"regexp"
"strings"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/github"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
"gopkg.in/yaml.v3"
)
type validateStep struct {
step *workflow.Step
logger *slog.Logger
fs afero.Fs
gh GitHub
rootDir string
}
var fullCommitSHAPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`)
func (v *validateStep) readAction(ctx context.Context, action *workflow.Action) error { //nolint:cyclop
if strings.HasPrefix(v.step.Uses, "./") {
// local action
if err := v.readLocalAction(action); err != nil {
return fmt.Errorf("read a local action file: %w", err)
}
return nil
}
// <owner>/<repo>[/<path>]@<ref>
fullPath, ref, ok := strings.Cut(v.step.Uses, "@")
if !ok {
return fmt.Errorf("invalid action format: %s", v.step.Uses)
}
elems := strings.Split(fullPath, "/")
owner := elems[0]
repo := elems[1]
path := strings.Join(elems[2:], "/")
sha := ref
if !fullCommitSHAPattern.MatchString(ref) {
// Get SHA of actions
s, _, err := v.gh.GetCommitSHA1(ctx, owner, repo, ref, "")
if err != nil {
return fmt.Errorf("get commit SHA1: %w", err)
}
sha = s
}
// Download actions and store them in $GHALINT_ROOT_DIR/actions
// Check if the action file exists
cachePath := filepath.Join(v.rootDir, "actions", owner, repo, sha, path, "action.yaml")
if f, err := afero.Exists(v.fs, cachePath); err != nil {
return fmt.Errorf("check if the action file exists: %w", err)
} else if f {
if err := workflow.ReadAction(v.fs, cachePath, action); err != nil {
return fmt.Errorf("read a cached action file: %w", err)
}
return nil
}
// Download action.yaml or action.yml
content, err := v.download(ctx, &downloadInput{
Owner: owner,
Repo: repo,
Path: path,
Ref: sha,
})
if err != nil {
return fmt.Errorf("download action file: %w", err)
}
// write action.yaml to $GHALINT_ROOT_DIR/actions/<owner>/<repo>/<path>
if err := v.fs.MkdirAll(filepath.Dir(cachePath), dirPermission); err != nil {
return fmt.Errorf("create action directory: %w", err)
}
if err := afero.WriteFile(v.fs, cachePath, []byte(content), filePermission); err != nil {
return fmt.Errorf("write action file: %w", err)
}
if err := yaml.Unmarshal([]byte(content), action); err != nil {
return fmt.Errorf("unmarshal action file: %w", err)
}
return nil
}
const (
filePermission = 0o644
dirPermission = 0o755
)
type downloadInput struct {
Owner string
Repo string
Path string
Ref string
}
func (v *validateStep) download(ctx context.Context, input *downloadInput) (string, error) {
for _, file := range []string{"action.yaml", "action.yml"} {
content, _, _, err := v.gh.GetContents(ctx, input.Owner, input.Repo, filepath.Join(input.Path, file), &github.RepositoryContentGetOptions{
Ref: input.Ref,
})
if err != nil {
slogerr.WithError(v.logger, err).Debug("get action file")
continue
}
s, err := content.GetContent()
if err != nil {
return "", fmt.Errorf("get content: %w", err)
}
return s, nil
}
return "", errors.New("action file can't be downloaded")
}
func (v *validateStep) validate(ctx context.Context) error {
// Validate inputs
if v.step.Uses == "" {
return nil
}
v.logger = v.logger.With("action", v.step.Uses)
action := &workflow.Action{}
if err := v.readAction(ctx, action); err != nil {
return fmt.Errorf("read action: %w", err)
}
validKeys := map[string]struct{}{}
requiredKeys := map[string]struct{}{}
validKeysArray := make([]string, 0, len(action.Inputs))
requiredKeysArray := []string{}
for key, input := range action.Inputs {
validKeysArray = append(validKeysArray, key)
validKeys[key] = struct{}{}
if input.Required {
requiredKeys[key] = struct{}{}
requiredKeysArray = append(requiredKeysArray, key)
}
}
validKeysS := strings.Join(validKeysArray, ", ")
requiredKeysS := strings.Join(requiredKeysArray, ", ")
v.logger = v.logger.With(
"valid_inputs", validKeysS,
"required_inputs", requiredKeysS,
)
failed := false
// Check if the input is valid
for key := range v.step.With {
if _, ok := action.Inputs[key]; !ok {
v.logger.Error("invalid input key", "input_key", key)
failed = true
}
}
// Check if required keys are set
for key := range requiredKeys {
if _, ok := v.step.With[key]; !ok {
v.logger.Error("required key is not set", "input_key", key)
failed = true
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
func (v *validateStep) readLocalAction(action *workflow.Action) error {
found := false
for _, file := range []string{"action.yaml", "action.yml"} {
p := filepath.Join(v.step.Uses, file)
if f, err := afero.Exists(v.fs, p); err != nil {
return fmt.Errorf("check if the action file exists: %w", err)
} else if !f {
continue
}
found = true
if err := workflow.ReadAction(v.fs, p, action); err != nil {
return fmt.Errorf("read a local action file: %w", err)
}
}
if !found {
return errors.New("local action file not found")
}
return nil
}
================================================
FILE: pkg/controller/schema/workflow.go
================================================
package schema
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)
func (c *Controller) runWorkflow(ctx context.Context) error {
filePaths, err := workflow.List(c.fs)
if err != nil {
return fmt.Errorf("find workflow files: %w", err)
}
failed := false
for _, filePath := range filePaths {
logger := c.logger.With("workflow_file_path", filePath)
vw := &validateWorkflow{
workflow: filePath,
logger: logger,
fs: c.fs,
gh: c.gh,
rootDir: c.rootDir,
}
if err := vw.validate(ctx); err != nil {
failed = true
if !errors.Is(err, urfave.ErrSilent) {
slogerr.WithError(logger, err).Error("validate workflow")
}
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
type validateWorkflow struct {
workflow string
logger *slog.Logger
fs afero.Fs
gh GitHub
rootDir string
}
func (v *validateWorkflow) validate(ctx context.Context) error {
wf := &workflow.Workflow{
FilePath: v.workflow,
}
if err := workflow.Read(v.fs, v.workflow, wf); err != nil {
return fmt.Errorf("read a workflow file: %w", err)
}
failed := false
for name, job := range wf.Jobs {
vj := &validateJob{
job: job,
logger: v.logger.With("job_key", name),
fs: v.fs,
gh: v.gh,
rootDir: v.rootDir,
}
if err := vj.validate(ctx); err != nil {
failed = true
if !errors.Is(err, urfave.ErrSilent) {
slogerr.WithError(v.logger, err).Error("validate job")
}
}
}
if failed {
return urfave.ErrSilent
}
return nil
}
================================================
FILE: pkg/github/github.go
================================================
package github
import (
"context"
"log/slog"
"net/http"
"os"
"github.com/google/go-github/v86/github"
"github.com/suzuki-shunsuke/urfave-cli-v3-util/keyring/ghtoken"
"golang.org/x/oauth2"
)
type (
ListOptions = github.ListOptions
Reference = github.Reference
Response = github.Response
RepositoryTag = github.RepositoryTag
RepositoryRelease = github.RepositoryRelease
Client = github.Client
GitObject = github.GitObject
Commit = github.Commit
RepositoryContentGetOptions = github.RepositoryContentGetOptions
RepositoryContent = github.RepositoryContent
)
func New(ctx context.Context, logger *slog.Logger) *Client {
return github.NewClient(getHTTPClientForGitHub(ctx, logger, getGitHubToken()))
}
func getGitHubToken() string {
return os.Getenv("GITHUB_TOKEN")
}
func checkKeyringEnabled() bool {
return os.Getenv("GHALINT_KEYRING_ENABLED") == "true"
}
func getHTTPClientForGitHub(ctx context.Context, logger *slog.Logger, token string) *http.Client {
if token == "" {
if checkKeyringEnabled() {
return oauth2.NewClient(ctx, ghtoken.NewTokenSource(logger, KeyService))
}
return http.DefaultClient
}
return oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
))
}
================================================
FILE: pkg/github/keyring.go
================================================
package github
const (
KeyService = "suzuki-shunsuke/ghalint"
)
================================================
FILE: pkg/policy/action_ref_should_be_full_length_commit_sha_policy.go
================================================
package policy
import (
"errors"
"log/slog"
"path"
"regexp"
"strings"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type ActionRefShouldBeSHAPolicy struct {
sha1Pattern *regexp.Regexp
sha256Pattern *regexp.Regexp
}
func NewActionRefShouldBeSHAPolicy() *ActionRefShouldBeSHAPolicy {
return &ActionRefShouldBeSHAPolicy{
sha1Pattern: regexp.MustCompile(`\b[0-9a-f]{40}\b`),
sha256Pattern: regexp.MustCompile(`\b[0-9a-f]{64}\b`),
}
}
func (p *ActionRefShouldBeSHAPolicy) Name() string {
return "action_ref_should_be_full_length_commit_sha"
}
func (p *ActionRefShouldBeSHAPolicy) ID() string {
return "008"
}
func (p *ActionRefShouldBeSHAPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, _ *JobContext, job *workflow.Job) error {
return p.apply(cfg, job.Uses)
}
func (p *ActionRefShouldBeSHAPolicy) ApplyStep(_ *slog.Logger, cfg *config.Config, _ *StepContext, step *workflow.Step) error {
return p.apply(cfg, step.Uses)
}
func (p *ActionRefShouldBeSHAPolicy) apply(cfg *config.Config, uses string) error {
action := p.checkUses(uses)
if action == "" || p.excluded(action, cfg.Excludes) {
return nil
}
return slogerr.With(errors.New("action ref should be full length SHA"), //nolint:wrapcheck
"action", action,
)
}
func (p *ActionRefShouldBeSHAPolicy) checkUses(uses string) string {
if uses == "" {
return ""
}
if ref, ok := strings.CutPrefix(uses, "docker://"); ok {
repoAndTag, digest, hasDigest := strings.Cut(ref, "@sha256:")
if hasDigest && p.sha256Pattern.MatchString(digest) {
return ""
}
repo := repoAndTag
lastColon := strings.LastIndex(repoAndTag, ":")
lastSlash := strings.LastIndex(repoAndTag, "/")
if lastColon != -1 && lastColon > lastSlash {
repo = repoAndTag[:lastColon]
}
return "docker://" + repo
}
action, tag, ok := strings.Cut(uses, "@")
if !ok {
return ""
}
if p.sha1Pattern.MatchString(tag) {
return ""
}
return action
}
func (p *ActionRefShouldBeSHAPolicy) excluded(action string, excludes []*config.Exclude) bool {
for _, exclude := range excludes {
if exclude.PolicyName != p.Name() {
continue
}
if f, _ := path.Match(exclude.ActionName, action); f {
return true
}
}
return false
}
================================================
FILE: pkg/policy/action_ref_should_be_full_length_commit_sha_policy_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestActionRefShouldBeSHAPolicy_ApplyJob(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
job *workflow.Job
isErr bool
}{
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "slsa-framework/slsa-github-generator",
},
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml",
},
},
},
job: &workflow.Job{
Steps: []*workflow.Step{
{
Uses: "slsa-framework/slsa-github-generator@v1.5.0",
},
},
},
},
{
name: "job error",
isErr: true,
cfg: &config.Config{},
job: &workflow.Job{
Uses: "suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4",
},
},
{
name: "docker image with digest",
cfg: &config.Config{},
job: &workflow.Job{
Uses: "docker://rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with digest (no tag)",
cfg: &config.Config{},
job: &workflow.Job{
Uses: "docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with port and digest",
cfg: &config.Config{},
job: &workflow.Job{
Uses: "docker://registry.example.com:5000/myimage:1.0.0@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with tag",
isErr: true,
cfg: &config.Config{},
job: &workflow.Job{
Uses: "docker://rhysd/actionlint:latest",
},
},
{
name: "docker image with port and tag",
isErr: true,
cfg: &config.Config{},
job: &workflow.Job{
Uses: "docker://registry.example.com:5000/myimage:latest",
},
},
{
name: "exclude docker image with tag",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "docker://rhysd/actionlint",
},
},
},
job: &workflow.Job{
Uses: "docker://rhysd/actionlint:latest",
},
},
{
name: "exclude docker image with port and tag",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "docker://registry.example.com:5000/myimage",
},
},
},
job: &workflow.Job{
Uses: "docker://registry.example.com:5000/myimage:latest",
},
},
}
p := policy.NewActionRefShouldBeSHAPolicy()
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, d.cfg, nil, d.job); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
func TestActionRefShouldBeSHAPolicy_ApplyStep(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
step *workflow.Step
isErr bool
}{
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "slsa-framework/slsa-github-generator",
},
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml",
},
},
},
step: &workflow.Step{
Uses: "slsa-framework/slsa-github-generator@v1.5.0",
},
},
{
name: "exclude with glob pattern",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "slsa-framework/*",
},
},
},
step: &workflow.Step{
Uses: "slsa-framework/slsa-github-generator@v1.5.0",
},
},
{
name: "step error",
isErr: true,
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "actions/checkout",
},
},
},
step: &workflow.Step{
Uses: "slsa-framework/slsa-github-generator@v1.5.0",
ID: "generate",
Name: "Generate SLSA Provenance",
},
},
{
name: "docker image with digest",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "docker://rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with digest (no tag)",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with port and digest",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "docker://registry.example.com:5000/myimage:1.0.0@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9",
},
},
{
name: "docker image with tag",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "docker://rhysd/actionlint:latest",
},
},
{
name: "docker image with port and tag",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "docker://registry.example.com:5000/myimage:latest",
},
},
{
name: "exclude docker image with tag",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "docker://rhysd/actionlint",
},
},
},
step: &workflow.Step{
Uses: "docker://rhysd/actionlint:latest",
},
},
{
name: "exclude docker image with port and tag",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "action_ref_should_be_full_length_commit_sha",
ActionName: "docker://registry.example.com:5000/myimage",
},
},
},
step: &workflow.Step{
Uses: "docker://registry.example.com:5000/myimage:latest",
},
},
}
p := policy.NewActionRefShouldBeSHAPolicy()
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyStep(logger, d.cfg, nil, d.step); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/action_shell_is_required.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type ActionShellIsRequiredPolicy struct{}
func (p *ActionShellIsRequiredPolicy) Name() string {
return "action_shell_is_required"
}
func (p *ActionShellIsRequiredPolicy) ID() string {
return "011"
}
func (p *ActionShellIsRequiredPolicy) ApplyStep(_ *slog.Logger, _ *config.Config, _ *StepContext, step *workflow.Step) error {
if step.Run != "" && step.Shell == "" {
return errors.New("shell is required if run is set")
}
return nil
}
================================================
FILE: pkg/policy/action_shell_is_required_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestActionShellIsRequiredPolicy_ApplyStep(t *testing.T) {
t.Parallel()
data := []struct {
name string
step *workflow.Step
isErr bool
}{
{
name: "pass",
step: &workflow.Step{
Run: "echo hello",
Shell: "bash",
},
},
{
name: "step error",
isErr: true,
step: &workflow.Step{
Run: "echo hello",
},
},
}
p := &policy.ActionShellIsRequiredPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyStep(logger, nil, nil, d.step); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/checkout_persist_credentials_should_be_false.go
================================================
package policy
import (
"errors"
"log/slog"
"strings"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type CheckoutPersistCredentialShouldBeFalsePolicy struct{}
func (p *CheckoutPersistCredentialShouldBeFalsePolicy) Name() string {
return "checkout_persist_credentials_should_be_false"
}
func (p *CheckoutPersistCredentialShouldBeFalsePolicy) ID() string {
return "013"
}
func (p *CheckoutPersistCredentialShouldBeFalsePolicy) ApplyStep(_ *slog.Logger, cfg *config.Config, stepCtx *StepContext, step *workflow.Step) error {
if p.excluded(stepCtx, cfg.Excludes) {
return nil
}
if !strings.HasPrefix(step.Uses, "actions/checkout@") {
return nil
}
f, ok := step.With["persist-credentials"]
if !ok {
return errors.New("persist-credentials should be false")
}
if f != "false" {
return errors.New("persist-credentials should be false")
}
return nil
}
func (p *CheckoutPersistCredentialShouldBeFalsePolicy) excluded(stepCtx *StepContext, excludes []*config.Exclude) bool {
for _, exclude := range excludes {
if exclude.PolicyName != p.Name() {
continue
}
if stepCtx.Action != nil {
if exclude.ActionFilePath != stepCtx.FilePath {
continue
}
return true
}
if exclude.JobName != stepCtx.Job.Name {
continue
}
if exclude.WorkflowFilePath != stepCtx.Job.Workflow.FilePath {
continue
}
return true
}
return false
}
================================================
FILE: pkg/policy/checkout_persist_credentials_should_be_false_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestCheckoutPersistCredentialShouldBeFalsePolicy_ApplyStep(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
step *workflow.Step
stepCtx *policy.StepContext
isErr bool
}{
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "checkout_persist_credentials_should_be_false",
WorkflowFilePath: ".github/workflows/test.yml",
JobName: "test",
},
},
},
stepCtx: &policy.StepContext{
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yml",
},
},
},
step: &workflow.Step{
Uses: "actions/checkout@v4",
},
},
{
name: "persist-credentials is not set",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "checkout_persist_credentials_should_be_false",
JobName: "test-2",
WorkflowFilePath: ".github/workflows/test.yml",
},
},
},
stepCtx: &policy.StepContext{
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yml",
},
},
},
step: &workflow.Step{
Uses: "actions/checkout@v4",
},
isErr: true,
},
{
name: "persist-credentials is true",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "checkout_persist_credentials_should_be_false",
JobName: "test-2",
},
},
},
stepCtx: &policy.StepContext{
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yml",
},
},
},
step: &workflow.Step{
Uses: "actions/checkout@v4",
With: map[string]string{
"persist-credentials": "true",
},
},
isErr: true,
},
{
name: "persist-credentials is false",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "checkout_persist_credentials_should_be_false",
JobName: "test-2",
},
},
},
stepCtx: &policy.StepContext{
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yml",
},
},
},
step: &workflow.Step{
Uses: "actions/checkout@v4",
With: map[string]string{
"persist-credentials": "false",
},
},
},
{
name: "not checkout",
cfg: &config.Config{
Excludes: []*config.Exclude{},
},
stepCtx: &policy.StepContext{
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yml",
},
},
},
step: &workflow.Step{
Uses: "actions/cache@v4",
},
},
}
p := &policy.CheckoutPersistCredentialShouldBeFalsePolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/context.go
================================================
package policy
import "github.com/suzuki-shunsuke/ghalint/pkg/workflow"
type WorkflowContext struct {
FilePath string
Workflow *workflow.Workflow
}
type JobContext struct {
Name string
Workflow *WorkflowContext
Job *workflow.Job
}
type StepContext struct {
FilePath string
Action *workflow.Action
Job *JobContext
}
================================================
FILE: pkg/policy/deny_inherit_secrets.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type DenyInheritSecretsPolicy struct{}
func (p *DenyInheritSecretsPolicy) Name() string {
return "deny_inherit_secrets"
}
func (p *DenyInheritSecretsPolicy) ID() string {
return "004"
}
func (p *DenyInheritSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, jobCtx *JobContext, job *workflow.Job) error {
if checkExcludes(p.Name(), jobCtx, cfg) {
return nil
}
if job.Secrets.Inherit() {
return errors.New("`secrets: inherit` should not be used. Only required secrets should be passed explicitly")
}
return nil
}
================================================
FILE: pkg/policy/deny_inherit_secrets_test.go
================================================
//nolint:funlen
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"gopkg.in/yaml.v3"
)
func TestDenyInheritSecretsPolicy_ApplyJob(t *testing.T) {
t.Parallel()
data := []struct {
name string
job string
cfg *config.Config
jobCtx *policy.JobContext
isErr bool
}{
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "deny_inherit_secrets",
WorkflowFilePath: ".github/workflows/test.yaml",
JobName: "foo",
},
},
},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
Name: "foo",
},
job: `secrets: inherit`,
},
{
name: "not exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "deny_inherit_secrets",
WorkflowFilePath: ".github/workflows/test.yaml",
JobName: "bar",
},
},
},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
Name: "foo",
},
job: `secrets: inherit`,
isErr: true,
},
{
name: "error",
job: `secrets: inherit`,
cfg: &config.Config{},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
Name: "foo",
},
isErr: true,
},
{
name: "pass",
cfg: &config.Config{},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
Name: "foo",
},
job: `secrets:
foo: ${{secrets.API_KEY}}`,
},
}
p := &policy.DenyInheritSecretsPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
job := &workflow.Job{}
if err := yaml.Unmarshal([]byte(d.job), job); err != nil {
t.Fatal(err)
}
if err := p.ApplyJob(logger, d.cfg, d.jobCtx, job); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/deny_job_container_latest_image.go
================================================
package policy
import (
"errors"
"log/slog"
"strings"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type DenyJobContainerLatestImagePolicy struct{}
func (p *DenyJobContainerLatestImagePolicy) Name() string {
return "deny_job_container_latest_image"
}
func (p *DenyJobContainerLatestImagePolicy) ID() string {
return "007"
}
func (p *DenyJobContainerLatestImagePolicy) ApplyJob(logger *slog.Logger, _ *config.Config, _ *JobContext, job *workflow.Job) error {
if job.Container == nil {
return nil
}
if job.Container.Image == "" {
return errors.New("job container should have image")
}
if strings.Contains(job.Container.Image, "${{") {
logger.Debug("job container image contains `${{`; skipping latest image check")
return nil
}
_, tag, ok := strings.Cut(job.Container.Image, ":")
if !ok {
return errors.New("job container image should be <image name>:<tag>")
}
if tag == "latest" {
return errors.New("job container image tag should not be `latest`")
}
return nil
}
================================================
FILE: pkg/policy/deny_job_container_latest_image_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestDenyJobContainerLatestImagePolicy_ApplyJob(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
job *workflow.Job
isErr bool
}{
{
name: "pass",
job: &workflow.Job{
Container: &workflow.Container{
Image: "node:18",
},
},
},
{
name: "job container should have image",
job: &workflow.Job{
Container: &workflow.Container{},
},
isErr: true,
},
{
name: "job container image should have tag",
job: &workflow.Job{
Container: &workflow.Container{
Image: "node",
},
},
isErr: true,
},
{
name: "latest",
job: &workflow.Job{
Container: &workflow.Container{
Image: "node:latest",
},
},
isErr: true,
},
{
name: "Use variables",
job: &workflow.Job{
Container: &workflow.Container{
Image: "mirror.gcr.io/${{needs.list.outputs.image}}",
},
},
isErr: false,
},
}
p := &policy.DenyJobContainerLatestImagePolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, nil, nil, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/deny_read_all_policy.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type DenyReadAllPermissionPolicy struct{}
func (p *DenyReadAllPermissionPolicy) Name() string {
return "deny_read_all_permission"
}
func (p *DenyReadAllPermissionPolicy) ID() string {
return "002"
}
func (p *DenyReadAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {
wfReadAll := jobCtx.Workflow.Workflow.Permissions.ReadAll()
if job.Permissions.ReadAll() {
return errors.New("don't use read-all permission")
}
if job.Permissions.IsNil() && wfReadAll {
return errors.New("don't use read-all permission")
}
return nil
}
================================================
FILE: pkg/policy/deny_read_all_policy_test.go
================================================
package policy_test //nolint:dupl
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestDenyReadAllPermissionPolicy_ApplyJob(t *testing.T) {
t.Parallel()
data := []struct {
name string
jobCtx *policy.JobContext
job *workflow.Job
isErr bool
}{
{
name: "don't use read-all",
job: &workflow.Job{
Permissions: workflow.NewPermissions(true, false, nil),
},
isErr: true,
},
{
name: "job permissions is null and workflow permissions is read-all",
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{
Permissions: workflow.NewPermissions(true, false, nil),
},
},
},
job: &workflow.Job{},
isErr: true,
},
{
name: "pass",
job: &workflow.Job{
Permissions: workflow.NewPermissions(false, false, map[string]string{
"contents": "read",
}),
},
},
}
p := &policy.DenyReadAllPermissionPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
if d.jobCtx == nil {
d.jobCtx = &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{},
},
}
}
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/deny_write_all_policy.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type DenyWriteAllPermissionPolicy struct{}
func (p *DenyWriteAllPermissionPolicy) Name() string {
return "deny_write_all_permission"
}
func (p *DenyWriteAllPermissionPolicy) ID() string {
return "003"
}
func (p *DenyWriteAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {
wfWriteAll := jobCtx.Workflow.Workflow.Permissions.WriteAll()
if job.Permissions.WriteAll() {
return errors.New("don't use write-all permission")
}
if job.Permissions.IsNil() && wfWriteAll {
return errors.New("don't use write-all permission")
}
return nil
}
================================================
FILE: pkg/policy/deny_write_all_policy_test.go
================================================
package policy_test //nolint:dupl
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestDenyWriteAllPermissionPolicy_ApplyJob(t *testing.T) {
t.Parallel()
data := []struct {
name string
jobCtx *policy.JobContext
job *workflow.Job
isErr bool
}{
{
name: "don't use write-all",
job: &workflow.Job{
Permissions: workflow.NewPermissions(false, true, nil),
},
isErr: true,
},
{
name: "job permissions is null and workflow permissions is write-all",
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{
Permissions: workflow.NewPermissions(false, true, nil),
},
},
},
job: &workflow.Job{},
isErr: true,
},
{
name: "pass",
job: &workflow.Job{
Permissions: workflow.NewPermissions(false, false, map[string]string{
"contents": "write",
}),
},
},
}
p := &policy.DenyWriteAllPermissionPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
if d.jobCtx == nil {
d.jobCtx = &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{},
},
}
}
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/error.go
================================================
package policy
import "errors"
var (
errPermissionHyphenIsRequired = errors.New("an input `permission-*` is required")
errPermissionsIsRequired = errors.New("the input `permissions` is required")
errRepositoriesIsRequired = errors.New("the input `repositories` is required")
errEmpty = errors.New("")
)
================================================
FILE: pkg/policy/github_app_should_limit_permissions.go
================================================
package policy
import (
"log/slog"
"strings"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type GitHubAppShouldLimitPermissionsPolicy struct{}
func (p *GitHubAppShouldLimitPermissionsPolicy) Name() string {
return "github_app_should_limit_permissions"
}
func (p *GitHubAppShouldLimitPermissionsPolicy) ID() string {
return "010"
}
func (p *GitHubAppShouldLimitPermissionsPolicy) ApplyStep(_ *slog.Logger, _ *config.Config, _ *StepContext, step *workflow.Step) (ge error) { //nolint:cyclop
action := p.checkUses(step.Uses)
if action == "" {
return nil
}
defer func() {
if ge != nil {
ge = slogerr.With(ge,
"action", action,
)
}
}()
switch action {
case "tibdex/github-app-token":
if step.With == nil {
return errPermissionsIsRequired
}
if _, ok := step.With["permissions"]; !ok {
return errPermissionsIsRequired
}
case "actions/create-github-app-token":
if step.With == nil {
return errPermissionsIsRequired
}
err := errPermissionHyphenIsRequired
for k := range step.With {
if strings.HasPrefix(k, "permission-") {
err = nil
break
}
}
if err != nil {
return err
}
}
return nil
}
func (p *GitHubAppShouldLimitPermissionsPolicy) checkUses(uses string) string {
if uses == "" {
return ""
}
action, _, _ := strings.Cut(uses, "@")
return action
}
================================================
FILE: pkg/policy/github_app_should_limit_permissions_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestGitHubAppShouldLimitPermissionsPolicy_ApplyStep(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
stepCtx *policy.StepContext
step *workflow.Step
isErr bool
}{
{
name: "tibdex/github-app-token fail",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
},
},
},
{
name: "tibdex/github-app-token success",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
"permissions": "{}",
},
},
},
{
name: "actions/create-github-app-token fail",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "actions/create-github-app-token@v1.12.0",
ID: "token",
With: map[string]string{
"app-id": "xxx",
"private-key": "xxx",
},
},
},
{
name: "actions/create-github-app-token succeed",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "actions/create-github-app-token@v1.12.0",
ID: "token",
With: map[string]string{
"app-id": "xxx",
"private-key": "xxx",
"permission-issues": "write",
},
},
},
}
p := &policy.GitHubAppShouldLimitPermissionsPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
if d.stepCtx == nil {
d.stepCtx = &policy.StepContext{
FilePath: ".github/workflows/test.yaml",
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
},
}
}
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/github_app_should_limit_repositories.go
================================================
package policy
import (
"log/slog"
"strings"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type GitHubAppShouldLimitRepositoriesPolicy struct{}
func (p *GitHubAppShouldLimitRepositoriesPolicy) Name() string {
return "github_app_should_limit_repositories"
}
func (p *GitHubAppShouldLimitRepositoriesPolicy) ID() string {
return "009"
}
func (p *GitHubAppShouldLimitRepositoriesPolicy) ApplyStep(logger *slog.Logger, cfg *config.Config, stepCtx *StepContext, step *workflow.Step) (ge error) { //nolint:cyclop
action := p.checkUses(step.Uses)
if action == "" {
return nil
}
defer func() {
if ge != nil {
ge = slogerr.With(ge,
"action", action,
)
}
}()
if p.excluded(cfg, stepCtx, step) {
logger.Debug("this step is ignored")
return nil
}
if action == "tibdex/github-app-token" {
if step.With == nil {
return errRepositoriesIsRequired
}
if _, ok := step.With["repositories"]; !ok {
return errRepositoriesIsRequired
}
return nil
}
if action == "actions/create-github-app-token" {
if step.With == nil {
return errRepositoriesIsRequired
}
if _, ok := step.With["repositories"]; ok {
return nil
}
if _, ok := step.With["owner"]; ok {
return errRepositoriesIsRequired
}
return nil
}
return nil
}
func (p *GitHubAppShouldLimitRepositoriesPolicy) checkUses(uses string) string {
if uses == "" {
return ""
}
action, _, _ := strings.Cut(uses, "@")
return action
}
func (p *GitHubAppShouldLimitRepositoriesPolicy) excluded(cfg *config.Config, stepCtx *StepContext, step *workflow.Step) bool {
for _, exclude := range cfg.Excludes {
if exclude.PolicyName != p.Name() {
continue
}
if exclude.FilePath() != stepCtx.FilePath {
continue
}
if stepCtx.Job != nil && exclude.JobName != stepCtx.Job.Name {
continue
}
if exclude.StepID != step.ID {
continue
}
return true
}
return false
}
================================================
FILE: pkg/policy/github_app_should_limit_repositories_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestGitHubAppShouldLimitRepositoriesPolicy_ApplyStep(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
stepCtx *policy.StepContext
step *workflow.Step
isErr bool
}{
{
name: "tibdex/github-app-token fail",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
},
},
},
{
name: "tibdex/github-app-token success",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
"repositories": "{}",
},
},
},
{
name: "actions/create-github-app-token fail",
isErr: true,
cfg: &config.Config{},
step: &workflow.Step{
Uses: "actions/create-github-app-token@v2",
ID: "token",
With: map[string]string{
"app-id": "xxx",
"private-key": "xxx",
"owner": "xxx",
},
},
},
{
name: "actions/create-github-app-token success",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "actions/create-github-app-token@v2",
ID: "token",
With: map[string]string{
"app-id": "xxx",
"private-key": "xxx",
"owner": "xxx",
"repositories": "foo,bar",
},
},
},
{
name: "actions/create-github-app-token success no owner",
cfg: &config.Config{},
step: &workflow.Step{
Uses: "actions/create-github-app-token@v2",
ID: "token",
With: map[string]string{
"app-id": "xxx",
"private-key": "xxx",
},
},
},
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "github_app_should_limit_repositories",
WorkflowFilePath: ".github/workflows/test.yaml",
JobName: "test",
StepID: "token",
},
},
},
stepCtx: &policy.StepContext{
FilePath: ".github/workflows/test.yaml",
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
},
},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
},
},
},
{
name: "exclude action",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "github_app_should_limit_repositories",
ActionFilePath: "foo/action.yaml",
StepID: "token",
},
},
},
stepCtx: &policy.StepContext{
FilePath: "foo/action.yaml",
},
step: &workflow.Step{
Uses: "tibdex/github-app-token@v2",
ID: "token",
With: map[string]string{
"app_id": "xxx",
"private_key": "xxx",
},
},
},
}
p := &policy.GitHubAppShouldLimitRepositoriesPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
if d.stepCtx == nil {
d.stepCtx = &policy.StepContext{
FilePath: ".github/workflows/test.yaml",
Job: &policy.JobContext{
Name: "test",
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
},
}
}
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {
if d.isErr {
return
}
t.Fatal(err)
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/job_permissions_policy.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type JobPermissionsPolicy struct{}
func (p *JobPermissionsPolicy) Name() string {
return "job_permissions"
}
func (p *JobPermissionsPolicy) ID() string {
return "001"
}
func (p *JobPermissionsPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {
wf := jobCtx.Workflow.Workflow
wfPermissions := wf.Permissions.Permissions()
if wfPermissions != nil && len(wfPermissions) == 0 {
// workflow's permissions is `{}`
return nil
}
if len(wf.Jobs) < 2 && wfPermissions != nil {
// workflow permissions is set and there is only one job
return nil
}
if job.Permissions.IsNil() {
return errors.New("job should have permissions")
}
return nil
}
================================================
FILE: pkg/policy/job_permissions_policy_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestJobPermissionsPolicy_ApplyJob(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
jobCtx *policy.JobContext
job *workflow.Job
isErr bool
}{
{
name: "workflow permissions is empty",
job: &workflow.Job{},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{
Permissions: workflow.NewPermissions(false, false, map[string]string{}),
Jobs: map[string]*workflow.Job{
"foo": {},
},
},
},
},
},
{
name: "workflow has only one job",
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{
Permissions: workflow.NewPermissions(false, false, map[string]string{
"contents": "read",
}),
Jobs: map[string]*workflow.Job{
"foo": {},
},
},
},
},
job: &workflow.Job{},
},
{
name: "job should have permissions",
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
Workflow: &workflow.Workflow{
Permissions: &workflow.Permissions{},
Jobs: map[string]*workflow.Job{
"foo": {},
"bar": {},
},
},
},
},
job: &workflow.Job{},
isErr: true,
},
}
p := &policy.JobPermissionsPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/job_secrets_policy.go
================================================
package policy
import (
"errors"
"log/slog"
"regexp"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type JobSecretsPolicy struct {
secretPattern *regexp.Regexp
githubTokenPattern *regexp.Regexp
}
func NewJobSecretsPolicy() *JobSecretsPolicy {
return &JobSecretsPolicy{
secretPattern: regexp.MustCompile(`\${{ *secrets\.[^ ]+ *}}`),
githubTokenPattern: regexp.MustCompile(`\${{ *github\.token+ *}}`),
}
}
func (p *JobSecretsPolicy) Name() string {
return "job_secrets"
}
func (p *JobSecretsPolicy) ID() string {
return "006"
}
func checkExcludes(policyName string, jobCtx *JobContext, cfg *config.Config) bool {
for _, exclude := range cfg.Excludes {
if exclude.PolicyName == policyName && jobCtx.Workflow.FilePath == exclude.WorkflowFilePath && jobCtx.Name == exclude.JobName {
return true
}
}
return false
}
func (p *JobSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, jobCtx *JobContext, job *workflow.Job) error {
if checkExcludes(p.Name(), jobCtx, cfg) {
return nil
}
if len(job.Steps) < 2 { //nolint:mnd
return nil
}
for envName, envValue := range job.Env {
if p.secretPattern.MatchString(envValue) {
return slogerr.With(errors.New("secret should not be set to job's env"), //nolint:wrapcheck
"env_name", envName,
)
}
if p.githubTokenPattern.MatchString(envValue) {
return slogerr.With(errors.New("github.token should not be set to job's env"), //nolint:wrapcheck
"env_name", envName,
)
}
}
return nil
}
================================================
FILE: pkg/policy/job_secrets_policy_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestJobSecretsPolicy_ApplyJob(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
jobCtx *policy.JobContext
job *workflow.Job
isErr bool
}{
{
name: "exclude",
cfg: &config.Config{
Excludes: []*config.Exclude{
{
PolicyName: "job_secrets",
WorkflowFilePath: ".github/workflows/test.yaml",
JobName: "foo",
},
},
},
jobCtx: &policy.JobContext{
Workflow: &policy.WorkflowContext{
FilePath: ".github/workflows/test.yaml",
},
Name: "foo",
},
job: &workflow.Job{
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{github.token}}",
},
Steps: []*workflow.Step{
{},
{},
},
},
},
{
name: "job has only one step",
cfg: &config.Config{},
jobCtx: &policy.JobContext{},
job: &workflow.Job{
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{github.token}}",
},
Steps: []*workflow.Step{
{},
},
},
},
{
name: "secret should not be set to job's env",
cfg: &config.Config{},
jobCtx: &policy.JobContext{},
job: &workflow.Job{
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{secrets.GITHUB_TOKEN}}",
},
Steps: []*workflow.Step{
{},
{},
},
},
isErr: true,
},
{
name: "github token should not be set to job's env",
cfg: &config.Config{},
jobCtx: &policy.JobContext{},
job: &workflow.Job{
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{github.token}}",
},
Steps: []*workflow.Step{
{},
{},
},
},
isErr: true,
},
{
name: "pass",
cfg: &config.Config{},
jobCtx: &policy.JobContext{},
job: &workflow.Job{
Env: map[string]string{
"FOO": "foo",
},
Steps: []*workflow.Step{
{},
{},
},
},
},
}
p := policy.NewJobSecretsPolicy()
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, d.cfg, d.jobCtx, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/job_timeout_minutes_is_required.go
================================================
package policy
import (
"errors"
"log/slog"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type JobTimeoutMinutesIsRequiredPolicy struct{}
func (p *JobTimeoutMinutesIsRequiredPolicy) Name() string {
return "job_timeout_minutes_is_required"
}
func (p *JobTimeoutMinutesIsRequiredPolicy) ID() string {
return "012"
}
func (p *JobTimeoutMinutesIsRequiredPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, _ *JobContext, job *workflow.Job) error {
if job.TimeoutMinutes != nil {
return nil
}
if job.Uses != "" {
// when a reusable workflow is called with "uses", "timeout-minutes" is not available.
return nil
}
for _, step := range job.Steps {
if step.TimeoutMinutes == nil {
return errors.New("job's timeout-minutes is required")
}
}
return nil
}
================================================
FILE: pkg/policy/job_timeout_minutes_is_required_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestJobTimeoutMinutesIsRequiredPolicy_ApplyJob(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
job *workflow.Job
isErr bool
}{
{
name: "normal",
job: &workflow.Job{
TimeoutMinutes: 30,
Steps: []*workflow.Step{
{
Run: "echo hello",
},
},
},
},
{
name: "expression is used",
job: &workflow.Job{
TimeoutMinutes: "${{ matrix.timeout-minutes }}",
Steps: []*workflow.Step{
{
Run: "echo hello",
},
},
},
},
{
name: "workflow using reusable workflow",
job: &workflow.Job{
Uses: "suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@v0.2.3",
},
},
{
name: "job should have timeout-minutes",
job: &workflow.Job{
Steps: []*workflow.Step{
{
Run: "echo hello",
},
},
},
isErr: true,
},
{
name: "all steps have timeout-minutes",
job: &workflow.Job{
Steps: []*workflow.Step{
{
Run: "echo hello",
TimeoutMinutes: 60,
},
{
Run: "echo hello",
TimeoutMinutes: 60,
},
},
},
},
{
name: "expression is used in step's timeout-minutes",
job: &workflow.Job{
Steps: []*workflow.Step{
{
Run: "echo hello",
TimeoutMinutes: "${{ matrix.timeout-minutes }}",
},
{
Run: "echo hello",
TimeoutMinutes: 60,
},
},
},
},
}
p := &policy.JobTimeoutMinutesIsRequiredPolicy{}
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyJob(logger, nil, nil, d.job); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/policy/workflow_secrets_policy.go
================================================
package policy
import (
"log/slog"
"regexp"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
type WorkflowSecretsPolicy struct {
secretPattern *regexp.Regexp
githubTokenPattern *regexp.Regexp
}
func NewWorkflowSecretsPolicy() *WorkflowSecretsPolicy {
return &WorkflowSecretsPolicy{
secretPattern: regexp.MustCompile(`\${{ *secrets\.[^ ]+ *}}`),
githubTokenPattern: regexp.MustCompile(`\${{ *github\.token+ *}}`),
}
}
func (p *WorkflowSecretsPolicy) Name() string {
return "workflow_secrets"
}
func (p *WorkflowSecretsPolicy) ID() string {
return "005"
}
func (p *WorkflowSecretsPolicy) ApplyWorkflow(logger *slog.Logger, _ *config.Config, _ *WorkflowContext, wf *workflow.Workflow) error {
if len(wf.Jobs) < 2 { //nolint:mnd
return nil
}
failed := false
for envName, envValue := range wf.Env {
if p.secretPattern.MatchString(envValue) {
failed = true
logger.Error("secret should not be set to workflow's env", "env_name", envName)
}
if p.githubTokenPattern.MatchString(envValue) {
failed = true
logger.Error("github.token should not be set to workflow's env", "env_name", envName)
}
}
if failed {
return errEmpty
}
return nil
}
================================================
FILE: pkg/policy/workflow_secrets_policy_test.go
================================================
package policy_test
import (
"log/slog"
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/config"
"github.com/suzuki-shunsuke/ghalint/pkg/policy"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
)
func TestWorkflowSecretsPolicy_ApplyWorkflow(t *testing.T) { //nolint:funlen
t.Parallel()
data := []struct {
name string
cfg *config.Config
wf *workflow.Workflow
isErr bool
}{
{
name: "workflow has only one job",
cfg: &config.Config{},
wf: &workflow.Workflow{
FilePath: ".github/workflows/test.yaml",
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{github.token}}",
},
Jobs: map[string]*workflow.Job{
"foo": {},
},
},
},
{
name: "secret should not be set to workflow's env",
cfg: &config.Config{},
wf: &workflow.Workflow{
FilePath: ".github/workflows/test.yaml",
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{secrets.GITHUB_TOKEN}}",
},
Jobs: map[string]*workflow.Job{
"foo": {},
"bar": {},
},
},
isErr: true,
},
{
name: "github token should not be set to workflow's env",
cfg: &config.Config{},
wf: &workflow.Workflow{
FilePath: ".github/workflows/test.yaml",
Env: map[string]string{ //nolint:gosec
"GITHUB_TOKEN": "${{github.token}}",
},
Jobs: map[string]*workflow.Job{
"foo": {},
"bar": {},
},
},
isErr: true,
},
{
name: "pass",
cfg: &config.Config{},
wf: &workflow.Workflow{
FilePath: ".github/workflows/test.yaml",
Env: map[string]string{
"FOO": "foo",
},
Jobs: map[string]*workflow.Job{
"foo": {},
"bar": {},
},
},
},
}
p := policy.NewWorkflowSecretsPolicy()
logger := slog.New(slog.DiscardHandler)
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
if err := p.ApplyWorkflow(logger, d.cfg, nil, d.wf); err != nil {
if !d.isErr {
t.Fatal(err)
}
return
}
if d.isErr {
t.Fatal("error must be returned")
}
})
}
}
================================================
FILE: pkg/workflow/container.go
================================================
package workflow
import (
"errors"
)
type Container struct {
Image string
}
func (c *Container) UnmarshalYAML(unmarshal func(any) error) error {
var val any
if err := unmarshal(&val); err != nil {
return err
}
return convContainer(val, c)
}
func convContainer(src any, c *Container) error { //nolint:cyclop
switch p := src.(type) {
case string:
c.Image = p
return nil
case map[any]any:
for k, v := range p {
key, ok := k.(string)
if !ok {
continue
}
if key != "image" {
continue
}
image, ok := v.(string)
if !ok {
return errors.New("image must be a string")
}
c.Image = image
return nil
}
return nil
case map[string]any:
for k, v := range p {
if k != "image" {
continue
}
image, ok := v.(string)
if !ok {
return errors.New("image must be a string")
}
c.Image = image
return nil
}
return nil
default:
return errors.New("container must be a map or string")
}
}
================================================
FILE: pkg/workflow/container_test.go
================================================
package workflow_test
import (
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"gopkg.in/yaml.v3"
)
func TestContainer_UnmarshalYAML(t *testing.T) {
t.Parallel()
data := []struct {
name string
yaml string
image string
}{
{
name: "normal",
yaml: "image: node:18",
image: "node:18",
},
{
name: "string",
yaml: "node:18",
image: "node:18",
},
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
c := &workflow.Container{}
if err := yaml.Unmarshal([]byte(d.yaml), c); err != nil {
t.Fatal(err)
}
if d.image != c.Image {
t.Fatalf("got %v, wanted %v", c.Image, d.image)
}
})
}
}
================================================
FILE: pkg/workflow/job_secrets.go
================================================
package workflow
import (
"errors"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type JobSecrets struct {
m map[string]string
inherit bool
}
func (js *JobSecrets) Secrets() map[string]string {
return js.m
}
func (js *JobSecrets) Inherit() bool {
return js != nil && js.inherit
}
func (js *JobSecrets) UnmarshalYAML(unmarshal func(any) error) error {
var val any
if err := unmarshal(&val); err != nil {
return err
}
return convJobSecrets(val, js)
}
func convJobSecrets(src any, dest *JobSecrets) error { //nolint:cyclop
switch p := src.(type) {
case string:
switch p {
case "inherit":
dest.inherit = true
return nil
default:
return slogerr.With(errors.New("job secrets must be a map or `inherit`"), "secrets", p) //nolint:wrapcheck
}
case map[any]any:
m := make(map[string]string, len(p))
for k, v := range p {
ks, ok := k.(string)
if !ok {
return errors.New("secrets key must be string")
}
vs, ok := v.(string)
if !ok {
return errors.New("secrets value must be string")
}
m[ks] = vs
}
dest.m = m
return nil
case map[string]any:
m := make(map[string]string, len(p))
for k, v := range p {
vs, ok := v.(string)
if !ok {
return errors.New("secrets value must be string")
}
m[k] = vs
}
dest.m = m
return nil
default:
return errors.New("secrets must be map[string]string or 'inherit'")
}
}
================================================
FILE: pkg/workflow/job_secrets_test.go
================================================
package workflow_test
import (
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"gopkg.in/yaml.v3"
)
func TestJobSecrets_UnmarshalYAML(t *testing.T) {
t.Parallel()
data := []struct {
name string
yaml string
inherit bool
}{
{
name: "not inherit",
yaml: `token: ${{github.token}}`,
},
{
name: "inherit",
yaml: `inherit`,
inherit: true,
},
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
js := &workflow.JobSecrets{}
if err := yaml.Unmarshal([]byte(d.yaml), js); err != nil {
t.Fatal(err)
}
inherit := js.Inherit()
if d.inherit != inherit {
t.Fatalf("got %v, wanted %v", inherit, d.inherit)
}
})
}
}
================================================
FILE: pkg/workflow/list_workflows.go
================================================
package workflow
import (
"fmt"
"github.com/spf13/afero"
)
func List(fs afero.Fs) ([]string, error) {
files, err := afero.Glob(fs, ".github/workflows/*.yml")
if err != nil {
return nil, fmt.Errorf("find .github/workflows/*.yml: %w", err)
}
files2, err := afero.Glob(fs, ".github/workflows/*.yaml")
if err != nil {
return nil, fmt.Errorf("find .github/workflows/*.yaml: %w", err)
}
return append(files, files2...), nil
}
================================================
FILE: pkg/workflow/permissions.go
================================================
package workflow
import (
"errors"
"github.com/suzuki-shunsuke/slog-error/slogerr"
)
type Permissions struct {
m map[string]string
readAll bool
writeAll bool
}
func NewPermissions(readAll, writeAll bool, m map[string]string) *Permissions {
return &Permissions{
m: m,
readAll: readAll,
writeAll: writeAll,
}
}
func (ps *Permissions) Permissions() map[string]string {
if ps == nil {
return nil
}
return ps.m
}
func (ps *Permissions) ReadAll() bool {
if ps == nil {
return false
}
return ps.readAll
}
func (ps *Permissions) WriteAll() bool {
if ps == nil {
return false
}
return ps.writeAll
}
func (ps *Permissions) IsNil() bool {
if ps == nil {
return true
}
return ps.m == nil && !ps.readAll && !ps.writeAll
}
func (ps *Permissions) UnmarshalYAML(unmarshal func(any) error) error {
var val any
if err := unmarshal(&val); err != nil {
return err
}
return convPermissions(val, ps)
}
func convPermissions(src any, dest *Permissions) error { //nolint:cyclop
switch p := src.(type) {
case string:
switch p {
case "read-all":
dest.readAll = true
return nil
case "write-all":
dest.writeAll = true
return nil
default:
return slogerr.With(errors.New("unknown permissions"), "permission", p) //nolint:wrapcheck
}
case map[any]any:
m := make(map[string]string, len(p))
for k, v := range p {
ks, ok := k.(string)
if !ok {
return errors.New("permissions key must be string")
}
vs, ok := v.(string)
if !ok {
return errors.New("permissions value must be string")
}
m[ks] = vs
}
dest.m = m
return nil
case map[string]any:
m := make(map[string]string, len(p))
for k, v := range p {
vs, ok := v.(string)
if !ok {
return errors.New("permissions value must be string")
}
m[k] = vs
}
dest.m = m
return nil
default:
return errors.New("permissions must be map[string]string or 'read-all' or 'write-all'")
}
}
================================================
FILE: pkg/workflow/permissions_test.go
================================================
package workflow_test
import (
"testing"
"github.com/suzuki-shunsuke/ghalint/pkg/workflow"
"gopkg.in/yaml.v3"
)
func TestPermissions_UnmarshalYAML(t *testing.T) {
t.Parallel()
data := []struct {
name string
yaml string
readAll bool
writeAll bool
}{
{
name: "not read-all and write-all",
yaml: `contents: read`,
},
{
name: "read-all",
yaml: `read-all`,
readAll: true,
},
{
name: "write-all",
yaml: `write-all`,
writeAll: true,
},
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
t.Parallel()
p := &workflow.Permissions{}
if err := yaml.Unmarshal([]byte(d.yaml), p); err != nil {
t.Fatal(err)
}
readAll := p.ReadAll()
writeAll := p.WriteAll()
if d.readAll != readAll {
t.Fatalf("readAll got %v, wanted %v", readAll, d.readAll)
}
if d.writeAll != writeAll {
t.Fatalf("writeAll got %v, wanted %v", writeAll, d.writeAll)
}
})
}
}
================================================
FILE: pkg/workflow/read_action.go
================================================
package workflow
import (
"errors"
"fmt"
"io"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"gopkg.in/yaml.v3"
)
func ReadAction(fs afero.Fs, p string, action *Action) error {
f, err := fs.Open(p)
if err != nil {
return fmt.Errorf("open an action file: %w", err)
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(action); err != nil {
err := fmt.Errorf("parse an action file as YAML: %w", err)
if errors.Is(err, io.EOF) {
return slogerr.With(err, "reference", "https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md") //nolint:wrapcheck
}
return err
}
return nil
}
================================================
FILE: pkg/workflow/read_workflow.go
================================================
package workflow
import (
"errors"
"fmt"
"io"
"github.com/spf13/afero"
"github.com/suzuki-shunsuke/slog-error/slogerr"
"gopkg.in/yaml.v3"
)
func Read(fs afero.Fs, p string, wf *Workflow) error {
f, err := fs.Open(p)
if err != nil {
return fmt.Errorf("open a workflow file: %w", err)
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(wf); err != nil {
err := fmt.Errorf("parse a workflow file as YAML: %w", err)
if errors.Is(err, io.EOF) {
return slogerr.With(err, "reference", "https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md") //nolint:wrapcheck
}
return err
}
return nil
}
================================================
FILE: pkg/workflow/workflow.go
================================================
package workflow
import (
"fmt"
"strconv"
"gopkg.in/yaml.v3"
)
type Workflow struct {
FilePath string `yaml:"-"`
Jobs map[string]*Job
Env map[string]string
Permissions *Permissions
}
type Job struct {
Permissions *Permissions
Env map[string]string
Steps []*Step
Secrets *JobSecrets
Container *Container
Uses string
TimeoutMinutes any `yaml:"timeout-minutes"`
With map[string]any
}
type Step struct {
Uses string
ID string
Name string
Run string
Shell string
With With
TimeoutMinutes any `yaml:"timeout-minutes"`
}
type With map[string]string
func (w With) UnmarshalYAML(b []byte) error {
a := map[string]any{}
if err := yaml.Unmarshal(b, &a); err != nil {
return err //nolint:wrapcheck
}
for k, v := range a {
switch c := v.(type) {
case string:
w[k] = c
case int:
w[k] = strconv.Itoa(c)
case float64:
w[k] = fmt.Sprint(c)
case bool:
w[k] = strconv.FormatBool(c)
default:
return fmt.Errorf("unsupported type: %T", c)
}
}
return nil
}
type Action struct {
Runs *Runs
Inputs map[string]*Input
}
type Runs struct {
Image string
Steps []*Step
}
type Input struct {
Required bool
Type string
}
================================================
FILE: renovate.json5
================================================
{
extends: [
"github>suzuki-shunsuke/renovate-config#4.0.0",
"github>suzuki-shunsuke/renovate-config:nolimit#4.0.0",
"github>suzuki-shunsuke/renovate-config:go-directive#4.0.0",
"github>aquaproj/aqua-renovate-config#2.12.1",
"github>aquaproj/aqua-renovate-config:file#2.12.1(aqua/imports/.*\\.ya?ml)",
],
}
================================================
FILE: scripts/coverage.sh
================================================
#!/usr/bin/env bash
set -eu
set -o pipefail
cd "$(dirname "$0")/.."
if [ $# -eq 0 ]; then
target="$(go list ./... | fzf)"
profile=.coverage/$target/coverage.txt
mkdir -p .coverage/"$target"
elif [ $# -eq 1 ]; then
target=$1
mkdir -p .coverage/"$target"
profile=.coverage/$target/coverage.txt
target=./$target
else
echo "too many arguments are given: $*" >&2
exit 1
fi
go test "$target" -coverprofile="$profile" -covermode=atomic
go tool cover -html="$profile"
================================================
FILE: scripts/generate-usage.sh
================================================
#!/usr/bin/env bash
set -eu
cd "$(dirname "$0")/.."
help=$(ghalint help-all)
echo -n "# Usage
<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->
$help" > docs/usage.md
================================================
FILE: test-action.yaml
================================================
name: test
description: test
inputs:
github_token:
description: ""
required: false
default: ${{ github.token }}
runs:
using: composite
steps:
# checkout_persist_credentials_should_be_false
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
# action_ref_should_be_full_length_commit_sha
- uses: tibdex/github-app-token@v2.1.0
id: token1
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
# github_app_should_limit_repositories
# github_app_should_limit_permissions
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
id: token2
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
repositories: >-
["${{github.event.repository.name}}"]
permissions: >-
{
"contents": "write"
}
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token3
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
# github_app_should_limit_repositories
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token4
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
repositories: "repo1,repo2"
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token5
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
- run: echo hello
# action_shell_is_required
================================================
FILE: test-workflow.yaml
================================================
name: test
on: pull_request
env:
# Workflow should not set secrets to environment variables
FOO: bar
GITHUB_TOKEN: ${{github.token}}
API_KEY: ${{secrets.API_KEY}}
jobs:
release:
# action_ref_should_be_full_length_commit_sha
uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.5.0
# deny_inherit_secrets
secrets: inherit
permissions: {}
foo:
# job_permissions
runs-on: ubuntu-latest
env:
# job_secrets
FOO: bar
GITHUB_TOKEN: ${{github.token}}
API_KEY: ${{secrets.API_KEY}}
steps:
# checkout_persist_credentials_should_be_false
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: echo hello
- run: echo hello
read-all:
runs-on: ubuntu-latest
# deny_read_all_permission
permissions: read-all
env:
# If the job has only one job, it's okay to set secrets to job's environment variables
FOO: bar
GITHUB_TOKEN: ${{github.token}}
API_KEY: ${{secrets.API_KEY}}
steps:
- run: echo hello
write-all:
runs-on: ubuntu-latest
# deny_write_all_permission
permissions: write-all
steps:
# action_ref_should_be_full_length_commit_sha
- uses: tibdex/github-app-token@v2.1.0
id: token1
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
# github_app_should_limit_repositories
# github_app_should_limit_permissions
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
id: token2
with:
app_id: ${{secrets.APP_ID}}
private_key: ${{secrets.PRIVATE_KEY}}
repositories: >-
["${{github.event.repository.name}}"]
permissions: >-
{
"contents": "write"
}
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token3
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
# github_app_should_limit_repositories
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token4
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
owner: ${{github.repository_owner}}
repositories: "repo1,repo2"
- uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1
id: token5
with:
app-id: ${{vars.APP_ID}}
private-key: ${{secrets.PRIVATE_KEY}}
container-job:
runs-on: ubuntu-latest
permissions: {}
container:
image: node:latest # deny_job_container_latest_image
gitextract_p8kwv86v/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── actionlint.yaml │ ├── autofix.yaml │ ├── check-commit-signing.yaml │ ├── release.yaml │ ├── test.yaml │ └── workflow_call_test.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _typos.toml ├── aqua/ │ ├── aqua-checksums.json │ ├── aqua.yaml │ └── imports/ │ ├── cmdx.yaml │ ├── cosign.yaml │ ├── ghalint.yaml │ ├── go-licenses.yaml │ ├── golangci-lint.yaml │ ├── goreleaser.yaml │ ├── reviewdog.yaml │ ├── syft.yaml │ └── typos.yaml ├── cmd/ │ ├── gen-jsonschema/ │ │ └── main.go │ └── ghalint/ │ └── main.go ├── cmdx.yaml ├── docs/ │ ├── codes/ │ │ ├── 001.md │ │ └── 002.md │ ├── install.md │ ├── policies/ │ │ ├── 001.md │ │ ├── 002.md │ │ ├── 003.md │ │ ├── 004.md │ │ ├── 005.md │ │ ├── 006.md │ │ ├── 007.md │ │ ├── 008.md │ │ ├── 009.md │ │ ├── 010.md │ │ ├── 011.md │ │ ├── 012.md │ │ └── 013.md │ └── usage.md ├── go.mod ├── go.sum ├── json-schema/ │ └── ghalint.json ├── pkg/ │ ├── action/ │ │ └── find.go │ ├── cli/ │ │ ├── app.go │ │ ├── experiment/ │ │ │ ├── command.go │ │ │ └── validateinput/ │ │ │ └── command.go │ │ ├── gflags/ │ │ │ └── gflags.go │ │ ├── run.go │ │ └── run_action.go │ ├── config/ │ │ ├── config.go │ │ └── config_test.go │ ├── controller/ │ │ ├── act/ │ │ │ ├── controller.go │ │ │ └── run.go │ │ ├── controller.go │ │ ├── run.go │ │ └── schema/ │ │ ├── action.go │ │ ├── controller.go │ │ ├── job.go │ │ ├── reusable_workflow.go │ │ ├── run.go │ │ ├── step.go │ │ └── workflow.go │ ├── github/ │ │ ├── github.go │ │ └── keyring.go │ ├── policy/ │ │ ├── action_ref_should_be_full_length_commit_sha_policy.go │ │ ├── action_ref_should_be_full_length_commit_sha_policy_test.go │ │ ├── action_shell_is_required.go │ │ ├── action_shell_is_required_test.go │ │ ├── checkout_persist_credentials_should_be_false.go │ │ ├── checkout_persist_credentials_should_be_false_test.go │ │ ├── context.go │ │ ├── deny_inherit_secrets.go │ │ ├── deny_inherit_secrets_test.go │ │ ├── deny_job_container_latest_image.go │ │ ├── deny_job_container_latest_image_test.go │ │ ├── deny_read_all_policy.go │ │ ├── deny_read_all_policy_test.go │ │ ├── deny_write_all_policy.go │ │ ├── deny_write_all_policy_test.go │ │ ├── error.go │ │ ├── github_app_should_limit_permissions.go │ │ ├── github_app_should_limit_permissions_test.go │ │ ├── github_app_should_limit_repositories.go │ │ ├── github_app_should_limit_repositories_test.go │ │ ├── job_permissions_policy.go │ │ ├── job_permissions_policy_test.go │ │ ├── job_secrets_policy.go │ │ ├── job_secrets_policy_test.go │ │ ├── job_timeout_minutes_is_required.go │ │ ├── job_timeout_minutes_is_required_test.go │ │ ├── workflow_secrets_policy.go │ │ └── workflow_secrets_policy_test.go │ └── workflow/ │ ├── container.go │ ├── container_test.go │ ├── job_secrets.go │ ├── job_secrets_test.go │ ├── list_workflows.go │ ├── permissions.go │ ├── permissions_test.go │ ├── read_action.go │ ├── read_workflow.go │ └── workflow.go ├── renovate.json5 ├── scripts/ │ ├── coverage.sh │ └── generate-usage.sh ├── test-action.yaml └── test-workflow.yaml
SYMBOL INDEX (195 symbols across 61 files)
FILE: cmd/gen-jsonschema/main.go
function main (line 11) | func main() {
function core (line 17) | func core() error {
FILE: cmd/ghalint/main.go
function main (line 10) | func main() {
FILE: pkg/action/find.go
function Find (line 9) | func Find(fs afero.Fs) ([]string, error) {
FILE: pkg/cli/app.go
type RunArgs (line 15) | type RunArgs struct
type RunActionArgs (line 19) | type RunActionArgs struct
function Run (line 25) | func Run(ctx context.Context, logger *slogutil.Logger, env *urfave.Env) ...
type Runner (line 100) | type Runner struct
FILE: pkg/cli/experiment/command.go
function New (line 10) | func New(logger *slogutil.Logger, fs afero.Fs, validateInputArgs *valida...
FILE: pkg/cli/experiment/validateinput/command.go
type Args (line 18) | type Args struct
function New (line 22) | func New(logger *slogutil.Logger, fs afero.Fs, args *Args) *cli.Command {
type Runner (line 36) | type Runner struct
method Action (line 40) | func (r *Runner) Action(ctx context.Context, logger *slogutil.Logger, ...
function GetRootDir (line 60) | func GetRootDir() (string, error) {
FILE: pkg/cli/gflags/gflags.go
type GlobalFlags (line 3) | type GlobalFlags struct
FILE: pkg/cli/run.go
method Run (line 11) | func (r *Runner) Run(ctx context.Context, logger *slogutil.Logger, args ...
FILE: pkg/cli/run_action.go
method RunAction (line 11) | func (r *Runner) RunAction(ctx context.Context, logger *slogutil.Logger,...
FILE: pkg/config/config.go
type Config (line 15) | type Config struct
type Exclude (line 19) | type Exclude struct
method FilePath (line 28) | func (e *Exclude) FilePath() string {
function Find (line 35) | func Find(fs afero.Fs) string {
function Read (line 53) | func Read(fs afero.Fs, cfg *Config, filePath string) error {
function Validate (line 69) | func Validate(cfg *Config) error {
function ConvertPath (line 78) | func ConvertPath(cfg *Config) {
function convertPath (line 84) | func convertPath(exclude *Exclude) {
function validate (line 89) | func validate(exclude *Exclude) error { //nolint:cyclop
FILE: pkg/config/config_test.go
function TestValidate (line 9) | func TestValidate(t *testing.T) { //nolint:funlen
FILE: pkg/controller/act/controller.go
type Controller (line 7) | type Controller struct
function New (line 11) | func New(fs afero.Fs) *Controller {
FILE: pkg/controller/act/run.go
method Run (line 17) | func (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFile...
method listFiles (line 47) | func (c *Controller) listFiles(args ...string) ([]string, error) {
method validateAction (line 55) | func (c *Controller) validateAction(logger *slog.Logger, cfg *config.Con...
type Policy (line 70) | type Policy interface
function withPolicyReference (line 75) | func withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {
method applyStepPolicies (line 82) | func (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config....
method applyStepPolicy (line 93) | func (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Co...
method readConfig (line 113) | func (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) ...
FILE: pkg/controller/controller.go
type Controller (line 12) | type Controller struct
function New (line 16) | func New(fs afero.Fs) *Controller {
type WorkflowPolicy (line 22) | type WorkflowPolicy interface
type JobPolicy (line 28) | type JobPolicy interface
type StepPolicy (line 34) | type StepPolicy interface
FILE: pkg/controller/run.go
method Run (line 15) | func (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFile...
method validateWorkflow (line 57) | func (c *Controller) validateWorkflow(logger *slog.Logger, cfg *config.C...
type Policy (line 94) | type Policy interface
function withPolicyReference (line 99) | func withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {
method applyJobPolicies (line 106) | func (c *Controller) applyJobPolicies(logger *slog.Logger, cfg *config.C...
method applyJobPolicy (line 117) | func (c *Controller) applyJobPolicy(logger *slog.Logger, cfg *config.Con...
method applyStepPolicies (line 135) | func (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config....
method applyStepPolicy (line 146) | func (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Co...
method readConfig (line 177) | func (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) ...
FILE: pkg/controller/schema/action.go
method runActions (line 15) | func (c *Controller) runActions(ctx context.Context) error {
type validateAction (line 41) | type validateAction struct
method validate (line 49) | func (v *validateAction) validate(ctx context.Context) error {
FILE: pkg/controller/schema/controller.go
type Controller (line 11) | type Controller struct
function New (line 18) | func New(fs afero.Fs, logger *slog.Logger, gh GitHub, rootDir string) *C...
type GitHub (line 27) | type GitHub interface
FILE: pkg/controller/schema/job.go
type validateJob (line 15) | type validateJob struct
method validate (line 23) | func (v *validateJob) validate(ctx context.Context) error {
FILE: pkg/controller/schema/reusable_workflow.go
method validateReusableWorkflow (line 21) | func (v *validateJob) validateReusableWorkflow(ctx context.Context) error {
type ReusableWorkflow (line 42) | type ReusableWorkflow struct
type On (line 46) | type On struct
method UnmarshalYAML (line 50) | func (o *On) UnmarshalYAML(unmarshal func(any) error) error { //nolint...
type WorkflowCall (line 105) | type WorkflowCall struct
method validateWorkflow (line 109) | func (v *validateJob) validateWorkflow(wf *ReusableWorkflow) error {
function readReusableWorkflow (line 148) | func readReusableWorkflow(fs afero.Fs, p string, wf *ReusableWorkflow) e...
method read (line 166) | func (v *validateJob) read(ctx context.Context, wf *ReusableWorkflow) er...
FILE: pkg/controller/schema/run.go
method Run (line 12) | func (c *Controller) Run(ctx context.Context) error {
FILE: pkg/controller/schema/step.go
type validateStep (line 20) | type validateStep struct
method readAction (line 30) | func (v *validateStep) readAction(ctx context.Context, action *workflo...
method download (line 102) | func (v *validateStep) download(ctx context.Context, input *downloadIn...
method validate (line 120) | func (v *validateStep) validate(ctx context.Context) error {
method readLocalAction (line 169) | func (v *validateStep) readLocalAction(action *workflow.Action) error {
constant filePermission (line 91) | filePermission = 0o644
constant dirPermission (line 92) | dirPermission = 0o755
type downloadInput (line 95) | type downloadInput struct
FILE: pkg/controller/schema/workflow.go
method runWorkflow (line 15) | func (c *Controller) runWorkflow(ctx context.Context) error {
type validateWorkflow (line 43) | type validateWorkflow struct
method validate (line 51) | func (v *validateWorkflow) validate(ctx context.Context) error {
FILE: pkg/github/github.go
function New (line 27) | func New(ctx context.Context, logger *slog.Logger) *Client {
function getGitHubToken (line 31) | func getGitHubToken() string {
function checkKeyringEnabled (line 35) | func checkKeyringEnabled() bool {
function getHTTPClientForGitHub (line 39) | func getHTTPClientForGitHub(ctx context.Context, logger *slog.Logger, to...
FILE: pkg/github/keyring.go
constant KeyService (line 4) | KeyService = "suzuki-shunsuke/ghalint"
FILE: pkg/policy/action_ref_should_be_full_length_commit_sha_policy.go
type ActionRefShouldBeSHAPolicy (line 15) | type ActionRefShouldBeSHAPolicy struct
method Name (line 27) | func (p *ActionRefShouldBeSHAPolicy) Name() string {
method ID (line 31) | func (p *ActionRefShouldBeSHAPolicy) ID() string {
method ApplyJob (line 35) | func (p *ActionRefShouldBeSHAPolicy) ApplyJob(_ *slog.Logger, cfg *con...
method ApplyStep (line 39) | func (p *ActionRefShouldBeSHAPolicy) ApplyStep(_ *slog.Logger, cfg *co...
method apply (line 43) | func (p *ActionRefShouldBeSHAPolicy) apply(cfg *config.Config, uses st...
method checkUses (line 53) | func (p *ActionRefShouldBeSHAPolicy) checkUses(uses string) string {
method excluded (line 80) | func (p *ActionRefShouldBeSHAPolicy) excluded(action string, excludes ...
function NewActionRefShouldBeSHAPolicy (line 20) | func NewActionRefShouldBeSHAPolicy() *ActionRefShouldBeSHAPolicy {
FILE: pkg/policy/action_ref_should_be_full_length_commit_sha_policy_test.go
function TestActionRefShouldBeSHAPolicy_ApplyJob (line 12) | func TestActionRefShouldBeSHAPolicy_ApplyJob(t *testing.T) { //nolint:fu...
function TestActionRefShouldBeSHAPolicy_ApplyStep (line 134) | func TestActionRefShouldBeSHAPolicy_ApplyStep(t *testing.T) { //nolint:f...
FILE: pkg/policy/action_shell_is_required.go
type ActionShellIsRequiredPolicy (line 11) | type ActionShellIsRequiredPolicy struct
method Name (line 13) | func (p *ActionShellIsRequiredPolicy) Name() string {
method ID (line 17) | func (p *ActionShellIsRequiredPolicy) ID() string {
method ApplyStep (line 21) | func (p *ActionShellIsRequiredPolicy) ApplyStep(_ *slog.Logger, _ *con...
FILE: pkg/policy/action_shell_is_required_test.go
function TestActionShellIsRequiredPolicy_ApplyStep (line 11) | func TestActionShellIsRequiredPolicy_ApplyStep(t *testing.T) {
FILE: pkg/policy/checkout_persist_credentials_should_be_false.go
type CheckoutPersistCredentialShouldBeFalsePolicy (line 12) | type CheckoutPersistCredentialShouldBeFalsePolicy struct
method Name (line 14) | func (p *CheckoutPersistCredentialShouldBeFalsePolicy) Name() string {
method ID (line 18) | func (p *CheckoutPersistCredentialShouldBeFalsePolicy) ID() string {
method ApplyStep (line 22) | func (p *CheckoutPersistCredentialShouldBeFalsePolicy) ApplyStep(_ *sl...
method excluded (line 39) | func (p *CheckoutPersistCredentialShouldBeFalsePolicy) excluded(stepCt...
FILE: pkg/policy/checkout_persist_credentials_should_be_false_test.go
function TestCheckoutPersistCredentialShouldBeFalsePolicy_ApplyStep (line 12) | func TestCheckoutPersistCredentialShouldBeFalsePolicy_ApplyStep(t *testi...
FILE: pkg/policy/context.go
type WorkflowContext (line 5) | type WorkflowContext struct
type JobContext (line 10) | type JobContext struct
type StepContext (line 16) | type StepContext struct
FILE: pkg/policy/deny_inherit_secrets.go
type DenyInheritSecretsPolicy (line 11) | type DenyInheritSecretsPolicy struct
method Name (line 13) | func (p *DenyInheritSecretsPolicy) Name() string {
method ID (line 17) | func (p *DenyInheritSecretsPolicy) ID() string {
method ApplyJob (line 21) | func (p *DenyInheritSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *confi...
FILE: pkg/policy/deny_inherit_secrets_test.go
function TestDenyInheritSecretsPolicy_ApplyJob (line 14) | func TestDenyInheritSecretsPolicy_ApplyJob(t *testing.T) {
FILE: pkg/policy/deny_job_container_latest_image.go
type DenyJobContainerLatestImagePolicy (line 12) | type DenyJobContainerLatestImagePolicy struct
method Name (line 14) | func (p *DenyJobContainerLatestImagePolicy) Name() string {
method ID (line 18) | func (p *DenyJobContainerLatestImagePolicy) ID() string {
method ApplyJob (line 22) | func (p *DenyJobContainerLatestImagePolicy) ApplyJob(logger *slog.Logg...
FILE: pkg/policy/deny_job_container_latest_image_test.go
function TestDenyJobContainerLatestImagePolicy_ApplyJob (line 11) | func TestDenyJobContainerLatestImagePolicy_ApplyJob(t *testing.T) { //no...
FILE: pkg/policy/deny_read_all_policy.go
type DenyReadAllPermissionPolicy (line 11) | type DenyReadAllPermissionPolicy struct
method Name (line 13) | func (p *DenyReadAllPermissionPolicy) Name() string {
method ID (line 17) | func (p *DenyReadAllPermissionPolicy) ID() string {
method ApplyJob (line 21) | func (p *DenyReadAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *conf...
FILE: pkg/policy/deny_read_all_policy_test.go
function TestDenyReadAllPermissionPolicy_ApplyJob (line 11) | func TestDenyReadAllPermissionPolicy_ApplyJob(t *testing.T) {
FILE: pkg/policy/deny_write_all_policy.go
type DenyWriteAllPermissionPolicy (line 11) | type DenyWriteAllPermissionPolicy struct
method Name (line 13) | func (p *DenyWriteAllPermissionPolicy) Name() string {
method ID (line 17) | func (p *DenyWriteAllPermissionPolicy) ID() string {
method ApplyJob (line 21) | func (p *DenyWriteAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *con...
FILE: pkg/policy/deny_write_all_policy_test.go
function TestDenyWriteAllPermissionPolicy_ApplyJob (line 11) | func TestDenyWriteAllPermissionPolicy_ApplyJob(t *testing.T) {
FILE: pkg/policy/github_app_should_limit_permissions.go
type GitHubAppShouldLimitPermissionsPolicy (line 12) | type GitHubAppShouldLimitPermissionsPolicy struct
method Name (line 14) | func (p *GitHubAppShouldLimitPermissionsPolicy) Name() string {
method ID (line 18) | func (p *GitHubAppShouldLimitPermissionsPolicy) ID() string {
method ApplyStep (line 22) | func (p *GitHubAppShouldLimitPermissionsPolicy) ApplyStep(_ *slog.Logg...
method checkUses (line 61) | func (p *GitHubAppShouldLimitPermissionsPolicy) checkUses(uses string)...
FILE: pkg/policy/github_app_should_limit_permissions_test.go
function TestGitHubAppShouldLimitPermissionsPolicy_ApplyStep (line 12) | func TestGitHubAppShouldLimitPermissionsPolicy_ApplyStep(t *testing.T) {...
FILE: pkg/policy/github_app_should_limit_repositories.go
type GitHubAppShouldLimitRepositoriesPolicy (line 12) | type GitHubAppShouldLimitRepositoriesPolicy struct
method Name (line 14) | func (p *GitHubAppShouldLimitRepositoriesPolicy) Name() string {
method ID (line 18) | func (p *GitHubAppShouldLimitRepositoriesPolicy) ID() string {
method ApplyStep (line 22) | func (p *GitHubAppShouldLimitRepositoriesPolicy) ApplyStep(logger *slo...
method checkUses (line 62) | func (p *GitHubAppShouldLimitRepositoriesPolicy) checkUses(uses string...
method excluded (line 70) | func (p *GitHubAppShouldLimitRepositoriesPolicy) excluded(cfg *config....
FILE: pkg/policy/github_app_should_limit_repositories_test.go
function TestGitHubAppShouldLimitRepositoriesPolicy_ApplyStep (line 12) | func TestGitHubAppShouldLimitRepositoriesPolicy_ApplyStep(t *testing.T) ...
FILE: pkg/policy/job_permissions_policy.go
type JobPermissionsPolicy (line 11) | type JobPermissionsPolicy struct
method Name (line 13) | func (p *JobPermissionsPolicy) Name() string {
method ID (line 17) | func (p *JobPermissionsPolicy) ID() string {
method ApplyJob (line 21) | func (p *JobPermissionsPolicy) ApplyJob(_ *slog.Logger, _ *config.Conf...
FILE: pkg/policy/job_permissions_policy_test.go
function TestJobPermissionsPolicy_ApplyJob (line 11) | func TestJobPermissionsPolicy_ApplyJob(t *testing.T) { //nolint:funlen
FILE: pkg/policy/job_secrets_policy.go
type JobSecretsPolicy (line 13) | type JobSecretsPolicy struct
method Name (line 25) | func (p *JobSecretsPolicy) Name() string {
method ID (line 29) | func (p *JobSecretsPolicy) ID() string {
method ApplyJob (line 42) | func (p *JobSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config...
function NewJobSecretsPolicy (line 18) | func NewJobSecretsPolicy() *JobSecretsPolicy {
function checkExcludes (line 33) | func checkExcludes(policyName string, jobCtx *JobContext, cfg *config.Co...
FILE: pkg/policy/job_secrets_policy_test.go
function TestJobSecretsPolicy_ApplyJob (line 12) | func TestJobSecretsPolicy_ApplyJob(t *testing.T) { //nolint:funlen
FILE: pkg/policy/job_timeout_minutes_is_required.go
type JobTimeoutMinutesIsRequiredPolicy (line 11) | type JobTimeoutMinutesIsRequiredPolicy struct
method Name (line 13) | func (p *JobTimeoutMinutesIsRequiredPolicy) Name() string {
method ID (line 17) | func (p *JobTimeoutMinutesIsRequiredPolicy) ID() string {
method ApplyJob (line 21) | func (p *JobTimeoutMinutesIsRequiredPolicy) ApplyJob(_ *slog.Logger, _...
FILE: pkg/policy/job_timeout_minutes_is_required_test.go
function TestJobTimeoutMinutesIsRequiredPolicy_ApplyJob (line 11) | func TestJobTimeoutMinutesIsRequiredPolicy_ApplyJob(t *testing.T) { //no...
FILE: pkg/policy/workflow_secrets_policy.go
type WorkflowSecretsPolicy (line 11) | type WorkflowSecretsPolicy struct
method Name (line 23) | func (p *WorkflowSecretsPolicy) Name() string {
method ID (line 27) | func (p *WorkflowSecretsPolicy) ID() string {
method ApplyWorkflow (line 31) | func (p *WorkflowSecretsPolicy) ApplyWorkflow(logger *slog.Logger, _ *...
function NewWorkflowSecretsPolicy (line 16) | func NewWorkflowSecretsPolicy() *WorkflowSecretsPolicy {
FILE: pkg/policy/workflow_secrets_policy_test.go
function TestWorkflowSecretsPolicy_ApplyWorkflow (line 12) | func TestWorkflowSecretsPolicy_ApplyWorkflow(t *testing.T) { //nolint:fu...
FILE: pkg/workflow/container.go
type Container (line 7) | type Container struct
method UnmarshalYAML (line 11) | func (c *Container) UnmarshalYAML(unmarshal func(any) error) error {
function convContainer (line 19) | func convContainer(src any, c *Container) error { //nolint:cyclop
FILE: pkg/workflow/container_test.go
function TestContainer_UnmarshalYAML (line 10) | func TestContainer_UnmarshalYAML(t *testing.T) {
FILE: pkg/workflow/job_secrets.go
type JobSecrets (line 9) | type JobSecrets struct
method Secrets (line 14) | func (js *JobSecrets) Secrets() map[string]string {
method Inherit (line 18) | func (js *JobSecrets) Inherit() bool {
method UnmarshalYAML (line 22) | func (js *JobSecrets) UnmarshalYAML(unmarshal func(any) error) error {
function convJobSecrets (line 30) | func convJobSecrets(src any, dest *JobSecrets) error { //nolint:cyclop
FILE: pkg/workflow/job_secrets_test.go
function TestJobSecrets_UnmarshalYAML (line 10) | func TestJobSecrets_UnmarshalYAML(t *testing.T) {
FILE: pkg/workflow/list_workflows.go
function List (line 9) | func List(fs afero.Fs) ([]string, error) {
FILE: pkg/workflow/permissions.go
type Permissions (line 9) | type Permissions struct
method Permissions (line 23) | func (ps *Permissions) Permissions() map[string]string {
method ReadAll (line 30) | func (ps *Permissions) ReadAll() bool {
method WriteAll (line 37) | func (ps *Permissions) WriteAll() bool {
method IsNil (line 44) | func (ps *Permissions) IsNil() bool {
method UnmarshalYAML (line 51) | func (ps *Permissions) UnmarshalYAML(unmarshal func(any) error) error {
function NewPermissions (line 15) | func NewPermissions(readAll, writeAll bool, m map[string]string) *Permis...
function convPermissions (line 59) | func convPermissions(src any, dest *Permissions) error { //nolint:cyclop
FILE: pkg/workflow/permissions_test.go
function TestPermissions_UnmarshalYAML (line 10) | func TestPermissions_UnmarshalYAML(t *testing.T) {
FILE: pkg/workflow/read_action.go
function ReadAction (line 13) | func ReadAction(fs afero.Fs, p string, action *Action) error {
FILE: pkg/workflow/read_workflow.go
function Read (line 13) | func Read(fs afero.Fs, p string, wf *Workflow) error {
FILE: pkg/workflow/workflow.go
type Workflow (line 10) | type Workflow struct
type Job (line 17) | type Job struct
type Step (line 28) | type Step struct
type With (line 38) | type With
method UnmarshalYAML (line 40) | func (w With) UnmarshalYAML(b []byte) error {
type Action (line 62) | type Action struct
type Runs (line 67) | type Runs struct
type Input (line 72) | type Input struct
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (189K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 193,
"preview": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/di"
},
{
"path": ".github/workflows/actionlint.yaml",
"chars": 282,
"preview": "---\nname: actionlint\non: pull_request\njobs:\n actionlint:\n runs-on: ubuntu-24.04\n timeout-minutes: 10\n permissi"
},
{
"path": ".github/workflows/autofix.yaml",
"chars": 297,
"preview": "---\nname: autofix.ci\non: pull_request\npermissions: {}\njobs:\n autofix:\n runs-on: ubuntu-24.04\n permissions: {}\n "
},
{
"path": ".github/workflows/check-commit-signing.yaml",
"chars": 475,
"preview": "---\nname: Check if all commits are signed\non:\n pull_request_target:\n branches: [main]\nconcurrency:\n group: ${{ gith"
},
{
"path": ".github/workflows/release.yaml",
"chars": 364,
"preview": "---\nname: Release\non:\n push:\n tags: [v*]\njobs:\n release:\n uses: suzuki-shunsuke/go-release-workflow/.github/work"
},
{
"path": ".github/workflows/test.yaml",
"chars": 511,
"preview": "---\nname: test\non: pull_request\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: tru"
},
{
"path": ".github/workflows/workflow_call_test.yaml",
"chars": 342,
"preview": "---\nname: test (workflow_call)\non: workflow_call\npermissions: {}\njobs:\n test:\n uses: suzuki-shunsuke/go-test-full-wo"
},
{
"path": ".gitignore",
"chars": 36,
"preview": "dist\n.coverage\nthird_party_licenses\n"
},
{
"path": ".golangci.yml",
"chars": 699,
"preview": "version: \"2\"\nlinters:\n default: all\n disable:\n - depguard\n - err113\n - exhaustruct\n - godot\n - ireturn\n"
},
{
"path": ".goreleaser.yml",
"chars": 1823,
"preview": "version: 2\nproject_name: ghalint\n\narchives:\n - format_overrides:\n - goos: windows\n formats: [zip]\n files"
},
{
"path": "CONTRIBUTING.md",
"chars": 113,
"preview": "# Contributing\n\nPlease read the following document.\n\n- https://github.com/suzuki-shunsuke/oss-contribution-guide\n"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2023 Shunsuke Suzuki\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 8873,
"preview": "# ghalint\n\n[](https://deepwiki.com/suzuki-shunsuke/ghalint)\n[Install](doc"
},
{
"path": "_typos.toml",
"chars": 55,
"preview": "[default.extend-words]\nERRO = \"ERRO\"\nintoto = \"intoto\"\n"
},
{
"path": "aqua/aqua-checksums.json",
"chars": 10268,
"preview": "{\n \"checksums\": [\n {\n \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_darwin_amd64.tar.gz\",\n "
},
{
"path": "aqua/aqua.yaml",
"chars": 351,
"preview": "---\n# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json\n# aq"
},
{
"path": "aqua/imports/cmdx.yaml",
"chars": 48,
"preview": "packages:\n - name: suzuki-shunsuke/cmdx@v2.0.2\n"
},
{
"path": "aqua/imports/cosign.yaml",
"chars": 43,
"preview": "packages:\n - name: sigstore/cosign@v3.0.6\n"
},
{
"path": "aqua/imports/ghalint.yaml",
"chars": 51,
"preview": "packages:\n - name: suzuki-shunsuke/ghalint@v1.5.6\n"
},
{
"path": "aqua/imports/go-licenses.yaml",
"chars": 46,
"preview": "packages:\n - name: google/go-licenses@v2.0.1\n"
},
{
"path": "aqua/imports/golangci-lint.yaml",
"chars": 51,
"preview": "packages:\n - name: golangci/golangci-lint@v2.12.2\n"
},
{
"path": "aqua/imports/goreleaser.yaml",
"chars": 50,
"preview": "packages:\n - name: goreleaser/goreleaser@v2.15.4\n"
},
{
"path": "aqua/imports/reviewdog.yaml",
"chars": 48,
"preview": "packages:\n - name: reviewdog/reviewdog@v0.21.0\n"
},
{
"path": "aqua/imports/syft.yaml",
"chars": 41,
"preview": "packages:\n - name: anchore/syft@v1.44.0\n"
},
{
"path": "aqua/imports/typos.yaml",
"chars": 43,
"preview": "packages:\n - name: crate-ci/typos@v1.46.2\n"
},
{
"path": "cmd/gen-jsonschema/main.go",
"chars": 406,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/suzuki-shunsuke/gen-go-jsonschema/jsonschema\"\n\t\"github.com/suzuki-shu"
},
{
"path": "cmd/ghalint/main.go",
"chars": 204,
"preview": "package main\n\nimport (\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urf"
},
{
"path": "cmdx.yaml",
"chars": 1170,
"preview": "---\n# cmdx - task runner\n# https://github.com/suzuki-shunsuke/cmdx\ntasks:\n - name: test\n short: t\n description: t"
},
{
"path": "docs/codes/001.md",
"chars": 1061,
"preview": "# parse a workflow file as YAML: EOF\n\n```console\n$ ghalint run\nERRO[0000] read a workflow file "
},
{
"path": "docs/codes/002.md",
"chars": 458,
"preview": "# read a configuration file: parse configuration file as YAML: EOF\n\n```console\n$ ghalint run\nFATA[0000] ghalint failed "
},
{
"path": "docs/install.md",
"chars": 3090,
"preview": "# Install\n\nghalint is written in Go. So you only have to install a binary in your `PATH`.\n\nThere are some ways to instal"
},
{
"path": "docs/policies/001.md",
"chars": 1062,
"preview": "# job_permissions\n\nAll jobs should have the field [permissions](https://docs.github.com/en/actions/using-workflows/workf"
},
{
"path": "docs/policies/002.md",
"chars": 581,
"preview": "# deny_read_all_permission\n\n[`read-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax-f"
},
{
"path": "docs/policies/003.md",
"chars": 586,
"preview": "# deny_write_all_permission\n\n[`write-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax"
},
{
"path": "docs/policies/004.md",
"chars": 1115,
"preview": "# deny_inherit_secrets\n\n[`secrets: inherit`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-gith"
},
{
"path": "docs/policies/005.md",
"chars": 999,
"preview": "# workflow_secrets\n\nWorkflows should not set secrets to environment variables.\n\n## Examples\n\n:x:\n\n```yaml\nname: test\nenv"
},
{
"path": "docs/policies/006.md",
"chars": 1108,
"preview": "# job_secrets\n\nJob should not set secrets to environment variables.\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n foo:\n runs-on:"
},
{
"path": "docs/policies/007.md",
"chars": 432,
"preview": "# deny_job_container_latest_image\n\nJob's container image tag should not be `latest`.\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n "
},
{
"path": "docs/policies/008.md",
"chars": 1727,
"preview": "# action_ref_should_be_full_length_commit_sha\n\naction's ref should be full length commit SHA\n\n## Examples\n\n:x:\n\n```\nacti"
},
{
"path": "docs/policies/009.md",
"chars": 2343,
"preview": "# github_app_should_limit_repositories\n\nGitHub Actions issuing GitHub Access tokens from GitHub Apps should limit reposi"
},
{
"path": "docs/policies/010.md",
"chars": 1847,
"preview": "# github_app_should_limit_permissions\n\nGitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permiss"
},
{
"path": "docs/policies/011.md",
"chars": 300,
"preview": "# action_shell_is_required\n\n`shell` is required if `run` is set\n\n## Examples\n\n:x:\n\n```yaml\n- run: echo hello\n```\n\n⭕\n\n```"
},
{
"path": "docs/policies/012.md",
"chars": 2096,
"preview": "# job_timeout_minutes_is_required\n\nAll jobs should set [timeout-minutes](https://docs.github.com/en/actions/using-workfl"
},
{
"path": "docs/policies/013.md",
"chars": 1579,
"preview": "# checkout_persist_credentials_should_be_false\n\n[actions/checkout](https://github.com/actions/checkout)'s input `persist"
},
{
"path": "docs/usage.md",
"chars": 2721,
"preview": "# Usage\n\n<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->\n\n```console\n$ ghalint -"
},
{
"path": "go.mod",
"chars": 1417,
"preview": "module github.com/suzuki-shunsuke/ghalint\n\ngo 1.26.3\n\nrequire (\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/google/go-github"
},
{
"path": "go.sum",
"chars": 7365,
"preview": "al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=\nal.essio.dev/pkg/shellescape v1.5.1/"
},
{
"path": "json-schema/ghalint.json",
"chars": 974,
"preview": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://github.com/suzuki-shunsuke/ghalint/pkg/"
},
{
"path": "pkg/action/find.go",
"chars": 539,
"preview": "package action\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc Find(fs afero.Fs) ([]string, error) {\n\tpatterns := []"
},
{
"path": "pkg/cli/app.go",
"chars": 2231,
"preview": "package cli\n\nimport (\n\t\"context\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment\"\n\t\"g"
},
{
"path": "pkg/cli/experiment/command.go",
"chars": 644,
"preview": "package experiment\n\nimport (\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment/validatei"
},
{
"path": "pkg/cli/experiment/validateinput/command.go",
"chars": 1845,
"preview": "package validateinput\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/spf13/afer"
},
{
"path": "pkg/cli/gflags/gflags.go",
"chars": 95,
"preview": "package gflags\n\ntype GlobalFlags struct {\n\tLogColor string\n\tLogLevel string\n\tConfig string\n}\n"
},
{
"path": "pkg/cli/run.go",
"chars": 549,
"preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller\"\n\t\"github.com/suzuki-shunsu"
},
{
"path": "pkg/cli/run_action.go",
"chars": 573,
"preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller/act\"\n\t\"github.com/suzuki-sh"
},
{
"path": "pkg/config/config.go",
"chars": 4273,
"preview": "package config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-"
},
{
"path": "pkg/config/config_test.go",
"chars": 1541,
"preview": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n)\n\nfunc TestValidate(t *testi"
},
{
"path": "pkg/controller/act/controller.go",
"chars": 164,
"preview": "package act\n\nimport (\n\t\"github.com/spf13/afero\"\n)\n\ntype Controller struct {\n\tfs afero.Fs\n}\n\nfunc New(fs afero.Fs) *Contr"
},
{
"path": "pkg/controller/act/run.go",
"chars": 3783,
"preview": "package act\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/action\"\n\t\"github.com/suzuk"
},
{
"path": "pkg/controller/controller.go",
"chars": 840,
"preview": "package controller\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"g"
},
{
"path": "pkg/controller/run.go",
"chars": 5537,
"preview": "package controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.co"
},
{
"path": "pkg/controller/schema/action.go",
"chars": 1580,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke"
},
{
"path": "pkg/controller/schema/controller.go",
"chars": 759,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/gith"
},
{
"path": "pkg/controller/schema/job.go",
"chars": 1085,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke"
},
{
"path": "pkg/controller/schema/reusable_workflow.go",
"chars": 5847,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/s"
},
{
"path": "pkg/controller/schema/run.go",
"chars": 668,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzu"
},
{
"path": "pkg/controller/schema/step.go",
"chars": 5201,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/spf"
},
{
"path": "pkg/controller/schema/workflow.go",
"chars": 1696,
"preview": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke"
},
{
"path": "pkg/github/github.go",
"chars": 1383,
"preview": "package github\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/google/go-github/v86/github\"\n\t\"github.co"
},
{
"path": "pkg/github/keyring.go",
"chars": 66,
"preview": "package github\n\nconst (\n\tKeyService = \"suzuki-shunsuke/ghalint\"\n)\n"
},
{
"path": "pkg/policy/action_ref_should_be_full_length_commit_sha_policy.go",
"chars": 2310,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/co"
},
{
"path": "pkg/policy/action_ref_should_be_full_length_commit_sha_policy_test.go",
"chars": 6710,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/action_shell_is_required.go",
"chars": 595,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/action_shell_is_required_test.go",
"chars": 871,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/checkout_persist_credentials_should_be_false.go",
"chars": 1430,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com"
},
{
"path": "pkg/policy/checkout_persist_credentials_should_be_false_test.go",
"chars": 3394,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/context.go",
"chars": 343,
"preview": "package policy\n\nimport \"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\ntype WorkflowContext struct {\n\tFilePath string"
},
{
"path": "pkg/policy/deny_inherit_secrets.go",
"chars": 685,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/deny_inherit_secrets_test.go",
"chars": 2267,
"preview": "//nolint:funlen\npackage policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t"
},
{
"path": "pkg/policy/deny_job_container_latest_image.go",
"chars": 1054,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com"
},
{
"path": "pkg/policy/deny_job_container_latest_image_test.go",
"chars": 1440,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/deny_read_all_policy.go",
"chars": 745,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/deny_read_all_policy_test.go",
"chars": 1483,
"preview": "package policy_test //nolint:dupl\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"g"
},
{
"path": "pkg/policy/deny_write_all_policy.go",
"chars": 756,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/deny_write_all_policy_test.go",
"chars": 1488,
"preview": "package policy_test //nolint:dupl\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"g"
},
{
"path": "pkg/policy/error.go",
"chars": 339,
"preview": "package policy\n\nimport \"errors\"\n\nvar (\n\terrPermissionHyphenIsRequired = errors.New(\"an input `permission-*` is required\""
},
{
"path": "pkg/policy/github_app_should_limit_permissions.go",
"chars": 1438,
"preview": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-sh"
},
{
"path": "pkg/policy/github_app_should_limit_permissions_test.go",
"chars": 2270,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/github_app_should_limit_repositories.go",
"chars": 1992,
"preview": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-sh"
},
{
"path": "pkg/policy/github_app_should_limit_repositories_test.go",
"chars": 3811,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/job_permissions_policy.go",
"chars": 851,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/job_permissions_policy_test.go",
"chars": 1770,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/job_secrets_policy.go",
"chars": 1604,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/"
},
{
"path": "pkg/policy/job_secrets_policy_test.go",
"chars": 2473,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/job_timeout_minutes_is_required.go",
"chars": 830,
"preview": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/job_timeout_minutes_is_required_test.go",
"chars": 1992,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzu"
},
{
"path": "pkg/policy/workflow_secrets_policy.go",
"chars": 1242,
"preview": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shu"
},
{
"path": "pkg/policy/workflow_secrets_policy_test.go",
"chars": 2039,
"preview": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzu"
},
{
"path": "pkg/workflow/container.go",
"chars": 966,
"preview": "package workflow\n\nimport (\n\t\"errors\"\n)\n\ntype Container struct {\n\tImage string\n}\n\nfunc (c *Container) UnmarshalYAML(unmar"
},
{
"path": "pkg/workflow/container_test.go",
"chars": 688,
"preview": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfu"
},
{
"path": "pkg/workflow/job_secrets.go",
"chars": 1403,
"preview": "package workflow\n\nimport (\n\t\"errors\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype JobSecrets struct {\n\tm "
},
{
"path": "pkg/workflow/job_secrets_test.go",
"chars": 721,
"preview": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfu"
},
{
"path": "pkg/workflow/list_workflows.go",
"chars": 436,
"preview": "package workflow\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc List(fs afero.Fs) ([]string, error) {\n\tfiles, err :"
},
{
"path": "pkg/workflow/permissions.go",
"chars": 1948,
"preview": "package workflow\n\nimport (\n\t\"errors\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype Permissions struct {\n\tm "
},
{
"path": "pkg/workflow/permissions_test.go",
"chars": 963,
"preview": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfu"
},
{
"path": "pkg/workflow/read_action.go",
"chars": 641,
"preview": "package workflow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/slog-error/slo"
},
{
"path": "pkg/workflow/read_workflow.go",
"chars": 631,
"preview": "package workflow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/slog-error/slo"
},
{
"path": "pkg/workflow/workflow.go",
"chars": 1305,
"preview": "package workflow\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype Workflow struct {\n\tFilePath string `yaml:\"-"
},
{
"path": "renovate.json5",
"chars": 331,
"preview": "{\n extends: [\n \"github>suzuki-shunsuke/renovate-config#4.0.0\",\n \"github>suzuki-shunsuke/renovate-config:nolimit#4"
},
{
"path": "scripts/coverage.sh",
"chars": 482,
"preview": "#!/usr/bin/env bash\n\nset -eu\nset -o pipefail\n\ncd \"$(dirname \"$0\")/..\"\n\nif [ $# -eq 0 ]; then\n target=\"$(go list ./... |"
},
{
"path": "scripts/generate-usage.sh",
"chars": 211,
"preview": "#!/usr/bin/env bash\n\nset -eu\n\ncd \"$(dirname \"$0\")/..\"\n\nhelp=$(ghalint help-all)\n\necho -n \"# Usage\n\n<!-- This is generate"
},
{
"path": "test-action.yaml",
"chars": 1775,
"preview": "name: test\ndescription: test\ninputs:\n github_token:\n description: \"\"\n required: false\n default: ${{ github.tok"
},
{
"path": "test-workflow.yaml",
"chars": 2835,
"preview": "name: test\non: pull_request\nenv:\n # Workflow should not set secrets to environment variables\n FOO: bar\n GITHUB_TOKEN:"
}
]
About this extraction
This page contains the full source code of the suzuki-shunsuke/ghalint GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (161.4 KB), approximately 54.1k tokens, and a symbol index with 195 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.