Pipelines
A pipeline needs a printer that exists for the length of one run and leaves nothing behind. Workspaces are created anonymously in a single call, so the whole lifecycle fits in a job.
GitHub Actions
name: receipt-tests
on: [push]
env:
VP_HOST: virtual-printer.online
VP_API: https://virtual-printer.online/api
jobs:
receipts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create a throwaway workspace and printer
id: printer
run: |
WORKSPACE_ID=$(uuidgen)
PRINTER_ID=$(uuidgen)
TOKEN=$(curl -sf -X POST "$VP_API/workspaces" \
-H 'Content-Type: application/json' \
-d "{\"Id\":\"$WORKSPACE_ID\",\"WorkspaceName\":\"ci-${{ github.run_id }}\"}" \
| jq -r .token)
ACCESS=$(curl -sf -X POST "$VP_API/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"Token\":\"$TOKEN\"}" | jq -r .accessToken)
PORT=$(curl -sf -X POST "$VP_API/printers" \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d "{\"Printer\":{\"Id\":\"$PRINTER_ID\",\"DisplayName\":\"ci\"},
\"Settings\":{\"Protocol\":\"EscPos\",\"WidthInDots\":576,
\"HeightInDots\":null,\"EmulateBufferCapacity\":false,
\"BufferDrainRate\":null,\"BufferMaxCapacity\":null}}" \
| jq -r .settings.tcpListenPort)
echo "access=$ACCESS" >> "$GITHUB_OUTPUT"
echo "printer=$PRINTER_ID" >> "$GITHUB_OUTPUT"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Run the tests against it
env:
PRINTER_HOST: ${{ env.VP_HOST }}
PRINTER_PORT: ${{ steps.printer.outputs.port }}
run: ./gradlew test # or npm test, dotnet test, pytest ...
- name: Assert on what was printed
run: |
curl -sf "$VP_API/printers/${{ steps.printer.outputs.printer }}/documents/canvas?limit=10" \
-H "Authorization: Bearer ${{ steps.printer.outputs.access }}" > receipts.json
jq -e '.result.items | length > 0' receipts.json
- name: Delete the workspace
if: always()
run: |
curl -sf -X DELETE "$VP_API/workspaces" \
-H "Authorization: Bearer ${{ steps.printer.outputs.access }}"
Two details that matter more than they look:
- The workspace is deleted in an
always()step. Without it a failed run leaves a workspace behind, and they accumulate. - The IP whitelist. A new workspace allows only the address that created it - which, here, is the runner itself, so a pipeline that creates its own workspace needs no whitelist changes at all. A pipeline reusing a long-lived workspace does: hosted runners have changing addresses, so either add the ranges or turn the whitelist off for that workspace and accept that the port is public.
GitLab CI
The same shape, with before_script for setup and after_script for the cleanup that must run
regardless of outcome:
receipts:
image: alpine:3
before_script:
- apk add --no-cache curl jq
- ./ci/create-printer.sh # writes PRINTER_PORT and VP_ACCESS to printer.env
script:
- . ./printer.env && ./run-tests.sh
after_script:
- . ./printer.env && curl -sf -X DELETE "$VP_API/workspaces" -H "Authorization: Bearer $VP_ACCESS"
Jenkins
In a declarative pipeline, create the printer in a stage and delete it in post { always { ... } }.
The commands are identical; only the syntax around them changes.
Keeping the setup out of the pipeline
Everything above is curl so that it reads the same in any CI system, but this logic usually belongs
in the test suite itself rather than in YAML - see
Automated tests, which does the same round trip from test code.