Docs CI Triggers — Webhook-based Pipeline Kick-off

CI Triggers — Webhook-based Pipeline Kick-off

CI Triggers let you launch a load test, fuzz run, test plan, or any
other Mockarty task on an ephemeral runner spawned by your own
CI/CD pipeline. Mockarty fires a webhook (GitLab, GitHub Actions,
Jenkins, CircleCI, or a custom HTTP endpoint), passes a one-time
dispatch token in the payload, and waits for the spawned runner
to claim the work and report results.

This page covers the CI Triggers feature, which works alongside
Ephemeral Runners.

CI Triggers are part of the base offering — no separate license
required.


When to use

Use CI Triggers when you want to keep test execution inside your CI
infrastructure (e.g. private GitLab Runners, GitHub-hosted runners
with secrets, on-prem Jenkins agents) rather than running a long-lived
Mockarty runner alongside the admin node.

Typical scenarios:

  • Per-PR load tests — every pull request opens a child GitLab
    pipeline that launches a short perf test against the deployed
    preview environment.
  • Nightly fuzzing — a scheduled GitHub Actions workflow asks
    Mockarty to fuzz the API, gets the runner Docker image, and tears
    the runner down when the campaign finishes.
  • Manual smoke from chat ops — Slack /perf slash command hits
    Mockarty which kicks off a Jenkins job that boots a runner.

Concepts

Term Meaning
Trigger A saved configuration: URL + method + body template + auth + status polling rules. Created once, reused across launches.
Run One launch through a Trigger. Joined to a Mockarty task by the dispatch token.
Dispatch token A 64-character single-use secret that Mockarty mints per launch and embeds in the trigger body. The spawned runner presents this token when it claims the task — so only that one runner can pick it up.
External job ID The ID returned by the CI endpoint (pipeline.id, GitHub run_id, Jenkins build number, etc.). Extracted via JSONPath from the response body.
Status polling Mockarty polls a follow-up URL on the trigger’s schedule to track the external job state. When the state hits success/failure, the linked Mockarty task is closed with the same outcome.

Setting up a Trigger

Triggers are managed via the REST API — there is no UI page for them, so
use the API directly or a small wrapper script.

Built-in templates (GET /api/v1/ci/templates) pre-fill defaults for
the common providers:

  • GitLab Pipeline — create-pipeline API (POST /projects/:id/pipeline) with a variables array; one PRIVATE-TOKEN PAT covers both fire and status polling.
  • GitHub workflow_dispatch — repository_dispatch event.
  • Jenkins Build — parameterised job.
  • CircleCI Pipeline — v2 pipeline endpoint.
  • Custom Webhook — blank slate.

Test a saved trigger with POST /api/v1/ci/triggers/{id}/test — it
fires a dry-run against your CI endpoint and returns the rendered
request URL, body, response status, and parsed external job ID without
creating a Mockarty task. An unsaved configuration can be dry-run the
same way with POST /api/v1/ci/trigger-test (the trigger config goes
in the request body; include id to merge over a saved trigger — a
blank authSecret keeps the stored secret). The Test trigger
button in the editor uses this, so you can iterate on URL, body, and
auth before the first save.

For a saved trigger, the /test body is optional. Pass
{"extra":{"BRANCH":"main"}} to preview how sample CI environment
variables render. Malformed JSON returns 400 without sending the dry-run.

The trigger table in Settings → CI Triggers shows the latest run
of each trigger (state + time), and the clock button opens the full
run history — state, linked task, external job ID, timing, and the
last error for each dispatch. List recent runs over the API with
GET /api/v1/ci/runs?triggerId={id}&limit=50.

curl -X POST "$MOCKARTY/api/v1/ci/triggers?namespace=team-a" \
  -H "X-API-Key: $MOCKARTY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "gitlab-perf",
    "templateKind": "gitlab_pipeline",
    "triggerUrl": "https://gitlab.example.com/api/v4/projects/42/pipeline",
    "triggerBodyTemplate": "{\n  \"ref\": \"main\",\n  \"variables\": [\n    {\"key\": \"MOCKARTY_DISPATCH_TOKEN\", \"value\": \"{{.DispatchToken}}\"},\n    {\"key\": \"MOCKARTY_TASK_ID\", \"value\": \"{{.TaskID}}\"}\n  ]\n}",
    "statusUrlTemplate": "https://gitlab.example.com/api/v4/projects/42/pipelines/{{.ExternalJobID}}",
    "statusIdJsonpath": "$.id",
    "statusStateJsonpath": "$.status",
    "statusSuccessValues": ["success"],
    "statusFailureValues": ["failed", "canceled"],
    "statusPendingValues": ["created", "pending", "running", "preparing", "scheduled"],
    "pollIntervalSec": 10,
    "pollTimeoutSec": 1800,
    "authKind": "header",
    "authSecret": "PRIVATE-TOKEN: glpat-xxxxxxxxxxxxxxxx",
    "enabled": true
  }'

The response includes the generated id; the authSecret field is
returned as *** on every subsequent GET.


Launching a task through a Trigger

Performance test

cURL

curl -X POST "$MOCKARTY/api/v1/perf/run" \
  -H "X-API-Key: $MOCKARTY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "configId": "perf-config-uuid",
    "ciTriggerId": "trigger-uuid"
  }'

Go SDK

task, err := client.Perf().RunWithOptions(ctx, mockarty.PerfRunRequest{
    ConfigID:    "perf-config-uuid",
    CITriggerID: "trigger-uuid",
})
// task.ID is the launched task identifier; use it to poll status.

Python SDK

task = client.perf.run({
    "configId": "perf-config-uuid",
    "ciTriggerId": "trigger-uuid",
})
# poll the linked CI run:
ci_run = client.ci_triggers.get_run_by_task(task.task_id)

Java SDK

Map<String, Object> req = new HashMap<>();
req.put("configId", "perf-config-uuid");
req.put("ciTriggerId", "trigger-uuid");
Map<String, Object> task = client.perf().run(req);

CLI: there is intentionally no --ci-trigger flag on mockarty-cli perf run — wire CI launches from your pipeline by POSTing the JSON directly (or list triggers with mockarty-cli ci triggers list to discover the id).

What happens:

  1. Mockarty looks up the trigger and validates it’s enabled + in your
    namespace.
  2. Generates a fresh 64-hex dispatch_token.
  3. Renders the trigger body template with the new token + task ID.
  4. POSTs the rendered body to the trigger URL with the configured auth.
  5. Parses the response with statusIdJsonpath and stores the
    external job ID on the run.
  6. Submits the Mockarty task with the same dispatch_token.
  7. The CI pipeline spawns a Mockarty runner that calls
    /api/v1/runner/tasks/pull with claim_tokens=[<token>] — only that
    runner can claim the task.
  8. Mockarty polls the statusUrlTemplate on the trigger’s schedule;
    when the external state hits success/failure, the local task
    is closed with the same outcome.

Fuzz run

Same shape — add ciTriggerId to the body of POST /api/v1/fuzzing/run.

If the dispatch fails (CI endpoint down, auth wrong, template error),
the launch is rejected with HTTP 422 and a clear message — the
Mockarty task is never created.

Test Plan run

POST /api/v1/test-plans/:id/run accepts the same ciTriggerId
field (plus an optional ciEnv object — see
Passing environment variables).
The trigger fires once for the whole plan and one CI runner
spawned by your pipeline picks up and executes every item in the
plan end-to-end.

curl -X POST $MOCKARTY/api/v1/test-plans/$PLAN_ID/run \
  -H "X-API-Key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ciTriggerId":"'"$TRIGGER_ID"'"}'

Test Case run

Test cases can also launch through a CI trigger. Pick a saved
trigger from the Launch via CI trigger dropdown in the run
dialog on the test case page, or send ciTriggerId in the API
request:

curl -X POST $MOCKARTY/api/v1/namespaces/$NS/test-cases/$CASE_ID/run \
  -H "X-API-Key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ciTriggerId":"'"$TRIGGER_ID"'"}'

Mockarty fires your CI trigger first. If everything succeeds the
test case run starts and your external CI can follow progress with:

curl "$MOCKARTY/api/v1/ci/runs?taskId=$CASE_RUN_ID" \
  -H "X-API-Key: $TOKEN"

If something goes wrong nothing is started — you get a clear
error and can retry once the cause is fixed:

  • The trigger does not exist, is disabled, or belongs to another
    workspace → 422.
  • The CI side is unreachable (network error, timeout, bad
    response) → 502.
  • The ciTriggerId is not a valid UUID → 400.

Leaving ciTriggerId empty (or omitting it) runs the test case
normally without involving any external CI.


Available variables

Body and status URL templates have access to these fields via Go
text/template syntax. Missing top-level fields error; missing
Extra map keys render empty.

Variable Available at Description
{{.TaskID}} both The Mockarty task ID (UUID).
{{.DispatchToken}} both The single-use 64-hex token.
{{.Namespace}} both The owning namespace.
{{.CoordinatorURL}} both URL the runner should connect back to.
{{.User}} both Launching user’s email (audit / display).
{{.Labels}} both []string of required runner labels.
{{.LabelExpr}} both Label selector expression (optional).
{{.TaskType}} both "performance" / "fuzzing" / "api_test" / ...
{{.ExternalJobID}} status URL only Job ID returned by the CI endpoint. Empty at launch time.
{{.Now}} both Current UTC time.
{{.Extra.foo}} both One launch-time environment variable (see below).

Functions available in templates: json, jsonEscape, join,
upper, lower, replace, default, now, gitlabVars,
githubInputs, jenkinsParams.


Passing environment variables to the CI job

When you launch a run through a trigger you can attach environment variables
that are forwarded to the CI job — pass them per launch, no need to bake them
into the trigger.

  • In the UI: pick a CI trigger in the run dialog, then add KEY / value
    rows in the Environment variables grid that appears.
  • In the API: add an ciEnv object to the launch request:
curl -X POST "$MOCKARTY/api/v1/namespaces/default/test-cases/$CASE_ID/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"ciTriggerId": "...", "ciEnv": {"BRANCH": "main", "SUITE": "smoke"}}'

Keys must be a conventional env identifier ([A-Za-z_][A-Za-z0-9_]*); up to 50
vars, each value up to 8 KiB. Invalid keys are rejected with a clear error.

The built-in templates forward every passed variable automatically, in the
right syntax for each provider, via these helpers:

Helper Renders (per var) Use in
{{gitlabVars .Extra}} , {"key": "KEY", "value": "value"} GitLab variables array
{{githubInputs .Extra}} , "KEY": "value" GitHub client_payload / CircleCI parameters
{{jenkinsParams .Extra}} &KEY=value (URL-encoded) Jenkins build-with-parameters body

Place the helper right after the last fixed entry — it emits the leading
separator itself, so an empty map adds nothing. A launch with
ciEnv: {BRANCH: "main"} against the GitLab template renders
{"key": "BRANCH", "value": "main"} in the pipeline variables array.

Preview it before saving: the trigger Test button (POST /api/v1/ci/triggers/:id/test) accepts the same extra object and shows the
rendered body.


Auth modes

Mode authSecret format Sent as
none (empty) No auth header.
bearer glpat-xxxx Authorization: Bearer glpat-xxxx
basic user:pass or b64:<encoded> Authorization: Basic <base64>
header Header-Name: value The literal header.

The authSecret is redacted on every API response (returned as
***). To update without re-typing, PATCH the trigger with an empty
authSecret field — the existing value is preserved.


Status mapping

Mockarty classifies the external state on every poll:

  • If the state matches any of statusSuccessValues → task succeeds.
  • If the state matches any of statusFailureValues → task fails
    with the value of statusErrorJsonpath as the error message.
  • If the state matches any of statusPendingValues → keep polling.
  • Anything else → keep polling (logged at debug).

Polling stops after pollTimeoutSec seconds with the message
CI: status poll timeout after Ns. Last state: <state>.


Built-in templates

GET /api/v1/ci/templates returns the catalogue used by the UI
picker. Each entry pre-fills URL placeholder, body template, JSONPath
defaults, and success/failure/pending value sets for the named
provider.


Storing secrets safely

Pasting a long-lived API token straight into the trigger editor
works, but you have to update every trigger when you rotate the
token. Instead, save the token once in your Mockarty secret store
and point the trigger at it:

  1. Open Settings → Secret stores and create a store (or reuse
    an existing one), e.g. ci-vault.

  2. Add a key to that store, e.g. gitlab_pat_prod, with the actual
    token as its value.

  3. In the trigger editor, set Secret to:

    {{secret:ci-vault/gitlab_pat_prod}}
    

Mockarty looks up the real value at fire time. The token is never
written to the trigger row, never returned by the API, and never
appears in logs. Rotate the value in Secret stores and every
trigger that references it picks it up on the next fire — no edit
required.

The same {{secret:store/key}} syntax works inside any header value
and inside the body template — useful when your CI expects a custom
authentication header rather than the standard Authorization.

If the reference points at a missing key, the launch returns 422
with the reference name (config error you fix in seconds). If the
secret store backend itself is unavailable, you get 503 instead
(infrastructure issue — retry once the backend is reachable again).


Auth from an integration

If you already configured a GitHub, GitLab, or Jenkins
integration in Settings → Integrations, a trigger can use it as
its authentication source instead of carrying its own secret:

  1. In the trigger editor open Authentication and pick
    From integration.
  2. Choose the integration from the list (only GitHub / GitLab /
    Jenkins integrations qualify — those are the CI systems Mockarty
    can call).

The credential is resolved fresh on every fire and status poll,
and the right header shape is applied automatically:

Integration What the trigger sends
GitHub Authorization: Bearer <token>
GitLab PRIVATE-TOKEN: <token>
Jenkins Authorization: Basic <user>:<api token>

Rotate the token once in Settings → Integrations — every linked
trigger picks it up automatically, mid-run polls included. Via the
API, set integrationId on the trigger to link, or send an empty
string to unlink and fall back to authKind/authSecret. Saving a
trigger with an integration that is disabled, missing a credential,
or of an unsupported kind returns 422 with the exact reason.

GitHub Actions provider

When you bind a Test Plan to a GitHub integration, Mockarty fires a
real GitHub Actions run, follows it live to a final state, and can stop
it — no leaving Mockarty.

Addressing. Fill the config fields like this:

Field Value Example
Project reference owner/repo (optionally owner/repo:workflow.yml to inline the workflow) octo-org/checkout
Workflow The workflow file name or numeric id ci.yml
Branch The Git ref the workflow runs against main

To fire a repository_dispatch event instead of a
workflow_dispatch, set the workflow field to
repository_dispatch:<event-type> (for example
repository_dispatch:mockarty-trigger). Your workflow must declare the
matching trigger:

on:
  workflow_dispatch:      # for the default mode
    inputs:
      deploy_env:
        type: string
  repository_dispatch:    # for repository_dispatch:<event-type>
    types: [mockarty-trigger]

Variables. Config defaultVariables (and any per-fire overrides)
are sent as workflow inputs for workflow_dispatch, or as
client_payload for repository_dispatch.

Finding the run. A GitHub dispatch is accepted with no run id, so
Mockarty locates the run it just created automatically and starts
tracking it — the trigger fires, and within a few seconds you see the
live GitHub run and its URL in Mockarty.

Token scopes. The GitHub personal-access token on the integration
needs repo (read the run) and workflow (dispatch it). A
fine-grained token needs Actions: read and write on the target
repositories.

GitHub Enterprise Server. Set the integration’s base_url to your
appliance origin (e.g. https://github.mycorp.example); Mockarty talks
to its REST API automatically.


Ready-to-paste CI pipeline templates

Mockarty fires your trigger, then your CI side spins up a Mockarty
runner that picks up the actual test work by dispatch token. The
templates below cover the runner side for the four most common CI
systems. Save them next to your existing pipeline file, set the
required secrets (typically a Mockarty integration token + the
Mockarty admin URL), and the trigger will work end-to-end.

GitLab CI (.gitlab-ci.yml)

mockarty-runner:
  stage: test
  image: ghcr.io/mockarty/mockarty-runner:latest
  variables:
    # Mockarty injects these into the request body when the trigger
    # fires; map them through GitLab pipeline variables so the runner
    # picks up the same task as the parent dispatch.
    MOCKARTY_URL: $MOCKARTY_URL
    MOCKARTY_INTEGRATION_TOKEN: $MOCKARTY_INTEGRATION_TOKEN
    MOCKARTY_DISPATCH_TOKEN: $DISPATCH_TOKEN
  rules:
    - if: '$DISPATCH_TOKEN'
  script:
    - mockarty-runner serve
        --coordinator="$MOCKARTY_URL"
        --token="$MOCKARTY_INTEGRATION_TOKEN"
        --claim-token="$MOCKARTY_DISPATCH_TOKEN"
        --ephemeral

Trigger URL: https://gitlab.example.com/api/v4/projects/<id>/pipeline
Body template:

{
  "token": "{{secret:ci-vault/gitlab_pipeline_token}}",
  "ref": "main",
  "variables": [{"key": "DISPATCH_TOKEN", "value": "{{.DispatchToken}}"}]
}

GitHub Actions (.github/workflows/mockarty.yml)

name: Mockarty runner
on:
  repository_dispatch:
    types: [mockarty-run]
jobs:
  serve:
    runs-on: ubuntu-latest
    steps:
      - uses: docker://ghcr.io/mockarty/mockarty-runner:latest
        with:
          args: serve
            --coordinator=${{ secrets.MOCKARTY_URL }}
            --token=${{ secrets.MOCKARTY_INTEGRATION_TOKEN }}
            --claim-token=${{ github.event.client_payload.dispatch_token }}
            --ephemeral

Trigger URL: https://api.github.com/repos/<org>/<repo>/dispatches
Auth: Bearer, secret {{secret:ci-vault/github_pat}} (PAT with
repo scope or a fine-grained token with Contents: read + Actions: write).
Body template:

{
  "event_type": "mockarty-run",
  "client_payload": { "dispatch_token": "{{.DispatchToken}}" }
}

Jenkins (Jenkinsfile)

pipeline {
  agent { docker { image 'ghcr.io/mockarty/mockarty-runner:latest' } }
  parameters {
    string(name: 'DISPATCH_TOKEN', defaultValue: '')
  }
  stages {
    stage('Serve') {
      when { expression { return params.DISPATCH_TOKEN?.trim() } }
      steps {
        sh """
          mockarty-runner serve \\
            --coordinator="\$MOCKARTY_URL" \\
            --token="\$MOCKARTY_INTEGRATION_TOKEN" \\
            --claim-token="\$DISPATCH_TOKEN" \\
            --ephemeral
        """
      }
    }
  }
}

Trigger URL:
https://jenkins.example.com/job/<job>/buildWithParameters?token=<job_token>
Auth: Basic, secret {{secret:ci-vault/jenkins_basic}}
(user:password base64-pair).
Body template:

DISPATCH_TOKEN={{.DispatchToken}}

CircleCI (config.yml + pipeline trigger)

version: 2.1
parameters:
  dispatch_token:
    type: string
    default: ""
jobs:
  serve:
    docker:
      - image: ghcr.io/mockarty/mockarty-runner:latest
    steps:
      - run: |
          mockarty-runner serve \
            --coordinator="$MOCKARTY_URL" \
            --token="$MOCKARTY_INTEGRATION_TOKEN" \
            --claim-token="<< pipeline.parameters.dispatch_token >>" \
            --ephemeral
workflows:
  mockarty:
    when: << pipeline.parameters.dispatch_token >>
    jobs:
      - serve

Trigger URL: https://circleci.com/api/v2/project/<slug>/pipeline
Auth: Header, secret {{secret:ci-vault/circle_token}} mapped to
Circle-Token: ….
Body template:

{
  "branch": "main",
  "parameters": { "dispatch_token": "{{.DispatchToken}}" }
}

Security model

  • Namespace scoping — triggers + runs are scoped to a namespace.
    A caller in NS-A cannot read or cancel a trigger/run in NS-B; the
    API returns 404 (existence is not leaked) rather than 403.
  • Single-use tokens — each dispatch token lets exactly one
    runner pick up exactly one task. A leaked token cannot be reused
    for anything else.
  • Redacted secrets — your auth secret is never returned in
    plain text by the API.
  • Delete goes to the Recycle Bin — a deleted trigger leaves every
    listing at once and moves to the namespace’s Recycle Bin (entity type
    CI Trigger), where it can be restored until the retention window
    ends. Runs it already started keep their history and come back with
    it; the final purge removes the runs together with the trigger and is
    refused while one of them is still in flight. The API answers the
    delete with recoverable: true.

Limitations

  • During a brief cluster leader handover, status polling pauses for
    one tick; in-flight CI runs are not lost.
  • There is no built-in “cancel CI job” callback — POST /ci/runs/:id/ cancel marks the local run as cancelled and fails the linked task
    but does NOT call any remote cancel API.

See also