Full Code of suzuki-shunsuke/ghalint for AI

main d507057d2a3d cached
113 files
161.4 KB
54.1k tokens
195 symbols
1 requests
Download .txt
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

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
Download .txt
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
Download .txt
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[![Ask DeepWiki](https://deepwiki.com/badge.svg)](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.

Copied to clipboard!