Skip to content

feat: add zero-config auto mode to GitHub Action#2797

Merged
srod merged 3 commits intodevelopfrom
feat/zero-config-action
Jan 25, 2026
Merged

feat: add zero-config auto mode to GitHub Action#2797
srod merged 3 commits intodevelopfrom
feat/zero-config-action

Conversation

@srod
Copy link
Owner

@srod srod commented Jan 24, 2026

Summary

Adds zero-config auto mode to the GitHub Action, enabling automatic file discovery and minification without explicit input/output configuration.

What's New

Auto Mode (auto: true)

  • Automatically discovers source files using smart default patterns
  • Detects file types and selects appropriate compressors
  • Preserves directory structure in output
  • Parallel processing (4 concurrent) for efficient batch minification

New Inputs

Input Description Default
auto Enable zero-config mode false
patterns Custom glob patterns (comma-separated) Smart defaults
output-dir Output directory dist
ignore Additional ignore patterns -
dry-run Preview mode without processing false

Supported File Types

Extension Compressor
.js, .mjs, .cjs, .jsx @node-minify/terser
.css @node-minify/lightningcss
.html, .htm @node-minify/html-minifier
.json @node-minify/jsonminify
.svg @node-minify/svgo

Smart Defaults

  • Included: src/, app/, lib/, styles/, root-level files
  • Ignored: node_modules, dist, build, .next, *.min.*, *.d.ts, __tests__, dotfiles

Example Usage

# Zero-config - discovers and minifies all supported files
- uses: srod/node-minify@v1
  with:
    auto: 'true'

# Custom patterns
- uses: srod/node-minify@v1
  with:
    auto: 'true'
    patterns: 'assets/**/*.{js,css}'
    output-dir: 'build'

# Dry run preview
- uses: srod/node-minify@v1
  with:
    auto: 'true'
    dry-run: 'true'

Security Hardening

This PR includes comprehensive security measures for path handling:

Protection Implementation
Absolute path rejection inputs.ts rejects absolute output-dir paths
Path traversal prevention Segment-based .. detection (not substring matching)
Output directory validation path.resolve() + path.relative() to prevent source overlap
Input sanitization generateOutputPath() normalizes and strips traversal sequences

Testing

  • 173 action package tests (all passing)
  • 85%+ code coverage (up from 55%)
  • 5 CI integration jobs for zero-config scenarios
  • Cross-platform (Ubuntu + Windows)

New Test Files

File Tests Coverage
runAutoMode.test.ts 16 Auto mode orchestration
runExplicitMode.test.ts 12 Explicit mode orchestration
minify.test.ts 8 Minification wrapper
index.test.ts 14 Entry point routing
discover.test.ts 16 File discovery + security
validate.test.ts 10 Path validation

Code Review Feedback Addressed

Greptile

  • ✅ Replaced nested ternary with detectFileType() utility

CodeRabbit

  • ✅ Secured generateOutputPath against absolute paths
  • ✅ Fixed nested .. traversal escaping output directory
  • ✅ Added @returns JSDoc tags to exported functions

Cubic

  • ✅ Robust validateOutputDir using path.resolve() + path.relative()
  • ✅ Segment-based path traversal check (not substring)

Documentation

  • Updated docs/src/content/docs/github-action.md
  • Updated packages/action/README.md

Breaking Changes

None. This is a backward-compatible addition. Explicit mode continues to work as before.


Verification Checklist

  • All 15 CI checks passing
  • TypeScript compiles without errors
  • Linting passes
  • Test coverage >80%
  • Security review complete
  • Documentation updated

Summary by CodeRabbit

  • New Features

    • Zero‑config "Auto Mode": automatic file discovery, type detection, compressor selection, parallel processing, per‑type grouped summaries, and dry‑run preview
    • New action inputs: auto, patterns, output‑dir, ignore, dry‑run (input/output now optional)
    • Public wildcard options and default ignore patterns exported
  • Bug Fixes

    • Validation preventing output directory from overlapping discovered source paths
  • Documentation

    • Added Zero‑Config Mode docs and README examples
  • Chores / Tests / CI

    • Expanded unit tests and added zero‑config CI workflows and summary aggregation

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 24, 2026

🦋 Changeset detected

Latest commit: c21e335

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 31 packages
Name Type
@node-minify/action Minor
@node-minify/utils Minor
@node-minify/babel-minify Minor
@node-minify/benchmark Minor
@node-minify/clean-css Minor
@node-minify/cli Minor
@node-minify/core Minor
@node-minify/crass Minor
@node-minify/cssnano Minor
@node-minify/csso Minor
@node-minify/esbuild Minor
@node-minify/google-closure-compiler Minor
@node-minify/html-minifier Minor
@node-minify/imagemin Minor
@node-minify/jsonminify Minor
@node-minify/lightningcss Minor
@node-minify/minify-html Minor
@node-minify/no-compress Minor
@node-minify/oxc Minor
@node-minify/sharp Minor
@node-minify/sqwish Minor
@node-minify/svgo Minor
@node-minify/swc Minor
@node-minify/terser Minor
@node-minify/uglify-es Minor
@node-minify/uglify-js Minor
@node-minify/yui Minor
@node-minify/examples Minor
@node-minify/docs Minor
@node-minify/run Minor
@node-minify/types Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a zero‑config "auto" mode to the GitHub Action: file discovery, file‑type detection, compressor selection, output‑dir validation, chunked processing, and grouped reporting; expands action inputs, docs, wildcards API, CI test jobs, and extensive unit tests.

Changes

Cohort / File(s) Summary
Workflows
​.github/workflows/test-action.yml
Added three zero‑config test jobs (test-zero-config, test-zero-config-dry-run, test-zero-config-custom-patterns) and extended test-summary dependencies; standardized minification step inputs.
Action manifests
packages/action/action.yml, action.yml
Made input/output optional; added inputs auto, patterns, output-dir, ignore, dry-run.
Types & Inputs
packages/action/src/types.ts, packages/action/src/inputs.ts, packages/action/__tests__/action.test.ts, packages/action/__tests__/inputs.test.ts
Extended ActionInputs with auto‑mode fields (auto, patterns, outputDir, additionalIgnore, dryRun); added comma parsing, output‑dir sanitization/validation, defaults and parsing for patterns/ignore, and tests.
Discovery & Detection
packages/action/src/discover.ts, packages/action/src/autoDetect.ts, packages/action/__tests__/discover.test.ts, packages/action/__tests__/autoDetect.test.ts
Added discovery utilities (DEFAULT_PATTERNS, DEFAULT_IGNORES, discoverFiles, generateOutputPath) and file‑type detection/compressor selection (detectFileType, selectCompressor, groupFilesByType) with tests.
Orchestration & Minification
packages/action/src/index.ts, packages/action/src/minify.ts, packages/action/src/benchmark.ts, packages/action/__tests__/runAutoMode.test.ts, packages/action/__tests__/runExplicitMode.test.ts, packages/action/__tests__/minify.test.ts
Added chunkArray, runAutoMode, runExplicitMode, and run() dispatcher; auto mode discovers, groups, validates compressors, chunk‑processes files, and aggregates metrics; small guards/adjustments in benchmark/minify and many orchestration tests.
Validation
packages/action/src/validate.ts, packages/action/__tests__/validate.test.ts
New validateOutputDir to prevent output dir from residing inside source patterns; tests for overlap detection and error messages.
Reporting
packages/action/src/reporters/summary.ts, packages/action/__tests__/reporters/summary.test.ts
New generateAutoModeSummary to group results by file type and render per‑type tables and totals; tests added.
Utilities
packages/utils/src/wildcards.ts, packages/utils/src/index.ts, packages/utils/__tests__/wildcards-ignore.test.ts
Added WildcardOptions and DEFAULT_IGNORES; wildcards API accepts options (backwards‑compatible); re‑exports updated and tests added.
Tests (misc)
packages/action/__tests__/*, packages/utils/__tests__/*
Large set of new and expanded tests covering chunking, parseInputs (auto), discover/autoDetect, reporters, validate, runAutoMode/runExplicitMode, minify, wildcards, and related mocks.
Docs & Misc
docs/src/content/docs/github-action.md, packages/action/README.md, .gitignore
Zero‑config docs and README snippets added; .sisyphus/ and .claude/ added to .gitignore.
Import path fixes
packages/action/src/annotations.ts, packages/action/src/comment.ts
Adjusted relative import paths (parent → sibling) in a couple modules.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant A as Action
    participant D as Discover
    participant T as Detector
    participant S as Selector
    participant M as Minifier
    participant R as Reporter

    U->>A: Trigger action (auto:true, patterns?, output-dir?, dry-run?)
    A->>D: discoverFiles(patterns, ignore, cwd, dryRun)
    D-->>A: file list
    A->>T: groupFilesByType(file list)
    T-->>A: grouped files by FileType
    loop per FileType
        A->>S: selectCompressor(FileType)
        S-->>A: CompressorSelection
        A->>M: process files (chunkArray -> runMinification)
        M-->>A: FileResult entries
    end
    A->>R: generateAutoModeSummary(all results, inputs)
    R-->>A: rendered summary
    A-->>U: completion status / outputs
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Review effort 3/5

Poem

🐇 I hopped through globs and sniffed each file,
Picked compressors quick and worked with style,
Dry‑run peeked, chunks crunched through the night,
Tables and totals gleamed in tidy light,
🥕 Bytes tucked in burrows, neat and bright.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add zero-config auto mode to GitHub Action' clearly and directly describes the primary change: introducing a zero-config auto mode feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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


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.

@qodo-code-review
Copy link

qodo-code-review bot commented Jan 24, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Path traversal write

Description: In auto mode, user-controlled patterns/output-dir can lead to path traversal and
filesystem write outside the workspace because discovered file paths are combined via
generateOutputPath(file, inputs.outputDir) and then written to
path.join(inputs.workingDirectory, outputPath) without sanitizing .. segments or rejecting
absolute paths (e.g., output-dir: ../out or patterns yielding ../secrets.js).
index.ts [112-167]

Referred Code
async function runAutoMode(inputs: ActionInputs): Promise<void> {
    const files = discoverFiles({
        patterns: inputs.patterns,
        ignore: inputs.additionalIgnore,
        workingDirectory: inputs.workingDirectory,
        dryRun: inputs.dryRun,
    });

    if (files.length === 0) {
        warning("No files found matching patterns");
        return;
    }

    if (inputs.dryRun) {
        info(`[dry-run] Would process ${files.length} files`);
        return;
    }

    const grouped = groupFilesByType(files);

    // Check compressor availability first


 ... (clipped 35 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing validation path: Auto mode fails to validate output-dir safety when using default discovery patterns,
leaving an unhandled edge case where output may overlap discovered inputs and cause
repeated processing.

Referred Code
if (auto && patterns.length > 0) {
    validateOutputDir(outputDir, patterns);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Output-dir not validated: output-dir is only validated when auto mode has custom patterns, so external input can
bypass overlap protection when defaults are used.

Referred Code
const auto = getBooleanInput("auto");
const dryRun = getBooleanInput("dry-run");
const outputDir = getInput("output-dir") || "dist";
const patterns = parseCommaSeparated(getInput("patterns"));
const additionalIgnore = parseCommaSeparated(getInput("ignore"));

const input = getInput("input");
const output = getInput("output");

if (!auto && (!input || !output)) {
    throw new Error(
        "Explicit mode requires both 'input' and 'output'. Enable 'auto' mode or provide both inputs."
    );
}

if (auto && patterns.length > 0) {
    validateOutputDir(outputDir, patterns);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
User-facing internals: The action surfaces detailed dependency guidance (e.g., exact package names/install
commands) via setFailed(error.message), which may be considered overly detailed for
user-facing errors depending on policy expectations.

Referred Code
try {
    await resolveCompressor(compressor);
} catch {
    throw new Error(
        `Compressor for ${type} files not found. Run: npm install ${pkg}`
    );
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured error logs: Auto mode logs per-file failure details (file paths and error messages) via warning()
without structured logging or redaction, which may unintentionally expose sensitive
path/context depending on repository content.

Referred Code
if (failures.length > 0) {
    warning(`${failures.length} files failed to minify:`);
    for (const { file, error } of failures) {
        warning(`  - ${file}: ${error}`);
    }

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

qodo-code-review bot commented Jan 24, 2026

PR Code Suggestions ✨

Latest suggestions up to a8273bb

CategorySuggestion                                                                                                                                    Impact
Possible issue
Always ignore generated outputs
Suggestion Impact:The commit adds an auto-generated ignore glob for inputs.outputDir and appends it to the discoverFiles ignore list alongside any additionalIgnore entries, preventing output files from being rediscovered.

code diff:

+    // Auto-ignore output directory to prevent re-processing minified files
+    const outputDirIgnore = `**/${inputs.outputDir.replace(/\\/g, "/")}/**`;
+
     const files = discoverFiles({
         patterns: inputs.patterns,
-        ignore: inputs.additionalIgnore,
+        ignore: [...(inputs.additionalIgnore ?? []), outputDirIgnore],
         workingDirectory: inputs.workingDirectory,

To prevent re-processing of minified files, dynamically add the configured
outputDir to the ignore list during file discovery in auto mode.

packages/action/src/index.ts [125-130]

+const outputDirIgnore = `**/${inputs.outputDir.replace(/\\/g, "/")}/**`;
+
 const files = discoverFiles({
     patterns: inputs.patterns,
-    ignore: inputs.additionalIgnore,
+    ignore: [...(inputs.additionalIgnore ?? []), outputDirIgnore],
     workingDirectory: inputs.workingDirectory,
     dryRun: inputs.dryRun,
 });

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: This is a valid and important point. The current implementation risks creating an infinite loop of minification if the output directory is included in the source patterns. Explicitly ignoring the outputDir makes the auto-mode significantly more robust and prevents unexpected behavior.

Medium
Detect overlap for root globs
Suggestion Impact:The commit added the suggested workingDirectory parameter and uses it to resolve outputDir and baseDir. It also changed behavior for patterns whose baseDir is empty (root-level globs), though instead of treating them as project-root (".") and checking overlap, it skips them entirely.

code diff:

+ * @param workingDirectory - Base directory for resolving relative paths (defaults to ".")
  * @throws Error if outputDir is inside any source pattern
  */
-export function validateOutputDir(outputDir: string, patterns: string[]): void {
-    const absOutputDir = path.resolve(outputDir);
+export function validateOutputDir(
+    outputDir: string,
+    patterns: string[],
+    workingDirectory = "."
+): void {
+    const absOutputDir = path.resolve(workingDirectory, outputDir);
 
     for (const pattern of patterns) {
         // Extract base directory from glob pattern (everything before first *)
         const baseDir = pattern.split("*")[0];
 
-        if (baseDir) {
-            const absBaseDir = path.resolve(baseDir);
+        // Skip root-level patterns (e.g., "*.js") - they don't recurse into subdirs
+        // so outputDir in a subdir like "dist" won't cause infinite loops
+        if (!baseDir || baseDir === "") {
+            continue;
+        }
 
-            // Check if output dir is same as or inside base dir
-            const rel = path.relative(absBaseDir, absOutputDir);
+        const absBaseDir = path.resolve(workingDirectory, baseDir);
 
-            // If rel is empty, they are same.
-            // If rel does not start with '..', output is inside base.
-            // Exception: check if rel is absolute (different drives on Windows)
-            const isInside =
-                rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
+        // Check if output dir is same as or inside base dir
+        const rel = path.relative(absBaseDir, absOutputDir);
 
-            if (isInside) {
-                throw new Error(
-                    `output-dir cannot be inside source pattern: '${outputDir}' overlaps with '${pattern}'`
-                );
-            }
+        // If rel is empty, they are same.
+        // If rel does not start with '..', output is inside base.
+        // Exception: check if rel is absolute (different drives on Windows)
+        const isInside =
+            rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
+
+        if (isInside) {
+            throw new Error(
+                `output-dir cannot be inside source pattern: '${outputDir}' overlaps with '${pattern}'`
+            );
         }

Improve validateOutputDir to correctly handle root-level glob patterns (like
/*.js) by treating them as relative to the project root, and support a custom
workingDirectory.**

packages/action/src/validate.ts [17-43]

-export function validateOutputDir(outputDir: string, patterns: string[]): void {
-    const absOutputDir = path.resolve(outputDir);
+export function validateOutputDir(
+    outputDir: string,
+    patterns: string[],
+    workingDirectory = "."
+): void {
+    const absOutputDir = path.resolve(workingDirectory, outputDir);
 
     for (const pattern of patterns) {
         // Extract base directory from glob pattern (everything before first *)
-        const baseDir = pattern.split("*")[0];
+        // If the pattern starts with a wildcard (e.g. "**/*.js"), treat base as project root.
+        const baseDir = pattern.split("*")[0] || ".";
 
-        if (baseDir) {
-            const absBaseDir = path.resolve(baseDir);
+        const absBaseDir = path.resolve(workingDirectory, baseDir);
 
-            // Check if output dir is same as or inside base dir
-            const rel = path.relative(absBaseDir, absOutputDir);
+        // Check if output dir is same as or inside base dir
+        const rel = path.relative(absBaseDir, absOutputDir);
 
-            // If rel is empty, they are same.
-            // If rel does not start with '..', output is inside base.
-            // Exception: check if rel is absolute (different drives on Windows)
-            const isInside =
-                rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
+        const isInside =
+            rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
 
-            if (isInside) {
-                throw new Error(
-                    `output-dir cannot be inside source pattern: '${outputDir}' overlaps with '${pattern}'`
-                );
-            }
+        if (isInside) {
+            throw new Error(
+                `output-dir cannot be inside source pattern: '${outputDir}' overlaps with '${pattern}'`
+            );
         }
     }
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a flaw where validateOutputDir would not check root-level glob patterns (e.g., **/*.js), which is a significant oversight. The proposed change correctly handles these patterns and also improves correctness when a custom workingDirectory is used, making the validation much more effective.

Medium
Skip unsupported file types

In auto mode, skip and warn about unsupported file types instead of failing if
the no-compress package is missing.

packages/action/src/index.ts [150-163]

 const grouped = groupFilesByType(files);
 
-// Check compressor availability first
-for (const [type, typeFiles] of Object.entries(grouped)) {
+// Skip unknown file types (don't fail auto mode on unrelated assets)
+const unknownFiles = grouped.unknown;
+if (unknownFiles.length > 0) {
+    warning(
+        `Skipping ${unknownFiles.length} files with unknown type (unsupported extensions)`
+    );
+    for (const file of unknownFiles) {
+        warning(`  - ${file}`);
+    }
+    grouped.unknown = [];
+}
+
+// Check compressor availability first (supported types only)
+for (const [type, typeFiles] of Object.entries(grouped) as Array<
+    [Exclude<FileType, "unknown">, string[]]
+>) {
     if (typeFiles.length === 0) continue;
-    const { compressor, package: pkg } = selectCompressor(type as FileType);
+
+    const { compressor, package: pkg } = selectCompressor(type);
     try {
         await resolveCompressor(compressor);
     } catch {
         throw new Error(
             `Compressor for ${type} files not found. Run: npm install ${pkg}`
         );
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the auto-mode would fail if it finds unsupported file types and the @node-minify/no-compress package is not installed, which is a likely scenario. Ignoring unknown files makes the new feature more robust and user-friendly.

Medium
Validate paths under working directory
Suggestion Impact:The commit adds a workingDir variable from the "working-directory" input (defaulting to ".") and passes it as the third argument to validateOutputDir, matching the intent of the suggestion.

code diff:

     if (auto) {
         const patternsToValidate =
             patterns.length > 0 ? patterns : DEFAULT_PATTERNS;
-        validateOutputDir(outputDir, patternsToValidate);
+        const workingDir = getInput("working-directory") || ".";
+        validateOutputDir(outputDir, patternsToValidate, workingDir);
     }

Pass the workingDirectory to validateOutputDir to ensure path validation is
correct when the action runs in a non-default directory.

packages/action/src/inputs.ts [82-86]

 if (auto) {
     const patternsToValidate =
         patterns.length > 0 ? patterns : DEFAULT_PATTERNS;
-    validateOutputDir(outputDir, patternsToValidate);
+    validateOutputDir(outputDir, patternsToValidate, getInput("working-directory") || ".");
 }

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: This suggestion is a logical follow-up to the improvement in validateOutputDir. Passing the workingDirectory to the validation function is necessary for it to work correctly in all scenarios, ensuring paths are resolved consistently with the rest of the action's logic.

Medium
  • Update

Previous suggestions

✅ Suggestions up to commit a7a1c1f
CategorySuggestion                                                                                                                                    Impact
Possible issue
Import comment and annotation functions
Suggestion Impact:The commit adds imports for addAnnotations and postPRComment from ./annotations.ts and ./comment.ts, and removes the previous imports from ./reporters/*, aligning the entrypoint with the suggested fix to prevent runtime errors.

code diff:

+import { addAnnotations } from "./annotations.ts";
 import type { FileType } from "./autoDetect.ts";
 import { groupFilesByType, selectCompressor } from "./autoDetect.ts";
 import { runBenchmark } from "./benchmark.ts";
 import { checkThresholds } from "./checks.ts";
+import { postPRComment } from "./comment.ts";
 import { compareWithBase } from "./compare.ts";
 import { discoverFiles, generateOutputPath } from "./discover.ts";
 import { parseInputs, validateCompressor } from "./inputs.ts";
 import { runMinification } from "./minify.ts";
 import { setBenchmarkOutputs, setMinifyOutputs } from "./outputs.ts";
-import { addAnnotations } from "./reporters/annotations.ts";
-import { postPRComment } from "./reporters/comment.ts";

Add missing imports for postPRComment and addAnnotations to prevent runtime
errors in explicit mode.

packages/action/src/index.ts [17-18]

 import { compareWithBase } from "./compare.ts";
+import { postPRComment } from "./comment.ts";
+import { addAnnotations } from "./annotations.ts";
 import { discoverFiles, generateOutputPath } from "./discover.ts";

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion fixes a critical bug where the application would crash with a ReferenceError when attempting to post PR comments or add annotations, as the required functions were not imported.

High
Import performance API
Suggestion Impact:The commit adds `import { performance } from "node:perf_hooks";`, making the dependency explicit as suggested (though the exact placement differs).

code diff:

 import { mkdir, stat } from "node:fs/promises";
 import path from "node:path";
+import { performance } from "node:perf_hooks";
 import { info, setFailed, warning } from "@actions/core";

Import performance from node:perf_hooks to make the dependency on the
Performance Hooks API explicit.

packages/action/src/index.ts [7]

+import { performance } from "node:perf_hooks";
 import { mkdir, stat } from "node:fs/promises";

[Suggestion processed]

Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies the use of performance.now() without an explicit import, and while performance is global in recent Node.js versions, adding the import improves code clarity and robustness.

Low
General
Fix hidden-files glob pattern
Suggestion Impact:Updated the documentation entry for hidden files by removing the trailing space in the glob pattern (`**/.* ` -> `**/.*`).

code diff:

-- `**/.* ` (hidden files)
+- `**/.*` (hidden files)

Remove the trailing space from the /.* glob pattern in the documentation to
correctly match hidden files.**

docs/src/content/docs/github-action.md [39-45]

 **Excluded By Default**:
 - `**/node_modules/**`
 - `**/dist/**`, `**/build/**`, `**/.next/**`
 - `**/*.min.{js,css}` (already minified)
 - `**/__tests__/**` (test files)
-- `**/.* ` (hidden files)
+- `**/.*` (hidden files)
 - `**/*.d.ts` (TypeScript declarations)

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: This suggestion corrects a typo in the documentation's glob pattern for hidden files, which would cause a mismatch between the documented behavior and the actual implementation, preventing correct exclusion of dotfiles.

Medium
Use boolean type for dry-run input

Change the dry-run input in action.yml to use type: boolean and a boolean
default for better clarity and UI representation.

packages/action/action.yml [29-32]

 dry-run:
   description: "Preview mode - show what would be processed without minifying"
   required: false
-  default: "false"
+  type: boolean
+  default: false
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly recommends using type: boolean for the dry-run input in action.yml, which is a best practice that improves usability in the GitHub UI.

Low
Use boolean type for auto input

Change the auto input in action.yml to use type: boolean and a boolean default
for better clarity and UI representation.

packages/action/action.yml [15-18]

 auto:
   description: "Enable zero-config auto mode"
   required: false
-  default: "false"
+  type: boolean
+  default: false
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly recommends using type: boolean for the auto input in action.yml, which is a best practice that improves usability in the GitHub UI.

Low

@greptile-apps
Copy link

greptile-apps bot commented Jan 24, 2026

Greptile Overview

Greptile Summary

This PR adds zero-config auto mode to the GitHub Action, enabling automatic file discovery and minification without explicit input/output configuration. The implementation includes comprehensive security hardening for path handling and extensive test coverage.

Key Changes:

  • Auto Mode Implementation: New runAutoMode() function in index.ts orchestrates file discovery, type detection, compressor selection, and parallel processing (4 concurrent files)
  • File Discovery: Smart default patterns for common project structures (src/, app/, lib/) with standard ignores (node_modules, dist, *.min.*, etc.)
  • Security Hardening: Multiple layers of path traversal prevention including segment-based .. detection, absolute path rejection, and output directory validation
  • Type Detection: Automatic compressor selection based on file extensions (.js → terser, .css → lightningcss, .html → html-minifier, etc.)
  • Backward Compatibility: Explicit mode continues to work unchanged; new inputs are optional with sensible defaults

Test Coverage:

  • 85%+ code coverage with 173 passing tests
  • Comprehensive edge case testing including path traversal, dry-run mode, partial failures, and cross-platform scenarios
  • 5 new CI integration jobs for zero-config scenarios

Security Measures:

  • Input sanitization in inputs.ts prevents absolute paths and .. segments in output-dir
  • validateOutputDir() prevents output directory from overlapping with source patterns
  • generateOutputPath() normalizes paths and strips traversal sequences before joining with output directory

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation demonstrates excellent engineering practices with comprehensive security hardening, extensive test coverage (85%+, 173 tests), thorough edge case handling, and backward compatibility. The security measures address previous review feedback with multiple layers of path traversal prevention. All CI checks pass, and the code is well-documented with clear separation of concerns.
  • No files require special attention

Important Files Changed

Filename Overview
packages/action/src/index.ts Added zero-config auto mode orchestration with file discovery, parallel processing, and proper error handling
packages/action/src/discover.ts New file discovery module with path traversal sanitization and smart defaults
packages/action/src/validate.ts New validation module preventing output directory overlap with source patterns
packages/action/src/inputs.ts Added auto mode input parsing with path traversal prevention and output directory validation
packages/action/src/autoDetect.ts New file type detection and compressor selection logic for auto mode

Sequence Diagram

sequenceDiagram
    participant User
    participant run
    participant parseInputs
    participant runAutoMode
    participant discoverFiles
    participant validateOutputDir
    participant groupFilesByType
    participant resolveCompressor
    participant minify
    participant setMinifyOutputs
    
    User->>run: Trigger GitHub Action
    run->>parseInputs: Parse and validate inputs
    parseInputs->>parseInputs: Check if auto mode enabled
    
    alt Auto Mode Disabled
        parseInputs-->>parseInputs: Validate input/output required
    end
    
    parseInputs->>validateOutputDir: Validate output-dir path
    validateOutputDir->>validateOutputDir: Check for ".." segments
    validateOutputDir->>validateOutputDir: Check if absolute path
    validateOutputDir->>validateOutputDir: Check overlap with source patterns
    validateOutputDir-->>parseInputs: Validation result
    
    parseInputs-->>run: Return ActionInputs
    
    alt inputs.auto === true
        run->>runAutoMode: Execute auto mode
        runAutoMode->>discoverFiles: Discover files with patterns
        discoverFiles->>discoverFiles: Apply default patterns/ignores
        discoverFiles-->>runAutoMode: Return file list
        
        alt No files found
            runAutoMode->>setMinifyOutputs: Set empty outputs
            runAutoMode-->>run: Return
        end
        
        alt Dry run mode
            runAutoMode->>runAutoMode: Log files and exit
            runAutoMode-->>run: Return
        end
        
        runAutoMode->>groupFilesByType: Group files by extension
        groupFilesByType-->>runAutoMode: Return grouped files
        
        loop For each file type
            runAutoMode->>resolveCompressor: Check compressor availability
            resolveCompressor-->>runAutoMode: Compressor found or error
        end
        
        runAutoMode->>runAutoMode: Create output directory
        
        loop Process files in chunks of 4
            par Parallel processing within chunk
                runAutoMode->>minify: Minify file
                minify-->>runAutoMode: Return minified content
            end
        end
        
        runAutoMode->>setMinifyOutputs: Set results and totals
        runAutoMode-->>run: Complete
    else inputs.auto === false
        run->>run: Execute explicit mode (original flow)
    end
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 168 to 178
const fileType = grouped.js.includes(file)
? "js"
: grouped.css.includes(file)
? "css"
: grouped.html.includes(file)
? "html"
: grouped.json.includes(file)
? "json"
: grouped.svg.includes(file)
? "svg"
: "unknown";
Copy link

Choose a reason for hiding this comment

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

use detectFileType(file) instead of this complex nested ternary chain

Suggested change
const fileType = grouped.js.includes(file)
? "js"
: grouped.css.includes(file)
? "css"
: grouped.html.includes(file)
? "html"
: grouped.json.includes(file)
? "json"
: grouped.svg.includes(file)
? "svg"
: "unknown";
const fileType = detectFileType(file);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/action/src/index.ts
Line: 168:178

Comment:
use `detectFileType(file)` instead of this complex nested ternary chain

```suggestion
                const fileType = detectFileType(file);
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@srod srod force-pushed the feat/zero-config-action branch from baca889 to 3554ebf Compare January 24, 2026 18:11
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 30 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="docs/src/content/docs/github-action.md">

<violation number="1" location="docs/src/content/docs/github-action.md:44">
P3: The dotfiles ignore glob includes a trailing space, so it won’t match hidden files as intended. Remove the whitespace so the pattern is valid.</violation>
</file>

<file name="packages/action/src/inputs.ts">

<violation number="1" location="packages/action/src/inputs.ts:72">
P2: Auto mode skips outputDir validation when patterns aren’t provided, even though default patterns are still used. This allows output directories under `src/`/`app/` etc. to be rediscovered and re-minified. Validate against the effective patterns in auto mode, not only custom patterns.</violation>
</file>

<file name="packages/action/src/validate.ts">

<violation number="1" location="packages/action/src/validate.ts:32">
P2: The validation compares raw strings without resolving relative segments. An output-dir like "../src" (or on Windows, a path with backslashes) bypasses this check, so the output can still overlap with the source tree and trigger the infinite-loop risk this function is meant to prevent.</violation>
</file>

<file name="packages/action/src/index.ts">

<violation number="1" location="packages/action/src/index.ts:81">
P2: Auto mode never checks `fail-on-increase`/`min-reduction` thresholds, so the action can report success even when total reduction violates configured thresholds.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@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: 3

🤖 Fix all issues with AI agents
In `@packages/action/src/discover.ts`:
- Around line 69-81: The generateOutputPath function currently uses
path.join(outputDir, inputFile) which will ignore outputDir when inputFile is
absolute; update generateOutputPath to detect absolute input paths (using
path.isAbsolute or equivalent) and convert them to a path relative to the
repository or to outputDir (e.g., via path.relative or stripping leading path
separators) before joining so the resultant path always resides under outputDir;
ensure you still preserve the input file's directory structure and handle edge
cases like inputFile === path.sep or empty relative results in the function
generateOutputPath.

In `@packages/action/src/index.ts`:
- Around line 112-128: runAutoMode currently logs a warning and returns when
discoverFiles finds no files, which leaves expected action outputs unset; update
runAutoMode to explicitly set the action outputs to sensible defaults (e.g.,
file count = 0, processed files = empty list, summary/message = "No files
found") before returning and also generate the same summary/notice that would
appear when files are processed so downstream consumers receive consistent
outputs; locate and update the runAutoMode function and the places that normally
call core.setOutput / createSummary (or the action's output helpers) to emit
these default values when files.length === 0.

In `@packages/action/src/validate.ts`:
- Around line 7-14: The JSDoc for the exported function that validates the
output directory (the function taking parameters outputDir and patterns) is
missing a `@returns` tag; update its comment to include an `@returns` describing the
function's return type and behavior (e.g., "void" or "void — throws Error if
outputDir is inside any source pattern"), so the exported JSDoc explicitly
documents the return value and exception behavior.
♻️ Duplicate comments (5)
docs/src/content/docs/github-action.md (1)

40-45: Remove trailing space in dotfiles ignore glob.

The code span contains a trailing space, which breaks the glob and triggers MD038.

🧹 Proposed fix
- - `**/.* ` (hidden files)
+ - `**/.*` (hidden files)
packages/action/src/validate.ts (1)

23-33: Normalize paths before overlap checks.

Raw string comparison can be bypassed with .. segments or Windows separators, reintroducing the infinite-loop risk.

🔒 Proposed fix
+import path from "node:path";
+
 export function validateOutputDir(outputDir: string, patterns: string[]): void {
     for (const pattern of patterns) {
         // Extract base directory from glob pattern (everything before first *)
         const baseDir = pattern.split("*")[0];
@@
         if (baseDir) {
-            const baseDirNormalized = baseDir.endsWith("/")
-                ? baseDir.slice(0, -1)
-                : baseDir;
-            const outputDirNormalized = outputDir.endsWith("/")
-                ? outputDir.slice(0, -1)
-                : outputDir;
-
-            if (
-                outputDirNormalized === baseDirNormalized ||
-                outputDirNormalized.startsWith(`${baseDirNormalized}/`)
-            ) {
+            const baseDirNormalized = baseDir.endsWith("/")
+                ? baseDir.slice(0, -1)
+                : baseDir;
+            const outputDirNormalized = outputDir.endsWith("/")
+                ? outputDir.slice(0, -1)
+                : outputDir;
+            const baseDirResolved = path.resolve(baseDirNormalized);
+            const outputDirResolved = path.resolve(outputDirNormalized);
+            const relative = path.relative(baseDirResolved, outputDirResolved);
+
+            if (
+                relative === "" ||
+                (!relative.startsWith("..") && !path.isAbsolute(relative))
+            ) {
                 throw new Error(
                     `output-dir cannot be inside source pattern: '${outputDir}' overlaps with '${pattern}'`
                 );
             }
         }
     }
 }
packages/action/src/inputs.ts (1)

72-74: Validate outputDir against effective patterns even when none are provided.
Auto mode still uses default discovery patterns, but validation is skipped when patterns is empty — so an outputDir under src//app/ can be rediscovered.

🔧 Proposed fix
-import { validateOutputDir } from "./validate.ts";
+import { DEFAULT_PATTERNS } from "./discover.ts";
+import { validateOutputDir } from "./validate.ts";
@@
-    if (auto && patterns.length > 0) {
-        validateOutputDir(outputDir, patterns);
-    }
+    if (auto) {
+        const effectivePatterns =
+            patterns.length > 0 ? patterns : DEFAULT_PATTERNS;
+        validateOutputDir(outputDir, effectivePatterns);
+    }
packages/action/src/index.ts (2)

168-178: Use detectFileType(file) instead of nested ternary chain.

This complex nested ternary is hard to read and duplicates logic already available in detectFileType. The function is already imported from ./autoDetect.ts.

Proposed fix
-                const fileType = grouped.js.includes(file)
-                    ? "js"
-                    : grouped.css.includes(file)
-                      ? "css"
-                      : grouped.html.includes(file)
-                        ? "html"
-                        : grouped.json.includes(file)
-                          ? "json"
-                          : grouped.svg.includes(file)
-                            ? "svg"
-                            : "unknown";
+                const fileType = detectFileType(file);

275-283: Auto mode skips threshold validation.

runExplicitMode calls checkThresholds (line 96) and fails the action if thresholds are violated. runAutoMode skips this check entirely, meaning fail-on-increase and min-reduction inputs are silently ignored in auto mode.

Proposed fix
     setMinifyOutputs(minifyResult);

     if (inputs.reportSummary) {
         await generateSummary(minifyResult);
     }

+    const thresholdError = checkThresholds(totalReduction, inputs);
+    if (thresholdError) {
+        setFailed(thresholdError);
+        return;
+    }
+
     info(
         `✅ Auto mode complete! Processed ${allResults.length} files with ${totalReduction.toFixed(1)}% total reduction in ${totalTimeMs}ms`
     );
🧹 Nitpick comments (8)
packages/utils/__tests__/wildcards-ignore.test.ts (2)

9-10: Import paths should use .ts extension per coding guidelines.

The local imports use .js extension, but the coding guidelines specify using .ts extension for local import paths.

Proposed fix
-import type { WildcardOptions } from "../src/wildcards.js";
-import { DEFAULT_IGNORES, wildcards } from "../src/wildcards.js";
+import type { WildcardOptions } from "../src/wildcards.ts";
+import { DEFAULT_IGNORES, wildcards } from "../src/wildcards.ts";

101-113: Test name doesn't match the test behavior.

The test is named "should use DEFAULT_IGNORES when no ignore option provided" but actually tests explicit usage of DEFAULT_IGNORES. The comment on line 104 correctly states the intent ("Test that we can use DEFAULT_IGNORES explicitly"), but the test name is misleading.

Consider renaming to clarify the actual behavior being tested:

Proposed fix
-    test("should use DEFAULT_IGNORES when no ignore option provided", () => {
+    test("should allow explicit use of DEFAULT_IGNORES as ignore option", () => {
packages/utils/src/wildcards.ts (1)

45-64: Missing @returns JSDoc tag on exported function.

Per coding guidelines, all exported functions must have JSDoc comments with @param and @returns tags. The wildcards function is missing the @returns tag.

Proposed fix
 /**
  * Handle wildcards in a path, get the real path of each file.
  * `@param` input - Path with wildcards
  * `@param` options - Options object or string publicFolder for backward compatibility
+ * `@returns` Object containing the resolved input file paths
  */
 export function wildcards(
packages/action/src/benchmark.ts (1)

48-53: Minor inconsistency: use local input variable in error message.

Line 50 uses inputs.input while the rest of the function uses the destructured input variable. For consistency, use the local variable.

Suggested fix
     if (!fileResult) {
         throw new Error(
-            `Benchmark failed: no results for input "${inputs.input}". ` +
+            `Benchmark failed: no results for input "${input}". ` +
                 "Check that the file exists and at least one compressor succeeded."
         );
     }
.github/workflows/test-action.yml (1)

104-108: Actionlint false positives - new inputs are defined in action.yml.

The static analysis errors for undefined inputs (auto, patterns, output-dir, ignore, dry-run) are false positives. These inputs are correctly defined in action.yml (lines 15-32) in this PR. The linter likely ran against a stale version.

However, these explicit parameters are redundant for explicit-mode tests since auto: 'false' and dry-run: 'false' are already the defaults. Consider removing them to reduce noise.

Optional: Remove redundant defaults
       - name: Minify with terser
         id: minify-terser
         uses: ./
         with:
           input: "test-action/input.js"
           output: "test-action/output.min.js"
           compressor: "terser"
-          auto: 'false'
-          patterns: ''
-          output-dir: 'dist'
-          ignore: ''
-          dry-run: 'false'
packages/action/src/index.ts (3)

153-153: Magic number for concurrency limit.

The chunk size 4 is hardcoded without explanation. Consider extracting this to a named constant or making it configurable for clarity and future flexibility.

Suggested improvement
+const AUTO_MODE_CONCURRENCY = 4;
+
 // Process files with concurrency limit
 const allResults: FileResult[] = [];
 const failures: { file: string; error: string }[] = [];
-const chunks = chunkArray(files, 4);
+const chunks = chunkArray(files, AUTO_MODE_CONCURRENCY);

180-184: Redundant compressor resolution per file.

resolveCompressor is called twice for the same compressor: once in the validation loop (lines 133-143) and again here for each file. Since compressor resolution may involve async I/O, consider caching resolved compressors by type to avoid redundant lookups.

Suggested optimization
+    // Pre-resolve compressors for each file type
+    const resolvedCompressors = new Map<FileType, { compressorFn: unknown; label: string }>();
     for (const [type, typeFiles] of Object.entries(grouped)) {
         if (typeFiles.length === 0) continue;
         const { compressor, package: pkg } = selectCompressor(type as FileType);
         try {
-            await resolveCompressor(compressor);
+            const resolved = await resolveCompressor(compressor);
+            resolvedCompressors.set(type as FileType, {
+                compressorFn: resolved.compressor,
+                label: resolved.label,
+            });
         } catch {
             throw new Error(
                 `Compressor for ${type} files not found. Run: npm install ${pkg}`
             );
         }
     }

Then use the cached value in the file processing loop instead of calling resolveCompressor again.


217-217: Unnecessary type assertion.

The as FileResult & { compressor: string } assertion suggests a type mismatch. If FileResult already includes compressor, the assertion is redundant. If not, consider extending FileResult in the type definition rather than using inline intersection.

Comment on lines 7 to 17
/**
* Validates that the output directory is not inside any source pattern.
* Prevents infinite loop risk when output overlaps with source files.
*
* @param outputDir - The output directory path
* @param patterns - Array of source file patterns (glob patterns)
* @throws Error if outputDir is inside any source pattern
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add @returns to exported JSDoc.

Exported functions must include @returns in JSDoc.

📝 Proposed fix
 /**
  * Validates that the output directory is not inside any source pattern.
  * Prevents infinite loop risk when output overlaps with source files.
  *
  * `@param` outputDir - The output directory path
  * `@param` patterns - Array of source file patterns (glob patterns)
+ * `@returns` void
  * `@throws` Error if outputDir is inside any source pattern
  */
As per coding guidelines.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Validates that the output directory is not inside any source pattern.
* Prevents infinite loop risk when output overlaps with source files.
*
* @param outputDir - The output directory path
* @param patterns - Array of source file patterns (glob patterns)
* @throws Error if outputDir is inside any source pattern
*/
/**
* Validates that the output directory is not inside any source pattern.
* Prevents infinite loop risk when output overlaps with source files.
*
* `@param` outputDir - The output directory path
* `@param` patterns - Array of source file patterns (glob patterns)
* `@returns` void
* `@throws` Error if outputDir is inside any source pattern
*/
🤖 Prompt for AI Agents
In `@packages/action/src/validate.ts` around lines 7 - 14, The JSDoc for the
exported function that validates the output directory (the function taking
parameters outputDir and patterns) is missing a `@returns` tag; update its comment
to include an `@returns` describing the function's return type and behavior (e.g.,
"void" or "void — throws Error if outputDir is inside any source pattern"), so
the exported JSDoc explicitly documents the return value and exception behavior.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/action/src/inputs.ts">

<violation number="1" location="packages/action/src/inputs.ts:64">
P3: The path traversal guard is too broad: `includes("..")` blocks legitimate directory names containing `..` even when they are not traversal segments. This can reject valid `output-dir` values. Check for `..` path segments instead of substring matches.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/action/src/index.ts`:
- Around line 38-43: The chunkArray function lacks validation for the size
parameter and will loop infinitely when size is zero or behave incorrectly for
negative values; add a defensive check at the start of chunkArray(size) to
validate that size is a positive integer (e.g., if (!Number.isInteger(size) ||
size <= 0) throw new TypeError or return a clear error), so the function fails
fast with an informative message; keep the rest of the logic (the for loop and
slicing) unchanged and reference the chunkArray<T>(array: T[], size: number)
signature when making the change.
♻️ Duplicate comments (2)
packages/action/src/index.ts (2)

121-124: Set outputs when no files are discovered.
Currently auto mode returns without outputs; downstream steps won’t see expected values.

🛠️ Suggested default outputs
     if (files.length === 0) {
         warning("No files found matching patterns");
+        setMinifyOutputs({
+            files: [],
+            compressor: "auto",
+            totalOriginalSize: 0,
+            totalMinifiedSize: 0,
+            totalReduction: 0,
+            totalTimeMs: 0,
+        });
         return;
     }

267-284: Auto mode should enforce reduction thresholds.
Explicit mode calls checkThresholds, but auto mode doesn’t—so fail-on-increase / min-reduction can be bypassed.

🛠️ Suggested threshold check
     if (inputs.reportSummary) {
         await generateSummary(minifyResult);
     }
 
+    const thresholdError = checkThresholds(minifyResult.totalReduction, inputs);
+    if (thresholdError) {
+        setFailed(thresholdError);
+        return;
+    }
+
     info(
         `✅ Auto mode complete! Processed ${allResults.length} files with ${totalReduction.toFixed(1)}% total reduction in ${totalTimeMs}ms`
     );

@srod srod force-pushed the feat/zero-config-action branch from 09e7477 to f74b577 Compare January 24, 2026 20:19
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/action/src/inputs.ts">

<violation number="1" location="packages/action/src/inputs.ts:64">
P2: The new validation normalizes the path before checking for "..", which strips mid-path traversal segments (e.g., "dist/../build"), letting disallowed ".." segments slip through. Validate the raw segments instead of the normalized path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@srod
Copy link
Owner Author

srod commented Jan 24, 2026

@greptile

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/action/src/index.ts`:
- Around line 63-67: The JSDoc for exported functions is missing `@returns` tags;
update the JSDoc for runExplicitMode, runAutoMode, and run to include an
`@returns` description that matches each function's actual return type (e.g.,
Promise<void> or the specific value returned) and a brief description of what
the caller receives; ensure the new `@returns` tag follows the existing `@param`
tags and mirrors the function signatures in index.ts so doc comments remain
accurate for exported APIs.
🧹 Nitpick comments (2)
packages/action/src/index.ts (2)

150-160: Preserve underlying error context when a compressor fails to resolve.
The bare catch drops the original error details. Capture it and use instanceof Error to append useful context. As per coding guidelines, error handling should use err instanceof Error.

🔧 Suggested adjustment
-        } catch {
-            throw new Error(
-                `Compressor for ${type} files not found. Run: npm install ${pkg}`
-            );
-        }
+        } catch (err) {
+            const detail =
+                err instanceof Error && err.message
+                    ? ` (${err.message})`
+                    : "";
+            throw new Error(
+                `Compressor for ${type} files not found. Run: npm install ${pkg}${detail}`
+            );
+        }

258-273: Auto-mode totalTimeMs likely overstates real runtime.
Summing per-file timeMs inflates the total when files are processed in parallel. Consider measuring wall-clock time around the whole auto-mode processing and reporting that instead (or alongside).

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/action/src/discover.ts`:
- Around line 80-91: Normalize the input path (use path.normalize on inputFile /
cleanInput) before stripping traversal sequences so internal ".." segments are
resolved, then remove any leading "../" or "..\" sequences and use
path.join(outputDir, cleanInput); finally verify the final path is inside
outputDir (e.g., with path.relative(outputDir, finalPath) and reject or throw if
it starts with ".." or is absolute) to block any nested traversal escaping;
reference variables: cleanInput, inputFile, outputDir, and path.join.
🧹 Nitpick comments (1)
packages/action/__tests__/discover.test.ts (1)

185-215: Add a regression test for nested traversal.
If you harden generateOutputPath, add a case like foo/../../bar.js to ensure it can’t escape the output directory.

✅ Suggested test
     test("should sanitize directory traversal in input path", () => {
         const result = generateOutputPath("../../test.log", "dist");
         expect(result).toBe(path.join("dist", "test.log"));
     });
+
+    test("should sanitize nested traversal in input path", () => {
+        const result = generateOutputPath("foo/../../bar.js", "dist");
+        expect(result).toBe(path.join("dist", "bar.js"));
+    });

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/action/src/validate.ts`:
- Around line 20-35: The loop over patterns in validate.ts incorrectly skips
patterns whose baseDir is empty (e.g., patterns starting with "**/") because
baseDir is falsy; update the logic in the for (const pattern of patterns) loop
so that baseDir is normalized to a valid directory (for example use
path.resolve(baseDir || ".") or treat empty baseDir as the repository/workspace
root) before computing absBaseDir and rel, ensuring the overlap check that
computes isInside (using absBaseDir, absOutputDir and
path.relative/path.isAbsolute) runs for every pattern rather than being gated on
baseDir truthiness.
♻️ Duplicate comments (1)
packages/action/src/validate.ts (1)

9-16: Add missing @returns in exported JSDoc.
This exported function is missing a @returns tag, which is required for packages/*/src/**/*.ts. As per coding guidelines, please add it.

📝 Suggested update
 /**
  * Validates that the output directory is not inside any source pattern.
  * Prevents infinite loop risk when output overlaps with source files.
  *
  * `@param` outputDir - The output directory path
  * `@param` patterns - Array of source file patterns (glob patterns)
+ * `@returns` void
  * `@throws` Error if outputDir is inside any source pattern
  */
🧹 Nitpick comments (1)
packages/action/src/index.ts (1)

150-205: Cache per‑type compressor resolution to avoid repeated work.
You already group by type, but still call detectFileType/selectCompressor/resolveCompressor for each file. Caching resolved compressors once per type would reduce repeated module resolution and per-file overhead.

♻️ Possible refactor sketch
+    const compressorCache = new Map<
+        FileType,
+        { compressor: unknown; label: string; type?: string }
+    >();

     // Check compressor availability first
     for (const [type, typeFiles] of Object.entries(grouped)) {
         if (typeFiles.length === 0) continue;
-        const { compressor, package: pkg } = selectCompressor(type as FileType);
+        const { compressor, package: pkg, type: minifyType } = selectCompressor(
+            type as FileType
+        );
         try {
-            await resolveCompressor(compressor);
+            const resolved = await resolveCompressor(compressor);
+            compressorCache.set(type as FileType, {
+                ...resolved,
+                type: minifyType,
+            });
         } catch {
             throw new Error(
                 `Compressor for ${type} files not found. Run: npm install ${pkg}`
             );
         }
     }

             chunk.map(async (file) => {
                 const outputPath = generateOutputPath(file, inputs.outputDir);
                 const inputPath = path.join(inputs.workingDirectory, file);
                 const fullOutputPath = path.join(
                     inputs.workingDirectory,
                     outputPath
                 );

                 // Ensure output directory exists
                 await mkdir(path.dirname(fullOutputPath), { recursive: true });

-                const fileType = detectFileType(file);
-
-                const { compressor, type } = selectCompressor(
-                    fileType as FileType
-                );
-                const { compressor: compressorFn, label } =
-                    await resolveCompressor(compressor);
+                const fileType = detectFileType(file) as FileType;
+                const cached = compressorCache.get(fileType);
+                if (!cached) {
+                    throw new Error(`Unsupported file type: ${file}`);
+                }
+                const { compressor: compressorFn, label, type } = cached;

Comment on lines 20 to 45
for (const pattern of patterns) {
// Extract base directory from glob pattern (everything before first *)
const baseDir = pattern.split("*")[0];

if (baseDir) {
const absBaseDir = path.resolve(baseDir);

// Check if output dir is same as or inside base dir
const rel = path.relative(absBaseDir, absOutputDir);

// If rel is empty, they are same.
// If rel does not start with '..', output is inside base.
// Exception: check if rel is absolute (different drives on Windows)
const isInside =
rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the validate.ts file
find . -path "*/packages/action/src/validate.ts" -type f

Repository: srod/node-minify

Length of output: 93


🏁 Script executed:

# Read the file to see the actual code
cat -n packages/action/src/validate.ts

Repository: srod/node-minify

Length of output: 1840


🏁 Script executed:

# Let's also check the full context of the validation function to understand its purpose
head -100 packages/action/src/validate.ts | cat -n

Repository: srod/node-minify

Length of output: 1840


Leading **/ globs bypass overlap checks, skipping validation entirely.
When a pattern starts with **/ (e.g., **/*.js), baseDir becomes an empty string, which is falsy, so the validation block never executes. This allows output directories that still match the pattern to pass unchecked, defeating the infinite loop prevention. Patterns with no static prefix must also be handled to ensure the overlap check always runs.

🛠️ Suggested fix
-        const baseDir = pattern.split("*")[0];
+        const baseDir = pattern.split("*")[0];
+        const hasDeepGlob =
+            pattern.startsWith("**/") || pattern.startsWith("**\\");
+        const effectiveBaseDir = baseDir || (hasDeepGlob ? "." : "");

-        if (baseDir) {
-            const absBaseDir = path.resolve(baseDir);
+        if (effectiveBaseDir) {
+            const absBaseDir = path.resolve(effectiveBaseDir);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const pattern of patterns) {
// Extract base directory from glob pattern (everything before first *)
const baseDir = pattern.split("*")[0];
if (baseDir) {
const absBaseDir = path.resolve(baseDir);
// Check if output dir is same as or inside base dir
const rel = path.relative(absBaseDir, absOutputDir);
// If rel is empty, they are same.
// If rel does not start with '..', output is inside base.
// Exception: check if rel is absolute (different drives on Windows)
const isInside =
rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
for (const pattern of patterns) {
// Extract base directory from glob pattern (everything before first *)
const baseDir = pattern.split("*")[0];
const hasDeepGlob =
pattern.startsWith("**/") || pattern.startsWith("**\\");
const effectiveBaseDir = baseDir || (hasDeepGlob ? "." : "");
if (effectiveBaseDir) {
const absBaseDir = path.resolve(effectiveBaseDir);
// Check if output dir is same as or inside base dir
const rel = path.relative(absBaseDir, absOutputDir);
// If rel is empty, they are same.
// If rel does not start with '..', output is inside base.
// Exception: check if rel is absolute (different drives on Windows)
const isInside =
rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
🤖 Prompt for AI Agents
In `@packages/action/src/validate.ts` around lines 20 - 35, The loop over patterns
in validate.ts incorrectly skips patterns whose baseDir is empty (e.g., patterns
starting with "**/") because baseDir is falsy; update the logic in the for
(const pattern of patterns) loop so that baseDir is normalized to a valid
directory (for example use path.resolve(baseDir || ".") or treat empty baseDir
as the repository/workspace root) before computing absBaseDir and rel, ensuring
the overlap check that computes isInside (using absBaseDir, absOutputDir and
path.relative/path.isAbsolute) runs for every pattern rather than being gated on
baseDir truthiness.

@srod srod force-pushed the feat/zero-config-action branch from 8b8405d to 3e30b66 Compare January 24, 2026 23:14
Copy link
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/utils/src/wildcards.ts (1)

89-109: Preserve publicFolder when no wildcards are present.

When hasWildcards is false, the code returns the original input array, which drops the publicFolder prefix. That breaks explicit paths in publicFolder mode.

🐛 Proposed fix
-    const processedPaths = hasWildcards
-        ? fg.globSync(inputWithPublicFolder, { ignore: options.ignore })
-        : input;
+    const processedPaths = hasWildcards
+        ? fg.globSync(inputWithPublicFolder, { ignore: options.ignore })
+        : inputWithPublicFolder;
🤖 Fix all issues with AI agents
In `@packages/utils/src/wildcards.ts`:
- Around line 45-64: Update the JSDoc for the exported function wildcards to
include an `@returns` tag that describes the function's return value (it delegates
to wildcardsArray or wildcardsString and ultimately returns an array of resolved
file paths, i.e., string[]). Edit the comment block above the wildcards function
to add a concise `@returns` line explaining it returns an array of matched/real
file paths (string[]), and ensure the wording references that it handles single
string or array input and returns resolved paths from
wildcardsArray/wildcardsString.
🧹 Nitpick comments (1)
packages/action/src/benchmark.ts (1)

48-53: Minor inconsistency: Use destructured input in error message.

For consistency with the rest of the function, use the destructured input variable instead of inputs.input in the error message at line 50.

♻️ Suggested fix
     const fileResult = result.files[0];
     if (!fileResult) {
         throw new Error(
-            `Benchmark failed: no results for input "${inputs.input}". ` +
+            `Benchmark failed: no results for input "${input}". ` +
                 "Check that the file exists and at least one compressor succeeded."
         );
     }

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/utils/src/wildcards.ts`:
- Around line 102-104: The processedPaths assignment in the wildcards function
uses inputWithPublicFolder only when hasWildcards is true, which ignores
options.publicFolder for non-wildcard inputs; change the logic so processedPaths
is inputWithPublicFolder when hasWildcards is false as well (i.e., use
inputWithPublicFolder instead of input), or if backward-compatibility is
required, add a clear comment in the wildcards function explaining the
intentional behavior; locate the processedPaths variable and
inputWithPublicFolder/hasWildcards references to implement the change or add the
explanatory comment.

@srod
Copy link
Owner Author

srod commented Jan 25, 2026

@greptile

@srod
Copy link
Owner Author

srod commented Jan 25, 2026

/review

@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Incorrect Metrics

Auto mode aggregates totalTimeMs by summing per-file durations even though files are processed concurrently, which can significantly over-report elapsed time and mislead summaries/thresholds. Consider tracking wall-clock time for the whole auto run (and optionally also reporting summed CPU time separately if needed).

    // Process files with concurrency limit
    const allResults: FileResult[] = [];
    const failures: { file: string; error: string }[] = [];
    const chunks = chunkArray(files, 4);

    for (const chunk of chunks) {
        const results = await Promise.allSettled(
            chunk.map(async (file) => {
                const outputPath = generateOutputPath(file, inputs.outputDir);
                const inputPath = path.join(inputs.workingDirectory, file);
                const fullOutputPath = path.join(
                    inputs.workingDirectory,
                    outputPath
                );

                // Ensure output directory exists
                await mkdir(path.dirname(fullOutputPath), { recursive: true });

                const fileType = detectFileType(file);

                const { compressor, type } = selectCompressor(
                    fileType as FileType
                );
                const { compressor: compressorFn, label } =
                    await resolveCompressor(compressor);

                const originalSize = await getFileSize(inputPath);
                const startTime = performance.now();

                await minify({
                    compressor: compressorFn,
                    input: inputPath,
                    output: fullOutputPath,
                    ...(type && { type }),
                });

                const endTime = performance.now();
                const timeMs = Math.round(endTime - startTime);
                const minifiedSize = await getFileSize(fullOutputPath);
                const reduction =
                    originalSize > 0
                        ? ((originalSize - minifiedSize) / originalSize) * 100
                        : 0;

                let gzipSize: number | undefined;
                if (inputs.includeGzip) {
                    gzipSize = await getFilesizeGzippedRaw(fullOutputPath);
                }

                return {
                    file,
                    originalSize,
                    minifiedSize,
                    reduction,
                    gzipSize,
                    timeMs,
                    compressor: label,
                } as FileResult & { compressor: string };
            })
        );

        for (const [i, result] of results.entries()) {
            const file = chunk[i];
            if (result.status === "fulfilled") {
                allResults.push(result.value);
            } else if (file !== undefined) {
                failures.push({
                    file,
                    error:
                        result.reason instanceof Error
                            ? result.reason.message
                            : String(result.reason),
                });
            }
        }
    }

    // Report results
    if (failures.length > 0) {
        warning(`${failures.length} files failed to minify:`);
        for (const { file, error } of failures) {
            warning(`  - ${file}: ${error}`);
        }
    }

    if (allResults.length === 0) {
        setFailed("All files failed to minify");
        return;
    }

    // Generate summary using existing function (stub for now - Task 6 will implement generateAutoModeSummary)
    const totalOriginalSize = allResults.reduce(
        (sum, r) => sum + r.originalSize,
        0
    );
    const totalMinifiedSize = allResults.reduce(
        (sum, r) => sum + r.minifiedSize,
        0
    );
    const totalReduction =
        totalOriginalSize > 0
            ? ((totalOriginalSize - totalMinifiedSize) / totalOriginalSize) *
              100
            : 0;
    const totalTimeMs = allResults.reduce((sum, r) => sum + r.timeMs, 0);

    const minifyResult: MinifyResult = {
        files: allResults,
        compressor: "auto",
        totalOriginalSize,
        totalMinifiedSize,
        totalReduction,
        totalTimeMs,
    };

    setMinifyOutputs(minifyResult);

    if (inputs.reportSummary) {
        await generateSummary(minifyResult);
    }

    const thresholdError = checkThresholds(minifyResult.totalReduction, inputs);
    if (thresholdError) {
        setFailed(thresholdError);
        return;
    }

    info(
        `✅ Auto mode complete! Processed ${allResults.length} files with ${totalReduction.toFixed(1)}% total reduction in ${totalTimeMs}ms`
    );
}
Missing Feature Parity

Auto mode does not implement PR commenting and annotations (explicit mode does). If this is intentional, it should be explicitly documented/guarded; otherwise consider adding the same reporting steps (or clearly disabling them) to avoid surprising behavior when users enable these inputs in auto mode.

export async function runAutoMode(inputs: ActionInputs): Promise<void> {
    const files = discoverFiles({
        patterns: inputs.patterns,
        ignore: inputs.additionalIgnore,
        workingDirectory: inputs.workingDirectory,
        dryRun: inputs.dryRun,
    });

    if (files.length === 0) {
        warning("No files found matching patterns");
        setMinifyOutputs({
            files: [],
            compressor: "auto",
            totalOriginalSize: 0,
            totalMinifiedSize: 0,
            totalReduction: 0,
            totalTimeMs: 0,
        });
        return;
    }

    if (inputs.dryRun) {
        info(`[dry-run] Would process ${files.length} files`);
        return;
    }

    const grouped = groupFilesByType(files);

    // Check compressor availability first
    for (const [type, typeFiles] of Object.entries(grouped)) {
        if (typeFiles.length === 0) continue;
        const { compressor, package: pkg } = selectCompressor(type as FileType);
        try {
            await resolveCompressor(compressor);
        } catch {
            throw new Error(
                `Compressor for ${type} files not found. Run: npm install ${pkg}`
            );
        }
    }

    // Create output directory
    await mkdir(path.join(inputs.workingDirectory, inputs.outputDir), {
        recursive: true,
    });

    // Process files with concurrency limit
    const allResults: FileResult[] = [];
    const failures: { file: string; error: string }[] = [];
    const chunks = chunkArray(files, 4);

    for (const chunk of chunks) {
        const results = await Promise.allSettled(
            chunk.map(async (file) => {
                const outputPath = generateOutputPath(file, inputs.outputDir);
                const inputPath = path.join(inputs.workingDirectory, file);
                const fullOutputPath = path.join(
                    inputs.workingDirectory,
                    outputPath
                );

                // Ensure output directory exists
                await mkdir(path.dirname(fullOutputPath), { recursive: true });

                const fileType = detectFileType(file);

                const { compressor, type } = selectCompressor(
                    fileType as FileType
                );
                const { compressor: compressorFn, label } =
                    await resolveCompressor(compressor);

                const originalSize = await getFileSize(inputPath);
                const startTime = performance.now();

                await minify({
                    compressor: compressorFn,
                    input: inputPath,
                    output: fullOutputPath,
                    ...(type && { type }),
                });

                const endTime = performance.now();
                const timeMs = Math.round(endTime - startTime);
                const minifiedSize = await getFileSize(fullOutputPath);
                const reduction =
                    originalSize > 0
                        ? ((originalSize - minifiedSize) / originalSize) * 100
                        : 0;

                let gzipSize: number | undefined;
                if (inputs.includeGzip) {
                    gzipSize = await getFilesizeGzippedRaw(fullOutputPath);
                }

                return {
                    file,
                    originalSize,
                    minifiedSize,
                    reduction,
                    gzipSize,
                    timeMs,
                    compressor: label,
                } as FileResult & { compressor: string };
            })
        );

        for (const [i, result] of results.entries()) {
            const file = chunk[i];
            if (result.status === "fulfilled") {
                allResults.push(result.value);
            } else if (file !== undefined) {
                failures.push({
                    file,
                    error:
                        result.reason instanceof Error
                            ? result.reason.message
                            : String(result.reason),
                });
            }
        }
    }

    // Report results
    if (failures.length > 0) {
        warning(`${failures.length} files failed to minify:`);
        for (const { file, error } of failures) {
            warning(`  - ${file}: ${error}`);
        }
    }

    if (allResults.length === 0) {
        setFailed("All files failed to minify");
        return;
    }

    // Generate summary using existing function (stub for now - Task 6 will implement generateAutoModeSummary)
    const totalOriginalSize = allResults.reduce(
        (sum, r) => sum + r.originalSize,
        0
    );
    const totalMinifiedSize = allResults.reduce(
        (sum, r) => sum + r.minifiedSize,
        0
    );
    const totalReduction =
        totalOriginalSize > 0
            ? ((totalOriginalSize - totalMinifiedSize) / totalOriginalSize) *
              100
            : 0;
    const totalTimeMs = allResults.reduce((sum, r) => sum + r.timeMs, 0);

    const minifyResult: MinifyResult = {
        files: allResults,
        compressor: "auto",
        totalOriginalSize,
        totalMinifiedSize,
        totalReduction,
        totalTimeMs,
    };

    setMinifyOutputs(minifyResult);

    if (inputs.reportSummary) {
        await generateSummary(minifyResult);
    }

    const thresholdError = checkThresholds(minifyResult.totalReduction, inputs);
    if (thresholdError) {
        setFailed(thresholdError);
        return;
    }

    info(
        `✅ Auto mode complete! Processed ${allResults.length} files with ${totalReduction.toFixed(1)}% total reduction in ${totalTimeMs}ms`
    );
}
Dead Code / API Mismatch

generateAutoModeSummary is introduced to handle grouped summaries, but auto mode still calls generateSummary and constructs a single MinifyResult, while generateAutoModeSummary expects MinifyResult[]. Verify intended API and wire it into auto mode (or remove/adjust it) to avoid unused or inconsistent reporting behavior.

/**
 * Generate a GitHub Actions summary for auto mode results, grouped by file type.
 *
 * Creates separate tables for each file type (JS, CSS, HTML, etc.), including
 * original/minified sizes and reduction percentages, and appends a grand total.
 *
 * @param results - Array of minification results to aggregate and display
 * @param inputs - Action inputs containing configuration such as includeGzip
 */
export async function generateAutoModeSummary(
    results: MinifyResult[],
    inputs: ActionInputs
): Promise<void> {
    if (results.length === 0) {
        await summary.addRaw("No files were processed.").write();
        return;
    }

    const allFiles = results.flatMap((r) => r.files);
    const groups: Record<FileType, FileResult[]> = {
        js: [],
        css: [],
        html: [],
        json: [],
        svg: [],
        unknown: [],
    };

    for (const file of allFiles) {
        groups[detectFileType(file.file)].push(file);
    }

    let totalOriginal = 0;
    let totalMinified = 0;

    summary.addHeading("📦 node-minify Auto Mode Results", 2);

    for (const type of Object.keys(groups) as FileType[]) {
        const files = groups[type];
        if (files.length === 0) continue;

        const emoji = getTypeEmoji(type);
        const label = TYPE_LABEL[type];
        summary.addHeading(`${emoji} ${label}`, 3);

        const rows = files.map((f) => {
            totalOriginal += f.originalSize;
            totalMinified += f.minifiedSize;

            const row = [
                { data: `\`${f.file}\`` },
                { data: prettyBytes(f.originalSize) },
                { data: prettyBytes(f.minifiedSize) },
                { data: `${f.reduction.toFixed(1)}%` },
            ];

            if (inputs.includeGzip) {
                row.push({
                    data: f.gzipSize != null ? prettyBytes(f.gzipSize) : "-",
                });
            }

            row.push({ data: `${f.timeMs}ms` });

            return row;
        });

        const headers = [
            { data: "File", header: true },
            { data: "Original", header: true },
            { data: "Minified", header: true },
            { data: "Reduction", header: true },
        ];

        if (inputs.includeGzip) {
            headers.push({ data: "Gzip", header: true });
        }

        headers.push({ data: "Time", header: true });

        summary.addTable([headers, ...rows]);
    }

    const totalReduction =
        totalOriginal > 0
            ? ((totalOriginal - totalMinified) / totalOriginal) * 100
            : 0;

    await summary
        .addBreak()
        .addRaw(
            `**Total:** ${prettyBytes(totalOriginal)}${prettyBytes(totalMinified)} (${totalReduction.toFixed(1)}% reduction)`
        )
        .write();
}

@srod
Copy link
Owner Author

srod commented Jan 25, 2026

/improve

@qodo-code-review
Copy link

Persistent suggestions updated to latest commit a8273bb

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In `@packages/action/src/index.ts`:
- Around line 148-151: Dry-run branch returns early without setting outputs,
unlike the empty-files branch; update the inputs.dryRun path so it sets the same
default outputs as the files.length === 0 branch before returning. Concretely,
locate the output-setting logic used when no files are found and invoke that
same logic (or duplicate the same setOutput calls) from the if (inputs.dryRun)
block right after the info(...) call so downstream consumers receive the
expected outputs.
♻️ Duplicate comments (1)
packages/action/src/validate.ts (1)

9-17: Add @returns tag to JSDoc for exported function.

Per coding guidelines, all exported functions must have JSDoc with @param and @returns tags. Since this function returns void, add @returns void or @returns {void} Nothing - throws on validation failure.

📝 Proposed fix
 /**
  * Validates that the output directory is not inside any source pattern.
  * Prevents infinite loop risk when output overlaps with source files.
  *
  * `@param` outputDir - The output directory path
  * `@param` patterns - Array of source file patterns (glob patterns)
  * `@param` workingDirectory - Base directory for resolving relative paths (defaults to ".")
+ * `@returns` void
  * `@throws` Error if outputDir is inside any source pattern
  */

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/action/src/index.ts">

<violation number="1" location="packages/action/src/index.ts:126">
P2: Normalize the output-dir before building the ignore glob. `./` prefixes produce an ignore pattern that won’t match discovered relative paths, so outputs can be re-processed and re-minified in auto mode.</violation>
</file>

<file name="packages/action/src/validate.ts">

<violation number="1" location="packages/action/src/validate.ts:29">
P2: Root-recursive glob patterns (e.g., `**/*.js`) are skipped here because `baseDir` is empty, so `outputDir` can overlap the matched tree and create self-including minification loops. The skip should only apply to non-recursive root-level patterns.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/action/src/index.ts">

<violation number="1" location="packages/action/src/index.ts:128">
P2: When `output-dir` is "./", normalization strips it to an empty string and the generated ignore glob becomes `**//**`, which matches everything and causes auto mode to discover zero files. Preserve "." for an empty normalized output-dir to avoid ignoring the entire workspace.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@srod srod force-pushed the feat/zero-config-action branch from c4ec306 to b1f6919 Compare January 25, 2026 20:03
Copy link
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@packages/action/__tests__/validate.test.ts`:
- Around line 49-53: The test uses "../outside" which is rejected earlier by
parseInputs (segments.includes("..")) so it never exercises validateOutputDir in
realistic flow; update the test to use a path that would pass parseInputs (e.g.,
"outside" or "./outside") so validateOutputDir is actually reachable, or add a
comment clarifying the test is intentionally isolating validateOutputDir; target
symbols: parseInputs and validateOutputDir.

In `@packages/utils/__tests__/wildcards-ignore.test.ts`:
- Around line 9-10: Update the two imports that reference "../src/wildcards.js"
to use the TypeScript module extension ".ts"; specifically change the import of
the type WildcardOptions and the values DEFAULT_IGNORES and wildcards so they
import from "../src/wildcards.ts" (ensure both import statements are updated to
reference WildcardOptions, DEFAULT_IGNORES, and wildcards from the .ts path).
🧹 Nitpick comments (12)
packages/utils/__tests__/wildcards-ignore.test.ts (1)

101-113: Test name is misleading.

The test is named "should use DEFAULT_IGNORES when no ignore option provided" but actually tests explicit usage of DEFAULT_IGNORES. The comment on line 104 acknowledges this ("Test that we can use DEFAULT_IGNORES explicitly"). The implementation doesn't automatically apply DEFAULT_IGNORES when ignore is omitted—it passes undefined.

Consider renaming for clarity:

✏️ Suggested rename
-    test("should use DEFAULT_IGNORES when no ignore option provided", () => {
+    test("should work when DEFAULT_IGNORES is explicitly provided as ignore option", () => {
         vi.mocked(fg.globSync).mockReturnValue(["app.js"]);

-        // Test that we can use DEFAULT_IGNORES explicitly
         const options: WildcardOptions = {
             ignore: DEFAULT_IGNORES,
         };
packages/utils/src/wildcards.ts (1)

51-54: Consider adding an explicit return type for API clarity.

The function's return type is inferred as {} | { input: string[] }. Adding an explicit return type would improve IDE support and make the API contract clearer.

✏️ Suggested type annotation
+interface WildcardResult {
+    input?: string[];
+}
+
 export function wildcards(
     input: string | string[],
     options?: WildcardOptions | string
-) {
+): WildcardResult {
packages/action/src/benchmark.ts (1)

50-56: Minor inconsistency in error message reference.

Line 50 still uses inputs.input in the error message while line 56 uses the destructured input. Both reference the same value, so this works correctly, but consider using the destructured input consistently for clarity.

♻️ Suggested consistency fix
     if (!fileResult) {
         throw new Error(
-            `Benchmark failed: no results for input "${inputs.input}". ` +
+            `Benchmark failed: no results for input "${input}". ` +
                 "Check that the file exists and at least one compressor succeeded."
         );
     }
packages/action/src/discover.ts (1)

18-35: Consider reusing DEFAULT_IGNORES from @node-minify/utils.

The DEFAULT_IGNORES array here is identical to the one exported from packages/utils/src/wildcards.ts. Importing it would avoid duplication and ensure consistency if the ignore patterns are updated in the future.

♻️ Proposed refactor
 import path from "node:path";
 import * as core from "@actions/core";
 import fg from "fast-glob";
+import { DEFAULT_IGNORES } from "@node-minify/utils";

 // ... keep DEFAULT_PATTERNS as-is ...

-export const DEFAULT_IGNORES = [
-    "**/node_modules/**",
-    "**/dist/**",
-    "**/build/**",
-    "**/.next/**",
-    "**/*.min.{js,css}",
-    "**/*.d.ts",
-    "**/__tests__/**",
-    "**/.*",
-];
+export { DEFAULT_IGNORES };
packages/action/__tests__/index.test.ts (1)

11-48: Consider adding tests for invalid size parameter.

The chunkArray implementation (per the relevant code snippet) throws a TypeError for non-positive or non-integer sizes. Adding tests for these error cases would improve coverage.

🧪 Suggested additional tests
test("throws TypeError for size of 0", () => {
    expect(() => chunkArray([1, 2, 3], 0)).toThrow(TypeError);
});

test("throws TypeError for negative size", () => {
    expect(() => chunkArray([1, 2, 3], -1)).toThrow(TypeError);
});

test("throws TypeError for non-integer size", () => {
    expect(() => chunkArray([1, 2, 3], 2.5)).toThrow(TypeError);
});
packages/action/__tests__/inputs.test.ts (1)

269-409: Consider adding tests for output-dir security validation.

The auto mode tests cover the main functionality well. However, based on the implementation in inputs.ts (lines 63-67), output-dir rejects absolute paths and .. segments. Consider adding tests to verify this security hardening:

💡 Suggested additional tests
test("throws error for absolute output-dir", () => {
    vi.mocked(getInput).mockImplementation((name: string) => {
        if (name === "output-dir") return "/absolute/path";
        return "";
    });
    vi.mocked(getBooleanInput).mockImplementation((name: string) => {
        if (name === "auto") return true;
        return false;
    });

    expect(() => parseInputs()).toThrow(
        'output-dir must be a relative path without ".." segments'
    );
});

test("throws error for output-dir with path traversal", () => {
    vi.mocked(getInput).mockImplementation((name: string) => {
        if (name === "output-dir") return "dist/../../../etc";
        return "";
    });
    vi.mocked(getBooleanInput).mockImplementation((name: string) => {
        if (name === "auto") return true;
        return false;
    });

    expect(() => parseInputs()).toThrow(
        'output-dir must be a relative path without ".." segments'
    );
});
packages/action/src/reporters/summary.ts (1)

136-156: Side effect in map callback accumulates totals.

The totalOriginal and totalMinified accumulation inside the map callback works correctly but mixes iteration concerns. This is a minor style observation—the current implementation is readable and functional.

♻️ Optional: Separate accumulation from row building
+       // Calculate type totals
+       for (const f of files) {
+           totalOriginal += f.originalSize;
+           totalMinified += f.minifiedSize;
+       }
+
        const rows = files.map((f) => {
-           totalOriginal += f.originalSize;
-           totalMinified += f.minifiedSize;
-
            const row = [
                { data: `\`${f.file}\`` },
                { data: prettyBytes(f.originalSize) },
packages/action/__tests__/discover.test.ts (1)

1-11: Missing copyright header.

Per coding guidelines, source files should include a copyright header. While this is a test file, the other test files in this PR include a header like /*! node-minify action tests - MIT Licensed */.

💡 Suggested fix
+/*! node-minify action tests - MIT Licensed */
+
 import path from "node:path";
 import * as core from "@actions/core";
packages/action/src/index.ts (4)

55-61: Consider enhancing the JSDoc for consistency.

While getFileSize is not exported (so the strict JSDoc requirement doesn't apply), adding @param and @returns tags would improve consistency with other functions in this file.

📝 Suggested JSDoc enhancement
 /**
  * Get the size of a file in bytes.
+ *
+ * `@param` filePath - Path to the file
+ * `@returns` The file size in bytes
  */
 async function getFileSize(filePath: string): Promise<number> {

300-312: Auto mode lacks PR comment and annotation support.

Unlike runExplicitMode (lines 82-89), runAutoMode doesn't support reportPRComment or reportAnnotations inputs. If this is intentional for the initial implementation, consider documenting this limitation. Otherwise, users may expect feature parity between modes.

📝 Optional: Add PR comment and annotation support for parity
     if (inputs.reportSummary) {
         await generateSummary(minifyResult);
     }
+
+    if (inputs.reportPRComment && context.payload.pull_request) {
+        const comparisons = await compareWithBase(minifyResult, inputs.githubToken);
+        await postPRComment(minifyResult, inputs.githubToken, comparisons);
+    }
+
+    if (inputs.reportAnnotations) {
+        addAnnotations(minifyResult);
+    }

     const thresholdError = checkThresholds(minifyResult.totalReduction, inputs);

347-347: Consider handling the promise rejection at the entry point.

The run() invocation doesn't handle potential unhandled promise rejections. While the internal error handling calls setFailed, an unhandled rejection could still occur if something unexpected throws.

📝 Suggested enhancement
-run();
+run().catch((error) => {
+    setFailed(error instanceof Error ? error.message : "Unhandled error in run()");
+});

201-205: Remove the redundant as FileType cast.

detectFileType(file) already returns type FileType explicitly (never null or unknown value), and COMPRESSOR_MAP provides entries for all FileType variants including "unknown". The cast is unnecessary.

Comment on lines +49 to +53
test("does not throw when output is outside using relative path traversal", () => {
expect(() => {
validateOutputDir("../outside", ["src/**"]);
}).not.toThrow();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find packages/action/src -name "*.ts" | head -20

Repository: srod/node-minify

Length of output: 538


🏁 Script executed:

rg -n "segments.includes\|segments\.some\|contains.*\.\." packages/action/src/ -A3 -B3

Repository: srod/node-minify

Length of output: 42


🏁 Script executed:

rg -n "validateOutputDir\|parseInputs" packages/action/src/inputs.ts -A5 -B2

Repository: srod/node-minify

Length of output: 42


🏁 Script executed:

cat -n packages/action/src/inputs.ts | head -80

Repository: srod/node-minify

Length of output: 3790


🏁 Script executed:

cat -n packages/action/src/validate.ts | head -100

Repository: srod/node-minify

Length of output: 2218


🏁 Script executed:

cat -n packages/action/__tests__/validate.test.ts | sed -n '40,60p'

Repository: srod/node-minify

Length of output: 878


🏁 Script executed:

rg -n "validateOutputDir" packages/action/src/inputs.ts -A5 -B5

Repository: srod/node-minify

Length of output: 936


Test case documents unreachable code path: ../outside is rejected before reaching validateOutputDir.

The test correctly asserts that validateOutputDir itself does not reject ../ segments—it only validates that output doesn't overlap with source patterns. However, in actual runtime flow, parseInputs rejects ../outside at line 65 (segments.includes("..")) before calling validateOutputDir at line 86. This means the test documents validateOutputDir's behavior in isolation, but the specific test case (../outside) will never actually reach validateOutputDir in production.

Either document that this test only validates validateOutputDir in isolation, or adjust the test to use a path that would realistically pass the .. check but still be relevant to validateOutputDir's validation logic.

🤖 Prompt for AI Agents
In `@packages/action/__tests__/validate.test.ts` around lines 49 - 53, The test
uses "../outside" which is rejected earlier by parseInputs
(segments.includes("..")) so it never exercises validateOutputDir in realistic
flow; update the test to use a path that would pass parseInputs (e.g., "outside"
or "./outside") so validateOutputDir is actually reachable, or add a comment
clarifying the test is intentionally isolating validateOutputDir; target
symbols: parseInputs and validateOutputDir.

srod added 2 commits January 25, 2026 21:26
Adds zero-config auto mode to the GitHub Action enabling automatic file
discovery and minification without explicit input/output configuration.

Features:
- Auto mode (auto: true) discovers source files using smart default patterns
- File type detection for JS, CSS, HTML, JSON, SVG with compressor selection
- Parallel processing (4 concurrent) for efficient batch minification
- Output directory structure preservation
- Dry-run preview mode with consistent output handling

New inputs: auto, patterns, output-dir, ignore, dry-run

Security hardening:
- Path traversal prevention with segment-based validation
- Absolute path rejection for output-dir
- Output directory overlap validation with source patterns
- Input sanitization in generateOutputPath()
- Auto-ignore outputDir during discovery (prevents re-processing)
- Normalize outputDir before building ignore glob
- Handle empty outputDir edge case

Also includes:
- @node-minify/utils: Add ignore patterns support to wildcards()
- @node-minify/utils: Fix publicFolder handling for non-wildcard paths
- @node-minify/utils: Add @returns JSDoc tag to wildcards()
Unit tests (176 total, 85%+ coverage):
- autoDetect.test.ts: file type detection, compressor selection
- discover.test.ts: file discovery, output path generation, security
- validate.test.ts: output directory validation
- inputs.test.ts: auto mode input parsing
- index.test.ts: run() entry point routing
- runAutoMode.test.ts: auto mode orchestration
- runExplicitMode.test.ts: explicit mode orchestration
- minify.test.ts: runMinification wrapper
- summary.test.ts: auto mode summary reporter
- wildcards-ignore.test.ts: ignore patterns support

CI integration tests:
- Zero Config Auto Mode
- Zero Config Dry Run
- Zero Config Custom Patterns
@srod srod force-pushed the feat/zero-config-action branch from b1f6919 to d02b331 Compare January 25, 2026 20:28
Copy link
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/action/src/benchmark.ts (1)

48-53: Minor inconsistency: error message uses inputs.input instead of destructured input.

For consistency with the rest of the function, the error message should use the destructured input variable.

Proposed fix
     const fileResult = result.files[0];
     if (!fileResult) {
         throw new Error(
-            `Benchmark failed: no results for input "${inputs.input}". ` +
+            `Benchmark failed: no results for input "${input}". ` +
                 "Check that the file exists and at least one compressor succeeded."
         );
     }
packages/action/README.md (1)

96-109: Document the new auto-mode inputs.

The inputs table is missing the five new auto-mode parameters mentioned in the PR objectives: auto, patterns, output-dir, ignore, and dry-run. Users need this reference documentation to understand available options.

Additionally, input and output are marked as "Required: Yes", but they should be optional when auto: true.

📝 Suggest adding auto-mode inputs to the table

Add the following rows to the inputs table:

 | `input` | Files to minify (glob pattern or path) | Yes | - |
 | `output` | Output file path | Yes | - |
+| `auto` | Enable zero-config auto discovery | No | `false` |
+| `patterns` | Custom glob patterns (comma-separated) | No | Smart defaults |
+| `output-dir` | Output directory for auto mode | No | `dist` |
+| `ignore` | Patterns to ignore (comma-separated) | No | `node_modules`, `dist`, etc. |
+| `dry-run` | Preview changes without writing files | No | `false` |
 | `compressor` | Compressor to use | No | `terser` |

And update the required column for input and output:

-| `input` | Files to minify (glob pattern or path) | Yes | - |
-| `output` | Output file path | Yes | - |
+| `input` | Files to minify (glob pattern or path) | Yes* | - |
+| `output` | Output file path | Yes* | - |

With a note: * Required unless auto: true is set.

🤖 Fix all issues with AI agents
In `@packages/action/README.md`:
- Around line 53-54: The "Install compressors" workflow example only installs
`@node-minify/terser` and `@node-minify/lightningcss` which will break auto mode for
HTML, JSON, and SVG files; update the README's example step named "Install
compressors" to include `@node-minify/html-minifier`, `@node-minify/jsonminify`, and
`@node-minify/svgo` OR add a short note next to that step explaining that users
must install the HTML/JSON/SVG compressors when their repo contains
.html/.json/.svg files so auto mode won't error.
♻️ Duplicate comments (2)
packages/action/src/validate.ts (2)

9-17: Add @returns tag to exported function JSDoc.

The JSDoc is missing a @returns tag. Per coding guidelines, all exported functions must have JSDoc comments with @param and @returns tags.

📝 Suggested fix
 /**
  * Validates that the output directory is not inside any source pattern.
  * Prevents infinite loop risk when output overlaps with source files.
  *
  * `@param` outputDir - The output directory path
  * `@param` patterns - Array of source file patterns (glob patterns)
  * `@param` workingDirectory - Base directory for resolving relative paths (defaults to ".")
+ * `@returns` void
  * `@throws` Error if outputDir is inside any source pattern
  */

29-33: Root-recursive glob patterns (**/) bypass overlap validation.

Patterns starting with **/ (e.g., **/*.js) result in an empty baseDir after split("*")[0], causing the validation to be skipped entirely. This allows outputDir to overlap with matched files, defeating the infinite-loop prevention this function is meant to provide.

Consider treating empty baseDir from recursive patterns as the current working directory:

🔧 Suggested fix
         // Extract base directory from glob pattern (everything before first *)
         const baseDir = pattern.split("*")[0];

-        // Skip root-level patterns (e.g., "*.js") - they don't recurse into subdirs
-        // so outputDir in a subdir like "dist" won't cause infinite loops
-        if (!baseDir || baseDir === "") {
+        // For root-level non-recursive patterns (e.g., "*.js"), skip validation
+        // as they don't recurse into subdirs like "dist"
+        const isRootRecursive = pattern.startsWith("**/") || pattern.startsWith("**\\");
+        if (!baseDir && !isRootRecursive) {
             continue;
         }

-        const absBaseDir = path.resolve(workingDirectory, baseDir);
+        // For recursive patterns, use working directory as base
+        const effectiveBaseDir = baseDir || ".";
+        const absBaseDir = path.resolve(workingDirectory, effectiveBaseDir);
🧹 Nitpick comments (6)
packages/action/src/reporters/summary.ts (1)

136-156: Consider separating accumulation from row mapping.

Accumulating totalOriginal and totalMinified inside the map callback mixes side effects with transformation, which can be harder to reason about.

♻️ Suggested refactor
+        // Calculate totals for this group
+        for (const f of files) {
+            totalOriginal += f.originalSize;
+            totalMinified += f.minifiedSize;
+        }
+
         const rows = files.map((f) => {
-            totalOriginal += f.originalSize;
-            totalMinified += f.minifiedSize;
-
             const row = [
                 { data: `\`${f.file}\`` },
                 { data: prettyBytes(f.originalSize) },
packages/action/__tests__/runAutoMode.test.ts (1)

1-1: Copyright header does not match the standard format.

The header should follow the standard format used in source files. Per coding guidelines, source files should include the copyright header /*! node-minify ... MIT Licensed */.

📝 Suggested fix
-/*! node-minify action tests - MIT Licensed */
+/*!
+ * node-minify
+ * Copyright (c) 2011-2026 Rodolphe Stoclin
+ * MIT Licensed
+ */
packages/action/__tests__/discover.test.ts (1)

1-4: Add standard copyright header to test file.

The test file is missing the standard copyright header that should be present in all source files.

📝 Suggested fix
+/*!
+ * node-minify
+ * Copyright (c) 2011-2026 Rodolphe Stoclin
+ * MIT Licensed
+ */
+
 import path from "node:path";
 import * as core from "@actions/core";
 import fg from "fast-glob";
 import { afterEach, describe, expect, test, vi } from "vitest";
packages/action/src/discover.ts (1)

26-35: DEFAULT_IGNORES duplicates definition in @node-minify/utils.

This constant is identical to DEFAULT_IGNORES exported from packages/utils/src/wildcards.ts. Consider importing it from @node-minify/utils to maintain a single source of truth and avoid divergence.

📝 Suggested refactor
+import { DEFAULT_IGNORES } from "@node-minify/utils";
-
-export const DEFAULT_IGNORES = [
-    "**/node_modules/**",
-    "**/dist/**",
-    "**/build/**",
-    "**/.next/**",
-    "**/*.min.{js,css}",
-    "**/*.d.ts",
-    "**/__tests__/**",
-    "**/.*",
-];
+
+export { DEFAULT_IGNORES };
packages/action/src/index.ts (1)

201-205: Remove redundant type cast.

detectFileType(file) already returns FileType, so the cast fileType as FileType is unnecessary on line 204.

📝 Suggested fix
                 const fileType = detectFileType(file);

-                const { compressor, type } = selectCompressor(
-                    fileType as FileType
-                );
+                const { compressor, type } = selectCompressor(fileType);
packages/utils/__tests__/wildcards-ignore.test.ts (1)

101-107: Align the test name with what it asserts.

This test passes DEFAULT_IGNORES explicitly, so “no ignore option” is misleading.

✏️ Rename suggestion
-    test("should use DEFAULT_IGNORES when no ignore option provided", () => {
+    test("should pass DEFAULT_IGNORES when provided", () => {

Comment on lines 53 to 55
- name: Install compressors
run: npm install @node-minify/terser @node-minify/lightningcss
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incomplete compressor installation may cause runtime errors.

The example installs only @node-minify/terser and @node-minify/lightningcss, but auto mode supports five file types. Users with HTML, JSON, or SVG files will encounter errors without @node-minify/html-minifier, @node-minify/jsonminify, and @node-minify/svgo.

📦 Suggest installing all supported compressors or adding a note

Option 1: Install all compressors in the example:

 - name: Install compressors
-  run: npm install `@node-minify/terser` `@node-minify/lightningcss`
+  run: npm install `@node-minify/terser` `@node-minify/lightningcss` `@node-minify/html-minifier` `@node-minify/jsonminify` `@node-minify/svgo`

Option 2: Add a note explaining which compressors are needed:

 - name: Install compressors
   run: npm install `@node-minify/terser` `@node-minify/lightningcss`
+  # Install additional compressors as needed:
+  # `@node-minify/html-minifier` (HTML), `@node-minify/jsonminify` (JSON), `@node-minify/svgo` (SVG)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Install compressors
run: npm install @node-minify/terser @node-minify/lightningcss
- name: Install compressors
run: npm install `@node-minify/terser` `@node-minify/lightningcss` `@node-minify/html-minifier` `@node-minify/jsonminify` `@node-minify/svgo`
Suggested change
- name: Install compressors
run: npm install @node-minify/terser @node-minify/lightningcss
- name: Install compressors
run: npm install `@node-minify/terser` `@node-minify/lightningcss`
# Install additional compressors as needed:
# `@node-minify/html-minifier` (HTML), `@node-minify/jsonminify` (JSON), `@node-minify/svgo` (SVG)
🤖 Prompt for AI Agents
In `@packages/action/README.md` around lines 53 - 54, The "Install compressors"
workflow example only installs `@node-minify/terser` and `@node-minify/lightningcss`
which will break auto mode for HTML, JSON, and SVG files; update the README's
example step named "Install compressors" to include `@node-minify/html-minifier`,
`@node-minify/jsonminify`, and `@node-minify/svgo` OR add a short note next to that
step explaining that users must install the HTML/JSON/SVG compressors when their
repo contains .html/.json/.svg files so auto mode won't error.

- Updated docs/src/content/docs/github-action.md with zero-config section
- Updated packages/action/README.md with usage examples and installation notes
- Added changeset for @node-minify/action and @node-minify/utils minor bump
- Added .sisyphus and .claude to .gitignore
@srod srod force-pushed the feat/zero-config-action branch from 7955b14 to c21e335 Compare January 25, 2026 20:41
@srod srod merged commit 409ea0b into develop Jan 25, 2026
15 checks passed
@srod srod deleted the feat/zero-config-action branch January 25, 2026 20:48
@codecov
Copy link

codecov bot commented Jan 25, 2026

Codecov Report

❌ Patch coverage is 97.66537% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.07%. Comparing base (fabcac6) to head (c21e335).
⚠️ Report is 4 commits behind head on develop.

Files with missing lines Patch % Lines
packages/action/src/index.ts 97.54% 3 Missing ⚠️
packages/action/src/benchmark.ts 75.00% 1 Missing ⚠️
packages/action/src/inputs.ts 95.45% 1 Missing ⚠️
packages/action/src/reporters/summary.ts 97.36% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2797      +/-   ##
===========================================
- Coverage    99.40%   95.07%   -4.33%     
===========================================
  Files           64       72       +8     
  Lines         1345     1686     +341     
  Branches       419      512      +93     
===========================================
+ Hits          1337     1603     +266     
- Misses           8       83      +75     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant