pinact: pin GitHub Actions to commit SHAs, update them, and verify version comments
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 and verify version annotations.
At a glance
- What is it?
- pinact is a Go CLI from suzuki-shunsuke that rewrites uses: lines in workflow and composite action files so Actions and reusable workflows run from a full-length commit SHA, and it can update those pins and check the version comment next to them.
- Who is it for?
- pinact fits repositories that already run third-party Actions and want every uses: line resolved to a 40-character SHA, plus a CI gate that fails when someone adds a tag reference. It is the wrong tool if your workflows run entirely on your own GitHub Enterprise Server without the API access pinact needs, or if you want a scanner that only reports findings: pinact edits files by default, and only -check or -fix=false turns it into a read-only check.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: a tag in uses: is a moving reference
A workflow line such as uses: actions/setup-go@v4 points at a tag. Whoever controls that repository can move the tag, and the next run of your workflow executes whatever it now points to. pinact's answer is to rewrite the line to the full commit SHA plus a version comment, so the run is tied to one object that cannot be retargeted. The README shows the before and after on a test workflow: actions/setup-go@v4 becomes actions/setup-go@7b8cf10d4e4a01d4992d18a89f4d7dc5a3e6d6f4 # v4.3.0, and actions/[email protected] becomes actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1. The audience is people who maintain workflows and composite actions in a repository, not people who only consume someone else's published action. pinact also handles reusable workflows, which the README lists alongside Actions as the two things it pins.
What pinact run touches when you give it no arguments
Run without a file argument, pinact walks a fixed set of paths: .github/workflows/*.yml and *.yaml, action.yml and action.yaml at the repository root, then */action.yml, */action.yaml, */*/action.yml, */*/action.yaml and the same pattern one directory deeper. That list is the whole default scope, and it is worth reading before the first run because it means nested action directories beyond three levels are not picked up unless you pass the path yourself. You can pass text files of any format, and the README gives pinact run README.md as the example, so documentation snippets holding uses: lines can be kept in sync with the workflows. The command edits files by default. Two flags change that: -check, or equivalently -fix=false, makes pinact report without writing. There is also -no-api, which does no GitHub API call and only checks that the reference is a 40-character SHA. In that mode pinact cannot fetch versions or SHAs, so it cannot pin anything; it is a syntax gate, nothing more.
Installing pinact and pinning a workflow for the first time
The README links to INSTALL.md for installation rather than repeating the steps, and the repository ships an aqua/ directory and an aqua-installer reference in its include and exclude examples, so aqua is one of the supported routes. After installing, run the command from the root of the repository whose workflows you want to rewrite. The README gives this exact invocation:
pinact runpinact calls the GitHub API to fetch releases and tags, and the README says that to avoid rate limiting you should pass a GitHub access token. The repository can read that token through keyrings or through ghtkn, and the go.mod file lists ghtkn-go-sdk and zalando/go-keyring as dependencies, which matches the README's claim. Expect a diff per changed line, printed with the file and line number as shown in the README's example output. If you want to see the scope before anything is written, run the same command in check mode:
pinact run -checkFor a first pass on a large repository, narrowing to one owner keeps the diff readable. The README's include example is:
pinact run -i "actions/.*" -i "^aquaproj/aqua-installer$"-i and -e take regular expressions and can be repeated. The README notes the value is matched as a partial match, the same way --branch-to-tag works, so anchor with ^ and $ when you need an exact name.
Updating pins and the minimum release age cooldown
Pinning solves drift; it creates a different problem, which is that a SHA never moves on its own. pinact run -update moves actions to their latest versions. The interesting part is the cooldown. With -min-age 7, pinact excludes releases younger than seven days when updating, and the README states that if no release meeting the given minimum age is found, pinact exits with an error. That failure mode is deliberate but it will break a scheduled update job on a project that only publishes fresh releases. The default is no minimum release age at all. The setting can come from the flag, from the PINACT_MIN_AGE environment variable, or from .pinact.yml, where min_age.value sets the default and rules[].min_age sets a per-rule value. The README's example rule sets min_age: 0 for actions owned by suzuki-shunsuke while the default stays at 7. For releases, pinact checks the PublishedAt date; for tags it checks the commit's Committer.Date, which the README notes requires an additional API call. Verification of current versions is opt-in: it happens only with -verify-min-age or when min_age.always is true, because checking every current version on every run is wasteful.
Version comments, branch pins, and SARIF output
A SHA on its own is unreadable, so pinact writes a comment such as # v4.3.0 next to it and can check that the comment still matches the pin. pinact run -verify-comment does that, and -verify and -v are shorter spellings. The README points to docs/codes/001.md for the details and to docs/codes/005.md for requiring a version comment on SHA-pinned actions, so there are at least two distinct diagnostic codes for comment problems. Branches are a separate case. By default pinact does not pin main or master. From v3.10.0, --branch-to-tag takes a regular expression and converts a matching branch to the latest stable tag of the action, falling back to a pre-release only when no stable tag exists; the README recommends anchoring short names like ^main$ so mainline does not match, and versions that match no supplied regexp still error out. --min-age is honored here too, skipping tags inside the cooldown window. For CI, pinact run --format sarif emits SARIF, which the README says is useful with reviewdog and GitHub SARIF Code Scanning; a separate pinact-action repository wraps the CLI for GitHub Actions.
Where pinact is the wrong tool
The -no-api mode is the clearest boundary. It cannot pin, because it has no way to resolve a tag to a SHA, and the README says so directly. If your CI has no outbound access to the GitHub API, pinact reduces to a check that references look like 40-character SHAs, and the pinning work has to happen somewhere else. The default path list is a second boundary: action directories deeper than the three levels in the README's glob list are outside the default scope, and text files are only included when named explicitly. The minimum release age check adds a third: with -update and a min-age that no available release satisfies, the command exits with an error rather than falling back to an older pin, so a cooldown can stall an automated update rather than degrade quietly. Finally, the README does not document a rollback command or a dry-run that writes a patch file instead of editing in place; the closest thing is -check, which reports without editing, so any recovery path runs through your own version control.
Alternatives and how they differ
Two names appear in the searches around pinact: ghalint and zizmor. Both are in the same neighbourhood, and the difference is in what they do to your files. pinact's default action is to rewrite uses: lines and leave a diff; ghalint and zizmor are linters, meaning they report problems and exit with a status rather than editing workflows. If your goal is a policy check that runs on every pull request without touching the tree, a linter fits that shape, and pinact's -check mode is the comparable surface. What pinact adds beyond a lint rule is the resolution step: it calls the GitHub API to turn a tag into a SHA, writes the SHA, writes the version comment, and can later update the pair while respecting a cooldown. A linter that only validates 40-character SHAs cannot do any of that, which is exactly the gap -no-api leaves open. The other adjacent tool in the searches is aqua, which installs CLI tools; the repository has an aqua/ directory and an aqua-installer example, so the two are used together rather than being substitutes.
Maintenance, licence, and what upgrades involve
The repository is not archived, and the last push was on 2026-07-30, the same date as the v4.1.1 release. The module path in go.mod is github.com/suzuki-shunsuke/pinact/v5 while the newest release is v4.1.1, which means the v5 line exists on main but has not been tagged as a release yet. Anyone building from source gets v5; anyone installing a release gets v4.1.1. That mismatch is worth knowing before you file a bug against behaviour you saw on main. The project is MIT licensed, which permits commercial and private use and modification with the licence and copyright notice retained; this is a description of the licence text, not legal advice, and the LICENSE file in the repository is the authoritative source. Upgrade cost is low on the surface because the CLI is a single binary, but the .pinact.yml schema is the part that can move: min_age, rules, conditions and always are configuration keys, and the repository ships a json-schema/ directory and a .pinact.yaml, so a config change is the thing to check between versions.
Editorial conclusion
pinact fits repositories that already run third-party Actions and want every uses: line resolved to a 40-character SHA, plus a CI gate that fails when someone adds a tag reference. It is the wrong tool if your workflows run entirely on your own GitHub Enterprise Server without the API access pinact needs, or if you want a scanner that only reports findings: pinact edits files by default, and only -check or -fix=false turns it into a read-only check. Before adopting it, run pinact run -check on a branch and read the diff, then decide whether to set min_age.value in .pinact.yml or leave the default of no minimum release age.
Frequently asked questions
How do I install pinact?
The README links to INSTALL.md for installation rather than listing steps inline, and the repository contains an aqua/ directory plus an aqua-installer reference in its examples, so aqua is one supported route. After installing, run pinact from the root of the repository whose workflows you want to pin.
Does pinact need a GitHub token?
The README says pinact calls the GitHub API to fetch releases and tags, and that you should pass a GitHub access token to avoid API rate limiting. It also states that the token can be read via keyrings or ghtkn, and go.mod lists ghtkn-go-sdk and zalando/go-keyring as dependencies.
Can pinact check workflows without editing them?
Yes. Passing -check, or equivalently -fix=false, makes pinact report whether actions are pinned without writing to files. Adding -no-api does no GitHub API call at all and only checks that references use a full-length 40-character commit SHA, in which case pinact cannot pin anything.
What does the minimum release age in pinact do?
Set with -min-age in days, the PINACT_MIN_AGE environment variable, or min_age.value and rules[].min_age in .pinact.yml, it excludes releases younger than the given age when updating with -update. The README notes that if no release meets the minimum age, pinact exits with an error, and that current versions are only verified when -verify-min-age is set or min_age.always is true.
Can pinact pin branches like main?
By default it does not. From v3.10.0, --branch-to-tag takes a regular expression and converts a matching branch to the latest stable tag of the action, using a pre-release only when no stable tag exists. The README recommends anchoring short names such as ^main$ so that mainline does not match, and versions matching no supplied regexp still error out.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/suzuki-shunsuke-pinact)