Skip to content

chore!: modernize toolchain (esm-only, tsdown, vitest, eslint 10)#827

Merged
tada5hi merged 2 commits into
masterfrom
chore/modernize-toolchain
May 20, 2026
Merged

chore!: modernize toolchain (esm-only, tsdown, vitest, eslint 10)#827
tada5hi merged 2 commits into
masterfrom
chore/modernize-toolchain

Conversation

@tada5hi
Copy link
Copy Markdown
Owner

@tada5hi tada5hi commented May 20, 2026

Summary

Brings the locter repo onto the same toolchain used by tada5hi/typescript-template. ESM-only output, faster build via tsdown, modern test runner via vitest, ESLint 10 flat config, and monoship for publishing.

This is a breaking changerequire('locter') no longer works.

Tooling changes

  • Build: rollup + @swc + tsc --emitDeclarationOnlytsdown (kept the build:types / build:js split so tsc --noEmit still gates the build on type errors).
  • Tests: jest + @swc/jestvitest + @vitest/coverage-v8. Coverage thresholds preserved (branches 65, functions/lines/statements 80).
  • Lint: ESLint 8 + .eslintrc + @tada5hi/eslint-config-typescriptESLint 10 flat config + @tada5hi/eslint-config. tsconfig.eslint.json removed.
  • Commitlint: commitlint.config.js (CJS) → commitlint.config.mjs.
  • Release: workspaces-publish + ad-hoc git-tag logic → tada5hi/monoship@v2.
  • TypeScript 5 → 6; tsconfig aligned with template (ESNext / bundler / noEmit / allowImportingTsExtensions).
  • CI: Node 22 → 24 (engines.node stays >=22.0.0).
  • Removed: rollup.config.mjs, test/jest.config.js, .eslintrc, tsconfig.eslint.json, commitlint.config.js, release.config.js, npm run commit.

Source changes

  • ModuleLoader now uses createRequire(import.meta.url) so loadSync and the Jest-segfault workaround keep working under pure ESM.
  • for…of refactors in locator/{async,sync}.ts, loader/module.ts, loader/built-in/conf/module.ts plus .includes() and optional-capture-group fixes — required by noUncheckedIndexedAccess in the new @tada5hi/tsconfig.
  • ESLint autofix tidied object-curly-newline and import ordering across several files (including the test/data/* fixtures — these are now ignored by ESLint going forward).

Breaking changes

  • ESM-only package. "type": "module"; exports only exposes import + types. Consumers on require('locter') must migrate.
  • Build output filenames changed: dist/index.cjs is gone; dist/index.d.tsdist/index.d.mts.

Test plan

  • npm install — 0 vulnerabilities
  • npm run buildtsc --noEmit clean, tsdown emits dist/index.mjs (15.47 kB) + dist/index.d.mts (6.02 kB)
  • npm test — 8 files, 38 tests passing
  • npm run lint — clean
  • CI green on Node 24
  • Sanity-check that import { locate, load } from 'locter' works from a fresh ESM consumer

Summary by CodeRabbit

Release Notes

  • Build & Tooling

    • Transitioned to modern build system with TypeScript and tsdown for JavaScript/type emission.
    • Switched testing framework to Vitest with improved configuration.
    • Updated CI/CD workflows with improved caching and Node version updates.
  • Package Distribution

    • Package is now ESM-only; CommonJS support removed.
  • Documentation

    • Updated project documentation, conventions, and testing guides for current toolchain.

Review Change Stack

BREAKING CHANGE: Package is now ESM-only. The CJS entry point
(dist/index.cjs / require('locter')) has been removed.

- Build: rollup + swc + tsc-for-types → tsdown (with split
  build:types / build:js scripts so tsc still typechecks)
- Tests: jest + @swc/jest → vitest + @vitest/coverage-v8
- Lint: ESLint 8 + .eslintrc + @tada5hi/eslint-config-typescript
  → ESLint 10 flat config + @tada5hi/eslint-config
- Commitlint: .js (CJS) → .mjs (ESM)
- Release: workspaces-publish + ad-hoc tagging → tada5hi/monoship@v2
- TypeScript 5 → 6; tsconfig aligned with tada5hi/typescript-template
  (ESNext/bundler/noEmit/allowImportingTsExtensions)
- CI: Node 22 → 24 (engines.node remains >=22.0.0)

Source adjustments:
- ModuleLoader uses createRequire(import.meta.url) so loadSync and
  the Jest segfault workaround still function under ESM.
- for-of refactors and includes()/regex-group fixes to satisfy
  noUncheckedIndexedAccess in the new @tada5hi/tsconfig.

Removed obsolete files: rollup.config.mjs, test/jest.config.js,
.eslintrc, tsconfig.eslint.json, commitlint.config.js,
release.config.js.
Copilot AI review requested due to automatic review settings May 20, 2026 13:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@tada5hi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 41 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d21f2a0-da6b-4d37-b2ee-c1bab6d23475

📥 Commits

Reviewing files that changed from the base of the PR and between 35bd296 and d5c38d1.

📒 Files selected for processing (3)
  • src/loader/built-in/module/utils.ts
  • test/unit/loader/module.spec.ts
  • test/unit/locator.spec.ts
📝 Walkthrough

Walkthrough

This pull request migrates the locter project from a CommonJS/Jest/Rollup toolchain to an ESM-only/Vitest/tsdown setup. The package now uses type: "module", bundles via tsdown, tests with Vitest, and runs on Node 24+. Configuration files are updated throughout, including CI/CD workflows, TypeScript settings, and ESLint setup. Source code is modernized with for...of loops and explicit ESM require handling.

Changes

Toolchain and Module System Conversion

Layer / File(s) Summary
Documentation of new toolchain setup
.agents/*, AGENTS.md
Agent guides, conventions, structure, and testing documentation updated to describe ESM-only packaging, tsdown bundling, Vitest test runner, flat ESLint config, and Node 24+ requirements.
Package manifest and module system conversion
package.json, .gitignore
Package marked ESM-only with "type": "module". Exports changed to import-only entry pointing to dist/index.mjs and types to dist/index.d.mts. Build/test/lint scripts reworked for tsdown and Vitest. dist/ removed from .gitignore. DevDependencies updated to replace Jest/Rollup/SWC with Vitest/tsdown.
TypeScript and build toolchain configuration
tsconfig.json, tsdown.config.ts
tsconfig.json switched to ES2022 target, ESNext modules, bundler module resolution, and noEmit: true. New tsdown.config.ts configures bundling of src/index.ts to dist/index.mjs with type declarations and sourcemaps.
Test framework infrastructure (Vitest)
test/vitest.config.ts, test/**/*.spec.ts
New Vitest config specifies test discovery glob, v8 coverage provider, and coverage thresholds. All test files now explicitly import describe, expect, it from vitest. Old Jest config removed.
Test files updated for ESM and environment detection
test/unit/*.spec.ts, test/data/*.{mjs,cts,cjs,mts}
Test locator paths switched to import.meta.dirname for ESM compatibility. Jest runtime environment detection test updated for Vitest. Test data export objects condensed to single-line formatting.
Source code modernization and ESM compatibility
src/loader/**/*.ts, src/locator/**/*.ts, src/utils/file-name.ts
Loop constructs refactored from index-based for to for...of iteration. Module loader now explicitly creates require function via createRequire(import.meta.url) for Jest compatibility. No public API changes.
Configuration system updates (ESLint, commitlint)
eslint.config.js, commitlint.config.mjs
New flat ESLint config exporting array from @tada5hi/eslint-config with additional ignores. Commitlint migrated to ESM export. Old .eslintrc, commitlint.config.js, and release.config.js removed.
GitHub Actions CI/CD pipeline updates
.github/workflows/main.yml, .github/workflows/release.yml, .github/actions/*, .github/dependabot.yml
Workflows updated to Node 24 primary version, action cache versions to v4, release-please to v5, and publishing to monoship v2. Build/test/lint jobs reordered; concurrency and expanded permissions added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops with glee through tsdown's flow,
Vitest gleams with ESM's glow,
Jest and Rollup fade away,
Modern tooling saves the day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: modernizing the toolchain with ESM-only packaging, tsdown bundler, vitest test runner, and ESLint 10 configuration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/modernize-toolchain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the repository’s Node/TypeScript toolchain to align with the tada5hi/typescript-template, moving the package to ESM-only distribution and updating build/test/lint/release automation accordingly (breaking require('locter') consumers).

Changes:

  • Replace Rollup/SWC + Jest with tsdown (bundle + dts) and Vitest (coverage via v8).
  • Adopt ESLint 10 flat config, TypeScript 6 + updated tsconfig settings, and update CI to Node 24.
  • Update runtime/module loading to work under pure ESM (via createRequire(import.meta.url)), plus small refactors to satisfy stricter TS settings.

Reviewed changes

Copilot reviewed 40 out of 43 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tsdown.config.ts Adds tsdown bundler configuration (ESM + dts + sourcemaps).
tsconfig.json Moves to ESNext/bundler resolution + noEmit typecheck setup.
tsconfig.eslint.json Removes ESLint-specific TS project file.
test/vitest.config.ts Adds Vitest configuration and coverage thresholds.
test/unit/utils.spec.ts Migrates test to Vitest and updates Jest-env expectation.
test/unit/locator.spec.ts Migrates to Vitest + ESM path handling (import.meta.dirname).
test/unit/loader/yaml.spec.ts Migrates to Vitest.
test/unit/loader/module.spec.ts Migrates to Vitest.
test/unit/loader/json.spec.ts Migrates to Vitest.
test/unit/loader/esm.spec.ts Migrates to Vitest.
test/unit/loader/conf.spec.ts Migrates to Vitest.
test/unit/loader/cjs.spec.ts Migrates to Vitest.
test/jest.config.js Removes Jest configuration.
test/data/file.cts Fixture formatting tweak.
test/data/file-default.mts Fixture formatting tweak.
test/data/file-default.mjs Fixture formatting tweak.
test/data/file-default.cts Fixture formatting tweak.
test/data/file-default.cjs Fixture formatting tweak.
src/utils/file-name.ts Uses .includes() instead of indexOf.
src/locator/sync.ts Refactors loops to for...of.
src/locator/async.ts Refactors loops to for...of.
src/loader/module.ts Refactors rule iteration + uses .includes().
src/loader/built-in/module/utils.ts Refactors module utils; compact property descriptors.
src/loader/built-in/module/module.ts Uses createRequire(import.meta.url) for ESM-safe sync loading.
src/loader/built-in/conf/module.ts Makes .conf parsing resilient to missing capture group.
rollup.config.mjs Removes Rollup build configuration.
release.config.js Removes semantic-release config.
package.json Switches to ESM-only exports, updates scripts, tooling deps, and publishing config.
eslint.config.js Adds ESLint 10 flat config using @tada5hi/eslint-config.
commitlint.config.mjs Converts commitlint config to ESM.
commitlint.config.js Removes CJS commitlint config.
AGENTS.md Updates contributor guide for new toolchain + ESM-only note.
.gitignore Updates ignore patterns (IDE/cache/tooling artifacts).
.github/workflows/release.yml Updates release pipeline to release-please v5 + monoship publishing.
.github/workflows/main.yml Updates CI triggers, concurrency, permissions, and Node version.
.github/dependabot.yml Adjusts Dependabot target branch and grouping rules.
.github/actions/install/action.yml Updates setup-node/cache actions and cache keying.
.github/actions/build/action.yml Updates cache action version and formatting.
.eslintrc Removes legacy ESLint config.
.agents/testing.md Updates testing docs to Vitest + ESM patterns.
.agents/structure.md Updates repo structure documentation for new config files/output.
.agents/conventions.md Updates conventions/docs for ESM-only + tsdown/vitest/monoship flow.
Comments suppressed due to low confidence (1)

src/loader/built-in/module/module.ts:25

  • There is trailing whitespace after isFilePath, in the utils import list. Please remove the trailing space to keep formatting consistent.
import {
    handleException,
    hasStringProperty,
    isFilePath, 
    isJestRuntimeEnvironment,
    isObject,
    isTsNodeRuntimeEnvironment,
    isTypeScriptError,
} from '../../../utils';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/loader/built-in/module/utils.ts Outdated
Comment thread test/unit/locator.spec.ts
Comment thread test/unit/loader/module.spec.ts
Comment thread package.json
Comment on lines 30 to +35
"scripts": {
"build:types": "tsc --emitDeclarationOnly",
"build:js": "rollup -c",
"build": "rimraf dist && npm run build:types && npm run build:js",
"commit": "npx git-cz",
"test": "cross-env NODE_ENV=test jest --config ./test/jest.config.js",
"test:coverage": "cross-env NODE_ENV=test jest --config ./test/jest.config.js --coverage",
"lint": "eslint --ext .js,.vue,.ts ./src",
"build:types": "tsc --noEmit",
"build:js": "tsdown",
"build": "npm run build:types && npm run build:js",
"test": "vitest --config test/vitest.config.ts --run",
"test:coverage": "vitest --config test/vitest.config.ts --run --coverage",
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/build/action.yml:
- Around line 9-13: The workflow currently references actions/cache@v4 which is
mutable; update the uses entry for the actions/cache step (the line containing
"uses: actions/cache@v4") to pin to the full commit SHA of the actions/cache
repository (replace `@v4` with @<full-length-commit-sha>) to prevent supply-chain
tampering, and optionally append a human-readable comment with the semantic
version (e.g., "# v4") for clarity; no other changes to the step (path/key) are
required.

In @.github/actions/install/action.yml:
- Around line 15-22: The workflow currently pins GitHub Actions with mutable
tags (uses: actions/setup-node@v4 and uses: actions/cache@v4); replace those tag
pins with the corresponding immutable full commit SHAs from the official action
repos (e.g., uses: actions/setup-node@<full-commit-sha> and uses:
actions/cache@<full-commit-sha>) so the action versions cannot be
repointed—locate the correct commit SHAs in the actions/setup-node and
actions/cache GitHub repos/releases and update the two uses: lines accordingly.

In @.github/workflows/main.yml:
- Around line 50-52: The checkout steps using actions/checkout@v6 should be
hardened: replace the tag pin with the full commit SHA for actions/checkout and
add persist-credentials: false to the step configuration; apply this change to
both occurrences of the Checkout step (the actions/checkout usage shown twice)
so the workflow no longer uses a mutable tag and does not leave GITHUB_TOKEN in
the Git config.

In @.github/workflows/release.yml:
- Around line 25-31: Pin external actions to immutable commit SHAs instead of
mutable tags: replace uses: googleapis/release-please-action@v5 and uses:
actions/checkout@v6 with their full commit SHA equivalents, and for the Checkout
step (the job that runs when steps.release.outputs.releases_created == 'true')
add persist-credentials: false under the checkout action inputs to harden
credentials; update any other occurrences (the other checkout/use lines
mentioned around 46-47 and 55-56) similarly so all third-party actions are
SHA-pinned and all checkout steps include persist-credentials: false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 058dd40c-4c0c-4f51-b15e-dc830159cb0c

📥 Commits

Reviewing files that changed from the base of the PR and between 66bdf4a and 35bd296.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (42)
  • .agents/conventions.md
  • .agents/structure.md
  • .agents/testing.md
  • .eslintrc
  • .github/actions/build/action.yml
  • .github/actions/install/action.yml
  • .github/dependabot.yml
  • .github/workflows/main.yml
  • .github/workflows/release.yml
  • .gitignore
  • AGENTS.md
  • commitlint.config.js
  • commitlint.config.mjs
  • eslint.config.js
  • package.json
  • release.config.js
  • rollup.config.mjs
  • src/loader/built-in/conf/module.ts
  • src/loader/built-in/module/module.ts
  • src/loader/built-in/module/utils.ts
  • src/loader/module.ts
  • src/locator/async.ts
  • src/locator/sync.ts
  • src/utils/file-name.ts
  • test/data/file-default.cjs
  • test/data/file-default.cts
  • test/data/file-default.mjs
  • test/data/file-default.mts
  • test/data/file.cts
  • test/jest.config.js
  • test/unit/loader/cjs.spec.ts
  • test/unit/loader/conf.spec.ts
  • test/unit/loader/esm.spec.ts
  • test/unit/loader/json.spec.ts
  • test/unit/loader/module.spec.ts
  • test/unit/loader/yaml.spec.ts
  • test/unit/locator.spec.ts
  • test/unit/utils.spec.ts
  • test/vitest.config.ts
  • tsconfig.eslint.json
  • tsconfig.json
  • tsdown.config.ts
💤 Files with no reviewable changes (6)
  • commitlint.config.js
  • .eslintrc
  • tsconfig.eslint.json
  • release.config.js
  • test/jest.config.js
  • rollup.config.mjs

Comment on lines +9 to +13
uses: actions/cache@v4
with:
path: |
**/dist/**
key: ${{ runner.os }}-build-${{ github.sha }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/actions/build/action.yml

Repository: tada5hi/locter

Length of output: 662


🌐 Web query:

GitHub Actions best practices pinning commit SHA vs tag 2024 2025

💡 Result:

As of May 2026, the consensus among security experts and official GitHub documentation is that pinning GitHub Actions to full-length commit SHAs is the industry best practice for production and high-security CI/CD environments [1][2][3][4]. Why Pinning to Commit SHA is Recommended: 1. Immutability and Security: Git tags are mutable, meaning they can be deleted, moved, or re-created by a repository maintainer to point to different commits [1][5]. If an action's repository or a maintainer's account is compromised, attackers can update existing tags to inject malicious code, which would then be automatically executed by your workflows [6][7][8][4]. Pinning to a full-length commit SHA ensures that your workflows always execute the exact, expected code, effectively mitigating this class of supply-chain attacks [1][5][6]. 2. Enforced Governance: GitHub now supports organization-level policies that administrators can enable to mandate full commit SHA pinning [1][9][10]. When this policy is active, any workflow attempting to use a tag or branch reference will fail, ensuring strict compliance across an organization [1][9]. 3. Controlled Updates: Pinning to a SHA eliminates the risk of silent, unexpected updates [1][3]. You can (and should) use automated tools like Dependabot, Renovate, or specialized CLI tools to track and open pull requests for action updates, allowing you to review changes before they are merged into your workflows [1][8][11][4]. Best Practices for Implementation: * Use Full-Length SHAs: Always use the full-length commit SHA, not a shortened version, to ensure uniqueness and security [2][3][12]. * Maintain Context: Since SHAs are not human-readable, append the version tag as a comment in your workflow file to maintain visibility into what version is being used [9][11]. Example: uses: actions/checkout@f43a0e5 # v4.2.1 * Leverage Automation: Configure Dependabot or similar dependency management tools to automatically monitor for updates to your pinned actions [1][8]. This balances security with the need to keep your tools up to date without manual overhead [1][5][4]. While tags remain convenient for development or low-risk, non-sensitive environments, they should be avoided for workflows handling secrets, production deployments, or any process where supply-chain integrity is a requirement [2][5][3][4].

Citations:


Pin actions/cache by commit SHA.

Using @v4 keeps this action vulnerable to supply-chain attacks—tags are mutable and can be retargeted if the repository or maintainer account is compromised. Pin to the full-length commit SHA instead, and optionally append the version as a comment for readability (e.g., actions/cache@<full-length-commit-sha> # v4).

Suggested patch
-          uses: actions/cache@v4
+          uses: actions/cache@<full-length-commit-sha> # v4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/build/action.yml around lines 9 - 13, The workflow currently
references actions/cache@v4 which is mutable; update the uses entry for the
actions/cache step (the line containing "uses: actions/cache@v4") to pin to the
full commit SHA of the actions/cache repository (replace `@v4` with
@<full-length-commit-sha>) to prevent supply-chain tampering, and optionally
append a human-readable comment with the semantic version (e.g., "# v4") for
clarity; no other changes to the step (path/key) are required.

Comment on lines +15 to +22
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}

- name: Use Node.js ${{ inputs.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
- name: Use cache
uses: actions/cache@v4
with:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Read the action file to verify the current state
cat -n .github/actions/install/action.yml | head -30

Repository: tada5hi/locter

Length of output: 1162


Pin setup-node and cache actions to commit SHAs instead of version tags.

Both action references use mutable tag pins (@v4) which can be repointed by a compromised repository. Replace with immutable full-length commit SHAs for security hardening.

Suggested patch
-          uses: actions/setup-node@v4
+          uses: actions/setup-node@<FULL_LENGTH_COMMIT_SHA>
...
-          uses: actions/cache@v4
+          uses: actions/cache@<FULL_LENGTH_COMMIT_SHA>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/install/action.yml around lines 15 - 22, The workflow
currently pins GitHub Actions with mutable tags (uses: actions/setup-node@v4 and
uses: actions/cache@v4); replace those tag pins with the corresponding immutable
full commit SHAs from the official action repos (e.g., uses:
actions/setup-node@<full-commit-sha> and uses: actions/cache@<full-commit-sha>)
so the action versions cannot be repointed—locate the correct commit SHAs in the
actions/setup-node and actions/cache GitHub repos/releases and update the two
uses: lines accordingly.

Comment on lines +50 to +52
- name: Checkout
uses: actions/checkout@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/main.yml | sed -n '45,75p'

Repository: tada5hi/locter

Length of output: 1014


🌐 Web query:

GitHub Actions checkout action best practices SHA pinning vs tag pinning security

💡 Result:

For security and supply chain integrity, pinning GitHub Actions to a full-length commit SHA is the recommended best practice [1][2][3]. Unlike version tags (e.g., v4), which are mutable and can be moved or deleted by an action's maintainer or a malicious actor [1][4][5], a full-length SHA points to an immutable commit in the repository [6][1][4]. Security Benefits of SHA Pinning 1. Immutability: Once pinned to a SHA, the workflow will always execute the exact same code, regardless of changes to tags or branches in the source repository [4][7][3]. 2. Mitigation of Supply Chain Attacks: If an action's repository is compromised, attackers often target mutable tags to inject malicious code [8][5]. Workflows pinned to a specific SHA are protected from these silent, unauthorized updates [5][9]. 3. Reproducibility: SHA pinning ensures that build environments remain consistent over time, which is critical for debugging and preventing unexpected deployment issues [7][3]. Best Practices for Implementation - Use Full-Length SHAs: Always use the full 40-character commit SHA rather than a shortened version to ensure unique identification [1][3]. - Use Informative Comments: To maintain readability and ease of maintenance, include the version tag in a comment on the same line as the SHA [10][6][7]: uses: actions/checkout@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v4.1.1 - Automate Updates: Because SHA pinning prevents automatic updates, use dependency management tools like Dependabot or Renovate to monitor for new releases and automate the creation of pull requests to update your pinned SHAs [10][1][11][3]. - Enforce Policies: Organizations can configure repository or organization-level policies to mandate full SHA pinning, causing any workflows that use mutable tags to fail [6][1][9]. - Verify Sources: Ensure the SHA comes from the official, trusted repository and not from a fork or untrusted source [1][3]. While tagging is more convenient for general use, it carries inherent trust risks. Only use tags for actions from creators you explicitly trust, and prioritize SHA pinning for critical, high-risk, or third-party actions [1][4]. Always be aware that pinning the top-level action does not automatically protect you from vulnerabilities in its transitive dependencies; you should audit those as well [11].

Citations:


🌐 Web query:

GitHub Actions checkout action persist-credentials false security best practice

💡 Result:

In GitHub Actions, setting persist-credentials: false in the actions/checkout action is considered a security best practice because it prevents the automatic persistence of authentication credentials (such as the GITHUB_TOKEN) into the local Git configuration [1][2][3]. When persist-credentials is set to its default value of true, the checkout action configures the local Git environment with authentication tokens [1][3]. This makes those credentials easily accessible to any subsequent steps or scripts running within the same job, effectively widening the surface area for credential exposure if a downstream step is compromised [4][5][6]. By setting persist-credentials: false, you opt out of this behavior, ensuring that authentication is not implicitly stored in the repository's Git configuration [1][3]. This is recommended by security guidelines, such as the OWASP GitHub Actions Security Cheat Sheet, to reduce the risk of credential leakage within the workflow environment [2]. Recent updates to the actions/checkout action have improved credential security by moving the stored credentials from the repository's internal.git/config file to a separate, temporary configuration file within $RUNNER_TEMP [1][7][3]. Despite this architectural improvement, explicitly setting persist-credentials: false remains a recommended practice to adhere to the principle of least privilege, ensuring that credentials are only used when absolutely necessary and are not readily available to unrelated job steps [2][6]. Key points regarding this security practice: - It limits the exposure of authentication tokens within the runner environment [4][6]. - It is particularly important if your job executes untrusted code or third-party actions [2]. - It does not prevent you from performing authenticated Git operations; if you need to push or fetch later in the job, you can still manage authentication explicitly using other methods if necessary [1]. - It is a complementary measure to other security practices, such as using read-only GITHUB_TOKEN permissions where possible and pinning actions to immutable commit SHAs to prevent supply chain attacks [8][9][10].

Citations:


Harden checkout steps: pin action SHAs and disable credential persistence.

The new actions/checkout steps at lines 50–51 and 70–71 use tag pinning (@v6) instead of full-length commit SHAs, which are mutable and vulnerable to supply chain attacks. Additionally, persist-credentials is not set to false, leaving the GITHUB_TOKEN accessible in the Git config for all subsequent steps.

Suggested fix
            - name: Checkout
-             uses: actions/checkout@v6
+             uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>
+             with:
+                 persist-credentials: false

Apply the same change to both checkout steps (lines 50–51 and 70–71).

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 50-51: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 51-51: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/main.yml around lines 50 - 52, The checkout steps using
actions/checkout@v6 should be hardened: replace the tag pin with the full commit
SHA for actions/checkout and add persist-credentials: false to the step
configuration; apply this change to both occurrences of the Checkout step (the
actions/checkout usage shown twice) so the workflow no longer uses a mutable tag
and does not leave GITHUB_TOKEN in the Git config.

Comment on lines +25 to +31
uses: googleapis/release-please-action@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Checkout
if: steps.release.outputs.releases_created == 'true'
uses: actions/checkout@v6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Find and read the release.yml workflow file
find . -name "release.yml" -o -name "release.yaml" | head -20

Repository: tada5hi/locter

Length of output: 90


🏁 Script executed:

cat -n ./.github/workflows/release.yml

Repository: tada5hi/locter

Length of output: 2091


🌐 Web query:

GitHub Actions security best practices pinning commit SHA vs version tag official

💡 Result:

GitHub Actions security best practice: pin third-party actions to a full-length commit SHA (immutable), not to a version tag. 1) Why commit-SHA pinning is the official “most secure” choice GitHub states that “Pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release” and that it mitigates the risk of malicious changes to the action’s repository because an attacker would need to generate a SHA-1 collision for a valid Git object payload [1]. GitHub also recommends verifying the SHA is from the action’s repository (not a fork) [1]. 2) Why version tag pinning is weaker GitHub explicitly says pinning to a tag is less secure and “only” do it if you trust the action’s creator [1]. GitHub notes that tags can be moved or deleted if a bad actor gains access to the repository holding the action [1]. GitHub changelog coverage of “Enforce SHA pinning” similarly describes that pinning to a specific commit SHA prevents malicious code added to a new or updated branch or tag from being automatically used, and that policies can require full SHAs (workflows that aren’t pinned fail) [2]. 3) Practical guidance (what to do in your workflows) - Prefer: uses: owner/repo@ (optionally with a comment indicating the intended version) [1][2]. - If you use tags (e.g., @v3) you are accepting mutability risk; GitHub’s docs say only do so if you trust the creator, and you still have residual risk if the action repo/tag is compromised [1]. - To keep SHA pinning maintainable, use automation such as Dependabot/Renovate to update pins, and validate workflows after updates (GitHub documents Dependabot workflow automation in general) [3]. Bottom line: For security, pin to full-length commit SHAs; tags are convenient but inherently mutable and therefore not the “official” strongest recommendation [1][2].

Citations:


🌐 Web query:

GitHub Actions actions/checkout persist-credentials false security best practice

💡 Result:

Setting persist-credentials: false in the actions/checkout GitHub Action is considered a security best practice because it prevents the automatic persistence of Git credentials (such as the GITHUB_TOKEN or SSH keys) into the local Git configuration of the runner [1][2][3]. By default, actions/checkout configures the local.git/config file with these credentials to enable subsequent Git commands within the same workflow job [1][3]. If this setting is left as true (the default), any process or malicious action running within that workflow can potentially read these credentials from the configuration file [4][5]. Setting this input to false ensures that credentials are not persisted, which reduces the risk of credential exposure if the workflow is compromised [2][6]. Key points regarding this practice: - Security Impact: It limits the potential for unauthorized access to repository credentials by ensuring they are not stored on disk in an easily readable format [5][2]. - Operational Impact: If you require authenticated Git operations later in your workflow (e.g., pushing code back to the repository using a custom action), setting this to false will cause those operations to fail unless you manually reconfigure authentication or provide the necessary credentials to those subsequent steps [1][6]. - Context: While this setting hardens the checkout process, it is one layer of a broader security strategy [7]. It is also recommended to use granular permissions (e.g., setting the default GITHUB_TOKEN to read-only at the repository level), pin actions to specific commit SHAs rather than mutable tags to prevent supply chain attacks, and use OIDC for cloud provider authentication where possible [7][8][9]. In summary, you should use persist-credentials: false unless your workflow explicitly requires authenticated Git operations that rely on the automatically persisted configuration [1][2].

Citations:


Pin all third-party GitHub Actions to commit SHAs, and harden checkout credentials.

The release workflow references external actions by mutable version tags and omits credential hardening on checkout. GitHub recommends pinning actions to full-length commit SHAs (immutable) rather than tags, which can be moved, deleted, or compromised. The checkout step should include persist-credentials: false to prevent credential persistence on disk and reduce exposure risk if the workflow is compromised.

Suggested patch
-            - name: Release Please
-              id: release
-              uses: googleapis/release-please-action@v5
+            - name: Release Please
+              id: release
+              uses: googleapis/release-please-action@<FULL_LENGTH_COMMIT_SHA>
...
-            - name: Checkout
-              if: steps.release.outputs.releases_created == 'true'
-              uses: actions/checkout@v6
+            - name: Checkout
+              if: steps.release.outputs.releases_created == 'true'
+              uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA>
+              with:
+                  persist-credentials: false
...
-            - name: Publish
-              if: steps.release.outputs.releases_created == 'true'
-              uses: tada5hi/monoship@v2
+            - name: Publish
+              if: steps.release.outputs.releases_created == 'true'
+              uses: tada5hi/monoship@<FULL_LENGTH_COMMIT_SHA>
...
-            - name: Upload report
-              if: steps.release.outputs.releases_created == 'true'
-              uses: codecov/codecov-action@v5
+            - name: Upload report
+              if: steps.release.outputs.releases_created == 'true'
+              uses: codecov/codecov-action@<FULL_LENGTH_COMMIT_SHA>

Also applies to: 46-47, 55-56

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 29-31: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 31-31: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 25 - 31, Pin external actions to
immutable commit SHAs instead of mutable tags: replace uses:
googleapis/release-please-action@v5 and uses: actions/checkout@v6 with their
full commit SHA equivalents, and for the Checkout step (the job that runs when
steps.release.outputs.releases_created == 'true') add persist-credentials: false
under the checkout action inputs to harden credentials; update any other
occurrences (the other checkout/use lines mentioned around 46-47 and 55-56)
similarly so all third-party actions are SHA-pinned and all checkout steps
include persist-credentials: false.

Three spots got stranded whitespace after the autofix split inline
imports onto multiple lines. Surfaced by PR review.
@tada5hi tada5hi merged commit c38df1f into master May 20, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants