File pinact-3.4.4.obscpio of Package pinact

07070100000000000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001500000000pinact-3.4.4/.claude07070100000001000081A4000000000000000000000001691B0086000000EF000000000000000000000000000000000000002300000000pinact-3.4.4/.claude/settings.json{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "^(Edit|Write|MultiEdit)$",
        "hooks": [
          {
            "type": "command",
            "command": "cmdx fmt && cmdx v"
          }
        ]
      }
    ]
  }
}
07070100000002000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001900000000pinact-3.4.4/.clinerules07070100000003000081A4000000000000000000000001691B008600000131000000000000000000000000000000000000003200000000pinact-3.4.4/.clinerules/01-project-guidelines.md# Project Guidelines Reference

For all project guidelines, conventions, and commands, please refer to [AI_GUIDE.md](../AI_GUIDE.md).

This includes:
- Language conventions
- Commit message format
- Code validation and testing commands
- Project structure
- Development workflow
- Error handling patterns
07070100000004000081A4000000000000000000000001691B0086000003C0000000000000000000000000000000000000002E00000000pinact-3.4.4/.clinerules/02-cline-specific.md# Cline-Specific Guidelines

## VS Code Integration

- Leverage VS Code's built-in features for file navigation and editing
- Use the integrated terminal for command execution
- Respect workspace settings and configurations

## File Operations

- Use VS Code's file explorer for navigation
- Prefer relative paths for portability
- Respect .gitignore patterns

## Error Reporting in VS Code

- Format errors with file paths as clickable links (e.g., `pkg/controller/run/parse_line.go:123`)
- Use VS Code's Problems panel format when appropriate
- Provide clear, actionable error messages in the output

## VS Code Terminal Usage

- Execute validation and test commands in the integrated terminal
- Show command output directly in the terminal
- Use VS Code's task runner integration when available

## Cline Chat Interface

- Use markdown formatting for better readability
- Keep responses concise and focused
- Provide file paths that VS Code can navigate to
07070100000005000081A4000000000000000000000001691B0086000002A5000000000000000000000000000000000000001B00000000pinact-3.4.4/.golangci.ymlversion: "2"
linters:
  default: all
  disable:
    - depguard
    - err113
    - exhaustive
    - exhaustruct
    - godot
    - godox
    - ireturn
    - lll
    - musttag
    - nlreturn
    - nonamedreturns
    - tagalign
    - tagliatelle
    - varnamelen
    - wsl
    - wsl_v5
    - noinlineerr
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gci
    - gofmt
    - gofumpt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$
07070100000006000081A4000000000000000000000001691B008600000816000000000000000000000000000000000000001D00000000pinact-3.4.4/.goreleaser.yml---
version: 2
project_name: pinact
archives:
  - name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
    files:
      - LICENSE
      - README.md
      - third_party_licenses/**/*
    format_overrides:
      - goos: windows
        formats: [zip]
builds:
  - binary: pinact
    main: cmd/pinact/main.go
    env:
      - CGO_ENABLED=0
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - arm64
release:
  prerelease: "true"
  header: |
    [Pull Requests](https://github.com/suzuki-shunsuke/pinact/pulls?q=is%3Apr+milestone%3A{{.Tag}}) | [Issues](https://github.com/suzuki-shunsuke/pinact/issues?q=is%3Aissue+milestone%3A{{.Tag}}) | https://github.com/suzuki-shunsuke/pinact/compare/{{.PreviousTag}}...{{.Tag}}

sboms:
  - id: default
    disable: false

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-pinact
      token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
    # 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/pinact
    description: Pin GitHub Actions versions
    license: MIT
    skip_upload: true
    hooks:
      post:
        install: |
          if OS.mac?
            system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/pinact"]
          end

scoops:
  -
    description: |
      Pin GitHub Action versions by full length commit SHA.
      Edit GitHub Workflow and Composite action files and pin versions of Actions and Reusable Workflows.
      pinact can also update their versions and verify version annotations
    license: MIT
    skip_upload: true
    repository:
      owner: suzuki-shunsuke
      name: scoop-bucket
      token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
07070100000007000081A4000000000000000000000001691B00860000013F000000000000000000000000000000000000001A00000000pinact-3.4.4/.pinact.yaml# yaml-language-server: $schema=json-schema/pinact.json
# pinact - https://github.com/suzuki-shunsuke/pinact
version: 3
ignore_actions:
  - name: actions/setup-java
    ref: v3
  - name: slsa-framework/slsa-github-generator/\.github/workflows/generator_generic_slsa3\.yml
    ref: .*
  - name: peaceiris/.*
    ref: .*
07070100000008000081A4000000000000000000000001691B0086000015F4000000000000000000000000000000000000001900000000pinact-3.4.4/AI_GUIDE.md# AI Assistant Guidelines for pinact

This document contains common guidelines for AI assistants working on the pinact project.
Individual AI-specific documents (like CLAUDE.md, CLINE.md) should reference this guide.

## Language

This project uses **English** for all code comments, documentation, and communication.

## Commit Messages

Follow [Conventional Commits](https://www.conventionalcommits.org/) specification:

### Format

```
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

### Common Types

- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools
- `ci`: Changes to CI configuration files and scripts

### Examples

```
feat: add GitHub token management via keyring
fix: handle empty configuration file correctly
docs: add function documentation to controller package
chore(deps): update dependency aquaproj/aqua-registry to v4.403.0
```

## Code Validation

After making code changes, **always run** the following commands to validate and test:

### Validation (go vet)

```bash
cmdx v
```
This command runs `go vet ./...` to check for common Go mistakes.

### Testing

```bash
cmdx t
```
This command runs all tests in the project.

Both commands should pass before committing changes.

## Project Structure

```
pinact/
├── cmd/           # Main applications
├── pkg/           # Go packages
│   ├── cli/       # CLI interface layer
│   ├── config/    # Configuration management
│   ├── controller/# Business logic
│   ├── github/    # GitHub API integration
│   └── util/      # Utility functions
├── testdata/      # Test fixtures
├── json-schema/   # JSON schema definitions
└── scripts/       # Build and utility scripts
```

## Package Responsibilities

### pkg/cli
Command-line interface layer that handles command parsing, flag processing, and routing to appropriate subcommands.

### pkg/config
Configuration management including reading, parsing, and validating .pinact.yaml files.

### pkg/controller
Business logic layer containing:
- `run`: Core pinning logic for GitHub Actions
- `migrate`: Configuration schema migration

### pkg/github
GitHub API client integration and authentication management.

### pkg/util
Common utility functions used across the codebase.

## Testing

- Run all tests: `cmdx t` or `go test ./...`
- Run specific package tests: `go test ./pkg/controller/run`
- Generate coverage: `./scripts/coverage.sh`

## Dependencies

This project uses:
- [aqua](https://aquaproj.github.io/) for tool version management
- [cmdx](https://github.com/suzuki-shunsuke/cmdx) for task runner
- [goreleaser](https://goreleaser.com/) for releases

## Code Style Guidelines

1. Follow standard Go conventions
2. Use meaningful variable and function names
3. Add comments for exported functions and types
4. Keep functions focused and small
5. Handle errors explicitly
6. Use context for cancellation and timeouts
7. Always end files with a newline character

## Pull Request Process

1. Create a feature branch from `main`
2. Make changes and ensure `cmdx v` and `cmdx t` pass
3. Write clear commit messages following Conventional Commits
4. Create PR with descriptive title and body
5. Wait for CI checks to pass
6. Request review if needed

## Important Commands

```bash
# Validate code (go vet)
cmdx v

# Run tests
cmdx t

# Build the project
go build ./cmd/pinact

# Generate JSON schema
cmdx js

# Run pinact locally
go run ./cmd/pinact run
```

## GitHub Actions Integration

The project includes GitHub Actions for:
- Testing on multiple platforms
- Linting and validation
- Release automation
- Security scanning

## Configuration

pinact supports configuration via:
- `.pinact.yaml` or `.github/pinact.yaml`
- Command-line flags
- Environment variables

## Environment Variables

- `GITHUB_TOKEN`: GitHub access token for API calls
- `PINACT_LOG_LEVEL`: Log level (debug, info, warn, error)
- `PINACT_CONFIG`: Path to configuration file
- `PINACT_KEYRING_ENABLED`: Enable keyring for token storage

## Debugging

Enable debug logging:
```bash
export PINACT_LOG_LEVEL=debug
pinact run
```

## Common Tasks

### Adding a New Command

1. Create new package under `pkg/cli/`
2. Implement command structure with `urfave/cli/v3`
3. Add controller logic under `pkg/controller/`
4. Register command in `pkg/cli/runner.go`
5. Add tests for new functionality

### Updating Schema

1. Modify `pkg/config/config.go`
2. Update JSON schema: `cmdx js`
3. Update documentation in `docs/schema_v*.md`
4. Add migration logic if needed

## File Naming Conventions

- Go source files: lowercase with underscores (e.g., `parse_line.go`)
- Test files: append `_test.go` to the source file name
- Internal test files: append `_internal_test.go` for internal testing

## Error Handling

- Always check and handle errors explicitly
- Use `fmt.Errorf` with `%w` for wrapping errors
- Add context to errors to aid debugging
- Use structured logging with logrus

## Documentation

- Add package-level documentation comments
- Document all exported functions, types, and constants
- Use examples in documentation where helpful
- Keep README and other docs up to date

## Resources

- [Project README](README.md)
- [Contributing Guidelines](CONTRIBUTING.md)
- [Installation Guide](INSTALL.md)
- [Usage Documentation](USAGE.md)
07070100000009000081A4000000000000000000000001691B008600000466000000000000000000000000000000000000001700000000pinact-3.4.4/CLAUDE.md# Claude-Specific Guidelines for pinact

This document contains Claude-specific guidelines. For general project guidelines, see [AI_GUIDE.md](AI_GUIDE.md).

## Core Guidelines

All general project guidelines are documented in [AI_GUIDE.md](AI_GUIDE.md). Please refer to that document for:
- Language conventions
- Commit message format
- Code validation and testing
- Project structure
- Code style guidelines
- Common tasks and workflows

## Claude-Specific Notes

### Context Window Management

- Be mindful of context window limits when reading large files
- Use file offset and limit parameters when appropriate
- Summarize lengthy outputs to conserve context

### Tool Usage

- Prefer batch operations when possible to improve efficiency
- Use the Task tool for complex multi-step operations
- Always validate changes with `cmdx v` and `cmdx t`

### Communication Style

- Keep responses concise and to the point
- Focus on actionable information
- Avoid unnecessary explanations unless requested

## Quick Reference

For quick access to common commands and guidelines, see [AI_GUIDE.md](AI_GUIDE.md#important-commands).
0707010000000A000081A4000000000000000000000001691B00860000035D000000000000000000000000000000000000001D00000000pinact-3.4.4/CONTRIBUTING.md# Contributing

Please read the following document.

- https://github.com/suzuki-shunsuke/oss-contribution-guide

## How To Develop

We use [aqua](https://aquaproj.github.io/) as a CLI version manager and [cmdx](https://github.com/suzuki-shunsuke/cmdx) as a task runner.

[How to install aqua](https://aquaproj.github.io/docs/install)

```sh
aqua i # Install development tools including cmdx
```

Show tasks:

```sh
cmdx help
```

Test:

```sh
cmdx t
```

Lint:

```sh
cmdx v # go vet
cmdx l # golangci-lint
```

## Add tests

In addition to Go's unit tests, we run integration tests in CI.

- [testdata](testdata)
- [workflow](https://github.com/suzuki-shunsuke/pinact/blob/b60761b24a99aa946c45623c2ef2e1e673c257cf/.github/workflows/wc-test.yaml#L34-L67)

If you change pinact's behaviour, please add tests.
Tests also make how the behaviour is changed clear.
0707010000000B000081A4000000000000000000000001691B008600000BC2000000000000000000000000000000000000001800000000pinact-3.4.4/INSTALL.md# Install

pinact is written in Go. So you only have to install a binary in your `PATH`.

There are some ways to install pinact.

1. [Homebrew](#homebrew)
1. [Scoop](#scoop)
1. [aqua](#aqua)
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 pinact using [Homebrew](https://brew.sh/).

[Homebrew Core Formula: pinact](https://formulae.brew.sh/formula/pinact)

```sh
brew install pinact
```

Or

```sh
brew install suzuki-shunsuke/pinact/pinact
```

## Scoop

You can install pinact using [Scoop](https://scoop.sh/).

```sh
scoop bucket add suzuki-shunsuke https://github.com/suzuki-shunsuke/scoop-bucket
scoop install pinact
```

## aqua

You can install pinact using [aqua](https://aquaproj.github.io/).

```sh
aqua g -i suzuki-shunsuke/pinact
```

## Build an executable binary from source code yourself using Go

```sh
go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest
```

## GitHub Releases

You can download an asset from [GitHub Releases](https://github.com/suzuki-shunsuke/pinact/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.0.0
asset=pinact_darwin_arm64.tar.gz
gh release download -R suzuki-shunsuke/pinact "$version" -p "$asset"
gh attestation verify "$asset" \
  -R suzuki-shunsuke/pinact \
  --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.0.0
asset=pinact_darwin_arm64.tar.gz
gh release download -R suzuki-shunsuke/pinact "$version" -p "$asset" -p multiple.intoto.jsonl
slsa-verifier verify-artifact "$asset" \
  --provenance-path multiple.intoto.jsonl \
  --source-uri github.com/suzuki-shunsuke/pinact \
  --source-tag "$version"
```

### 3. Cosign

You can install Cosign by aqua.

```sh
aqua g -i sigstore/cosign
```

```sh
version=v1.0.0
checksum_file="pinact_${version#v}_checksums.txt"
asset=pinact_darwin_arm64.tar.gz
gh release download "$version" \
  -R suzuki-shunsuke/pinact \
  -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
```
0707010000000C000081A4000000000000000000000001691B008600000430000000000000000000000000000000000000001500000000pinact-3.4.4/LICENSEMIT 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.
0707010000000D000081A4000000000000000000000001691B008600003EBF000000000000000000000000000000000000001700000000pinact-3.4.4/README.md# pinact

[NotebookLM](https://notebooklm.google.com/notebook/31982d97-104e-4778-9a8f-3b2c044a719d) | [Install](INSTALL.md) | [How to use](#how-to-use) | [Configuration](#configuration)

pinact is a CLI to edit GitHub Workflow and Composite action files and pin versions of Actions and Reusable Workflows.
pinact can also [update their versions](#update-actions), [verify version annotations](docs/codes/001.md), and [create reviews](#create-reviews).

```sh
pinact run
```

```diff
$ git diff
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 84bd67a..5d92e44 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -113,17 +113,17 @@ jobs:
     needs: path-filter
     permissions: {}
     steps:
-      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3
-      - uses: actions/setup-go@v4
+      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1
+      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
       - name: Cache Primes
         id: cache-primes
-        uses: actions/cache@v3.3.1
+        uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
         with:
           path: prime-numbers
           key: ${{ runner.os }}-primes

   actionlint:
-    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@v0.5.0
+    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@b6a5f966d4504893b2aeb60cf2b0de8946e48504 # v0.5.0
     with:
       aqua_version: v2.3.4
     permissions:
```

Creating reviews:

![review](https://github.com/user-attachments/assets/77e78d23-bd14-49ba-8097-751556fcf126)

## :bulb: NotebookLM for pinact

[You can ask any questions about pinact to NotebookLM.](https://notebooklm.google.com/notebook/31982d97-104e-4778-9a8f-3b2c044a719d)

## Motivation

It is a good manner to pin GitHub Actions versions by commit hash.
GitHub tags are mutable so they have a substantial security and reliability risk.

See also [Security hardening for GitHub Actions - GitHub Docs](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

:thumbsup:

```yaml
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
```

:thumbsdown:

```yaml
uses: actions/cache@v3
```

```yaml
uses: actions/cache@v3.3.1
```

## Why not using Renovate's helpers:pinGitHubActionDigestsToSemver preset?

The Renovate preset [helpers:pinGitHubActionDigestsToSemver](https://docs.renovatebot.com/presets-helpers/#helperspingithubactiondigeststosemver) is useful, but pinact is still useful:

1. Renovate can't pin actions in pull requests before merging them.
If you use linters such as [ghalint](https://github.com/suzuki-shunsuke/ghalint) in CI, you need to pin actions before merging pull requests
(ref. [ghalint policy to enforce actions to be pinned](https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/008.md))
2. Even if you use Renovate, sometimes you would want to update actions manually
3. pinact is useful for non Renovate users
4. [pinact supports verifying version annotations](https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/001.md)

## GitHub Access token

pinact calls GitHub REST API to get commit hashes and tags.
You can pass GitHub Access token via environment variable `GITHUB_TOKEN`.
If no GitHub Access token is passed, pinact calls GitHub REST API without access token.

### Manage GitHub Access token using Keyring

pinact >= v3.1.0

You can manage a GitHub Access token using secret store such as [Windows Credential Manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0), [macOS Keychain](https://en.wikipedia.org/wiki/Keychain_(software)), and [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring).

1. Configure a GitHub Access token by `pinact token set` command:

```console
$ pinact token set
Enter a GitHub access token: # Input GitHub Access token
```

or you can also pass a GitHub Access token via standard input:

```sh
echo "<github access token>" | pinact token set -stdin
```

2. Enable the feature by setting the environment variable `PINACT_KEYRING_ENABLED`:

```sh
export PINACT_KEYRING_ENABLED=true
```

Note that if the environment variable `GITHUB_TOKEN` is set, this feature gets disabled.

You can remove a GitHub Access token from keyring by `pinact token rm` command:

```sh
pinact token rm
```

## How to use

Please run `pinact run` on a Git repository root directory, then target files are fixed.

```sh
pinact run
```

Default target files are:

```
.github/workflows/*.yml
.github/workflows/*.yaml
action.yml
action.yaml
*/action.yml
*/action.yaml
*/*/action.yml
*/*/action.yaml
*/*/*/action.yml
*/*/*/action.yaml
```

You can change target files by command line arguments or configuration files.

e.g.

```sh
pinact run example.yaml
```

### Update actions

[#663](https://github.com/suzuki-shunsuke/pinact/pull/663) pinact >= v1.1.0

You can update actions using the `-update (-u)` option:

```sh
pinact run -u
```

### Fix example codes in documents

pinact can fix example codes in documents too.

```sh
pinact run README.md
```

### Create reviews

![review](https://github.com/user-attachments/assets/77e78d23-bd14-49ba-8097-751556fcf126)

As of pinact v3.3.0, pinact can create reviews by GitHub API.
A GitHub access token with `pull_requests:write` permission is required.

```sh
pinact run \
  -review \
  -repo-owner <repository owner> \
  -repo-name <repository name> \
  -pr <pull request number> \
  -sha <commit SHA to be reviewed>
```

If pinact is run via GitHub Actions `pull_request` event, options are auto-completed.

> [!WARNING]
> GitHub can't create pull request reviews on files not changed by the pull request.
> When pinact fails to create reviews, pinact outputs warning and creates [GitHub Actions error messages to log instead](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message).
> You can ignore the warning like this:
> ```
> WARN[0004] create a review comment                       error="create a review comment: POST https://api.github.com/repos/szksh-lab-2/test-github-action/pulls/317/comments: 422 Validation Failed [{Resource:PullRequestReviewComment Field:pull_request_review_thread.path Code:invalid Message:} {Resource:PullRequestReviewComment Field:pull_request_review_thread.diff_hunk Code:missing_field Message:}]" line="      - uses: suzuki-shunsuke/watch-star-action@feat/first-pr" line_number=14 pinact_version=3.3.0-5 program=pinact review_pr_number=317 review_repo_name=test-github-action review_repo_owner=szksh-lab-2 review_sha=92f0b04efdc10acb793e78bdd1f70958dd3fd9a3 workflow_file=.github/workflows/watch.yaml
> ```

![error-message-log](https://github.com/user-attachments/assets/0231dee4-4473-459b-8ea4-e4c6a1f417c8)

### Generate a configuration file `.pinact.yaml`

A configuration file is optional.
You can create a configuration file `.pinact.yaml` by `pinact init`.

```sh
pinact init
```

You can change the output path.

```sh
pinact init '.github/pinact.yaml'
```

About the configuration, please see [Configuration](#Configuration).

### Validation

`pinact >= v1.6.0` [#816](https://github.com/suzuki-shunsuke/pinact/pull/816)

Instead of fixing files, you can validate if actions are pinned by `--check` option:

```sh
pinact run --check
```

Using this option, pinact doesn't fix files.
If actions aren't pinned, the command fails.

```console
$ pinact run --check
ERRO[0000] parse a line                                  action=actions/checkout@v2 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml
ERRO[0000] parse a line                                  action=actions/cache@v3.3.1 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml
ERRO[0000] parse a line                                  action=rharkor/caching-for-turbo@v1.6 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml
ERRO[0000] parse a line                                  action=actions/checkout@v3 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml
ERRO[0000] parse a line                                  action=actions/checkout@v3 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml
ERRO[0000] parse a line                                  action=suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@v0.5.0 error="action isn't pinned" pinact_version= program=pinact workflow_file=testdata/foo.yaml

$ echo $?
1
```

If `-check` is set, files aren't fixed and no diff is outputted.
If you want to fix files, please use `-fix` option.

```sh
pinact run -check -fix
```

And if you want to output diff, please use `-diff` option.

```sh
pinact run -check -diff
```

### Verify version annotations

Please see [the document](docs/codes/001.md).

### Output diff

```console
$ pinact run -diff
INFO[0000] action isn't pinned
.github/workflows/test.yaml:8
-       - uses: actions/checkout@v4
+       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  pinact_version=v3.0.0-local program=pinact
```

### -diff, -check, -fix options

The behaviour of `pinact run` command is changed by command line options `-diff`, `-check`, and `-fix`.
This is a table how the behaviour is changed by these options.

options | Fix files | Exit with code 1 if actions aren't pinned | Output changes
--- | --- | --- | ---
No option | o | | |
-check | | o | |
-diff | | | o
-check -diff | | o | o
-check -fix | o | o | o
-fix -diff | o | | o

## Fix or exclude only specific actions

[#1082](https://github.com/suzuki-shunsuke/pinact/pull/1082) pinact >= v3.4.0

You can fix only specific actions using the `-include (-i) <regular expression>` option.
You can also exclude only specific actions using the `-exclude (-e) <regular expression>` option.

e.g.

```sh
pinact run -i "actions/.*" -i "^aquaproj/aqua-installer$"
```

```sh
pinact run -e "actions/.*" -e "^aquaproj/aqua-installer$"
```

## GitHub Actions

https://github.com/suzuki-shunsuke/pinact-action

We develop GitHub Actions to pin GitHub Actions and reusable workflows by pinact.

## Configuration

A configuration file is optional.
pinact supports a configuration file `.pinact.yaml`, `.github/pinact.yaml`, `.pinact.yml` or `.github/pinact.yml`.
You can also specify the configuration file path by the environment variable `PINACT_CONFIG` or command line option `-c`.

As of pinact v2.2.0, pinact configuration file has a schema version.

```yaml
version: 3
```

In general, you should use the latest schema version.

### Schema v3 (latest)

pinact v2.2.0 or later supports this version.

.pinact.yaml

e.g.

```yaml
version: 3
files:
  - pattern: .github/workflows/*.yml
  - pattern: .github/workflows/*.yaml
  - pattern: .github/actions/*/action.yml
  - pattern: .github/actions/*/action.yaml

ignore_actions:
  # 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
  - name: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml
    ref: "v\\d+\\.\\d+\\.\\d+"
  - name: suzuki-shunsuke/.*
    ref: main
```

#### `files`

This is optional.
A list of target files.

#### `files[].pattern`

This is required.
A glob pattern of target files.
[Go's path/filepath#Glob](https://pkg.go.dev/path/filepath#Glob) is used.
A relative path from pinact's configuration file.
If files are passed via positional command line arguments, the configuration is ignored.

e.g.

```yaml
files:
  - pattern: .github/workflows/*.yml
  - pattern: .github/workflows/*.yaml
  - pattern: README.md
```

#### `ignore_actions`

This is optional. A list of ignored actions and reusable workflows.

#### `ignore_actions[].name`

This is required.
A regular expression of ignored actions and reusable workflows.

```yaml
ignore_actions:
  - name: actions/.*
    ref: main
```

> [!WARNING]
> Regular expressions must match with action names exactly.
> For instance, `name: actions/` doesn't match with `actions/checkout`

Regarding regular expressions, [Go's regexp package is used.](https://pkg.go.dev/regexp)

#### `ignore_actions[].ref`

This is required.
A regular expression of ignored action versions (branch, tag, or commit hash).

> [!WARNING]
> Regular expressions must match with action names exactly.
> For instance, `ref: main` doesn't match with `malicious-main`

### Old Schemas

Please see [here](docs/old_schema.md).

### JSON Schema

- [pinact.json](json-schema/pinact.json)
- https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.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/pinact.json -d pinact.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/pinact/main/json-schema/pinact.json
```

Or pinning version:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/v1.1.2/json-schema/pinact.json
```

## Q. Why doesn't pinact pin some actions?

In some cases pinact doesn't pin versions intentionally, which may confuse you.
So we describe the reason here.

pinact doesn't pin actions whose versions aren't semver (e.g. `main`, `master`, `release/v1`).
This is because pinact is designed as a safe tool so that it doesn't change workflows behaviour.
pinact pins actions but doesn't change SHA of actions at the moment when pinact pins versions.

This design enables you to accept changes by pinact safely.

For instance, pinact changes the version `v1` to `v1.1.0` if their SHA are equivalent.
If there are no semver whose SHA is same with `v1`, pinact doesn't change the version.

And pinact doesn't change versions which aren't semver.
For instance, pinact doesn't change the version `main`.

```yaml
uses: actions/checkout@main
```

We don't want to pin `main` to full commit length SHA like the following because we can't update this following semantic versioning.

```yaml
uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 # main
```

Tools like Renovate can update the SHA, but it's not safe at all as `main` branch isn't stable.

And we don't want to change `main` to the latest semver like the following because SHA is changed and workflows may be broken.

```yaml
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
```

We don't want to pin branches as SHA of branches is changed.

pinact doesn't check if a version is a tag or a branch because we would like to reduce the number of API calls as much as possible.
If a version isn't semver, pinact judges it may be a branch so pinact doesn't pin it.

Please see also [#926](https://github.com/suzuki-shunsuke/pinact/issues/926).

## See also

- [Renovate github-actions Manager - Additional Information](https://docs.renovatebot.com/modules/manager/github-actions/#additional-information)
- [sethvargo/ratchet](https://github.com/sethvargo/ratchet) is a great tool, but there are [known issues](https://github.com/sethvargo/ratchet#known-issues).

## LICENSE

[MIT](LICENSE)
0707010000000E000081A4000000000000000000000001691B008600001353000000000000000000000000000000000000001600000000pinact-3.4.4/USAGE.md# Usage

<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->

```console
$ pinact --help
NAME:
   pinact - Pin GitHub Actions versions. https://github.com/suzuki-shunsuke/pinact

USAGE:
   pinact [global options] [command [command options]]

VERSION:
   3.4.3

COMMANDS:
   init        Create .pinact.yaml if it doesn't exist
   run         Pin GitHub Actions versions
   migrate     Migrate .pinact.yaml
   token       Manage GitHub Access token
   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-level string          log level [$PINACT_LOG_LEVEL]
   --config string, -c string  configuration file path [$PINACT_CONFIG]
   --help, -h                  show help
   --version, -v               print the version
```

## pinact init

```console
$ pinact init --help
NAME:
   pinact init - Create .pinact.yaml if it doesn't exist

USAGE:
   pinact init

DESCRIPTION:
   Create .pinact.yaml if it doesn't exist

   $ pinact init

   You can also pass configuration file path.

   e.g.

   $ pinact init .github/pinact.yaml


OPTIONS:
   --help, -h  show help
```

## pinact run

```console
$ pinact run --help
NAME:
   pinact run - Pin GitHub Actions versions

USAGE:
   pinact run

DESCRIPTION:
   If no argument is passed, pinact searches GitHub Actions workflow files from .github/workflows.

   $ pinact run

   You can also pass workflow file paths as arguments.

   e.g.

   $ pinact run .github/actions/foo/action.yaml .github/actions/bar/action.yaml


OPTIONS:
   --verify, -v                                                 Verify if pairs of commit SHA and version are correct
   --check                                                      Exit with a non-zero status code if actions are not pinned. If this is true, files aren't updated
   --update, -u                                                 Update actions to latest versions
   --review                                                     Create reviews
   --fix                                                        Fix code. By default, this is true. If -check or -diff is true, this is false by default
   --diff                                                       Output diff. By default, this is false
   --repo-owner string                                          GitHub repository owner [$GITHUB_REPOSITORY_OWNER]
   --repo-name string                                           GitHub repository name
   --sha string                                                 Commit SHA to be reviewed
   --pr int                                                     GitHub pull request number (default: 0)
   --include string, -i string [ --include string, -i string ]  A regular expression to fix actions
   --exclude string, -e string [ --exclude string, -e string ]  A regular expression to exclude actions
   --help, -h                                                   show help
```

## pinact migrate

```console
$ pinact migrate --help
NAME:
   pinact migrate - Migrate .pinact.yaml

USAGE:
   pinact migrate

DESCRIPTION:
   Migrate the version of .pinact.yaml

   $ pinact migrate


OPTIONS:
   --help, -h  show help
```

## pinact token

```console
$ pinact token --help
NAME:
   pinact token - Manage GitHub Access token

USAGE:
   pinact token [command [command options]]

DESCRIPTION:
   Manage GitHub Access token by keyring.

COMMANDS:
   set         Set GitHub Access token
   remove, rm  Remove GitHub Access token

OPTIONS:
   --help, -h  show help
```

### token set

```console
$ token set --help
NAME:
   pinact token set - Set GitHub Access token

USAGE:
   pinact token set

DESCRIPTION:
   Set GitHub Access token to keyring.

OPTIONS:
   --stdin     Read GitHub Access token from stdin
   --help, -h  show help
```

### token remove

```console
$ token remove --help
NAME:
   pinact token remove - Remove GitHub Access token

USAGE:
   pinact token remove

DESCRIPTION:
   Remove GitHub Access token from keyring.

OPTIONS:
   --help, -h  show help
```

## pinact version

```console
$ pinact version --help
NAME:
   pinact version - Show version

USAGE:
   pinact version

OPTIONS:
   --json, -j  Output version in JSON format
   --help, -h  show help
```

## pinact completion

```console
$ pinact completion --help
NAME:
   pinact completion - Output shell completion script for bash, zsh, fish, or Powershell

USAGE:
   pinact completion

DESCRIPTION:
   Output shell completion script for bash, zsh, fish, or Powershell.
   Source the output to enable completion.

   # .bashrc
   source <(pinact completion bash)

   # .zshrc
   source <(pinact completion zsh)

   # fish
   pinact completion fish > ~/.config/fish/completions/pinact.fish

   # Powershell
   Output the script to path/to/autocomplete/pinact.ps1 an run it.


OPTIONS:
   --help, -h  show help
```
0707010000000F000081A4000000000000000000000001691B008600000043000000000000000000000000000000000000001900000000pinact-3.4.4/_typos.toml[default.extend-words]
ERRO = "ERRO"
intoto = "intoto"
typ = "typ"
07070100000010000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001200000000pinact-3.4.4/aqua07070100000011000081A4000000000000000000000001691B008600003C68000000000000000000000000000000000000002600000000pinact-3.4.4/aqua/aqua-checksums.json{
  "checksums": [
    {
      "id": "github_release/github.com/anchore/syft/v1.37.0/syft_1.37.0_darwin_amd64.tar.gz",
      "checksum": "70E00D20E3D0A72D90FDEE607986F680CFACCC50C51A66E12030B96DF62770CF",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/anchore/syft/v1.37.0/syft_1.37.0_darwin_arm64.tar.gz",
      "checksum": "B676CB2E112FCF5EC25638CCA129F5DE92F4786A6C4628CD3C307992F1A6A50D",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/anchore/syft/v1.37.0/syft_1.37.0_linux_amd64.tar.gz",
      "checksum": "B81A0DC81B92265F4597659BBA5509E014C78228182804BB1BC97856AF26E326",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/anchore/syft/v1.37.0/syft_1.37.0_linux_arm64.tar.gz",
      "checksum": "52980B7FD4829AB9561527C775C5ADC7ECE43EB7F9026E456351CD5B5252FDDA",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/anchore/syft/v1.37.0/syft_1.37.0_windows_amd64.zip",
      "checksum": "7E3602F4B95FD916080EC9348805D79B33E6BB37C693D83AA2D3C85423CEAEA5",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/crate-ci/typos/v1.36.3/typos-v1.36.3-aarch64-apple-darwin.tar.gz",
      "checksum": "7F6C733BC33C8BFDBA9D684F1985DEAC219BB6E1AEDEB1A7F61132D150629CA3",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/crate-ci/typos/v1.36.3/typos-v1.36.3-aarch64-unknown-linux-musl.tar.gz",
      "checksum": "60B54649D8B723052F3CC46571B66C49866C4C28AC7FDBF023236A1A0A40146B",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/crate-ci/typos/v1.36.3/typos-v1.36.3-x86_64-apple-darwin.tar.gz",
      "checksum": "52D0BF951FF3BFED2C0D746E036F443E14A037D669CD6FF73FC64BB55E07794F",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/crate-ci/typos/v1.36.3/typos-v1.36.3-x86_64-pc-windows-msvc.zip",
      "checksum": "4A39D96D2777C3C0095B0CCE91D429C9F13F23CB2BB75E5E29EC508DCFBC655B",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/crate-ci/typos/v1.36.3/typos-v1.36.3-x86_64-unknown-linux-musl.tar.gz",
      "checksum": "2D51E68CEADE784C36603CD4EE23BB681FE5D808CE193BE45F24124FC0D3BA42",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-darwin-amd64.tar.gz",
      "checksum": "A7E684872B00637D642D088DDE783C1B871161A92678FCF13D07ABE6B5C32E36",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-darwin-arm64.tar.gz",
      "checksum": "0B3CBDC2A2472F60B538EBCCB1B2E1AE5D938A051C010591AA68C6EFD3706672",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-linux-amd64.tar.gz",
      "checksum": "C77313A77E19B06123962C411D9943CC0D092BBEC76B956104D18964E274902E",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-linux-arm64.tar.gz",
      "checksum": "48693A98A7F4556D1117300AAE240D0FE483DF8D6F36DFABA56504626101A66E",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-windows-amd64.zip",
      "checksum": "8D37563C2549E38135EAC46E778164D2C5B1E96B9211F2087814D74CA0F358A8",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/golangci/golangci-lint/v2.5.0/golangci-lint-2.5.0-windows-arm64.zip",
      "checksum": "B08F2AC149428307A14D733772720C84893D17E0D1FF2506623B00E7246229B1",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/goreleaser/goreleaser/v2.12.7/goreleaser_Darwin_all.tar.gz",
      "checksum": "9EBF84C40C8D69C85567283CE371E1BBCE4EC6FC87105D323476354AFCCC0D1D",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/goreleaser/goreleaser/v2.12.7/goreleaser_Linux_arm64.tar.gz",
      "checksum": "7948D8CE09CCE3D3A2FFF9202EAE47D2223958E508F849B340345583BB2C0E1F",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/goreleaser/goreleaser/v2.12.7/goreleaser_Linux_x86_64.tar.gz",
      "checksum": "DE95C5D1E728E18753C3DDCF02974D63D6432F0A7355A5AFA6813971A95A01B8",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/goreleaser/goreleaser/v2.12.7/goreleaser_Windows_arm64.zip",
      "checksum": "FF5D9692C4ACFF082869D0BF4995661C31F9E73BDA278EC50B07E7F8AC6C02E6",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/goreleaser/goreleaser/v2.12.7/goreleaser_Windows_x86_64.zip",
      "checksum": "D8BFA5643CE29645D0BB11BC2AAC8061579D14F720ED278E3D75EAD8F3F5B0D4",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/mvdan/gofumpt/v0.9.2/gofumpt_v0.9.2_darwin_amd64",
      "checksum": "4172B912EC514038605F334FEF9ED7B3F12CA3E40024CB0A622EAB3073A55E57",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/mvdan/gofumpt/v0.9.2/gofumpt_v0.9.2_darwin_arm64",
      "checksum": "C241FB742599A6CB0563D7377F59DEF65D451B23DD718DBC6DDF4AB5E695E8F1",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/mvdan/gofumpt/v0.9.2/gofumpt_v0.9.2_linux_amd64",
      "checksum": "72CF61B12FEF91EAB6DF6DB4A4284F30616B5EAD330112E28A1FA1CB15E57339",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/mvdan/gofumpt/v0.9.2/gofumpt_v0.9.2_linux_arm64",
      "checksum": "5ACAA5A554050F55FC81EF02A8B0D14AB6B3C058A84513885286DC52D3451645",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/mvdan/gofumpt/v0.9.2/gofumpt_v0.9.2_windows_amd64.exe",
      "checksum": "067236B55A8EF4547DDC7D78FBB7A38169DE15BAB02A1763CDE6A132C59DD35C",
      "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/rhysd/actionlint/v1.7.8/actionlint_1.7.8_darwin_amd64.tar.gz",
      "checksum": "16B85CAF792B34BCC40F7437736C4347680DA0A1B034353A85012DEBBD71A461",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/rhysd/actionlint/v1.7.8/actionlint_1.7.8_darwin_arm64.tar.gz",
      "checksum": "FFB1F6C429A51DC9F37AF9D11F96C16BD52F54B713BF7F8BD92F7FCE9FD4284A",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/rhysd/actionlint/v1.7.8/actionlint_1.7.8_linux_amd64.tar.gz",
      "checksum": "BE92C2652AB7B6D08425428797CEABEB16E31A781C07BC388456B4E592F3E36A",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/rhysd/actionlint/v1.7.8/actionlint_1.7.8_linux_arm64.tar.gz",
      "checksum": "4C65DBB2D59B409CDD75D47FFA8FA32AF8F0EEE573AC510468DC2275C48BF07C",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/rhysd/actionlint/v1.7.8/actionlint_1.7.8_windows_amd64.zip",
      "checksum": "8CAE2385EB69B93084EA2E6A4BCE57233FEB56AB16B3087BEA25CBC0ABADB0CC",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/rhysd/actionlint/v1.7.8/actionlint_1.7.8_windows_arm64.zip",
      "checksum": "76CBB407E2DE15DF97969AAE51D6FB46AAAE9E73A48DA06B1C3157CB9F2EE766",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/sigstore/cosign/v3.0.2/cosign-darwin-amd64",
      "checksum": "0FC2B6F16B900ABDFDA3153B11FC435A8CBE3830E8E820FE8AD5FE4149A5B472",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/sigstore/cosign/v3.0.2/cosign-darwin-arm64",
      "checksum": "3823B044DE184DA21E300BC5E20DD29D3FA9243AF3BA70C4A5DA1712F3385D46",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/sigstore/cosign/v3.0.2/cosign-linux-amd64",
      "checksum": "46DBDCB5467A3DFEC2526923D0B3365E40C8D9DC00EC23D5ACA3437449E8CBFD",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/sigstore/cosign/v3.0.2/cosign-linux-arm64",
      "checksum": "17FD784737CA54D7D8A343C82DA6C5D6DBDEE971E66644D923D1B057FB97D7ED",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/sigstore/cosign/v3.0.2/cosign-windows-amd64.exe",
      "checksum": "7A137280D8686665CEB4D8565DF2A0AC63F28031E014CDCAE5D56891A6C8A400",
      "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.3/ghalint_1.5.3_darwin_amd64.tar.gz",
      "checksum": "26B0E7F47F848EE17F7829D80CB75A8B6A77D87EADB3423A57B5FF5916DBEF24",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.3/ghalint_1.5.3_darwin_arm64.tar.gz",
      "checksum": "F5D463AEE96EA2485903218A12143478A517C084F94A3F5CA47F1D90440B4ADB",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.3/ghalint_1.5.3_linux_amd64.tar.gz",
      "checksum": "5FD6696E6F736750B60A67F0C7F7E43642BDA15348D55D261C4D386940A1C02F",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.3/ghalint_1.5.3_linux_arm64.tar.gz",
      "checksum": "40F870601461EF2FC4F5F3DF25EB79EC49506ABF1C45A0E4D02BBED0E1A88A5E",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.3/ghalint_1.5.3_windows_amd64.zip",
      "checksum": "3E6E644581EA3B947F7D1FAA6D2A4BBAC2AC19249F6E8AB29A808FD4BA4ADD47",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.5.3/ghalint_1.5.3_windows_arm64.zip",
      "checksum": "50A45BA46F9E2C51509F83B04C7F1869BD23CA39B2EFD3CA7106E6D0076EF199",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_darwin_amd64.tar.gz",
      "checksum": "EAD8931E0F048904E9D85E45E90F4E3BDF13B5D2EE5F2BCCC35782BC3EC4CD8F",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_darwin_arm64.tar.gz",
      "checksum": "0DB73E6DC366FBB1EABAAE3D65C9C1E2F8B8DD09D9022D436D46120DBD5B96FA",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_linux_amd64.tar.gz",
      "checksum": "F903F622DF9C89A694BEC4218AC2957D1E579627B662DF92E53FF725A9597E5D",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_linux_arm64.tar.gz",
      "checksum": "FE2F89E5B03A1EBF6079071CA5E2836350735A6B1000C975AE27525C4C077488",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_windows_amd64.zip",
      "checksum": "7AAADA136832D8E9D251515CB177D19B664EDB4072E63696BCEA42DD2B2D34E0",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/nllint/v0.1.2/nllint_windows_arm64.zip",
      "checksum": "C1C92F73778BECCB6422F004B1113B0AA7060C3757AF39EB8DF3E0573167D681",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_darwin_amd64.tar.gz",
      "checksum": "505965C3E18D862F35985082283502B597EFBB654C280D4A496B00C13AE564F0",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_darwin_arm64.tar.gz",
      "checksum": "30E289D6A5EEE87C1178A3FFF4A17528AED8460607544C0B4F4AD6C3BF65055D",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_linux_amd64.tar.gz",
      "checksum": "0F4ED8164659A554C5B06ED01158C9B29DA32DCB8A9AD22AE71B15419B946398",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_linux_arm64.tar.gz",
      "checksum": "398CAB2EA8A74D5F7099E4680DBE320866C6CFC74DB1EFA0704122E38700907C",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_windows_amd64.zip",
      "checksum": "21ACF44EC679ECB46015E6618837A11770FF6A0F0480BB4AD4819F4F5815AA6A",
      "algorithm": "sha256"
    },
    {
      "id": "github_release/github.com/suzuki-shunsuke/pinact/v3.4.3/pinact_windows_arm64.zip",
      "checksum": "F88449D9F319F9D0DD5B2A6A9C2E33047F15F80C74858FA0C4FC2F4FA332286A",
      "algorithm": "sha256"
    },
    {
      "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.436.0/registry.yaml",
      "checksum": "D10A9E3D8F3D28C8A75E536FD30C8B1C2E91C92637DF4BC97B61E375E133DDF271E277F9F7F628DBC8A1CA2C5F03CD4CAADB338EEB549B3C00C4BA9F5E444BEB",
      "algorithm": "sha512"
    }
  ]
}
07070100000012000081A4000000000000000000000001691B008600000161000000000000000000000000000000000000001C00000000pinact-3.4.4/aqua/aqua.yaml---
# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/v2.55.1/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.436.0 # renovate: depName=aquaproj/aqua-registry
import_dir: imports
07070100000013000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001A00000000pinact-3.4.4/aqua/imports07070100000014000081A4000000000000000000000001691B00860000002C000000000000000000000000000000000000002A00000000pinact-3.4.4/aqua/imports/actionlint.yamlpackages:
  - name: rhysd/actionlint@v1.7.8
07070100000015000081A4000000000000000000000001691B008600000030000000000000000000000000000000000000002400000000pinact-3.4.4/aqua/imports/cmdx.yamlpackages:
  - name: suzuki-shunsuke/cmdx@v2.0.2
07070100000016000081A4000000000000000000000001691B00860000002B000000000000000000000000000000000000002600000000pinact-3.4.4/aqua/imports/cosign.yamlpackages:
  - name: sigstore/cosign@v3.0.2
07070100000017000081A4000000000000000000000001691B008600000033000000000000000000000000000000000000002700000000pinact-3.4.4/aqua/imports/ghalint.yamlpackages:
  - name: suzuki-shunsuke/ghalint@v1.5.3
07070100000018000081A4000000000000000000000001691B00860000002E000000000000000000000000000000000000002B00000000pinact-3.4.4/aqua/imports/go-licenses.yamlpackages:
  - name: google/go-licenses@v2.0.1
07070100000019000081A4000000000000000000000001691B008600000029000000000000000000000000000000000000002700000000pinact-3.4.4/aqua/imports/gofumpt.yamlpackages:
  - name: mvdan/gofumpt@v0.9.2
0707010000001A000081A4000000000000000000000001691B008600000032000000000000000000000000000000000000002D00000000pinact-3.4.4/aqua/imports/golangci-lint.yamlpackages:
  - name: golangci/golangci-lint@v2.5.0
0707010000001B000081A4000000000000000000000001691B008600000032000000000000000000000000000000000000002900000000pinact-3.4.4/aqua/imports/goreleser.yamlpackages:
  - name: goreleaser/goreleaser@v2.12.7
0707010000001C000081A4000000000000000000000001691B008600000032000000000000000000000000000000000000002600000000pinact-3.4.4/aqua/imports/nllint.yamlpackages:
  - name: suzuki-shunsuke/nllint@v0.1.2
0707010000001D000081A4000000000000000000000001691B008600000032000000000000000000000000000000000000002600000000pinact-3.4.4/aqua/imports/pinact.yamlpackages:
  - name: suzuki-shunsuke/pinact@v3.4.3
0707010000001E000081A4000000000000000000000001691B008600000030000000000000000000000000000000000000002900000000pinact-3.4.4/aqua/imports/reviewdog.yamlpackages:
  - name: reviewdog/reviewdog@v0.21.0
0707010000001F000081A4000000000000000000000001691B008600000029000000000000000000000000000000000000002400000000pinact-3.4.4/aqua/imports/syft.yamlpackages:
  - name: anchore/syft@v1.37.0
07070100000020000081A4000000000000000000000001691B00860000002B000000000000000000000000000000000000002500000000pinact-3.4.4/aqua/imports/typos.yamlpackages:
  - name: crate-ci/typos@v1.36.3
07070100000021000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001100000000pinact-3.4.4/cmd07070100000022000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002000000000pinact-3.4.4/cmd/gen-jsonschema07070100000023000081A4000000000000000000000001691B008600000197000000000000000000000000000000000000002800000000pinact-3.4.4/cmd/gen-jsonschema/main.gopackage main

import (
	"fmt"
	"log"

	"github.com/suzuki-shunsuke/gen-go-jsonschema/jsonschema"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
)

func main() {
	if err := core(); err != nil {
		log.Fatal(err)
	}
}

func core() error {
	if err := jsonschema.Write(&config.Config{}, "json-schema/pinact.json"); err != nil {
		return fmt.Errorf("create or update a JSON Schema: %w", err)
	}
	return nil
}
07070100000024000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001800000000pinact-3.4.4/cmd/pinact07070100000025000081A4000000000000000000000001691B0086000003F4000000000000000000000000000000000000002000000000pinact-3.4.4/cmd/pinact/main.gopackage main

import (
	"context"
	"errors"
	"os"
	"os/signal"
	"syscall"

	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/cli"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/controller/run"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/log"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
)

var (
	version = ""
	commit  = "" //nolint:gochecknoglobals
	date    = "" //nolint:gochecknoglobals
)

type HasExitCode interface {
	ExitCode() int
}

func main() {
	logE := log.New("pinact", version)
	if err := core(logE); err != nil {
		if errors.Is(err, run.ErrActionsNotPinned) {
			os.Exit(1)
		}
		logerr.WithError(logE, err).Fatal("pinact failed")
	}
}

func core(logE *logrus.Entry) error {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	return cli.Run(ctx, logE, &urfave.LDFlags{ //nolint:wrapcheck
		Version: version,
		Commit:  commit,
		Date:    date,
	}, os.Args...)
}
07070100000026000081A4000000000000000000000001691B0086000005C2000000000000000000000000000000000000001700000000pinact-3.4.4/cmdx.yaml---
# the configuration file of 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: 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: coverage
  short: c
  description: coverage test
  usage: coverage test
  script: "bash scripts/coverage.sh {{.target}}"
  args:
  - name: target
- name: install
  short: i
  description: Build and install pinact
  usage: Build and install pinact by "go install" command
  script: |
    sha=""
    if git diff --quiet; then
      sha=$(git rev-parse HEAD)
    fi
    go install \
      -ldflags "-X main.version=v3.0.0-local -X main.commit=$sha -X main.date=$(date +"%Y-%m-%dT%H:%M:%SZ%:z" | tr -d '+')" \
      ./cmd/pinact
- name: run
  description: Run pinact via go run
  usage: Run pinact via go run
  script: |
    go run ./cmd/pinact {{._builtin.args_string}}
- name: usage
  description: Generate USAGE.md
  usage: Generate USAGE.md
  script: bash scripts/generate-usage.sh
- name: js
  description: Generate JSON Schema
  usage: Generate JSON Schema
  script: "go run ./cmd/gen-jsonschema"
- name: fmt
  description: Format code
  usage: Format code
  script: |
    git ls-files | grep -E "\.go$" | xargs gofumpt -l -w
    git ls-files | xargs nllint -f -s
07070100000027000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001200000000pinact-3.4.4/docs07070100000028000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001800000000pinact-3.4.4/docs/codes07070100000029000081A4000000000000000000000001691B008600000707000000000000000000000000000000000000001F00000000pinact-3.4.4/docs/codes/001.md# Verify version annotations

pinact >= v0.1.3

Please see the following code.

```yaml
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v3.5.1
```

You would assume the version of the action is v3.5.1 because the version annotation is "v3.5.1".
But the actual version is v2.7.0 because "ee0669bd1cc54295c223e0bb666b733df41de1c5" is the commit hash of v2.7.0.
Please check releases.

- https://github.com/actions/checkout/releases/tag/v3.5.1
- https://github.com/actions/checkout/releases/tag/v2.7.0

This indicates version annotations aren't necessarily correct.
Especially, attackers can specify a full commit SHA including a malicious code while setting a safe tag to the version annotation.
If a pull request includes changes of GitHub Actions, you should verify version annotations.

pinact v0.1.3 or newer can verify version annotations using `pinact run`'s `--verify` option.
This verification works only if the version annotation is semver and the version is full commit hash like the above example.
This option gets a full commit hash from a version annotation by GitHub API and compares it with the version.

e.g.

```console
$ pinact run --verify testdata/bar.yaml
ERRO[0000] parse a line                                  action=actions/checkout action_version=ee0669bd1cc54295c223e0bb666b733df41de1c5 commit_hash_of_version_annotation=83b7061638ee4956cf7545a6f7efe594e5ad0247 error="verify the version annotation: action_version must be equal to commit_hash_of_version_annotation" help_docs="https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/001.md" pinact_version= program=pinact version_annotation=v3.5.1 workflow_file=testdata/bar.yaml
```

Note that `--verify` option calls GitHub API to verify version annotations, which may cause API rate limiting.
0707010000002A000081A4000000000000000000000001691B008600000184000000000000000000000000000000000000001F00000000pinact-3.4.4/docs/codes/002.md# schema version is required

As of pinact v3, the `version` field is required in `.pinact.yaml`.

```yaml
version: 3
```

Please use the latest version as much as possible.

You can migrate the configuration file by `pinact migrate` command.

```sh
pinact migrate
```

Please see release notes of major updates too.

- [v3](https://github.com/suzuki-shunsuke/pinact/releases/tag/v3.0.0)
0707010000002B000081A4000000000000000000000001691B008600000247000000000000000000000000000000000000001F00000000pinact-3.4.4/docs/codes/003.md# this version was abandoned. Please update the schema version

This error means the schema version of your configuration file `.pinact.yaml` is old, so your pinact doesn't support the version.
You need to update the schema version, or downgrade pinact.
Please use the latest version of pinact and the schema version as much as possible.
Maybe you should update pinact.
You can migrate the configuration file by `pinact migrate` command.

```sh
pinact migrate
```

Please see release notes of major updates too.

- [v3](https://github.com/suzuki-shunsuke/pinact/releases/tag/v3.0.0)
0707010000002C000081A4000000000000000000000001691B00860000015F000000000000000000000000000000000000001F00000000pinact-3.4.4/docs/codes/004.md# pinact doesn't support this configuration format version. Maybe you need to update pinact

This error means your pinact doesn't support the schema version of your configuration file `.pinact.yaml`.
Maybe your pinact is old, or the schema version is invalid.

If the schema version is invalid, please fix it.
If your pinact is old, please update it.
0707010000002D000081A4000000000000000000000001691B00860000002A000000000000000000000000000000000000002000000000pinact-3.4.4/docs/old_schema.md# Old Schema

- [Schema v2](schema_v2.md)
0707010000002E000081A4000000000000000000000001691B00860000052D000000000000000000000000000000000000001F00000000pinact-3.4.4/docs/schema_v2.md# Schema v2

> [!WARNING]
> pinact v3 or later doesn't support this schema version.

```yaml
version: 2
```

`pinact <= v2.2.0`

pinact v2.2.0 or older supports this schema version.

## `files`

This is optional.
A list of target files.

## `files[].pattern`

This is required.
A regular expression of target files.
If files are passed via positional command line arguments, the configuration is ignored.

e.g.

```yaml
files:
  - pattern: ^\.github/workflows/.*\\.ya?ml$
```

> [!WARNING]
> Regular expressions doesn't necessarily match with action names exactly.
> For instance, `pattern: action\\.yaml` matches with `foo/action\\.yaml`

## `ignore_actions`

This is optional. A list of ignored actions and reusable workflows.

## `ignore_actions[].name`

This is required.
A regular expression of ignored actions and reusable workflows.

```yaml
ignore_actions:
  - name: actions/.*
    ref: main
```

> [!WARNING]
> Regular expressions doesn't necessarily match with action names exactly.
> For instance, `name: actions/` matches with `actions/checkout`

## `ignore_actions[].ref`

`pinact >= v2.1.0`

This is required.
A regular expression of ignored action versions (branch, tag, or commit hash).

> [!WARNING]
> Regular expressions must match with action names exactly.
> For instance, `ref: main` matches with `main`
0707010000002F000081A4000000000000000000000001691B008600000570000000000000000000000000000000000000001400000000pinact-3.4.4/go.modmodule github.com/suzuki-shunsuke/pinact/v3

go 1.25.4

require (
	github.com/fatih/color v1.18.0
	github.com/goccy/go-yaml v1.18.0
	github.com/google/go-cmp v0.7.0
	github.com/google/go-github/v78 v78.0.0
	github.com/hashicorp/go-version v1.7.0
	github.com/sirupsen/logrus v1.9.3
	github.com/spf13/afero v1.15.0
	github.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0
	github.com/suzuki-shunsuke/logrus-error v0.1.4
	github.com/suzuki-shunsuke/urfave-cli-v3-util v0.0.7
	github.com/urfave/cli/v3 v3.6.0
	golang.org/x/oauth2 v0.33.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/danieljoos/wincred v1.2.2 // indirect
	github.com/godbus/dbus/v5 v5.1.0 // indirect
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/invopop/jsonschema v0.13.0 // indirect
	github.com/kr/pretty v0.3.1 // 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/wk8/go-ordered-map/v2 v2.1.8 // indirect
	github.com/zalando/go-keyring v0.2.6 // indirect
	golang.org/x/sys v0.33.0 // indirect
	golang.org/x/term v0.32.0 // indirect
	golang.org/x/text v0.28.0 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
07070100000030000081A4000000000000000000000001691B008600001ECC000000000000000000000000000000000000001400000000pinact-3.4.4/go.sumal.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/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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/v78 v78.0.0 h1:b1tytzFE8i//lRVDx5Qh/EdJbtTPtSVD3nF7hraEs9w=
github.com/google/go-github/v78 v78.0.0/go.mod h1:Uxvdzy82AkNlC6JQ57se9TqvmgBT7RF0ouHDNg2jd6g=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
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/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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/logrus-error v0.1.4 h1:nWo98uba1fANHdZ9Y5pJ2RKs/PpVjrLzRp5m+mRb9KE=
github.com/suzuki-shunsuke/logrus-error v0.1.4/go.mod h1:WsVvvw6SKSt08/fB2qbnsKIMJA4K1MYCUprqsBJbMiM=
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.0.7 h1:fXAs8oA9Bfwfn/6L3uzMahnPERdqYzbjzFnuYL4M0co=
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.0.7/go.mod h1:LQlBLMZ1ZlFEv6bx20RAB9r3wCQnTBKxWpkl4btEo2k=
github.com/urfave/cli/v3 v3.6.0 h1:oIdArVjkdIXHWg3iqxgmqwQGC8NM0JtdgwQAj2sRwFo=
github.com/urfave/cli/v3 v3.6.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.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
07070100000031000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001900000000pinact-3.4.4/json-schema07070100000032000081A4000000000000000000000001691B00860000054C000000000000000000000000000000000000002500000000pinact-3.4.4/json-schema/pinact.json{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/suzuki-shunsuke/pinact/v3/pkg/config/config",
  "$ref": "#/$defs/Config",
  "$defs": {
    "Config": {
      "properties": {
        "version": {
          "type": "integer",
          "enum": [
            2,
            3
          ]
        },
        "files": {
          "items": {
            "$ref": "#/$defs/File"
          },
          "type": "array",
          "description": "Target files. If files are passed via positional command line arguments"
        },
        "ignore_actions": {
          "items": {
            "$ref": "#/$defs/IgnoreAction"
          },
          "type": "array",
          "description": "Actions and reusable workflows that pinact ignores"
        }
      },
      "additionalProperties": false,
      "type": "object"
    },
    "File": {
      "properties": {
        "pattern": {
          "type": "string"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "pattern"
      ]
    },
    "IgnoreAction": {
      "properties": {
        "name": {
          "type": "string"
        },
        "ref": {
          "type": "string"
        }
      },
      "additionalProperties": false,
      "type": "object",
      "required": [
        "name"
      ]
    }
  }
}
07070100000033000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001100000000pinact-3.4.4/pkg07070100000034000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001500000000pinact-3.4.4/pkg/cli07070100000035000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001D00000000pinact-3.4.4/pkg/cli/initcmd07070100000036000081A4000000000000000000000001691B008600000B07000000000000000000000000000000000000002800000000pinact-3.4.4/pkg/cli/initcmd/command.go// Package initcmd implements the 'pinact init' command.
// This package is responsible for generating pinact configuration files (.pinact.yaml)
// with default settings to help users quickly set up pinact in their repositories.
// It creates configuration templates that define target workflow files and
// action ignore patterns for the pinning process.
package initcmd

import (
	"context"
	"fmt"
	"os"

	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/controller/run"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/log"
	"github.com/urfave/cli/v3"
)

// New creates a new init command instance with the provided logger.
// It returns a CLI command that can be registered with the main CLI application.
func New(logE *logrus.Entry) *cli.Command {
	r := &runner{
		logE: logE,
	}
	return r.Command()
}

type runner struct {
	logE *logrus.Entry
}

// Command returns the CLI command definition for the init subcommand.
// It defines the command name, usage, description, and action handler.
func (r *runner) Command() *cli.Command {
	return &cli.Command{
		Name:  "init",
		Usage: "Create .pinact.yaml if it doesn't exist",
		Description: `Create .pinact.yaml if it doesn't exist

$ pinact init

You can also pass configuration file path.

e.g.

$ pinact init .github/pinact.yaml
`,
		Action: r.action,
	}
}

// action handles the execution of the init command.
// It creates a default .pinact.yaml configuration file in the specified location.
// The function sets up the necessary controllers and services, determines the output
// path for the configuration file, and delegates to the controller's Init method.
func (r *runner) action(ctx context.Context, c *cli.Command) error {
	pwd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("get the current directory: %w", err)
	}
	gh := github.New(ctx, r.logE)
	ctrl := run.New(&run.RepositoriesServiceImpl{
		Tags:                map[string]*run.ListTagsResult{},
		Releases:            map[string]*run.ListReleasesResult{},
		Commits:             map[string]*run.GetCommitSHA1Result{},
		RepositoriesService: gh.Repositories,
	}, gh.PullRequests, afero.NewOsFs(), nil, nil, &run.ParamRun{
		WorkflowFilePaths: c.Args().Slice(),
		ConfigFilePath:    c.String("config"),
		PWD:               pwd,
		IsVerify:          c.Bool("verify"),
		Check:             c.Bool("check"),
		Update:            c.Bool("update"),
	})

	if err := log.Set(r.logE, c.String("log-level"), "auto"); err != nil {
		return fmt.Errorf("configure logger: %w", err)
	}
	configFilePath := c.Args().First()
	if configFilePath == "" {
		configFilePath = c.String("config")
	}
	if configFilePath == "" {
		configFilePath = ".pinact.yaml"
	}
	return ctrl.Init(configFilePath) //nolint:wrapcheck
}
07070100000037000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001D00000000pinact-3.4.4/pkg/cli/migrate07070100000038000081A4000000000000000000000001691B0086000008F1000000000000000000000000000000000000002800000000pinact-3.4.4/pkg/cli/migrate/command.go// Package migrate implements the 'pinact migrate' command.
// This package handles the migration of pinact configuration files between
// different schema versions. It ensures smooth upgrades when pinact introduces
// new configuration formats or features, allowing users to automatically
// update their .pinact.yaml files to the latest schema version.
package migrate

import (
	"context"
	"fmt"

	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/controller/migrate"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/log"
	"github.com/urfave/cli/v3"
)

type runner struct {
	logE *logrus.Entry
}

// New creates a new migrate command for the CLI.
// It initializes a runner with the provided logger and returns
// the configured CLI command for migrating pinact configuration files.
//
// Parameters:
//   - logE: logrus entry for structured logging
//
// Returns a pointer to the configured CLI command.
func New(logE *logrus.Entry) *cli.Command {
	r := runner{
		logE: logE,
	}
	return r.Command()
}

// Command builds and returns the migrate CLI command configuration.
// It defines the command name, usage description, and action handler
// for the migrate subcommand.
//
// Returns a pointer to the configured CLI command.
func (r *runner) Command() *cli.Command {
	return &cli.Command{
		Name:  "migrate",
		Usage: "Migrate .pinact.yaml",
		Description: `Migrate the version of .pinact.yaml

$ pinact migrate
`,
		Action: r.action,
	}
}

// action executes the migrate command logic.
// It configures logging, creates the filesystem interface and controller,
// then performs the configuration file migration.
//
// Parameters:
//   - _: context (unused in this implementation)
//   - c: CLI command containing parsed flags and arguments
//
// Returns an error if migration fails or logging configuration fails.
func (r *runner) action(_ context.Context, c *cli.Command) error {
	if err := log.Set(r.logE, c.String("log-level"), "auto"); err != nil {
		return fmt.Errorf("configure logger: %w", err)
	}
	fs := afero.NewOsFs()
	ctrl := migrate.New(fs, config.NewFinder(fs), &migrate.Param{
		ConfigFilePath: c.String("config"),
	})

	return ctrl.Migrate(r.logE) //nolint:wrapcheck
}
07070100000039000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001900000000pinact-3.4.4/pkg/cli/run0707010000003A000081A4000000000000000000000001691B008600002B11000000000000000000000000000000000000002400000000pinact-3.4.4/pkg/cli/run/command.go// Package run implements the 'pinact run' command, the core functionality of pinact.
// This package orchestrates the main pinning process for GitHub Actions and reusable workflows,
// including version resolution, SHA pinning, update operations, and pull request review creation.
// It handles various modes of operation (check, diff, fix, update, review) and integrates
// with GitHub Actions CI environment for automated processing. The package also manages
// include/exclude patterns for selective action processing and coordinates with the
// controller layer to perform the actual file modifications.
package run

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/fatih/color"
	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/controller/run"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/log"
	"github.com/urfave/cli/v3"
)

// New creates a new run command for the CLI.
// It initializes a runner with the provided logger and returns
// the configured CLI command for pinning GitHub Actions versions.
//
// Parameters:
//   - logE: logrus entry for structured logging
//
// Returns a pointer to the configured CLI command.
func New(logE *logrus.Entry) *cli.Command {
	r := &runner{
		logE: logE,
	}
	return r.Command()
}

type runner struct {
	logE *logrus.Entry
}

// Command builds and returns the run CLI command configuration.
// It defines all flags, options, and the action handler for the run subcommand.
// This command handles the core pinning functionality with various modes
// like check, diff, fix, update, and review.
//
// Returns a pointer to the configured CLI command.
func (r *runner) Command() *cli.Command { //nolint:funlen
	return &cli.Command{
		Name:  "run",
		Usage: "Pin GitHub Actions versions",
		Description: `If no argument is passed, pinact searches GitHub Actions workflow files from .github/workflows.

$ pinact run

You can also pass workflow file paths as arguments.

e.g.

$ pinact run .github/actions/foo/action.yaml .github/actions/bar/action.yaml
`,
		Action: r.action,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:    "verify",
				Aliases: []string{"v"},
				Usage:   "Verify if pairs of commit SHA and version are correct",
			},
			&cli.BoolFlag{
				Name:  "check",
				Usage: "Exit with a non-zero status code if actions are not pinned. If this is true, files aren't updated",
			},
			&cli.BoolFlag{
				Name:    "update",
				Aliases: []string{"u"},
				Usage:   "Update actions to latest versions",
			},
			&cli.BoolFlag{
				Name:  "review",
				Usage: "Create reviews",
			},
			&cli.BoolFlag{
				Name:  "fix",
				Usage: "Fix code. By default, this is true. If -check or -diff is true, this is false by default",
			},
			&cli.BoolFlag{
				Name:  "diff",
				Usage: "Output diff. By default, this is false",
			},
			&cli.StringFlag{
				Name:    "repo-owner",
				Usage:   "GitHub repository owner",
				Sources: cli.EnvVars("GITHUB_REPOSITORY_OWNER"),
			},
			&cli.StringFlag{
				Name:  "repo-name",
				Usage: "GitHub repository name",
			},
			&cli.StringFlag{
				Name:  "sha",
				Usage: "Commit SHA to be reviewed",
			},
			&cli.IntFlag{
				Name:  "pr",
				Usage: "GitHub pull request number",
			},
			&cli.StringSliceFlag{
				Name:    "include",
				Aliases: []string{"i"},
				Usage:   "A regular expression to fix actions",
			},
			&cli.StringSliceFlag{
				Name:    "exclude",
				Aliases: []string{"e"},
				Usage:   "A regular expression to exclude actions",
			},
		},
	}
}

type Event struct {
	PullRequest *PullRequest `json:"pull_request"`
	Issue       *Issue       `json:"issue"`
	Repository  *Repository  `json:"repository"`
}

// RepoName extracts the repository name from the GitHub event.
// It safely accesses the repository information from the event payload.
//
// Returns the repository name or empty string if not available.
func (e *Event) RepoName() string {
	if e != nil && e.Repository != nil {
		return e.Repository.Name
	}
	return ""
}

// PRNumber extracts the pull request or issue number from the GitHub event.
// It checks both pull request and issue fields to find the number.
//
// Returns the PR/issue number or 0 if not available.
func (e *Event) PRNumber() int {
	if e == nil {
		return 0
	}
	if e.PullRequest != nil {
		return e.PullRequest.Number
	}
	if e.Issue != nil {
		return e.Issue.Number
	}
	return 0
}

// SHA extracts the commit SHA from the GitHub event.
// It looks for the SHA in the pull request head information.
//
// Returns the commit SHA or empty string if not available.
func (e *Event) SHA() string {
	if e == nil {
		return ""
	}
	if e.PullRequest != nil && e.PullRequest.Head != nil {
		return e.PullRequest.Head.SHA
	}
	return ""
}

type Issue struct {
	Number int `json:"number"`
}

type PullRequest struct {
	Number int   `json:"number"`
	Head   *Head `json:"head"`
}

type Repository struct {
	Owner *Owner `json:"owner"`
	Name  string `json:"name"`
}

type Owner struct {
	Login string `json:"login"`
}

type Head struct {
	SHA string `json:"sha"`
}

// action executes the main run command logic.
// It configures logging, processes GitHub Actions context, parses includes/excludes,
// sets up the controller, and executes the pinning operation.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - c: CLI command containing parsed flags and arguments
//
// Returns an error if the operation fails.
func (r *runner) action(ctx context.Context, c *cli.Command) error { //nolint:cyclop,funlen
	clr := "auto"
	isGitHubActions := os.Getenv("GITHUB_ACTIONS") == "true"
	if isGitHubActions {
		clr = "always"
		color.NoColor = false
	}
	if err := log.Set(r.logE, c.String("log-level"), clr); err != nil {
		return fmt.Errorf("configure logger: %w", err)
	}

	pwd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("get the current directory: %w", err)
	}

	gh := github.New(ctx, r.logE)
	fs := afero.NewOsFs()
	var review *run.Review
	if c.Bool("review") {
		review = &run.Review{
			RepoOwner:   c.String("repo-owner"),
			RepoName:    c.String("repo-name"),
			PullRequest: c.Int("pr"),
			SHA:         c.String("sha"),
		}
		if isGitHubActions {
			if err := r.setReview(fs, review); err != nil {
				logerr.WithError(r.logE, err).Error("set review information")
			}
		}
		if !review.Valid() {
			r.logE.Warn("skip creating reviews because the review information is invalid")
			review = nil
		}
	}
	includes, err := parseIncludes(c.StringSlice("include"))
	if err != nil {
		return err
	}
	excludes, err := parseExcludes(c.StringSlice("exclude"))
	if err != nil {
		return err
	}
	param := &run.ParamRun{
		WorkflowFilePaths: c.Args().Slice(),
		ConfigFilePath:    c.String("config"),
		PWD:               pwd,
		IsVerify:          c.Bool("verify"),
		Check:             c.Bool("check"),
		Update:            c.Bool("update"),
		Diff:              c.Bool("diff"),
		Fix:               true,
		IsGitHubActions:   isGitHubActions,
		Stderr:            os.Stderr,
		Review:            review,
		Includes:          includes,
		Excludes:          excludes,
	}
	if c.IsSet("fix") {
		param.Fix = c.Bool("fix")
	} else if param.Check || param.Diff {
		param.Fix = false
	}
	ctrl := run.New(&run.RepositoriesServiceImpl{
		Tags:                map[string]*run.ListTagsResult{},
		Releases:            map[string]*run.ListReleasesResult{},
		Commits:             map[string]*run.GetCommitSHA1Result{},
		RepositoriesService: gh.Repositories,
	}, gh.PullRequests, fs, config.NewFinder(fs), config.NewReader(fs), param)
	return ctrl.Run(ctx, r.logE) //nolint:wrapcheck
}

// parseIncludes compiles include regular expressions from command-line options.
// These patterns are used to filter which actions should be processed.
//
// Parameters:
//   - opts: slice of include pattern strings
//
// Returns compiled regular expressions or an error if compilation fails.
func parseIncludes(opts []string) ([]*regexp.Regexp, error) {
	includes := make([]*regexp.Regexp, len(opts))
	for i, include := range opts {
		r, err := regexp.Compile(include)
		if err != nil {
			return nil, fmt.Errorf("compile an include regexp: %w", logerr.WithFields(err, logrus.Fields{
				"regexp": include,
			}))
		}
		includes[i] = r
	}
	return includes, nil
}

// parseExcludes compiles exclude regular expressions from command-line options.
// These patterns are used to filter which actions should be skipped.
//
// Parameters:
//   - opts: slice of exclude pattern strings
//
// Returns compiled regular expressions or an error if compilation fails.
func parseExcludes(opts []string) ([]*regexp.Regexp, error) {
	excludes := make([]*regexp.Regexp, len(opts))
	for i, exclude := range opts {
		r, err := regexp.Compile(exclude)
		if err != nil {
			return nil, fmt.Errorf("compile an exclude regexp: %w", logerr.WithFields(err, logrus.Fields{
				"regexp": exclude,
			}))
		}
		excludes[i] = r
	}
	return excludes, nil
}

// setReview configures review information from GitHub Actions environment.
// It extracts repository name, pull request number, and commit SHA from
// environment variables and GitHub event payload.
//
// Parameters:
//   - fs: filesystem interface for reading event files
//   - review: review configuration to populate
//
// Returns an error if required information cannot be extracted.
func (r *runner) setReview(fs afero.Fs, review *run.Review) error {
	if review.RepoName == "" {
		repo := os.Getenv("GITHUB_REPOSITORY")
		_, repoName, ok := strings.Cut(repo, "/")
		if !ok {
			return fmt.Errorf("GITHUB_REPOSITORY is not set or invalid: %s", repo)
		}
		if repoName == "" {
			return fmt.Errorf("GITHUB_REPOSITORY is invalid: %s", repo)
		}
		review.RepoName = repoName
	}
	eventPath := os.Getenv("GITHUB_EVENT_PATH")
	if eventPath == "" {
		return nil
	}
	var ev *Event
	if review.PullRequest == 0 {
		ev = &Event{}
		if err := r.readEvent(fs, ev, eventPath); err != nil {
			return err
		}
		review.PullRequest = ev.PRNumber()
	}
	if review.SHA != "" {
		return nil
	}
	if ev == nil {
		ev = &Event{}
		if err := r.readEvent(fs, ev, eventPath); err != nil {
			return err
		}
	}
	review.SHA = ev.SHA()
	return nil
}

// readEvent reads and parses the GitHub event payload from file.
// It unmarshals the JSON event data into the provided Event struct.
//
// Parameters:
//   - fs: filesystem interface for file operations
//   - ev: Event struct to populate with parsed data
//   - eventPath: path to the GitHub event file
//
// Returns an error if the file cannot be read or parsed.
func (r *runner) readEvent(fs afero.Fs, ev *Event, eventPath string) error {
	event, err := fs.Open(eventPath)
	if err != nil {
		return fmt.Errorf("read GITHUB_EVENT_PATH: %w", err)
	}
	if err := json.NewDecoder(event).Decode(&ev); err != nil {
		return fmt.Errorf("unmarshal GITHUB_EVENT_PATH: %w", err)
	}
	return nil
}
0707010000003B000081A4000000000000000000000001691B008600000797000000000000000000000000000000000000001F00000000pinact-3.4.4/pkg/cli/runner.go// Package cli provides the command-line interface layer for pinact.
// This package serves as the main entry point for all CLI operations,
// handling command parsing, flag processing, and routing to appropriate subcommands.
// It orchestrates the overall CLI structure using urfave/cli framework and delegates
// actual business logic to controller packages.
package cli

import (
	"context"

	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/cli/initcmd"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/cli/migrate"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/cli/run"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/cli/token"
	"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave"
	"github.com/urfave/cli/v3"
)

// Run creates and executes the main pinact CLI application.
// It configures the command structure with global flags and subcommands,
// then runs the CLI with the provided arguments.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - ldFlags: linker flags containing build information
//   - args: command line arguments to parse and execute
//
// Returns an error if command parsing or execution fails.
func Run(ctx context.Context, logE *logrus.Entry, ldFlags *urfave.LDFlags, args ...string) error {
	return urfave.Command(logE, ldFlags, &cli.Command{ //nolint:wrapcheck
		Name:  "pinact",
		Usage: "Pin GitHub Actions versions. https://github.com/suzuki-shunsuke/pinact",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "log-level",
				Usage:   "log level",
				Sources: cli.EnvVars("PINACT_LOG_LEVEL"),
			},
			&cli.StringFlag{
				Name: "config",
				Aliases: []string{
					"c",
				},
				Usage:   "configuration file path",
				Sources: cli.EnvVars("PINACT_CONFIG"),
			},
		},
		Commands: []*cli.Command{
			initcmd.New(logE),
			run.New(logE),
			migrate.New(logE),
			token.New(logE),
		},
	}).Run(ctx, args)
}
0707010000003C000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001B00000000pinact-3.4.4/pkg/cli/token0707010000003D000081A4000000000000000000000001691B00860000046A000000000000000000000000000000000000002600000000pinact-3.4.4/pkg/cli/token/command.go// Package token implements the 'pinact token' command for secure GitHub token management.
// This package provides functionality to store and retrieve GitHub access tokens
// using the operating system's native credential storage (Windows Credential Manager,
// macOS Keychain, or GNOME Keyring). It offers a secure alternative to environment
// variables for managing authentication credentials, allowing users to persist tokens
// safely across sessions without exposing them in shell configurations.
package token

import (
	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
	ghTokenCLI "github.com/suzuki-shunsuke/urfave-cli-v3-util/keyring/ghtoken/cli"
	"github.com/urfave/cli/v3"
)

// New creates a new token command for the CLI.
// It initializes a GitHub token management command using the system keyring
// for secure credential storage and retrieval.
//
// Parameters:
//   - logE: logrus entry for structured logging
//
// Returns a pointer to the configured CLI command for token operations.
func New(logE *logrus.Entry) *cli.Command {
	return ghTokenCLI.New(logE, github.KeyService)
}
0707010000003E000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001800000000pinact-3.4.4/pkg/config0707010000003F000081A4000000000000000000000001691B0086000025AA000000000000000000000000000000000000002200000000pinact-3.4.4/pkg/config/config.go// Package config manages pinact configuration files and validation.
// This package is responsible for reading, parsing, and validating .pinact.yaml
// configuration files. It handles multiple schema versions, manages file patterns
// for targeting specific workflow files, and maintains ignore rules for excluding
// certain actions from the pinning process. The package provides interfaces for
// finding and reading configuration files from standard locations, ensuring
// backward compatibility while supporting schema evolution.
package config

import (
	"errors"
	"fmt"
	"path"
	"regexp"

	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Version       int             `json:"version,omitempty" jsonschema:"enum=2,enum=3"`
	Files         []*File         `json:"files,omitempty" jsonschema:"description=Target files. If files are passed via positional command line arguments, this is ignored"`
	IgnoreActions []*IgnoreAction `json:"ignore_actions,omitempty" yaml:"ignore_actions" jsonschema:"description=Actions and reusable workflows that pinact ignores"`
}

type File struct {
	Pattern string `json:"pattern"`
}

var (
	errUnsupportedConfigVersion = errors.New("pinact doesn't support this configuration format version. Maybe you need to update pinact")
	errAbandonedConfigVersion   = errors.New("this version was abandoned. Please update the schema version")
	errEmptyConfigVersion       = errors.New("schema version is required")
)

// validateSchemaVersion checks if the provided configuration schema version is supported.
// It validates against the current supported version (3) and provides helpful error
// messages for unsupported, abandoned, or missing versions.
//
// Parameters:
//   - v: schema version number to validate
//
// Returns an error if the version is not supported, nil if valid.
func validateSchemaVersion(v int) error {
	switch v {
	case 0:
		return logerr.WithFields(errEmptyConfigVersion, logrus.Fields{ //nolint:wrapcheck
			"docs": "https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/002.md",
		})
	case 2: //nolint:mnd
		return logerr.WithFields(errAbandonedConfigVersion, logrus.Fields{ //nolint:wrapcheck
			"docs": "https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/003.md",
		})
	case 3: //nolint:mnd
		return nil
	default:
		return logerr.WithFields(errUnsupportedConfigVersion, logrus.Fields{ //nolint:wrapcheck
			"docs": "https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/004.md",
		})
	}
}

// Init initializes and validates a File configuration.
// It validates the pattern field and ensures it's a valid glob pattern.
//
// Parameters:
//   - v: configuration schema version
//
// Returns an error if validation fails.
func (f *File) Init(v int) error {
	if f.Pattern == "" {
		return errors.New("pattern is required")
	}
	if err := validateSchemaVersion(v); err != nil {
		return err
	}
	_, err := path.Match(f.Pattern, "a")
	if err != nil {
		return fmt.Errorf("parse pattern as a glob: %w", err)
	}
	return nil
}

type IgnoreAction struct {
	Name       string `json:"name"`
	Ref        string `json:"ref,omitempty"`
	nameRegexp *regexp.Regexp
	refRegexp  *regexp.Regexp
}

// Init initializes and validates an IgnoreAction configuration.
// It compiles the name and ref patterns as regular expressions.
//
// Parameters:
//   - v: configuration schema version
//
// Returns an error if initialization or validation fails.
func (ia *IgnoreAction) Init(v int) error {
	if err := ia.initName(); err != nil {
		return err
	}
	if err := ia.initRef(v); err != nil {
		return err
	}
	return nil
}

// Match checks if an action matches the ignore criteria.
// It evaluates both name and ref patterns against the provided values.
//
// Parameters:
//   - name: action name to match against
//   - ref: action reference to match against
//   - version: configuration schema version
//
// Returns true if the action should be ignored, false otherwise, or an error if matching fails.
func (ia *IgnoreAction) Match(name, ref string, version int) (bool, error) {
	f, err := ia.matchName(name, version)
	if err != nil {
		return false, fmt.Errorf("match name: %w", err)
	}
	if !f {
		return false, nil
	}
	b, err := ia.matchRef(ref, version)
	if err != nil {
		return false, fmt.Errorf("match ref: %w", err)
	}
	return b, nil
}

// initName compiles the name pattern as a regular expression.
// It validates that a name pattern is provided and can be compiled.
//
// Returns an error if the name is empty or the regex compilation fails.
func (ia *IgnoreAction) initName() error {
	if ia.Name == "" {
		return errors.New("name is required")
	}
	r, err := regexp.Compile(ia.Name)
	if err != nil {
		return fmt.Errorf("compile name as a regular expression: %w", err)
	}
	ia.nameRegexp = r
	return nil
}

// initRef compiles the ref pattern as a regular expression.
// It validates that a ref pattern is provided and can be compiled.
//
// Parameters:
//   - v: configuration schema version
//
// Returns an error if the ref is empty or the regex compilation fails.
func (ia *IgnoreAction) initRef(v int) error {
	if err := validateSchemaVersion(v); err != nil {
		return err
	}
	if ia.Ref == "" {
		return errors.New("ref is required")
	}
	r, err := regexp.Compile(ia.Ref)
	if err != nil {
		return fmt.Errorf("compile ref as a regular expression: %w", err)
	}
	ia.refRegexp = r
	return nil
}

// matchName checks if the provided name matches the compiled name pattern.
// It performs exact string matching using the regular expression.
//
// Parameters:
//   - name: action name to match
//   - version: configuration schema version
//
// Returns true if the name matches exactly, false otherwise, or an error if validation fails.
func (ia *IgnoreAction) matchName(name string, version int) (bool, error) {
	if err := validateSchemaVersion(version); err != nil {
		return false, err
	}
	return ia.nameRegexp.FindString(name) == name, nil
}

// matchRef checks if the provided ref matches the compiled ref pattern.
// It performs exact string matching using the regular expression.
//
// Parameters:
//   - ref: action reference to match
//   - version: configuration schema version
//
// Returns true if the ref matches exactly, false otherwise, or an error if validation fails.
func (ia *IgnoreAction) matchRef(ref string, version int) (bool, error) {
	if err := validateSchemaVersion(version); err != nil {
		return false, err
	}
	return ia.refRegexp.FindString(ref) == ref, nil
}

// getConfigPath searches for a pinact configuration file in standard locations.
// It checks for .pinact.yaml, .github/pinact.yaml, .pinact.yml, and .github/pinact.yml
// in order of preference.
//
// Parameters:
//   - fs: filesystem interface for file operations
//
// Returns the path to the first found configuration file, empty string if none found, or an error.
func getConfigPath(fs afero.Fs) (string, error) {
	for _, path := range []string{".pinact.yaml", ".github/pinact.yaml", ".pinact.yml", ".github/pinact.yml"} {
		f, err := afero.Exists(fs, path)
		if err != nil {
			return "", fmt.Errorf("check if %s exists: %w", path, err)
		}
		if f {
			return path, nil
		}
	}
	return "", nil
}

type Finder struct {
	fs afero.Fs
}

// NewFinder creates a new configuration file finder.
// It initializes a Finder with the provided filesystem interface.
//
// Parameters:
//   - fs: filesystem interface for file operations
//
// Returns a pointer to the configured Finder.
func NewFinder(fs afero.Fs) *Finder {
	return &Finder{fs: fs}
}

// Find locates the configuration file path to use.
// If a specific path is provided, it returns that path.
// Otherwise, it searches for configuration files in standard locations.
//
// Parameters:
//   - configFilePath: explicit configuration file path or empty string
//
// Returns the configuration file path to use or an error if search fails.
func (f *Finder) Find(configFilePath string) (string, error) {
	if configFilePath != "" {
		return configFilePath, nil
	}
	p, err := getConfigPath(f.fs)
	if err != nil {
		return "", err
	}
	return p, nil
}

type Reader struct {
	fs afero.Fs
}

// NewReader creates a new configuration file reader.
// It initializes a Reader with the provided filesystem interface.
//
// Parameters:
//   - fs: filesystem interface for file operations
//
// Returns a pointer to the configured Reader.
func NewReader(fs afero.Fs) *Reader {
	return &Reader{fs: fs}
}

// Read loads and parses a configuration file.
// It reads the YAML file, validates the schema version, and initializes
// all configuration components including files and ignore actions.
//
// Parameters:
//   - cfg: Config struct to populate with parsed data
//   - configFilePath: path to the configuration file to read
//
// Returns an error if reading, parsing, or validation fails.
func (r *Reader) Read(cfg *Config, configFilePath string) error {
	if configFilePath == "" {
		return nil
	}
	f, err := r.fs.Open(configFilePath)
	if err != nil {
		return fmt.Errorf("open a configuration file: %w", err)
	}
	defer f.Close()
	if err := yaml.NewDecoder(f).Decode(cfg); err != nil {
		return fmt.Errorf("decode a configuration file as YAML: %w", err)
	}
	if err := validateSchemaVersion(cfg.Version); err != nil {
		return err
	}
	for _, file := range cfg.Files {
		if err := file.Init(cfg.Version); err != nil {
			return fmt.Errorf("initialize file: %w", err)
		}
	}
	for _, ia := range cfg.IgnoreActions {
		if err := ia.Init(cfg.Version); err != nil {
			return fmt.Errorf("initialize ignore_action: %w", err)
		}
	}
	return nil
}
07070100000040000081A4000000000000000000000001691B0086000003DF000000000000000000000000000000000000003000000000pinact-3.4.4/pkg/config/config_internal_test.gopackage config

import (
	"testing"

	"github.com/spf13/afero"
)

func Test_getConfigPath(t *testing.T) {
	t.Parallel()
	data := []struct {
		name  string
		paths []string
		exp   string
	}{
		{
			name:  "no config",
			paths: []string{},
			exp:   "",
		},
		{
			name:  "primary",
			paths: []string{".pinact.yaml"},
			exp:   ".pinact.yaml",
		},
		{
			name:  "another",
			paths: []string{".github/pinact.yaml"},
			exp:   ".github/pinact.yaml",
		},
		{
			name:  "both primary and others",
			paths: []string{".pinact.yaml", ".github/pinact.yaml"},
			exp:   ".pinact.yaml",
		},
	}
	for _, d := range data {
		t.Run(d.name, func(t *testing.T) {
			t.Parallel()
			fs := afero.NewMemMapFs()
			for _, path := range d.paths {
				if err := afero.WriteFile(fs, path, []byte(""), 0o644); err != nil {
					t.Fatal(err)
				}
			}
			got, err := getConfigPath(fs)
			if err != nil {
				t.Fatal(err)
			}
			if got != d.exp {
				t.Fatalf(`wanted %s, got %s`, d.exp, got)
			}
		})
	}
}
07070100000041000081A4000000000000000000000001691B0086000005E5000000000000000000000000000000000000002700000000pinact-3.4.4/pkg/config/config_test.gopackage config_test

import (
	"testing"

	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
)

func TestIgnoreAction_Match(t *testing.T) {
	t.Parallel()
	data := []struct {
		name          string
		ignoreAction  *config.IgnoreAction
		actionName    string
		actionRef     string
		configVersion int
		expected      bool
	}{
		{
			name: "match by name and ref (v3)",
			ignoreAction: &config.IgnoreAction{
				Name: "actions/checkout",
				Ref:  "main",
			},
			actionName:    "actions/checkout",
			actionRef:     "main",
			expected:      true,
			configVersion: 3,
		},
		{
			name: "not match (v3)",
			ignoreAction: &config.IgnoreAction{
				Name: "actions/checkout",
				Ref:  "main",
			},
			actionName:    "actions/checkout",
			actionRef:     "main-malicious",
			expected:      false,
			configVersion: 3,
		},
		{
			name: "not match name (v3)",
			ignoreAction: &config.IgnoreAction{
				Name: "actions/",
				Ref:  "main",
			},
			actionName:    "actions/checkout",
			actionRef:     "main",
			expected:      false,
			configVersion: 3,
		},
	}

	for _, d := range data {
		t.Run(d.name, func(t *testing.T) {
			t.Parallel()
			if err := d.ignoreAction.Init(d.configVersion); err != nil {
				t.Fatalf("failed to initialize ignore action: %v", err)
			}
			got, err := d.ignoreAction.Match(d.actionName, d.actionRef, d.configVersion)
			if err != nil {
				t.Fatalf("failed to match: %v", err)
			}
			if got != d.expected {
				t.Fatalf("wanted %v, got %v", d.expected, got)
			}
		})
	}
}
07070100000042000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001C00000000pinact-3.4.4/pkg/controller07070100000043000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002400000000pinact-3.4.4/pkg/controller/migrate07070100000044000081A4000000000000000000000001691B0086000010D1000000000000000000000000000000000000002B00000000pinact-3.4.4/pkg/controller/migrate/ast.gopackage migrate

import (
	"errors"
	"fmt"

	"github.com/goccy/go-yaml"
	"github.com/goccy/go-yaml/ast"
	"github.com/goccy/go-yaml/parser"
	"github.com/sirupsen/logrus"
)

// parseConfigAST parses and migrates a YAML configuration file using AST.
// It parses the YAML content, applies migrations to each document,
// and returns the updated YAML content as a string.
//
// Parameters:
//   - _: logrus entry (unused in current implementation)
//   - content: YAML configuration file content as bytes
//
// Returns the migrated YAML content as string and any error encountered.
func parseConfigAST(_ *logrus.Entry, content []byte) (string, error) {
	file, err := parser.ParseBytes(content, parser.ParseComments)
	if err != nil {
		return "", fmt.Errorf("parse a workflow file as YAML: %w", err)
	}
	for _, doc := range file.Docs {
		if err := parseDocAST(doc); err != nil {
			return "", err
		}
	}
	return file.String(), nil
}

// parseDocAST migrates a single YAML document node.
// It processes the document body to migrate ignore_actions and version fields
// to the latest configuration schema format.
//
// Parameters:
//   - doc: YAML document node to migrate
//
// Returns an error if migration fails.
func parseDocAST(doc *ast.DocumentNode) error {
	body, ok := doc.Body.(*ast.MappingNode)
	if !ok {
		return errors.New("document body must be *ast.MappingNode")
	}
	if err := migrateIgnoreActions(body); err != nil {
		return fmt.Errorf("migrate ignore_actions: %w", err)
	}
	if err := migrateVersion(body); err != nil {
		return fmt.Errorf("migrate version: %w", err)
	}
	return nil
}

// migrateIgnoreActions migrates the ignore_actions section of the configuration.
// It processes each ignore action to ensure they have required ref fields
// with default values if missing.
//
// Parameters:
//   - body: YAML mapping node containing the configuration
//
// Returns an error if migration fails.
func migrateIgnoreActions(body *ast.MappingNode) error {
	// ignore_actions:
	//   - name:
	//     ref:
	ignoreActionsNode := findNodeByKey(body.Values, "ignore_actions")
	if ignoreActionsNode == nil {
		return nil
	}
	switch seq := ignoreActionsNode.Value.(type) {
	case *ast.SequenceNode:
		for _, value := range seq.Values {
			if err := migrateIgnoreAction(value); err != nil {
				return fmt.Errorf("migrate ignore_actions: %w", err)
			}
		}
		return nil
	default:
		return errors.New("ignore_actions must be an array")
	}
}

// migrateIgnoreAction migrates a single ignore action configuration.
// It ensures the ignore action has a ref field, adding a default ".*"
// pattern if the ref field is missing.
//
// Parameters:
//   - body: YAML node representing an ignore action
//
// Returns an error if migration fails.
func migrateIgnoreAction(body ast.Node) error {
	// name:
	// ref:
	m, ok := body.(*ast.MappingNode)
	if !ok {
		return errors.New("ignore_action must be a mapping node")
	}

	if refNode := findNodeByKey(m.Values, "ref"); refNode != nil {
		return nil
	}

	node, err := yaml.ValueToNode(map[string]any{
		"ref": ".*",
	})
	if err != nil {
		return fmt.Errorf("convert ref to node: %w", err)
	}
	m.Merge(node.(*ast.MappingNode)) //nolint:forcetypeassert
	return nil
}

// migrateVersion migrates the version field of the configuration.
// It adds a version field if missing or updates an existing version
// to the current schema version (3).
//
// Parameters:
//   - body: YAML mapping node containing the configuration
//
// Returns an error if migration fails.
func migrateVersion(body *ast.MappingNode) error {
	// version:
	versionNode := findNodeByKey(body.Values, "version")
	if versionNode == nil {
		node, err := yaml.ValueToNode(map[string]any{
			"version": 3, //nolint:mnd
		})
		if err != nil {
			return fmt.Errorf("convert version to node: %w", err)
		}
		body.Merge(node.(*ast.MappingNode)) //nolint:forcetypeassert
		return nil
	}

	switch v := versionNode.Value.(type) {
	case *ast.IntegerNode:
		v.Token.Value = "3"
		v.Value = 3
		return nil
	default:
		return errors.New("version must be a number")
	}
}

func findNodeByKey(values []*ast.MappingValueNode, key string) *ast.MappingValueNode {
	for _, value := range values {
		k, ok := value.Key.(*ast.StringNode)
		if !ok {
			continue
		}
		if k.Value == key {
			return value
		}
	}
	return nil
}
07070100000045000081A4000000000000000000000001691B008600000420000000000000000000000000000000000000003200000000pinact-3.4.4/pkg/controller/migrate/controller.go// Package migrate handles configuration file migration between schema versions.
// This package provides the business logic for upgrading pinact configuration files
// when new schema versions are introduced. It ensures smooth transitions between
// different configuration formats, preserving user settings while adapting to new
// features and structural changes. The migration process maintains backward
// compatibility and helps users keep their configurations up-to-date with the
// latest pinact capabilities.
package migrate

import (
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
)

type Controller struct {
	fs        afero.Fs
	cfg       *config.Config
	param     *Param
	cfgFinder ConfigFinder
}

type ConfigFinder interface {
	Find(configFilePath string) (string, error)
}

type Param struct {
	ConfigFilePath string
}

func New(fs afero.Fs, cfgFinder ConfigFinder, param *Param) *Controller {
	return &Controller{
		param:     param,
		fs:        fs,
		cfg:       &config.Config{},
		cfgFinder: cfgFinder,
	}
}
07070100000046000081A4000000000000000000000001691B008600000F76000000000000000000000000000000000000002F00000000pinact-3.4.4/pkg/controller/migrate/migrate.gopackage migrate

import (
	"fmt"

	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
	"gopkg.in/yaml.v3"
)

// Migrate performs configuration file migration to the latest schema version.
// It finds the configuration file, reads and parses it, determines the required
// migration path, and applies necessary transformations to update the schema.
//
// Parameters:
//   - logE: logrus entry for structured logging
//
// Returns an error if migration fails, nil if successful or no migration needed.
func (c *Controller) Migrate(logE *logrus.Entry) error {
	// find and read .pinact.yaml
	p, err := c.cfgFinder.Find(c.param.ConfigFilePath)
	if err != nil {
		return fmt.Errorf("find a configurationfile: %w", err)
	}
	if p == "" {
		// if .pinact.yaml doesn't exist, return nil
		logE.Warn("no configuration file is found")
		return nil
	}
	c.param.ConfigFilePath = p

	content, err := afero.ReadFile(c.fs, p)
	if err != nil {
		return fmt.Errorf("read a file: %w", err)
	}

	cfg := &config.Config{}
	if err := yaml.Unmarshal(content, cfg); err != nil {
		return fmt.Errorf("parse a config file: %w", err)
	}
	c.cfg = cfg

	s, err := c.migrate(logE, content)
	if err != nil {
		return err
	}
	if s == "" {
		logE.Info("configuration file isn't changed")
		return nil
	}
	if err := c.edit(c.param.ConfigFilePath, s); err != nil {
		return fmt.Errorf("edit the configuration file: %w", err)
	}
	return nil
}

// edit writes the migrated configuration content back to the file.
// It preserves the original file permissions while updating the content
// with the migrated configuration.
//
// Parameters:
//   - file: path to the configuration file to update
//   - content: migrated configuration content
//
// Returns an error if file operations fail.
func (c *Controller) edit(file, content string) error {
	stat, err := c.fs.Stat(file)
	if err != nil {
		return fmt.Errorf("get configuration file stat: %w", err)
	}
	if err := afero.WriteFile(c.fs, file, []byte(content), stat.Mode()); err != nil {
		return fmt.Errorf("write the configuration file: %w", err)
	}
	return nil
}

// migrate determines and applies the appropriate migration strategy.
// It examines the current configuration version and routes to the
// corresponding migration function.
//
// Parameters:
//   - logE: logrus entry for structured logging
//   - content: original configuration file content
//
// Returns the migrated content as string and any error encountered.
func (c *Controller) migrate(logE *logrus.Entry, content []byte) (string, error) {
	switch c.cfg.Version {
	case 2: //nolint:mnd
		return c.migrateV2(logE, content)
	case 3: //nolint:mnd
		return "", nil
	case 0:
		return c.migrateEmptyVersion(logE, content)
	default:
		return "", fmt.Errorf("unsupported version: %d", c.cfg.Version)
	}
}

// migrateEmptyVersion migrates configuration files without version information.
// It handles legacy configuration files that don't have explicit version
// fields by applying AST-based migration.
//
// Parameters:
//   - logE: logrus entry for structured logging
//   - content: original configuration file content
//
// Returns the migrated content as string and any error encountered.
func (c *Controller) migrateEmptyVersion(logE *logrus.Entry, content []byte) (string, error) {
	return parseConfigAST(logE, content)
}

// migrateV2 migrates configuration files from version 2 to version 3.
// It applies necessary transformations to update the schema from version 2
// format to the current version 3 format.
//
// Parameters:
//   - logE: logrus entry for structured logging
//   - content: original configuration file content
//
// Returns the migrated content as string and any error encountered.
func (c *Controller) migrateV2(logE *logrus.Entry, content []byte) (string, error) {
	// Add code comment
	// Change version from 2 to 3
	// Set name_format and ref_format
	return parseConfigAST(logE, content)
}
07070100000047000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002000000000pinact-3.4.4/pkg/controller/run07070100000048000081A4000000000000000000000001691B00860000099C000000000000000000000000000000000000002E00000000pinact-3.4.4/pkg/controller/run/controller.go// Package run implements the core business logic for pinning GitHub Actions.
// This package contains the main controller that orchestrates the entire pinning process,
// including parsing workflow files, resolving action versions through GitHub API,
// converting mutable tags to immutable commit SHAs, and applying updates.
// It handles various operation modes (check, diff, fix, update), manages caching
// for API efficiency, and supports creating pull request reviews. The package
// provides a clean separation between the CLI layer and the actual file processing
// logic, coordinating with GitHub services and filesystem operations.
package run

import (
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
)

type Controller struct {
	repositoriesService RepositoriesService
	pullRequestsService PullRequestsService
	fs                  afero.Fs
	cfg                 *config.Config
	param               *ParamRun
	cfgFinder           ConfigFinder
	cfgReader           ConfigReader
	logger              *Logger
}

type ConfigFinder interface {
	Find(configFilePath string) (string, error)
}

type ConfigReader interface {
	Read(cfg *config.Config, configFilePath string) error
}

// New creates a new Controller for running pinact operations.
// It initializes the controller with all necessary dependencies for processing
// GitHub Actions workflow files, including GitHub API services, filesystem
// interface, configuration management, and operation parameters.
//
// Parameters:
//   - repositoriesService: GitHub API service for repository operations
//   - pullRequestsService: GitHub API service for pull request operations
//   - fs: filesystem interface for file operations
//   - cfgFinder: service for locating configuration files
//   - cfgReader: service for reading and parsing configuration files
//   - param: operation parameters and settings
//
// Returns a pointer to the configured Controller.
func New(repositoriesService RepositoriesService, pullRequestsService PullRequestsService, fs afero.Fs, cfgFinder ConfigFinder, cfgReader ConfigReader, param *ParamRun) *Controller {
	return &Controller{
		repositoriesService: repositoriesService,
		pullRequestsService: pullRequestsService,
		param:               param,
		fs:                  fs,
		cfgFinder:           cfgFinder,
		cfgReader:           cfgReader,
		cfg:                 &config.Config{},
		logger:              NewLogger(param.Stderr),
	}
}
07070100000049000081A4000000000000000000000001691B008600002AED000000000000000000000000000000000000002A00000000pinact-3.4.4/pkg/controller/run/github.gopackage run

import (
	"context"
	"errors"
	"fmt"

	"github.com/hashicorp/go-version"
	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
)

type RepositoriesService interface {
	ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error)
	GetCommitSHA1(ctx context.Context, owner, repo, ref, lastSHA string) (string, *github.Response, error)
	ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error)
}

type PullRequestsService interface {
	CreateComment(ctx context.Context, owner, repo string, number int, comment *github.PullRequestComment) (*github.PullRequestComment, *github.Response, error)
}

// GetCommitSHA1 retrieves the commit SHA for a given reference with caching.
// It first checks the cache and returns cached results if available.
// Otherwise, it calls the underlying service and caches the result.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - owner: repository owner
//   - repo: repository name
//   - ref: reference (tag, branch, or commit)
//   - lastSHA: last known SHA for optimization
//
// Returns the commit SHA, GitHub response, and any error.
func (r *RepositoriesServiceImpl) GetCommitSHA1(ctx context.Context, owner, repo, ref, lastSHA string) (string, *github.Response, error) {
	key := fmt.Sprintf("%s/%s/%s", owner, repo, ref)
	a, ok := r.Commits[key]
	if ok {
		return a.SHA, a.Response, a.err
	}
	sha, resp, err := r.RepositoriesService.GetCommitSHA1(ctx, owner, repo, ref, lastSHA)
	r.Commits[key] = &GetCommitSHA1Result{
		SHA:      sha,
		Response: resp,
		err:      err,
	}
	return sha, resp, err //nolint:wrapcheck
}

type ListTagsResult struct {
	Tags     []*github.RepositoryTag
	Response *github.Response
	err      error
}

type ListReleasesResult struct {
	Releases []*github.RepositoryRelease
	Response *github.Response
	err      error
}

type RepositoriesServiceImpl struct {
	RepositoriesService RepositoriesService
	Tags                map[string]*ListTagsResult
	Commits             map[string]*GetCommitSHA1Result
	Releases            map[string]*ListReleasesResult
}

type GetCommitSHA1Result struct {
	SHA      string
	Response *github.Response
	err      error
}

// ListTags retrieves repository tags with caching.
// It first checks the cache and returns cached results if available.
// Otherwise, it calls the underlying service and caches the result.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - owner: repository owner
//   - repo: repository name
//   - opts: GitHub API options for pagination and filtering
//
// Returns repository tags, GitHub response, and any error.
func (r *RepositoriesServiceImpl) ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) {
	key := fmt.Sprintf("%s/%s/%v", owner, repo, opts.Page)
	a, ok := r.Tags[key]
	if ok {
		return a.Tags, a.Response, a.err
	}
	tags, resp, err := r.RepositoriesService.ListTags(ctx, owner, repo, opts)
	r.Tags[key] = &ListTagsResult{
		Tags:     tags,
		Response: resp,
		err:      err,
	}
	return tags, resp, err //nolint:wrapcheck
}

// ListReleases retrieves repository releases with caching.
// It first checks the cache and returns cached results if available.
// Otherwise, it calls the underlying service and caches the result.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - owner: repository owner
//   - repo: repository name
//   - opts: GitHub API options for pagination and filtering
//
// Returns repository releases, GitHub response, and any error.
func (r *RepositoriesServiceImpl) ListReleases(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) {
	key := fmt.Sprintf("%s/%s/%v", owner, repo, opts.Page)
	a, ok := r.Releases[key]
	if ok {
		return a.Releases, a.Response, a.err
	}
	releases, resp, err := r.RepositoriesService.ListReleases(ctx, owner, repo, opts)
	r.Releases[key] = &ListReleasesResult{
		Releases: releases,
		Response: resp,
		err:      err,
	}
	return releases, resp, err //nolint:wrapcheck
}

// getLatestVersion determines the latest version of a repository.
// It first tries to get the latest version from releases, and if that fails
// or returns empty, it falls back to getting the latest version from tags.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - owner: repository owner
//   - repo: repository name
//   - currentVersion: current version to check if stable (empty string to include all versions)
//
// Returns the latest version string or an error.
func (c *Controller) getLatestVersion(ctx context.Context, logE *logrus.Entry, owner, repo, currentVersion string) (string, error) {
	isStable := isStableVersion(currentVersion)
	lv, err := c.getLatestVersionFromReleases(ctx, logE, owner, repo, isStable)
	if err != nil {
		logerr.WithError(logE, err).Debug("get the latest version from releases")
	}
	if lv != "" {
		return lv, nil
	}
	return c.getLatestVersionFromTags(ctx, logE, owner, repo, isStable)
}

func isStableVersion(v string) bool {
	if v == "" {
		return false
	}
	cv, err := version.NewVersion(v)
	return err == nil && cv.Prerelease() == ""
}

// compare evaluates a tag against the current latest version.
// It attempts to parse the tag as semantic version and compares it.
// If parsing fails, it falls back to string comparison.
//
// Parameters:
//   - latestSemver: current latest semantic version
//   - latestVersion: current latest version string
//   - tag: new tag to compare
//
// Returns the updated latest semantic version, latest version string, and any error.
func compare(latestSemver *version.Version, latestVersion, tag string) (*version.Version, string, error) {
	v, err := version.NewVersion(tag)
	if err != nil {
		if tag > latestVersion {
			latestVersion = tag
		}
		return latestSemver, latestVersion, fmt.Errorf("parse a tag as a semver: %w", err)
	}
	if latestSemver != nil {
		if v.GreaterThan(latestSemver) {
			return v, "", nil
		}
		return latestSemver, "", nil
	}
	return v, "", nil
}

// getLatestVersionFromReleases finds the latest version from repository releases.
// It retrieves releases from GitHub API and compares them to find the highest
// version using semantic versioning when possible, falling back to string comparison.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - owner: repository owner
//   - repo: repository name
//   - currentVersion: current version to check if stable (empty string to include all versions)
//
// Returns the latest version string or an error.
func (c *Controller) getLatestVersionFromReleases(ctx context.Context, logE *logrus.Entry, owner, repo string, isStable bool) (string, error) {
	opts := &github.ListOptions{
		PerPage: 30, //nolint:mnd
	}
	releases, _, err := c.repositoriesService.ListReleases(ctx, owner, repo, opts)
	if err != nil {
		return "", fmt.Errorf("list releases: %w", err)
	}

	var latestSemver *version.Version
	latestVersion := ""
	for _, release := range releases {
		// Skip prereleases if current version is stable (issue #1095)
		if isStable && release.GetPrerelease() {
			continue
		}
		tag := release.GetTagName()
		ls, lv, err := compare(latestSemver, latestVersion, tag)
		latestSemver = ls
		latestVersion = lv
		if err != nil {
			logerr.WithError(logE, err).WithField("tag", tag).Debug("compare tags")
			continue
		}
	}

	if latestSemver != nil {
		return latestSemver.Original(), nil
	}
	return latestVersion, nil
}

// getLatestVersionFromTags finds the latest version from repository tags.
// It retrieves tags from GitHub API and compares them to find the highest
// version using semantic versioning when possible, falling back to string comparison.
// It filters out prerelease versions when currentVersion is stable.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - owner: repository owner
//   - repo: repository name
//   - currentVersion: current version to check if stable (empty string to include all versions)
//
// Returns the latest version string or an error.
func (c *Controller) getLatestVersionFromTags(ctx context.Context, logE *logrus.Entry, owner, repo string, isStable bool) (string, error) {
	opts := &github.ListOptions{
		PerPage: 30, //nolint:mnd
	}
	tags, _, err := c.repositoriesService.ListTags(ctx, owner, repo, opts)
	if err != nil {
		return "", fmt.Errorf("list tags: %w", err)
	}

	var latestSemver *version.Version
	latestVersion := ""
	for _, tag := range tags {
		t := tag.GetName()

		// Skip prereleases if current version is stable (issue #1095)
		if isStable {
			if tv, err := version.NewVersion(t); err == nil && tv.Prerelease() != "" {
				continue
			}
		}

		ls, lv, err := compare(latestSemver, latestVersion, t)
		latestSemver = ls
		latestVersion = lv
		if err != nil {
			logerr.WithError(logE, err).WithField("tag", tag).Debug("compare tags")
			continue
		}
	}
	if latestSemver != nil {
		return latestSemver.Original(), nil
	}
	return latestVersion, nil
}

// review creates a pull request review comment.
// It constructs a comment with either a suggestion or error message and
// posts it to the specified pull request using the GitHub API.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - filePath: path to the file being reviewed
//   - sha: commit SHA for the review
//   - line: line number in the file
//   - suggestion: code suggestion text (mutually exclusive with err)
//   - err: error to report (mutually exclusive with suggestion)
//
// Returns the HTTP status code and any error.
func (c *Controller) review(ctx context.Context, filePath, sha string, line int, suggestion string, err error) (int, error) {
	cmt := &github.PullRequestComment{
		Body: github.Ptr(""),
		Path: github.Ptr(filePath),
		Line: github.Ptr(line),
	}
	if sha != "" {
		cmt.CommitID = github.Ptr(sha)
	}
	const header = "Reviewed by [pinact](https://github.com/suzuki-shunsuke/pinact)"
	switch {
	case suggestion != "":
		cmt.Body = github.Ptr(fmt.Sprintf("%s\n```suggestion\n%s\n```", header, suggestion))
	case err != nil:
		cmt.Body = github.Ptr(fmt.Sprintf("%s\n%s", header, err.Error()))
	default:
		return 0, errors.New("either suggestion or error must be provided")
	}
	_, resp, e := c.pullRequestsService.CreateComment(ctx, c.param.Review.RepoOwner, c.param.Review.RepoName, c.param.Review.PullRequest, cmt)
	code := 0
	if resp != nil {
		code = resp.StatusCode
	}
	if e != nil {
		return code, fmt.Errorf("create a review comment: %w", e)
	}
	return code, nil
}
0707010000004A000081A4000000000000000000000001691B008600003416000000000000000000000000000000000000003800000000pinact-3.4.4/pkg/controller/run/github_internal_test.gopackage run

import (
	"context"
	"errors"
	"testing"

	"github.com/hashicorp/go-version"
	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
)

func Test_compare(t *testing.T) { //nolint:funlen
	t.Parallel()
	tests := []struct {
		name              string
		latestSemver      *version.Version
		latestVersion     string
		tag               string
		wantSemver        string
		wantLatestVersion string
		wantErr           bool
	}{
		{
			name:              "new semver is greater than current semver",
			latestSemver:      version.Must(version.NewVersion("1.0.0")),
			latestVersion:     "",
			tag:               "2.0.0",
			wantSemver:        "2.0.0",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "new semver is less than current semver",
			latestSemver:      version.Must(version.NewVersion("2.0.0")),
			latestVersion:     "",
			tag:               "1.0.0",
			wantSemver:        "2.0.0",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "new semver equals current semver",
			latestSemver:      version.Must(version.NewVersion("1.0.0")),
			latestVersion:     "",
			tag:               "1.0.0",
			wantSemver:        "1.0.0",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "first semver with nil latest",
			latestSemver:      nil,
			latestVersion:     "",
			tag:               "1.2.3",
			wantSemver:        "1.2.3",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "semver with v prefix",
			latestSemver:      nil,
			latestVersion:     "",
			tag:               "v1.2.3",
			wantSemver:        "v1.2.3",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "invalid semver with greater string comparison",
			latestSemver:      nil,
			latestVersion:     "main",
			tag:               "release",
			wantSemver:        "",
			wantLatestVersion: "release",
			wantErr:           true,
		},
		{
			name:              "invalid semver with lesser string comparison",
			latestSemver:      nil,
			latestVersion:     "release",
			tag:               "main",
			wantSemver:        "",
			wantLatestVersion: "release",
			wantErr:           true,
		},
		{
			name:              "invalid semver as first tag",
			latestSemver:      nil,
			latestVersion:     "",
			tag:               "not-a-version",
			wantSemver:        "",
			wantLatestVersion: "not-a-version",
			wantErr:           true,
		},
		{
			name:              "invalid tag with existing semver",
			latestSemver:      version.Must(version.NewVersion("1.0.0")),
			latestVersion:     "",
			tag:               "invalid",
			wantSemver:        "1.0.0",
			wantLatestVersion: "invalid",
			wantErr:           true,
		},
		{
			name:              "compare with prerelease versions",
			latestSemver:      version.Must(version.NewVersion("1.0.0-alpha")),
			latestVersion:     "",
			tag:               "1.0.0",
			wantSemver:        "1.0.0",
			wantLatestVersion: "",
			wantErr:           false,
		},
		{
			name:              "compare with build metadata",
			latestSemver:      version.Must(version.NewVersion("1.0.0+build.1")),
			latestVersion:     "",
			tag:               "1.0.0+build.2",
			wantSemver:        "1.0.0+build.1",
			wantLatestVersion: "",
			wantErr:           false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			gotSemver, gotLatestVersion, err := compare(tt.latestSemver, tt.latestVersion, tt.tag)

			if (err != nil) != tt.wantErr {
				t.Errorf("compare() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			// Check semver result
			if tt.wantSemver == "" {
				if gotSemver != nil {
					t.Errorf("compare() gotSemver = %v, want nil", gotSemver)
				}
			} else {
				if gotSemver == nil {
					t.Errorf("compare() gotSemver = nil, want %v", tt.wantSemver)
				} else if gotSemver.Original() != tt.wantSemver {
					t.Errorf("compare() gotSemver = %v, want %v", gotSemver.Original(), tt.wantSemver)
				}
			}

			// Check latest version string result
			if gotLatestVersion != tt.wantLatestVersion {
				t.Errorf("compare() gotLatestVersion = %v, want %v", gotLatestVersion, tt.wantLatestVersion)
			}
		})
	}
}

// mockRepositoriesService is a mock implementation of RepositoriesService for testing
type mockRepositoriesService struct {
	listReleasesFunc func(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error)
	listTagsFunc     func(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error)
}

func (m *mockRepositoriesService) ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) {
	if m.listTagsFunc != nil {
		return m.listTagsFunc(ctx, owner, repo, opts)
	}
	return nil, nil, errors.New("not implemented")
}

func (m *mockRepositoriesService) GetCommitSHA1(_ context.Context, _, _, _, _ string) (string, *github.Response, error) {
	return "", nil, errors.New("not implemented")
}

func (m *mockRepositoriesService) ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) {
	if m.listReleasesFunc != nil {
		return m.listReleasesFunc(ctx, owner, repo, opts)
	}
	return nil, nil, errors.New("not implemented")
}

func TestController_getLatestVersionFromReleases(t *testing.T) { //nolint:funlen
	t.Parallel()
	tests := []struct {
		name        string
		releases    []*github.RepositoryRelease
		listErr     error
		isStable    bool
		wantVersion string
		wantErr     bool
	}{
		{
			name: "single semver release",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "multiple semver releases - returns highest",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v1.0.0")},
				{TagName: github.Ptr("v2.0.0")},
				{TagName: github.Ptr("v1.5.0")},
			},
			wantVersion: "v2.0.0",
			wantErr:     false,
		},
		{
			name: "mix of valid and invalid semver",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v1.0.0")},
				{TagName: github.Ptr("not-a-version")},
				{TagName: github.Ptr("v2.0.0")},
			},
			wantVersion: "v2.0.0",
			wantErr:     false,
		},
		{
			name: "only invalid versions - returns latest by string comparison",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("main")},
				{TagName: github.Ptr("release")},
				{TagName: github.Ptr("develop")},
			},
			wantVersion: "release",
			wantErr:     false,
		},
		{
			name:        "no releases",
			releases:    []*github.RepositoryRelease{},
			wantVersion: "",
			wantErr:     false,
		},
		{
			name:        "nil releases",
			releases:    nil,
			wantVersion: "",
			wantErr:     false,
		},
		{
			name: "prerelease versions",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v1.0.0-alpha")},
				{TagName: github.Ptr("v1.0.0-beta")},
				{TagName: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "build metadata versions",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v1.0.0+build.1")},
				{TagName: github.Ptr("v1.0.0+build.2")},
				{TagName: github.Ptr("v1.0.1")},
			},
			wantVersion: "v1.0.1",
			wantErr:     false,
		},
		{
			name: "releases with nil tag names",
			releases: []*github.RepositoryRelease{
				{TagName: nil},
				{TagName: github.Ptr("v1.0.0")},
				{TagName: nil},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name:        "API error",
			releases:    nil,
			listErr:     errors.New("API error"),
			wantVersion: "",
			wantErr:     true,
		},
		{
			name: "empty tag name",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("")},
				{TagName: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "stable version ignores prerelease when current is stable (issue #1095)",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v6-beta"), Prerelease: github.Ptr(true)},
				{TagName: github.Ptr("v5.0.0"), Prerelease: github.Ptr(false)},
				{TagName: github.Ptr("v4.3.0"), Prerelease: github.Ptr(false)},
			},
			isStable:    true,
			wantVersion: "v5.0.0",
			wantErr:     false,
		},
		{
			name: "prerelease version can update to newer prerelease (issue #1095)",
			releases: []*github.RepositoryRelease{
				{TagName: github.Ptr("v6-beta"), Prerelease: github.Ptr(true)},
				{TagName: github.Ptr("v5.0.0"), Prerelease: github.Ptr(false)},
			},
			isStable:    false,
			wantVersion: "v6-beta",
			wantErr:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			mockRepo := &mockRepositoriesService{
				listReleasesFunc: func(_ context.Context, _, _ string, _ *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) {
					return tt.releases, nil, tt.listErr
				},
			}

			c := &Controller{
				repositoriesService: mockRepo,
			}

			ctx := t.Context()
			logE := logrus.NewEntry(logrus.New())

			gotVersion, err := c.getLatestVersionFromReleases(ctx, logE, "owner", "repo", tt.isStable)

			if (err != nil) != tt.wantErr {
				t.Errorf("getLatestVersionFromReleases() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if gotVersion != tt.wantVersion {
				t.Errorf("getLatestVersionFromReleases() = %v, want %v", gotVersion, tt.wantVersion)
			}
		})
	}
}

func TestController_getLatestVersionFromTags(t *testing.T) { //nolint:funlen
	t.Parallel()
	tests := []struct {
		name        string
		tags        []*github.RepositoryTag
		listErr     error
		wantVersion string
		wantErr     bool
	}{
		{
			name: "single semver tag",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "multiple semver tags - returns highest",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0")},
				{Name: github.Ptr("v2.0.0")},
				{Name: github.Ptr("v1.5.0")},
			},
			wantVersion: "v2.0.0",
			wantErr:     false,
		},
		{
			name: "mix of valid and invalid semver",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0")},
				{Name: github.Ptr("not-a-version")},
				{Name: github.Ptr("v2.0.0")},
			},
			wantVersion: "v2.0.0",
			wantErr:     false,
		},
		{
			name: "only invalid versions - returns latest by string comparison",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("main")},
				{Name: github.Ptr("release")},
				{Name: github.Ptr("develop")},
			},
			wantVersion: "release",
			wantErr:     false,
		},
		{
			name:        "no tags",
			tags:        []*github.RepositoryTag{},
			wantVersion: "",
			wantErr:     false,
		},
		{
			name:        "nil tags",
			tags:        nil,
			wantVersion: "",
			wantErr:     false,
		},
		{
			name: "prerelease versions",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0-alpha")},
				{Name: github.Ptr("v1.0.0-beta")},
				{Name: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "build metadata versions",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0+build.1")},
				{Name: github.Ptr("v1.0.0+build.2")},
				{Name: github.Ptr("v1.0.1")},
			},
			wantVersion: "v1.0.1",
			wantErr:     false,
		},
		{
			name: "tags with nil names",
			tags: []*github.RepositoryTag{
				{Name: nil},
				{Name: github.Ptr("v1.0.0")},
				{Name: nil},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name:        "API error",
			tags:        nil,
			listErr:     errors.New("API error"),
			wantVersion: "",
			wantErr:     true,
		},
		{
			name: "empty tag name",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("")},
				{Name: github.Ptr("v1.0.0")},
			},
			wantVersion: "v1.0.0",
			wantErr:     false,
		},
		{
			name: "tags without v prefix",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("1.0.0")},
				{Name: github.Ptr("2.0.0")},
				{Name: github.Ptr("1.5.0")},
			},
			wantVersion: "2.0.0",
			wantErr:     false,
		},
		{
			name: "mixed v prefix and no prefix",
			tags: []*github.RepositoryTag{
				{Name: github.Ptr("v1.0.0")},
				{Name: github.Ptr("2.0.0")},
				{Name: github.Ptr("v1.5.0")},
			},
			wantVersion: "2.0.0",
			wantErr:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			mockRepo := &mockRepositoriesService{
				listTagsFunc: func(_ context.Context, _, _ string, _ *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) {
					return tt.tags, nil, tt.listErr
				},
			}

			c := &Controller{
				repositoriesService: mockRepo,
			}

			ctx := t.Context()
			logE := logrus.NewEntry(logrus.New())

			gotVersion, err := c.getLatestVersionFromTags(ctx, logE, "owner", "repo", false)

			if (err != nil) != tt.wantErr {
				t.Errorf("getLatestVersionFromTags() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if gotVersion != tt.wantVersion {
				t.Errorf("getLatestVersionFromTags() = %v, want %v", gotVersion, tt.wantVersion)
			}
		})
	}
}
0707010000004B000081A4000000000000000000000001691B008600000566000000000000000000000000000000000000002800000000pinact-3.4.4/pkg/controller/run/init.gopackage run

import (
	"fmt"
	"os"

	"github.com/spf13/afero"
)

const (
	templateConfig = `# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.json
# pinact - https://github.com/suzuki-shunsuke/pinact
version: 3
# files:
#   - pattern: action.yaml
#   - pattern: */action.yaml

ignore_actions:
# - name: slsa-framework/slsa-github-generator/\.github/workflows/generator_generic_slsa3\.yml
#   ref: v\d+\.\d+\.\d+
# - name: actions/.*
#   ref: main
# - name: suzuki-shunsuke/.*
#   ref: release-.*
`
	filePermission os.FileMode = 0o644
)

// Init creates a new pinact configuration file if it doesn't exist.
// It checks if the configuration file already exists and creates it with
// a template configuration if it doesn't exist.
//
// Parameters:
//   - configFilePath: path where the configuration file should be created
//
// Returns an error if file operations fail, nil if successful or file already exists.
func (c *Controller) Init(configFilePath string) error {
	f, err := afero.Exists(c.fs, configFilePath)
	if err != nil {
		return fmt.Errorf("check if a configuration file exists: %w", err)
	}
	if f {
		return nil
	}
	if err := afero.WriteFile(c.fs, configFilePath, []byte(templateConfig), filePermission); err != nil {
		return fmt.Errorf("create a configuration file: %w", err)
	}
	return nil
}
0707010000004C000081A4000000000000000000000001691B008600000414000000000000000000000000000000000000003200000000pinact-3.4.4/pkg/controller/run/list_workflows.gopackage run

import (
	"fmt"
	"path/filepath"

	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
)

// listWorkflows discovers GitHub Actions workflow and composite action files.
// It searches for YAML files in standard locations including .github/workflows
// and action.yaml files in various directory structures.
//
// Returns a slice of discovered file paths or an error if globbing fails.
func listWorkflows() ([]string, error) {
	patterns := []string{
		".github/workflows/*.yml",
		".github/workflows/*.yaml",
		"action.yml",
		"action.yaml",
		"*/action.yml",
		"*/action.yaml",
		"*/*/action.yml",
		"*/*/action.yaml",
		"*/*/*/action.yml",
		"*/*/*/action.yaml",
	}
	files := []string{}
	for _, pattern := range patterns {
		matches, err := filepath.Glob(pattern)
		if err != nil {
			return nil, fmt.Errorf("look for workflow or composite action files using glob: %w", logerr.WithFields(err, logrus.Fields{
				"pattern": pattern,
			}))
		}
		files = append(files, matches...)
	}
	return files, nil
}
0707010000004D000081A4000000000000000000000001691B0086000005FF000000000000000000000000000000000000002700000000pinact-3.4.4/pkg/controller/run/log.gopackage run

import (
	"fmt"
	"io"

	"github.com/fatih/color"
)

type colorFunc func(a ...interface{}) string

type Logger struct {
	stderr io.Writer
	red    colorFunc
	green  colorFunc
}

// NewLogger creates a new Logger with colored output.
// It initializes color functions for red and green text output
// and configures the stderr writer for log output.
//
// Parameters:
//   - stderr: writer for error output
//
// Returns a pointer to the configured Logger.
func NewLogger(stderr io.Writer) *Logger {
	return &Logger{
		red:    color.New(color.FgRed).SprintFunc(),
		green:  color.New(color.FgGreen).SprintFunc(),
		stderr: stderr,
	}
}

const levelError = "error"

// Output writes formatted log messages with color coding and line information.
// It displays the log level, message, file location, and optionally shows
// before/after line changes with color-coded diff format.
//
// Parameters:
//   - level: log level ("error" for red, others for default)
//   - message: log message to display
//   - line: line information including file path and line number
//   - newLine: new line content for diff display (empty for no diff)
func (l *Logger) Output(level, message string, line *Line, newLine string) {
	s := "INFO"
	if level == levelError {
		s = l.red("ERROR")
	}
	if newLine == "" {
		fmt.Fprintf(l.stderr, `%s %s
%s:%d
%s
`, s, message, line.File, line.Number, line.Line)
		return
	}
	fmt.Fprintf(l.stderr, `%s %s
%s:%d
%s
%s
`, s, message, line.File, line.Number, l.red("- "+line.Line), l.green("+ "+newLine))
}
0707010000004E000081A4000000000000000000000001691B008600003D97000000000000000000000000000000000000002E00000000pinact-3.4.4/pkg/controller/run/parse_line.gopackage run

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"strings"

	"github.com/hashicorp/go-version"
	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
)

var (
	usesPattern          = regexp.MustCompile(`^( *(?:- )?['"]?uses['"]? *: +)(['"]?)(.*?)@([^ '"]+)['"]?(?:( +# +(?:tag=)?)(v?\d+[^ ]*)(.*))?`)
	fullCommitSHAPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`)
	semverPattern        = regexp.MustCompile(`^v?\d+\.\d+\.\d+[^ ]*$`)
	shortTagPattern      = regexp.MustCompile(`^v?\d+(\.\d+)?$`)
)

type Action struct {
	Uses                    string
	Name                    string
	Version                 string
	VersionComment          string
	VersionCommentSeparator string
	RepoOwner               string
	RepoName                string
	Quote                   string
	Suffix                  string
}

type VersionType int

const (
	Semver VersionType = iota
	Shortsemver
	FullCommitSHA
	Empty
	Other
)

// getVersionType determines the type of version string.
// It analyzes the version format to classify it as semantic version,
// short semantic version, full commit SHA, empty, or other.
//
// Parameters:
//   - v: version string to analyze
//
// Returns the VersionType classification.
func getVersionType(v string) VersionType {
	if v == "" {
		return Empty
	}
	if fullCommitSHAPattern.MatchString(v) {
		return FullCommitSHA
	}
	if semverPattern.MatchString(v) {
		return Semver
	}
	if shortTagPattern.MatchString(v) {
		return Shortsemver
	}
	return Other
}

// parseAction extracts action information from a YAML line.
// It uses regular expressions to parse 'uses' statements and extract
// action name, version, comments, and formatting details.
//
// Parameters:
//   - line: YAML line containing a 'uses' statement
//
// Returns an Action struct with parsed information, or nil if no match.
func parseAction(line string) *Action {
	matches := usesPattern.FindStringSubmatch(line)
	if matches == nil {
		return nil
	}
	return &Action{
		Uses:                    matches[1], // " - uses: "
		Quote:                   matches[2], // empty, ', "
		Name:                    matches[3], // local action is excluded by the regular expression because local action doesn't have version @
		Version:                 matches[4], // full commit hash, main, v3, v3.0.0
		VersionCommentSeparator: matches[5], // empty, " # ", " # tag="
		VersionComment:          matches[6], // empty, v1, v3.0.0
		Suffix:                  matches[7],
	}
}

var ErrCantPinned = errors.New("action can't be pinned")

// ignoreAction checks if an action should be ignored based on configuration.
// It evaluates the action against all ignore rules in the configuration.
//
// Parameters:
//   - logE: logrus entry for structured logging
//   - action: action to check against ignore rules
//
// Returns true if the action should be ignored, false otherwise.
func (c *Controller) ignoreAction(logE *logrus.Entry, action *Action) bool {
	for _, ignoreAction := range c.cfg.IgnoreActions {
		f, err := ignoreAction.Match(action.Name, action.Version, c.cfg.Version)
		if err != nil {
			logerr.WithError(logE, err).Warn("match the action")
			continue
		}
		if f {
			return true
		}
	}
	return false
}

// excludeAction checks if an action should be excluded based on exclude patterns.
// It tests the action name against all configured exclude regular expressions.
//
// Parameters:
//   - actionName: name of the action to check
//
// Returns true if the action matches any exclude pattern, false otherwise.
func (c *Controller) excludeAction(actionName string) bool {
	for _, exclude := range c.param.Excludes {
		if exclude.MatchString(actionName) {
			return true
		}
	}
	return false
}

// excludeByIncludes checks if an action should be excluded due to include patterns.
// When include patterns are specified, only actions matching include patterns
// are processed, so this returns true if the action doesn't match any include pattern.
//
// Parameters:
//   - actionName: name of the action to check
//
// Returns true if includes are specified and action doesn't match any, false otherwise.
func (c *Controller) excludeByIncludes(actionName string) bool {
	if len(c.param.Includes) == 0 {
		return false
	}
	for _, include := range c.param.Includes {
		if include.MatchString(actionName) {
			return false
		}
	}
	return true
}

// parseLine processes a single line from a workflow file.
// It parses the line for action usage, applies filtering rules, and determines
// what modifications (if any) should be made based on the operation mode.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - line: workflow file line to process
//
// Returns the modified line content and any error encountered.
func (c *Controller) parseLine(ctx context.Context, logE *logrus.Entry, line string) (s string, e error) { //nolint:cyclop
	defer func() {
		e = logerr.WithFields(e, logE.Data)
	}()
	action := parseAction(line)
	if action == nil {
		// Ignore a line if the line doesn't use an action.
		logE.Debug("unmatch")
		return "", nil
	}

	logE = logE.WithField("action", action.Name+"@"+action.Version)

	if c.ignoreAction(logE, action) {
		logE.Debug("ignore the action")
		return "", nil
	}
	if c.excludeAction(action.Name) {
		logE.Debug("exclude the action")
		return "", nil
	}
	if c.excludeByIncludes(action.Name) {
		logE.Debug("exclude the action")
		return "", nil
	}

	if c.param.Check && !c.param.Diff && !c.param.Fix {
		if fullCommitSHAPattern.MatchString(action.Version) {
			return "", nil
		}
		return "", ErrActionNotPinned
	}

	if f := c.parseActionName(action); !f {
		logE.Debug("ignore line")
		return "", nil
	}

	switch getVersionType(action.VersionComment) {
	case Empty:
		return c.parseNoTagLine(ctx, logE, action)
	case Semver:
		// @xxx # v3.0.0
		return c.parseSemverTagLine(ctx, logE, action)
	case Shortsemver:
		// @xxx # v3
		// @<full commit hash> # v3
		logE = logE.WithField("version_annotation", action.VersionComment)
		return c.parseShortSemverTagLine(ctx, logE, action)
	default:
		if getVersionType(action.Version) == FullCommitSHA {
			return "", nil
		}
		return "", ErrCantPinned
	}
}

// parseNoTagLine processes actions without version comments.
// It handles pinning actions that don't have version annotations,
// either by updating to latest version or converting tags to commit SHAs.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - action: parsed action information
//
// Returns the modified line content and any error encountered.
func (c *Controller) parseNoTagLine(ctx context.Context, logE *logrus.Entry, action *Action) (string, error) { //nolint:cyclop
	typ := getVersionType(action.Version)
	switch typ {
	case Shortsemver, Semver:
	case FullCommitSHA:
		return "", nil
	default:
		return "", ErrCantPinned
	}
	// @xxx
	if c.param.Update {
		// get the latest version
		lv, err := c.getLatestVersion(ctx, logE, action.RepoOwner, action.RepoName, action.Version)
		if err != nil {
			return "", fmt.Errorf("get the latest version: %w", err)
		}
		sha, _, err := c.repositoriesService.GetCommitSHA1(ctx, action.RepoOwner, action.RepoName, lv, "")
		if err != nil {
			return "", fmt.Errorf("get a reference: %w", err)
		}
		return patchLine(action, sha, lv), nil
	}

	// Get commit hash from tag
	// https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#get-a-reference
	// > The :ref in the URL must be formatted as heads/<branch name> for branches and tags/<tag name> for tags. If the :ref doesn't match an existing ref, a 404 is returned.
	sha, _, err := c.repositoriesService.GetCommitSHA1(ctx, action.RepoOwner, action.RepoName, action.Version, "")
	if err != nil {
		return "", fmt.Errorf("get a reference: %w", err)
	}
	longVersion := action.Version
	if typ == Shortsemver {
		v, err := c.getLongVersionFromSHA(ctx, action, sha)
		if err != nil {
			return "", err
		}
		if v != "" {
			longVersion = v
		}
	}
	// @yyy # longVersion
	return patchLine(action, sha, longVersion), nil
}

// compareVersion compares two version strings.
// It attempts semantic version comparison first, falling back to
// string comparison if semantic parsing fails.
//
// Parameters:
//   - currentVersion: current version string
//   - newVersion: new version string to compare
//
// Returns true if newVersion is greater than currentVersion.
func compareVersion(currentVersion, newVersion string) bool {
	cv, err := version.NewVersion(currentVersion)
	if err != nil {
		return newVersion > currentVersion
	}
	nv, err := version.NewVersion(newVersion)
	if err != nil {
		return newVersion > currentVersion
	}
	return nv.GreaterThan(cv)
}

// parseSemverTagLine processes actions with semantic version comments.
// It handles updating semantic versions to latest and verifying that
// commit SHAs match their corresponding version tags.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - action: parsed action information
//
// Returns the modified line content and any error encountered.
func (c *Controller) parseSemverTagLine(ctx context.Context, logE *logrus.Entry, action *Action) (string, error) {
	// @xxx # v3.0.0
	if c.param.Update { //nolint:nestif
		// get the latest version
		lv, err := c.getLatestVersion(ctx, logE, action.RepoOwner, action.RepoName, action.VersionComment)
		if err != nil {
			return "", fmt.Errorf("get the latest version: %w", err)
		}
		if action.VersionComment == lv {
			return "", nil
		}
		if !compareVersion(action.VersionComment, lv) {
			logE.WithFields(logrus.Fields{
				"current_version": action.VersionComment,
				"new_version":     lv,
			}).Warn("skip updating because the current version is newer than the new version")
			return "", nil
		}
		if action.VersionComment != lv {
			sha, _, err := c.repositoriesService.GetCommitSHA1(ctx, action.RepoOwner, action.RepoName, lv, "")
			if err != nil {
				return "", fmt.Errorf("get the latest version: %w", err)
			}
			return patchLine(action, sha, lv), nil
		}
	}
	// verify commit hash
	if !c.param.IsVerify {
		return "", nil
	}
	// @xxx # v3.0.0
	// @<full commit hash> # v3.0.0
	if FullCommitSHA != getVersionType(action.Version) {
		return "", nil
	}
	if err := c.verify(ctx, action); err != nil {
		return "", fmt.Errorf("verify the version annotation: %w", err)
	}
	return "", nil
}

// parseShortSemverTagLine processes actions with short semantic version comments.
// It handles expanding short versions (like v3) to full versions (like v3.1.0)
// and updating to latest versions when requested.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - action: parsed action information
//
// Returns the modified line content and any error encountered.
func (c *Controller) parseShortSemverTagLine(ctx context.Context, logE *logrus.Entry, action *Action) (string, error) {
	// @xxx # v3
	// @<full commit hash> # v3
	if FullCommitSHA != getVersionType(action.Version) {
		return "", ErrCantPinned
	}
	if c.param.Update {
		lv, err := c.getLatestVersion(ctx, logE, action.RepoOwner, action.RepoName, action.VersionComment)
		if err != nil {
			return "", fmt.Errorf("get the latest version: %w", err)
		}
		sha, _, err := c.repositoriesService.GetCommitSHA1(ctx, action.RepoOwner, action.RepoName, lv, "")
		if err != nil {
			return "", fmt.Errorf("get the latest version: %w", err)
		}
		return patchLine(action, sha, lv), nil
	}
	// replace Shortsemer to Semver
	longVersion, err := c.getLongVersionFromSHA(ctx, action, action.Version)
	if err != nil {
		return "", err
	}
	if longVersion == "" {
		logE.Debug("a long tag whose SHA is same as SHA of the version annotation isn't found")
		return "", nil
	}
	return patchLine(action, action.Version, longVersion), nil
}

// patchLine reconstructs a workflow line with updated version and tag.
// It combines the action information with new version and tag to create
// the updated line with proper formatting and comments.
//
// Parameters:
//   - action: parsed action information
//   - version: new version (commit SHA or tag)
//   - tag: new tag for version comment
//
// Returns the reconstructed line string.
func patchLine(action *Action, version, tag string) string {
	sep := action.VersionCommentSeparator
	if sep == "" {
		sep = " # "
	}
	return action.Uses + action.Quote + action.Name + "@" + version + action.Quote + sep + tag + action.Suffix
}

// getLongVersionFromSHA finds the full semantic version tag for a commit SHA.
// It searches through repository tags to find a tag that points to the given
// commit and matches the version comment prefix.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - action: parsed action information
//   - sha: commit SHA to search for
//
// Returns the matching full version tag or empty string if not found.
func (c *Controller) getLongVersionFromSHA(ctx context.Context, action *Action, sha string) (string, error) {
	opts := &github.ListOptions{
		PerPage: 100, //nolint:mnd
	}
	// Get long tag from commit hash
	for range 10 {
		tags, resp, err := c.repositoriesService.ListTags(ctx, action.RepoOwner, action.RepoName, opts)
		if err != nil {
			return "", fmt.Errorf("list tags: %w", err)
		}
		for _, tag := range tags {
			if sha != tag.GetCommit().GetSHA() {
				continue
			}
			tagName := tag.GetName()
			if action.VersionComment == "" {
				if action.Version == tagName {
					continue
				}
			} else {
				if action.VersionComment == tagName {
					continue
				}
			}
			if strings.HasPrefix(tagName, action.VersionComment) {
				return tagName, nil
			}
		}
		if resp.NextPage == 0 {
			return "", nil
		}
		opts.Page = resp.NextPage
	}
	return "", nil
}

// parseActionName extracts repository owner and name from action name.
// It parses the action name to extract the repository owner and name
// components, which are needed for GitHub API calls.
//
// Parameters:
//   - action: action to parse (modifies RepoOwner and RepoName fields)
//
// Returns true if parsing successful, false if action name is invalid.
func (c *Controller) parseActionName(action *Action) bool {
	a := strings.Split(action.Name, "/")
	if len(a) == 1 {
		// If it fails to extract the repository owner and name, ignore the action.
		return false
	}
	action.RepoOwner = a[0]
	action.RepoName = a[1]
	return true
}

// verify checks that an action's version SHA matches its version comment.
// It validates that the commit SHA in the action version matches the
// commit SHA of the version specified in the comment.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - action: parsed action information to verify
//
// Returns an error if verification fails, nil if successful.
func (c *Controller) verify(ctx context.Context, action *Action) error {
	sha, _, err := c.repositoriesService.GetCommitSHA1(ctx, action.RepoOwner, action.RepoName, action.VersionComment, "")
	if err != nil {
		return fmt.Errorf("get a commit hash: %w", err)
	}
	if action.Version == sha {
		return nil
	}
	return logerr.WithFields(errors.New("action_version must be equal to commit_hash_of_version_annotation"), logrus.Fields{ //nolint:wrapcheck
		"action":                            action.Name,
		"action_version":                    action.Version,
		"version_annotation":                action.VersionComment,
		"commit_hash_of_version_annotation": sha,
		"help_docs":                         "https://github.com/suzuki-shunsuke/pinact/blob/main/docs/codes/001.md",
	})
}
0707010000004F000081A4000000000000000000000001691B008600001B98000000000000000000000000000000000000003C00000000pinact-3.4.4/pkg/controller/run/parse_line_internal_test.gopackage run

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/sirupsen/logrus"
	"github.com/spf13/afero"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/github"
)

func strP(s string) *string {
	return &s
}

func Test_parseAction(t *testing.T) { //nolint:funlen
	t.Parallel()
	data := []struct {
		name string
		line string
		exp  *Action
	}{
		{
			name: "unrelated",
			line: "unrelated",
		},
		{
			name: "checkout v3",
			line: "  - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3",
			exp: &Action{
				Uses:                    "  - uses: ",
				Name:                    "actions/checkout",
				Version:                 "8e5e7e5ab8b370d6c329ec480221332ada57f0ab",
				VersionCommentSeparator: " # ",
				VersionComment:          "v3",
			},
		},
		{
			name: "checkout v2",
			line: "  uses: actions/checkout@v2",
			exp: &Action{
				Uses:    "  uses: ",
				Name:    "actions/checkout",
				Version: "v2",
			},
		},
		{
			name: "checkout v3 (single quote)",
			line: `  - "uses": 'actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab' # v3`,
			exp: &Action{
				Uses:                    `  - "uses": `,
				Name:                    "actions/checkout",
				Version:                 "8e5e7e5ab8b370d6c329ec480221332ada57f0ab",
				VersionCommentSeparator: " # ",
				VersionComment:          "v3",
				Quote:                   "'",
			},
		},
		{
			name: "checkout v3 (double quote)",
			line: `  - 'uses': "actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab" # v3`,
			exp: &Action{
				Uses:                    `  - 'uses': `,
				Name:                    "actions/checkout",
				Version:                 "8e5e7e5ab8b370d6c329ec480221332ada57f0ab",
				VersionCommentSeparator: " # ",
				VersionComment:          "v3",
				Quote:                   `"`,
			},
		},
		{
			name: "checkout v2 (single quote)",
			line: `  "uses": 'actions/checkout@v2'`,
			exp: &Action{
				Uses:           `  "uses": `,
				Name:           "actions/checkout",
				Version:        "v2",
				VersionComment: "",
				Quote:          `'`,
			},
		},
		{
			name: "checkout v2 (double quote)",
			line: `  'uses': "actions/checkout@v2"`,
			exp: &Action{
				Uses:           `  'uses': `,
				Name:           "actions/checkout",
				Version:        "v2",
				VersionComment: "",
				Quote:          `"`,
			},
		},
		{
			name: "tag=",
			line: `      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # tag=v3`,
			exp: &Action{
				Uses:                    `      - uses: `,
				Name:                    "actions/checkout",
				Version:                 "83b7061638ee4956cf7545a6f7efe594e5ad0247",
				VersionCommentSeparator: " # tag=",
				VersionComment:          "v3",
				Quote:                   "",
			},
		},
	}
	for _, d := range data {
		t.Run(d.name, func(t *testing.T) {
			t.Parallel()
			act := parseAction(d.line)
			if diff := cmp.Diff(d.exp, act); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func TestController_parseLine(t *testing.T) { //nolint:funlen
	t.Parallel()
	data := []struct {
		name  string
		line  string
		exp   string
		isErr bool
	}{
		{
			name: "unrelated",
			line: "unrelated",
		},
		{
			name: "checkout v3",
			line: "  - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3",
			exp:  "  - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2",
		},
		{
			name: "checkout v2",
			line: "  uses: actions/checkout@v2",
			exp:  "  uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0",
		},
		{
			name: "single quote",
			line: `  - "uses": 'actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab' # v3`,
			exp:  `  - "uses": 'actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab' # v3.5.2`,
		},
		{
			name: "double quote",
			line: `  - 'uses': "actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab" # v3`,
			exp:  `  - 'uses': "actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab" # v3.5.2`,
		},
		{
			name: "checkout v2 (single quote)",
			line: `  "uses": 'actions/checkout@v2'`,
			exp:  `  "uses": 'actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5' # v2.7.0`,
		},
	}
	logE := logrus.NewEntry(logrus.New())
	for _, d := range data {
		t.Run(d.name, func(t *testing.T) {
			t.Parallel()
			fs := afero.NewMemMapFs()
			ctrl := New(&RepositoriesServiceImpl{
				Tags: map[string]*ListTagsResult{
					"actions/checkout/0": {
						Tags: []*github.RepositoryTag{
							{
								Name: strP("v3"),
								Commit: &github.Commit{
									SHA: strP("8e5e7e5ab8b370d6c329ec480221332ada57f0ab"),
								},
							},
							{
								Name: strP("v3.5.2"),
								Commit: &github.Commit{
									SHA: strP("8e5e7e5ab8b370d6c329ec480221332ada57f0ab"),
								},
							},
							{
								Name: strP("v2"),
								Commit: &github.Commit{
									SHA: strP("ee0669bd1cc54295c223e0bb666b733df41de1c5"),
								},
							},
							{
								Name: strP("v2.7.0"),
								Commit: &github.Commit{
									SHA: strP("ee0669bd1cc54295c223e0bb666b733df41de1c5"),
								},
							},
						},
						Response: &github.Response{},
					},
				},
				Releases: map[string]*ListReleasesResult{
					"actions/checkout/0": {
						Releases: []*github.RepositoryRelease{}, // Empty releases forces fallback to tags
						Response: &github.Response{},
					},
				},
				Commits: map[string]*GetCommitSHA1Result{
					"actions/checkout/v3": {
						SHA: "8e5e7e5ab8b370d6c329ec480221332ada57f0ab",
					},
					"actions/checkout/v2": {
						SHA: "ee0669bd1cc54295c223e0bb666b733df41de1c5",
					},
				},
			}, nil, fs, config.NewFinder(fs), config.NewReader(fs), &ParamRun{})
			line, err := ctrl.parseLine(t.Context(), logE, d.line)
			if err != nil {
				if d.isErr {
					return
				}
				t.Fatal(err)
			}
			if line != d.exp {
				t.Fatalf(`wanted %s, got %s`, d.exp, line)
			}
		})
	}
}

func Test_patchLine(t *testing.T) {
	t.Parallel()
	data := []struct {
		name    string
		tag     string
		version string
		action  *Action
		exp     string
	}{
		{
			name: "checkout v3",
			exp:  "  - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v3.5.2",
			action: &Action{
				Uses:                    "  - uses: ",
				Name:                    "actions/checkout",
				Version:                 "8e5e7e5ab8b370d6c329ec480221332ada57f0ab",
				VersionCommentSeparator: " # ",
				VersionComment:          "v3",
			},
			version: "ee0669bd1cc54295c223e0bb666b733df41de1c5",
			tag:     "v3.5.2",
		},
		{
			name: "checkout v2",
			exp:  "  uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.17.0",
			action: &Action{
				Uses:    "  uses: ",
				Name:    "actions/checkout",
				Version: "v2",
			},
			version: "ee0669bd1cc54295c223e0bb666b733df41de1c5",
			tag:     "v2.17.0",
		},
	}
	for _, d := range data {
		t.Run(d.name, func(t *testing.T) {
			t.Parallel()
			line := patchLine(d.action, d.version, d.tag)
			if line != d.exp {
				t.Fatalf(`wanted %s, got %s`, d.exp, line)
			}
		})
	}
}
07070100000050000081A4000000000000000000000001691B0086000021D4000000000000000000000000000000000000002700000000pinact-3.4.4/pkg/controller/run/run.gopackage run

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strings"

	"github.com/sirupsen/logrus"
	"github.com/suzuki-shunsuke/logrus-error/logerr"
	"github.com/suzuki-shunsuke/pinact/v3/pkg/config"
)

type ParamRun struct {
	WorkflowFilePaths []string
	ConfigFilePath    string
	PWD               string
	IsVerify          bool
	Update            bool
	Check             bool
	IsGitHubActions   bool
	Fix               bool
	Diff              bool
	Stderr            io.Writer
	Review            *Review
	Includes          []*regexp.Regexp
	Excludes          []*regexp.Regexp
}

type Review struct {
	RepoOwner   string
	RepoName    string
	PullRequest int
	SHA         string
}

// Fields returns structured log fields for the review configuration.
// It provides consistent field names for logging review information.
//
// Returns logrus.Fields with review configuration data.
func (r *Review) Fields() logrus.Fields {
	return logrus.Fields{
		"review_repo_owner": r.RepoOwner,
		"review_repo_name":  r.RepoName,
		"review_pr_number":  r.PullRequest,
		"review_sha":        r.SHA,
	}
}

// Valid checks if the review configuration has all required fields.
// It validates that repo owner, repo name, and pull request number are set.
//
// Returns true if the review configuration is valid for creating reviews.
func (r *Review) Valid() bool {
	return r != nil && r.RepoOwner != "" && r.RepoName != "" && r.PullRequest > 0
}

// Run executes the main pinact operation.
// It reads configuration, searches for workflow files, and processes each file
// to pin GitHub Actions versions according to the specified parameters.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//
// Returns an error if the operation fails or actions are not pinned in check mode.
func (c *Controller) Run(ctx context.Context, logE *logrus.Entry) error {
	if err := c.readConfig(); err != nil {
		return err
	}
	workflowFilePaths, err := c.searchFiles()
	if err != nil {
		return fmt.Errorf("search target files: %w", err)
	}

	failed := false
	for _, workflowFilePath := range workflowFilePaths {
		logE := logE.WithField("workflow_file", workflowFilePath)
		if err := c.runWorkflow(ctx, logE, workflowFilePath); err != nil {
			failed = true
			if errors.Is(err, ErrActionsNotPinned) {
				continue
			}
			if c.param.Check {
				logerr.WithError(logE, err).Error("check a workflow")
				continue
			}
			logerr.WithError(logE, err).Error("update a workflow")
		}
	}
	if failed {
		return ErrActionsNotPinned
	}
	return nil
}

// readConfig loads and processes the pinact configuration file.
// It finds the configuration file path and reads the configuration,
// updating the controller's configuration state.
//
// Returns an error if configuration cannot be found or read.
func (c *Controller) readConfig() error {
	p, err := c.cfgFinder.Find(c.param.ConfigFilePath)
	if err != nil {
		return fmt.Errorf("find a configurationfile: %w", err)
	}
	c.param.ConfigFilePath = p
	cfg := &config.Config{}
	if err := c.cfgReader.Read(cfg, c.param.ConfigFilePath); err != nil {
		return fmt.Errorf("read a config file: %w", err)
	}
	c.cfg = cfg
	return nil
}

var (
	ErrActionsNotPinned = errors.New("action aren't pinned")
	ErrActionNotPinned  = errors.New("action isn't pinned")
)

type Line struct {
	File   string
	Number int
	Line   string
}

// runWorkflow processes a single workflow file.
// It reads the file line by line, parses each line for actions,
// applies transformations, and optionally writes changes back to the file.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - workflowFilePath: path to the workflow file to process
//
// Returns an error if processing fails or actions are not pinned in check mode.
func (c *Controller) runWorkflow(ctx context.Context, logE *logrus.Entry, workflowFilePath string) error { //nolint:cyclop
	lines, err := c.readWorkflow(workflowFilePath)
	if err != nil {
		return err
	}
	changed := false
	failed := false
	for i, lineS := range lines {
		line := &Line{
			File:   workflowFilePath,
			Number: i + 1,
			Line:   lineS,
		}
		logE := logE.WithFields(logrus.Fields{
			"line_number": i + 1,
			"line":        lineS,
		})
		l, err := c.parseLine(ctx, logE, lineS)
		if err != nil {
			failed = true
			c.handleParseLineError(ctx, logE, line, err)
			continue
		}
		if l == "" || lineS == l {
			continue
		}
		logE = logE.WithField("new_line", l)
		changed = true
		if c.param.Check {
			failed = true
		}
		lines[i] = l
		c.handleChangedLine(ctx, logE, line, l)
	}
	// Fix file
	if changed && c.param.Fix {
		f, err := os.Create(workflowFilePath)
		if err != nil {
			return fmt.Errorf("create a workflow file: %w", err)
		}
		defer f.Close()
		if _, err := f.WriteString(strings.Join(lines, "\n") + "\n"); err != nil {
			return fmt.Errorf("write a workflow file: %w", err)
		}
	}
	// return error
	if failed {
		return ErrActionsNotPinned
	}
	return nil
}

// handleParseLineError handles errors that occur during line parsing.
// It outputs error messages, creates GitHub Actions annotations, and
// optionally creates pull request review comments.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - line: line information where the error occurred
//   - gErr: error that occurred during parsing
func (c *Controller) handleParseLineError(ctx context.Context, logE *logrus.Entry, line *Line, gErr error) {
	// Output error
	c.logger.Output(levelError, "failed to handle a line: "+gErr.Error(), line, "")
	if c.param.Review == nil {
		// Output GitHub Actions error
		if c.param.IsGitHubActions {
			fmt.Fprintf(c.param.Stderr, "::error file=%s,line=%d,title=pinact error::%s\n", line.File, line.Number, gErr)
		}
		return
	}
	// Create review
	if code, err := c.review(ctx, line.File, c.param.Review.SHA, line.Number, "", gErr); err != nil {
		level := logrus.ErrorLevel
		if code == http.StatusUnprocessableEntity {
			level = logrus.WarnLevel
		}
		logerr.WithError(logE, err).WithFields(c.param.Review.Fields()).Log(level, "create a review comment")
		// Output GitHub Actions error
		if c.param.IsGitHubActions {
			fmt.Fprintf(c.param.Stderr, "::error file=%s,line=%d,title=pinact error::%s\n", line.File, line.Number, gErr)
		}
	}
}

// handleChangedLine handles lines that have been modified.
// It creates review comments, GitHub Actions annotations, and outputs
// diff information depending on the operation mode.
//
// Parameters:
//   - ctx: context for cancellation and timeout control
//   - logE: logrus entry for structured logging
//   - line: original line information
//   - newLine: modified line content
func (c *Controller) handleChangedLine(ctx context.Context, logE *logrus.Entry, line *Line, newLine string) { //nolint:cyclop
	reviewed := false
	if c.param.Review != nil {
		// Create review
		if code, err := c.review(ctx, line.File, c.param.Review.SHA, line.Number, newLine, nil); err != nil {
			level := logrus.ErrorLevel
			if code == http.StatusUnprocessableEntity {
				level = logrus.WarnLevel
			}
			logerr.WithError(logE, err).WithFields(c.param.Review.Fields()).Log(level, "create a review comment")
		} else {
			reviewed = true
		}
	}
	// Output GitHub Actions error
	if c.param.IsGitHubActions && !reviewed {
		level := "notice"
		if c.param.Check {
			level = levelError
		}
		fmt.Fprintf(c.param.Stderr, "::%s file=%s,line=%d,title=pinact error::action isn't pinned\n", level, line.File, line.Number)
	}
	// Output diff
	if !c.param.Check && c.param.Fix && !c.param.Diff {
		return
	}
	level := "info"
	if c.param.Check {
		level = levelError
	}
	c.logger.Output(level, "action isn't pinned", line, newLine)
}

// readWorkflow reads a workflow file and returns its lines.
// It opens the file and scans it line by line, returning all lines
// as a slice of strings.
//
// Parameters:
//   - workflowFilePath: path to the workflow file to read
//
// Returns a slice of lines from the file and any error encountered.
func (c *Controller) readWorkflow(workflowFilePath string) ([]string, error) {
	workflowReadFile, err := os.Open(workflowFilePath)
	if err != nil {
		return nil, fmt.Errorf("open a workflow file: %w", err)
	}
	defer workflowReadFile.Close()
	scanner := bufio.NewScanner(workflowReadFile)
	lines := []string{}
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scan a workflow file: %w", err)
	}
	return lines, nil
}
07070100000051000081A4000000000000000000000001691B0086000004EF000000000000000000000000000000000000002F00000000pinact-3.4.4/pkg/controller/run/search_file.gopackage run

import (
	"fmt"
	"path/filepath"
)

// searchFiles determines which files to process based on configuration.
// It returns workflow file paths from command line arguments if provided,
// otherwise uses configured file patterns, or falls back to default discovery.
//
// Returns a slice of file paths to process and any error encountered.
func (c *Controller) searchFiles() ([]string, error) {
	if len(c.param.WorkflowFilePaths) != 0 {
		return c.param.WorkflowFilePaths, nil
	}
	if c.cfg != nil && len(c.cfg.Files) > 0 {
		return c.searchFilesByGlob()
	}
	return listWorkflows()
}

// searchFilesByGlob finds files using glob patterns from configuration.
// It applies each configured file pattern as a glob relative to the
// configuration file directory and collects all matching files.
//
// Returns a slice of matching file paths and any error encountered.
func (c *Controller) searchFilesByGlob() ([]string, error) {
	files := []string{}
	configFileDir := filepath.Dir(c.param.ConfigFilePath)
	for _, file := range c.cfg.Files {
		matches, err := filepath.Glob(filepath.Join(configFileDir, file.Pattern))
		if err != nil {
			return nil, fmt.Errorf("search target files: %w", err)
		}
		files = append(files, matches...)
	}
	return files, nil
}
07070100000052000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001800000000pinact-3.4.4/pkg/github07070100000053000081A4000000000000000000000001691B008600000D7F000000000000000000000000000000000000002200000000pinact-3.4.4/pkg/github/github.go// Package github provides GitHub API client integration and authentication.
// This package abstracts GitHub API operations, handling client creation with
// proper authentication through environment variables or OS keyring storage.
// It manages OAuth2 token-based authentication, provides type aliases for
// commonly used GitHub API types, and configures HTTP clients for API calls.
// The package supports both authenticated and unauthenticated API access,
// with automatic fallback mechanisms for different authentication sources.
package github

import (
	"context"
	"net/http"
	"os"

	"github.com/google/go-github/v78/github"
	"github.com/sirupsen/logrus"
	"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
	PullRequestComment = github.PullRequestComment
)

// New creates a new GitHub API client with authentication.
// It configures the client with appropriate HTTP client based on available
// authentication methods (environment token or keyring).
//
// Parameters:
//   - ctx: context for OAuth2 token source
//   - logE: logrus entry for structured logging
//
// Returns a configured GitHub API client.
func New(ctx context.Context, logE *logrus.Entry) *Client {
	return github.NewClient(getHTTPClientForGitHub(ctx, logE, getGitHubToken()))
}

// Ptr returns a pointer to the provided value.
// This is a convenience function that delegates to github.Ptr for
// creating pointers to values, commonly needed for GitHub API structs.
//
// Parameters:
//   - v: value to get a pointer to
//
// Returns a pointer to the value.
func Ptr[T any](v T) *T {
	return github.Ptr(v)
}

// getGitHubToken retrieves the GitHub token from environment variables.
// It reads the GITHUB_TOKEN environment variable for authentication.
//
// Returns the GitHub token string or empty string if not set.
func getGitHubToken() string {
	return os.Getenv("GITHUB_TOKEN")
}

// checkKeyringEnabled checks if keyring authentication is enabled.
// It examines the PINACT_KEYRING_ENABLED environment variable to determine
// if OS keyring should be used for token storage and retrieval.
//
// Returns true if keyring is enabled, false otherwise.
func checkKeyringEnabled() bool {
	return os.Getenv("PINACT_KEYRING_ENABLED") == "true"
}

// getHTTPClientForGitHub creates an HTTP client configured for GitHub API access.
// It handles authentication using environment token, keyring, or falls back
// to unauthenticated access. The client is configured with OAuth2 for authenticated requests.
//
// Parameters:
//   - ctx: context for OAuth2 token source
//   - logE: logrus entry for structured logging
//   - token: GitHub token for authentication (empty string for alternative auth)
//
// Returns an HTTP client configured for GitHub API access.
func getHTTPClientForGitHub(ctx context.Context, logE *logrus.Entry, token string) *http.Client {
	if token == "" {
		if checkKeyringEnabled() {
			return oauth2.NewClient(ctx, ghtoken.NewTokenSource(logE, KeyService))
		}
		return http.DefaultClient
	}
	return oauth2.NewClient(ctx, oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	))
}
07070100000054000081A4000000000000000000000001691B008600000041000000000000000000000000000000000000002300000000pinact-3.4.4/pkg/github/keyring.gopackage github

const (
	KeyService = "suzuki-shunsuke/pinact"
)
07070100000055000081A4000000000000000000000001691B008600000169000000000000000000000000000000000000001C00000000pinact-3.4.4/renovate.json5{
  extends: [
    "github>suzuki-shunsuke/renovate-config#3.3.1",
    "github>suzuki-shunsuke/renovate-config:nolimit#3.3.1",
    "github>suzuki-shunsuke/renovate-config:go-directive#3.3.1",
    "github>aquaproj/aqua-renovate-config#2.9.0",
    "github>aquaproj/aqua-renovate-config:file#2.9.0(aqua/imports/.*\\.ya?ml)",
  ],
  ignorePaths: ["testdata/**"],
}
07070100000056000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001500000000pinact-3.4.4/scripts07070100000057000081A4000000000000000000000001691B0086000001E2000000000000000000000000000000000000002100000000pinact-3.4.4/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"
07070100000058000081A4000000000000000000000001691B0086000000CA000000000000000000000000000000000000002700000000pinact-3.4.4/scripts/generate-usage.sh#!/usr/bin/env bash

set -eu

cd "$(dirname "$0")/.."

help=$(pinact help-all)

echo "# Usage

<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->

$help" > USAGE.md
07070100000059000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001600000000pinact-3.4.4/testdata0707010000005A000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001E00000000pinact-3.4.4/testdata/actions0707010000005B000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002400000000pinact-3.4.4/testdata/actions.after0707010000005C000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000003000000000pinact-3.4.4/testdata/actions.after/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
0707010000005D000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000002F00000000pinact-3.4.4/testdata/actions.after/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
0707010000005E000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002800000000pinact-3.4.4/testdata/actions.after/foo0707010000005F000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000003400000000pinact-3.4.4/testdata/actions.after/foo/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
07070100000060000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000003300000000pinact-3.4.4/testdata/actions.after/foo/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
07070100000061000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002C00000000pinact-3.4.4/testdata/actions.after/foo/bar07070100000062000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000003800000000pinact-3.4.4/testdata/actions.after/foo/bar/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
07070100000063000081A4000000000000000000000001691B00860000008E000000000000000000000000000000000000003700000000pinact-3.4.4/testdata/actions.after/foo/bar/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
07070100000064000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000002A00000000pinact-3.4.4/testdata/actions/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
07070100000065000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000002900000000pinact-3.4.4/testdata/actions/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
07070100000066000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002200000000pinact-3.4.4/testdata/actions/foo07070100000067000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000002E00000000pinact-3.4.4/testdata/actions/foo/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
07070100000068000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000002D00000000pinact-3.4.4/testdata/actions/foo/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
07070100000069000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000002600000000pinact-3.4.4/testdata/actions/foo/bar0707010000006A000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000003200000000pinact-3.4.4/testdata/actions/foo/bar/action.yamlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
0707010000006B000081A4000000000000000000000001691B00860000008A000000000000000000000000000000000000003100000000pinact-3.4.4/testdata/actions/foo/bar/action.ymlname: Test
description: Test
runs:
  using: composite
  steps:
    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
0707010000006C000081A4000000000000000000000001691B0086000002A8000000000000000000000000000000000000001F00000000pinact-3.4.4/testdata/bar.yaml---
name: bar
on: workflow_call
jobs:
  integration-test:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      # The version annotation is "v3.5.1", so you would think the version of the action is v3.5.1.
      # But the actual version is v2.7.0 because "ee0669bd1cc54295c223e0bb666b733df41de1c5" is the commit hash of v2.7.0.
      # https://github.com/actions/checkout/releases/tag/v3.5.1
      # https://github.com/actions/checkout/releases/tag/v2.7.0
      # This means version annotations aren't necessarily correct.
      # pinact run's --verify option verifies version annoations.
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v3.5.1
0707010000006D000081A4000000000000000000000001691B0086000004B4000000000000000000000000000000000000001F00000000pinact-3.4.4/testdata/foo.yaml---
name: foo
on: workflow_call
jobs:
  integration-test:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # tag=v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  tag=v3
      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247
      - uses: actions/checkout@v2
      - uses: actions/cache@v3.3.1
      - uses: actions/setup-java@v3
      - uses: rharkor/caching-for-turbo@v1.6
      - uses: peaceiris/actions-gh-pages@v4.0.0
      - 'uses': "actions/checkout@v3"
      - "uses": 'actions/checkout@v3'
      - uses: cardinalby/git-get-release-action@cf4593dd18e51a1ecfbfb1c68abac9910a8b1e0c # v1
  actionlint:
    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@v0.5.0
    with:
      aqua_version: v2.3.4
    permissions:
      pull-requests: write
      contents: read
  slsa-github-generator:
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
0707010000006E000081A4000000000000000000000001691B0086000005D2000000000000000000000000000000000000002500000000pinact-3.4.4/testdata/foo_after.yaml---
name: foo
on: workflow_call
jobs:
  integration-test:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # tag=v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  tag=v3.5.3
      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
      - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
      - uses: actions/setup-java@v3
      - uses: rharkor/caching-for-turbo@c3de885e542eec7eb01eb1a6a59e97c7a2448615 # v1.6
      - uses: peaceiris/actions-gh-pages@v4.0.0
      - 'uses': "actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744" # v3.6.0
      - "uses": 'actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744' # v3.6.0
      - uses: cardinalby/git-get-release-action@cf4593dd18e51a1ecfbfb1c68abac9910a8b1e0c # v1
  actionlint:
    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@b6a5f966d4504893b2aeb60cf2b0de8946e48504 # v0.5.0
    with:
      aqua_version: v2.3.4
    permissions:
      pull-requests: write
      contents: read
  slsa-github-generator:
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
0707010000006F000081A4000000000000000000000001691B008600000535000000000000000000000000000000000000002D00000000pinact-3.4.4/testdata/foo_exclude_after.yaml---
name: foo
on: workflow_call
jobs:
  integration-test:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # tag=v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  v3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  tag=v3
      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247
      - uses: actions/checkout@v2
      - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
      - uses: actions/setup-java@v3
      - uses: rharkor/caching-for-turbo@c3de885e542eec7eb01eb1a6a59e97c7a2448615 # v1.6
      - uses: peaceiris/actions-gh-pages@v4.0.0
      - 'uses': "actions/checkout@v3"
      - "uses": 'actions/checkout@v3'
      - uses: cardinalby/git-get-release-action@cf4593dd18e51a1ecfbfb1c68abac9910a8b1e0c # v1
  actionlint:
    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@b6a5f966d4504893b2aeb60cf2b0de8946e48504 # v0.5.0
    with:
      aqua_version: v2.3.4
    permissions:
      pull-requests: write
      contents: read
  slsa-github-generator:
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
07070100000070000081A4000000000000000000000001691B008600000551000000000000000000000000000000000000002D00000000pinact-3.4.4/testdata/foo_include_after.yaml---
name: foo
on: workflow_call
jobs:
  integration-test:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # tag=v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  v3.5.3
      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9  #  tag=v3.5.3
      - uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
      - uses: actions/cache@v3.3.1
      - uses: actions/setup-java@v3
      - uses: rharkor/caching-for-turbo@v1.6
      - uses: peaceiris/actions-gh-pages@v4.0.0
      - 'uses': "actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744" # v3.6.0
      - "uses": 'actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744' # v3.6.0
      - uses: cardinalby/git-get-release-action@cf4593dd18e51a1ecfbfb1c68abac9910a8b1e0c # v1
  actionlint:
    uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@v0.5.0
    with:
      aqua_version: v2.3.4
    permissions:
      pull-requests: write
      contents: read
  slsa-github-generator:
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
07070100000071000041ED000000000000000000000002691B008600000000000000000000000000000000000000000000001E00000000pinact-3.4.4/testdata/migrate07070100000072000081A4000000000000000000000001691B008600000116000000000000000000000000000000000000002B00000000pinact-3.4.4/testdata/migrate/.pinact.yaml# yaml-language-server: $schema=../../json-schema/pinact.json
# pinact - https://github.com/suzuki-shunsuke/pinact
ignore_actions:
  - name: actions/setup-java
  - name: slsa-framework/slsa-github-generator/\.github/workflows/generator_generic_slsa3\.yml
  - name: peaceiris/.*
07070100000073000081A4000000000000000000000001691B008600000145000000000000000000000000000000000000003000000000pinact-3.4.4/testdata/migrate/pinact.yaml.after# yaml-language-server: $schema=../../json-schema/pinact.json
# pinact - https://github.com/suzuki-shunsuke/pinact
ignore_actions:
  - name: actions/setup-java
    ref: .*
  - name: slsa-framework/slsa-github-generator/\.github/workflows/generator_generic_slsa3\.yml
    ref: .*
  - name: peaceiris/.*
    ref: .*
version: 3
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!403 blocks
openSUSE Build Service is sponsored by