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
/perfslash 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 avariablesarray; onePRIVATE-TOKENPAT covers both fire and status polling. - GitHub workflow_dispatch —
repository_dispatchevent. - 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:
- Mockarty looks up the trigger and validates it’s enabled + in your
namespace. - Generates a fresh 64-hex
dispatch_token. - Renders the trigger body template with the new token + task ID.
- POSTs the rendered body to the trigger URL with the configured auth.
- Parses the response with
statusIdJsonpathand stores the
external job ID on the run. - Submits the Mockarty task with the same
dispatch_token. - The CI pipeline spawns a Mockarty runner that calls
/api/v1/runner/tasks/pullwithclaim_tokens=[<token>]— only that
runner can claim the task. - Mockarty polls the
statusUrlTemplateon the trigger’s schedule;
when the external state hitssuccess/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
ciTriggerIdis 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
ciEnvobject 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 ofstatusErrorJsonpathas 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:
-
Open Settings → Secret stores and create a store (or reuse
an existing one), e.g.ci-vault. -
Add a key to that store, e.g.
gitlab_pat_prod, with the actual
token as its value. -
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:
- In the trigger editor open Authentication and pick
From integration. - 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 withrecoverable: 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/ cancelmarks the local run as cancelled and fails the linked task
but does NOT call any remote cancel API.
See also
- Ephemeral Runners (CI / Kubernetes) — the
underlying runner-claim mechanism CI Triggers build on. - Runner Labels and Targeting — how to select
the runner you spawned with extra constraints.