Skip to main content

CI usage

A typical pipeline validates and tests every change offline, renders previews of the changed templates for review, and publishes on the default branch.

GitHub Actions​

The action Formfeed templates (formfeed-dev/formfeed-action) runs these steps for you:

name: templates
on:
pull_request:
paths: ['templates/**', 'partials/**']
push:
branches: [main]
paths: ['templates/**', 'partials/**']

jobs:
templates:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # for the preview comment
steps:
- uses: actions/checkout@v4
- uses: formfeed-dev/formfeed-action@v1
with:
api-key: ${{ secrets.FORMFEED_TEST_KEY }}

- if: github.event_name == 'push'
uses: formfeed-dev/formfeed-action@v1
with:
api-key: ${{ secrets.FORMFEED_LIVE_KEY }}
validate: false
test: false
publish: true

Every run validates and tests the templates offline. On a pull request with an API key, the action renders the templates the pull request changes, uploads them as the artifact formfeed-preview and posts the summary as one comment that each run updates; a pull request from a fork runs without your secrets and skips the preview. With publish: true it pushes the templates and publishes them, or pushes them to a release channel with channel. It runs the published formfeed command line in the version its input version names; GitHub Actions lists every input.

Without the action​

The same steps written out, for a workflow that arranges them itself:

name: templates
on:
pull_request:
paths: ['templates/**', 'partials/**']
push:
branches: [main]
paths: ['templates/**', 'partials/**']

jobs:
templates:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # only for the pull-request comment below
env:
FORMFEED_API_KEY: ${{ secrets.FORMFEED_TEST_KEY }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # ci preview compares against the base branch
- uses: actions/setup-node@v4
with: { node-version: 22 }

- run: npx formfeed validate --ci
- run: npx formfeed test --ci

- if: github.event_name == 'pull_request'
run: npx formfeed ci preview --base origin/${{ github.base_ref }} --ci
- if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: formfeed-preview
path: |
formfeed-preview/
formfeed-preview.md

- if: github.event_name == 'push'
run: npx formfeed templates push --publish --message "${{ github.sha }}" --ci
env:
FORMFEED_API_KEY: ${{ secrets.FORMFEED_LIVE_KEY }}

validate and test run offline and need no key; only ci preview and templates push call the API.

What ci preview does​

formfeed ci preview renders the templates changed since --base through the API:

  • Which templates. It asks git for the files changed between --base and HEAD (git diff --name-only <base>...HEAD) and takes every template folder with a changed file. A changed file in the partials folder renders every template, because any of them may include it. --all renders every template. When git cannot answer, because it is not installed or the base ref is unknown (a shallow clone), every template is rendered too, so fetch the base branch first.
  • What it renders. Each template once, with its default data set (data/default.json, else the first file in data/), in its default format. HTML templates are rendered in the job and sent to the API as HTML, so the preview shows the files of the branch, not a pushed version. Word and PowerPoint templates are filled in the job and converted to PDF through the API, which needs the Starter plan or above.
  • What it writes. One file per template to formfeed-preview/ (<slug>.pdf, or the image format) and a Markdown summary to formfeed-preview.md: a table with template, data set, page count and a link to the rendered file, or the reason a template failed. The links are the renders' download links and expire with the output; with a test key that is after a day at most. Without changes, it renders nothing and the summary says so.
  • Options. --base <ref> (default origin/main), --out <dir> (default formfeed-preview), --summary <file> (default formfeed-preview.md), --all.
  • Exit code. 1 when at least one render finished as failed or a Word or PowerPoint template could not be filled; the summary lists it. A render the API refuses at once (a problem such as 422 page_limit_exceeded) stops the command with that problem's exit code, before the summary is written.

The key needs render:create and render:read, and file:read when templates show files of the file library with asset(): the CLI reads the library's address with it, and without it those pictures are missing from the preview. Renders with a test key are free. Post the summary as a pull-request comment with any comment action, for example:

- if: github.event_name == 'pull_request'
run: gh pr comment ${{ github.event.number }} --body-file formfeed-preview.md --edit-last || gh pr comment ${{ github.event.number }} --body-file formfeed-preview.md
env:
GH_TOKEN: ${{ github.token }}

GitLab CI​

templates:
image: node:22
variables:
GIT_DEPTH: 0
script:
- npx formfeed validate --ci
- npx formfeed test --ci
- git fetch origin "$CI_DEFAULT_BRANCH" # a job checks out its own branch only
- npx formfeed ci preview --base origin/$CI_DEFAULT_BRANCH --ci
artifacts:
paths: [formfeed-preview/, formfeed-preview.md]

publish-templates:
image: node:22
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- FORMFEED_API_KEY=$FORMFEED_LIVE_KEY npx formfeed templates push --publish --message "$CI_COMMIT_SHA" --ci

Store FORMFEED_API_KEY (a test key) and FORMFEED_LIVE_KEY as masked CI/CD variables.

Recommendations​

  • Use a test key for previews; its renders are free and its files are deleted after a day. Leave template:write off that key, so a pull request cannot change templates, and give it only to the key of the publish step on the default branch. Templates are the same for test and live keys, so either kind of key can publish; the examples use a live key there.
  • Keep data sets in data/*.json that cover the edge cases: empty lists, very long names, missing optional fields, a hundred line items. test renders every one of them; validate checks each template against its default data set (--data <name> picks another), for e-invoice templates the _invoice block of every data set, and for templates that declare pdf.ua what the source already shows. Only errors fail a step; warnings such as unknown-variable (a variable missing from the data) are printed and pass.
  • Add formfeed validate to a pre-commit hook for instant feedback.
  • Commit the generated types and run npx formfeed types --check --ci in the pipeline, so a schema or data change without regenerated types fails the build. See Typed template data.
  • templates push --publish creates and publishes a new version of every template folder that ignore does not leave out, changed or not; name one template (templates push invoice --publish) to publish only that one. Do not commit .formfeed/state.json when CI publishes: CI does not commit back the state it records, so the committed one is stale after the first publish, and the next push from it answers 409 conflict (exit code 2) until someone pulls again or pushes with --force.
  • Publishing on every merge puts each change live at once. To test first, push to a channel instead (templates push --channel staging) and publish in a separate step; see Release channels.
  • Pin the CLI version in your package.json (npm install -D formfeed) so CI and laptops agree.
  • Scripts can rely on the exit codes and on --json output.