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
--baseandHEAD(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.--allrenders 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 indata/), 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 toformfeed-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>(defaultorigin/main),--out <dir>(defaultformfeed-preview),--summary <file>(defaultformfeed-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:writeoff 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/*.jsonthat cover the edge cases: empty lists, very long names, missing optional fields, a hundred line items.testrenders every one of them;validatechecks each template against its default data set (--data <name>picks another), for e-invoice templates the_invoiceblock of every data set, and for templates that declarepdf.uawhat the source already shows. Only errors fail a step; warnings such asunknown-variable(a variable missing from the data) are printed and pass. - Add
formfeed validateto a pre-commit hook for instant feedback. - Commit the generated types and run
npx formfeed types --check --ciin the pipeline, so a schema or data change without regenerated types fails the build. See Typed template data. templates push --publishcreates and publishes a new version of every template folder thatignoredoes not leave out, changed or not; name one template (templates push invoice --publish) to publish only that one. Do not commit.formfeed/state.jsonwhen 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 answers409 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
--jsonoutput.