CSV imports fail late. Move structural checks into CI.

A dependency-free GitHub Actions pattern for catching malformed CSV structure while preserving reports and keeping the input on the runner.
CSV imports fail late. Move structural checks into CI.

CSV imports fail late. Move structural checks into CI.

A CSV can parse in one tool and still fail in the importer that matters. The expensive part is not noticing a broken delimiter or uneven row. It is noticing after a release, when the file has already reached an operator or customer.

For generic structural failures, a small preflight step belongs next to the code that produces the file. The check should be deliberately narrow: encoding, headers, row width, quoting, and exact duplicate rows. It should not pretend to know business rules or silently discard data.

A bounded GitHub Actions pattern

This workflow runs a dependency-free public Action against one UTF-8 CSV path. It gives the job read-only repository permissions, scopes triggers to the file and workflow, limits execution time, and preserves the normalized output and issue report even when the check fails.

name: CSV preflight

on:
  pull_request:
    paths:
      - data/catalog.csv
      - .github/workflows/csv-preflight.yml

permissions:
  contents: read

jobs:
  csv-preflight:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - name: Check CSV structure
        id: preflight
        uses: softpeanut/csv-preflight-action@eb04c527a46ce3bc8bfc711fde8e93ca947597ae
        with:
          path: data/catalog.csv
      - name: Keep diagnostic artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: csv-preflight-report
          path: ${{ runner.temp }}/csv-preflight/
          retention-days: 7

Pinning the Action to a full commit makes the third-party code immutable for that workflow review. if: always() is equally important: GitHub normally skips later steps after a failure, which would hide the report at the moment it is most useful.

What this catches—and what it does not

The checker reports invalid UTF-8, BOMs, empty or duplicate headers, uneven row widths, unclosed quoted fields, and exact duplicate rows. It emits a normalized CSV only when parsing completes. It does not delete duplicates, infer a target schema, run the destination importer, or guarantee that Shopify or any other external system accepts the result.

“The file stays on the runner” is a data-flow boundary, not a universal security promise. GitHub Actions and any uploaded artifact remain inside the repository’s trust boundary. Do not put secrets, regulated data, or customer exports into a repository merely because the validator itself makes no network request.

Reproduce it before adopting it

The Action source, tests, release tags, and a copy-ready guide are public:

The free path is complete for one generic file. If you want the same bounded workflow configured in one public repository, the separate fixed-scope service and its exclusions are published before payment: https://softpeanut.github.io/csv-preflight/ci-setup-terms.html

If this saved you time, the article can be zapped through the softpeanut tools Nostr profile. No personal information is needed.


Write a comment