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.

Next pageAsserting on a Receipt →