Validate CSV Files in GitHub Actions
Fail a workflow on broken structure, retain the diagnostic files, and keep the CSV on the runner.
A CSV import can fail long after review if a header is duplicated, one row has an extra column, or an export silently changes encoding. A CI check catches those structural problems next to the commit that introduced them. It should not pretend to know which duplicate is correct or rewrite business values.
A complete workflow
This workflow checks one UTF-8 file. The preflight step exits with status 1 when it finds issues, so the job fails. The artifact step uses if: always(), which preserves the normalized CSV and issue report even after that expected failure.
name: Validate import CSV
on:
pull_request:
paths:
- "data/import.csv"
push:
branches: [main]
paths:
- "data/import.csv"
permissions:
contents: read
jobs:
csv-preflight:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Check CSV structure
uses: softpeanut/csv-preflight-action@eb04c527a46ce3bc8bfc711fde8e93ca947597ae
with:
path: data/import.csv
normalized_path: ${{ runner.temp }}/import.normalized.csv
report_path: ${{ runner.temp }}/import.issues.csv
- name: Keep preflight artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: csv-preflight
path: |
${{ runner.temp }}/import.normalized.csv
${{ runner.temp }}/import.issues.csv
if-no-files-found: warn
The full commit SHA pins the checked v1.0.0 Action bytes. A shorter @v1 reference is easier to read, but a full SHA prevents a tag move from changing third-party code without a workflow diff. Apply the same pinning policy to every external Action when your threat model requires immutable dependencies.
What the Action checks
- UTF-8, UTF-8 BOM, unsupported UTF-16 markers, and invalid UTF-8;
- comma, tab, semicolon, or pipe delimiter inference;
- quoted delimiters, escaped quotes, and quoted newlines;
- empty or duplicate headers;
- rows whose column count differs from the header;
- exact duplicate data rows.
It preserves data rows. Duplicate rows and uneven rows are reported, not deleted, padded, or truncated. Empty and repeated header names are made unique only in the normalized output. The issue report records the detected type, row, and detail.
Why the artifact step needs always()
GitHub normally skips later steps after a failure. That would hide the report at the moment it is most useful. if: always() lets the artifact upload run while leaving the original preflight step—and therefore the job—failed. Reviewers get both a red check and the evidence needed to fix it.
For an encoding or syntax rejection, a normalized file may not exist. if-no-files-found: warn allows the report to upload without turning that expected absence into a second error. Do not use continue-on-error on the preflight step unless the workflow is intentionally advisory; doing so converts data-quality failures into green jobs.
Keep the permissions narrow
The Action is a composite wrapper around a dependency-free Node.js CLI. It reads the checked-out path and writes only to the paths supplied by the workflow. It does not upload the CSV or require an API key. The workflow grants contents: read, which is enough for checkout.
“No upload” is a data-flow statement, not a universal security guarantee. The runner and any later artifact step are still part of your trust boundary. Do not place secrets or regulated data in a repository or artifact merely because the validator itself has no network call. Configure artifact retention and repository access for the sensitivity of the file.
Scope the trigger to the file
The paths filter avoids running the job on unrelated changes. For generated CSV, include the generator source paths as well so a code change that alters output can exercise the check. If several files must be validated, a matrix can call the free Action once per file, but it does not produce a combined manifest.
The free boundary is one generic file up to 10 MiB per invocation. The separate Pro edition processes up to 20 files in one run and adds named import profiles, one ZIP, a manifest, and JSON output. It is optional; the workflow above is complete and usable without buying anything.
Reproduce a failure locally
Download the same one-file CLI and run it with Node.js 20 or newer:
node cli.mjs data/import.csv \
--output /tmp/import.normalized.csv \
--report /tmp/import.issues.csv
echo $?
The local command and Action share the same implementation and exit-code contract. Existing output paths are never overwritten, which prevents a stale report from being mistaken for the current run. Remove or choose new output paths deliberately before rerunning.
Use schema checks separately
Structural validation cannot decide whether a price is negative, a customer ID exists, or a date belongs to the required timezone. Add importer-specific schema and business-rule checks after structural parsing. Keep their reports separate so reviewers can distinguish malformed CSV from valid CSV containing invalid domain values.
Start with the browser checker, inspect the standalone Action source and tests, review the public 1,583-row reproduction, or use the public v1.0.0 Action release. For offline batch and team use, the published Pro terms describe the completed product. If you want this exact one-file workflow configured against a public or sanitized repository, the separate $99 fixed-scope setup terms define every deliverable and exclusion before payment.