# 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

```yaml
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:

```yaml
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](/docs/ci-cd), which does the same round trip from test code.
