GitHub Action - ThinkHumanDotDev/marmot GitHub Wiki
GitHub Action
Marmot ships a GitHub Action that runs from your workflows against your Marmot instance:
run: runs the checks of the monitors you list now ("Check now"), waits for the results, writes a table to the job summary and fails the job when a monitor is down or an assertion fails. Use it after a deploy to make CI fail when the new version does not pass the checks Marmot runs in production.apply: applies a monitors-as-code file, likemarmot monitors apply -f marmot.yaml -y. On pull requests it only plans and shows the diff in the job summary.
- uses: thinkhumandotdev/marmot/[email protected]
with:
url: https://status.example.com
api-key: ${{ secrets.MARMOT_WRITE_KEY }} # a write key: checks are writes
org: '1'
monitors: api, website
The action lives in the action/ folder of the Marmot repository and reuses the
CLI's code (the API client, plan and apply). It runs on the runner's Node.js; nothing is built or
installed in your job, and it talks to Marmot only through the
management API.
Setup
- Create an organization API key under Settings → API keys and store it as a repository or environment
secret (
MARMOT_WRITE_KEYbelow).runandapplyneed a write key: checks and changes are writes. A read key is enough for plans (mode: applywith a dry run); a read key cannot list notification channels, so its plans leave channel changes out (see Plan and apply). - Note the organization id (shown on the API keys page) and the URL of your instance. The runner must be able to reach the instance; for an instance on a private network use a self-hosted runner.
The connection inputs fall back to the environment variables the CLI reads (MARMOT_URL,
MARMOT_API_KEY, MARMOT_ORG), so a job-level env: block can set them once for several steps.
Inputs
| Input | Default | Description |
|---|---|---|
url |
$MARMOT_URL |
Base URL of the instance, e.g. https://status.example.com. |
api-key |
$MARMOT_API_KEY |
Organization API key (mk_…). Pass it from a secret. |
org |
$MARMOT_ORG |
Organization id. |
mode |
run |
run (check now) or apply (monitors as code). |
monitors |
run: monitor keys or ids (12 or #12; a key wins when both match), separated by newlines, commas or spaces. |
|
config |
Monitors file (YAML or JSON). apply: the file to apply. run: when monitors is empty, every monitor of the file is checked, except paused (active: false) and push monitors. |
|
fail-on-degraded |
false |
run: also fail when a check is degraded (slower than the monitor's threshold). |
dry-run |
auto |
apply: true only plans, false applies. auto plans on pull_request, pull_request_target and merge_group events and applies on every other event (push, schedule, …). |
prune |
false |
apply: delete monitors that have a key but are no longer in the file (as --prune; monitors without a key are never deleted). |
Outputs
| Output | Mode | Description |
|---|---|---|
result |
run |
passed or failed. |
passed |
run |
Number of passing checks. |
failed |
run |
Number of failing checks. |
skipped |
run |
Number of skipped checks (monitors in maintenance). |
json |
run |
The results as a JSON array: ref, id, key, name, status, ping, statusCode, message, assertions, verdict. |
changes |
apply |
Number of changes in the plan (monitors to create, update or delete and tags to create). |
plan |
apply |
The plan as text, in the diff format of the job summary (e.g. for a pull request comment). |
dry-run |
apply |
true when the step only planned. |
Run checks after a deploy
The action calls POST /api/orgs/:orgId/monitors/:id/check for each monitor and waits for the result
(up to the monitor's timeout). The result is stored as a manual heartbeat, like Check now in the UI:
a failing check after a deploy shows up in the monitor's history and notifies as usual.
| Check result | Verdict |
|---|---|
up |
passed |
degraded |
passed with a warning; failed with fail-on-degraded |
down, or any failed assertion |
failed |
pending (the check failed, the monitor has retries left) |
failed |
| in a maintenance window | skipped |
| unknown key or id, paused or push monitor, timeout, API error | failed (shown as error) |
The job fails (exit code 1) when at least one check failed. Each failure is also an error annotation on
the run. Checks run three at a time. Instances limit on-demand checks per organization
(ON_DEMAND_CHECKS_PER_MINUTE, 30 per minute by default); when the limit is hit the action waits and
retries.
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh # your deployment
- name: Run Marmot's checks against the new version
uses: thinkhumandotdev/marmot/[email protected]
with:
url: https://status.example.com
api-key: ${{ secrets.MARMOT_WRITE_KEY }} # a write key: checks are writes
org: '1'
monitors: |
api
website
checkout
fail-on-degraded: 'true'
If the deployment needs a moment before it serves traffic, wait for it first (for example with a
curl --retry loop): the action checks once and does not retry a failing check. To check every monitor of
your monitors file instead of a list, pass config: marmot.yaml and leave monitors out.
The job summary looks like this:
2 passed, 1 failed, 0 skipped · https://status.example.com
Result Monitor Status Response Message ✅ passed API apiup 87 ms · HTTP 200 200 - OK ✅ passed Website websiteup 140 ms · HTTP 200 200 - OK ❌ failed Checkout checkoutdown 230 ms · HTTP 502 Assertion failed: status equals 200 (got 502)
Monitors as code in pull requests
Plan on pull requests (the diff goes to the job summary and nothing changes), apply on the main branch.
With dry-run: auto (the default) one step does both:
# .github/workflows/monitors.yml
name: monitors
on:
pull_request:
paths: [marmot.yaml]
push:
branches: [main]
paths: [marmot.yaml]
jobs:
monitors:
runs-on: ubuntu-latest
# Applies on main only; one apply at a time.
concurrency: marmot-monitors
steps:
- uses: actions/checkout@v4
- uses: thinkhumandotdev/marmot/[email protected]
env:
# ${VARIABLES} referenced in marmot.yaml
API_HEALTH_TOKEN: ${{ secrets.API_HEALTH_TOKEN }}
with:
mode: apply
config: marmot.yaml
url: https://status.example.com
org: '1'
api-key: ${{ secrets.MARMOT_WRITE_KEY }}
prune: 'true'
The summary shows the plan as a diff (+ create, ! update, - delete), the warnings (monitors with a key
that are not in the file, ambiguous adoptions) and whether it was applied. Credential fields (passwords,
tokens, connection strings, headers) only show that they change, never their values; still, the job
summary is visible to everyone who can read the repository's Actions runs.
To use a read key for plans and a write key for applying, pass
api-key: ${{ github.event_name == 'push' && secrets.MARMOT_WRITE_KEY || secrets.MARMOT_READ_KEY }}.
Pull requests from forks get no secrets: plans only run for branches of the repository itself.
To also post the plan on the pull request, use the plan output, for example with the GitHub CLI
(the job then needs permissions: pull-requests: write):
- uses: thinkhumandotdev/marmot/[email protected]
id: marmot
with:
mode: apply
config: marmot.yaml
url: https://status.example.com
org: '1'
api-key: ${{ secrets.MARMOT_READ_KEY }}
- if: github.event_name == 'pull_request' && steps.marmot.outputs.changes != '0'
env:
GH_TOKEN: ${{ github.token }}
PLAN: ${{ steps.marmot.outputs.plan }}
run: printf '### Marmot plan\n\n```diff\n%s\n```\n' "$PLAN" | gh pr comment ${{ github.event.number }} --body-file -
An invalid file (syntax, unknown fields, unknown channel names, unset ${VARIABLES}) fails the step with
the problems listed in the summary. A change the API refuses stops the apply there; what was applied
before stays, and the next run picks up from the current state (see Plan and apply).
Versions
The action is versioned with Marmot: the release tag vX.Y.Z of the repository is the action's version, and
thinkhumandotdev/marmot/[email protected] runs the action of that release. Use the version of your instance
(or a newer one), and pin a tag or a commit SHA. While Marmot is 0.x there is no moving v0 tag, and
@main follows development. The code is in src/action/ and bundled into action/dist/index.mjs
(pnpm build:action, see Development).
Other CI systems
Elsewhere, use the CLI directly: marmot monitors check KEY exits with code 4 when the result is
down, marmot monitors plan -f FILE / marmot monitors apply -f FILE -y work as in the action (see the
exit codes). The CLI is one file, /app/dist/cli/marmot.mjs in the Marmot image, and
needs Node.js 20 or later.
GitLab CI, using the Marmot image as the job image:
# .gitlab-ci.yml
.marmot:
image:
name: ghcr.io/thinkhumandotdev/marmot:0.5.0
entrypoint: ['']
variables:
MARMOT_URL: https://status.example.com
MARMOT_ORG: '1'
# MARMOT_API_KEY: a masked CI/CD variable
post-deploy-checks:
extends: .marmot
stage: .post
script:
- for key in api website checkout; do node /app/dist/cli/marmot.mjs monitors check "$key" || exit 1; done
monitors-plan:
extends: .marmot
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
script:
- node /app/dist/cli/marmot.mjs monitors plan -f marmot.yaml
monitors-apply:
extends: .marmot
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- node /app/dist/cli/marmot.mjs monitors apply -f marmot.yaml -y
CircleCI, copying the CLI out of the image:
# .circleci/config.yml
version: 2.1
jobs:
marmot:
docker:
- image: cimg/node:lts
environment:
MARMOT_URL: https://status.example.com
MARMOT_ORG: '1'
# MARMOT_API_KEY: in a context or the project's environment variables
steps:
- checkout
- setup_remote_docker
- run: docker run --rm --entrypoint cat ghcr.io/thinkhumandotdev/marmot:0.5.0 /app/dist/cli/marmot.mjs > marmot.mjs
- run: node marmot.mjs monitors apply -f marmot.yaml -y
- run: for key in api website checkout; do node marmot.mjs monitors check "$key" || exit 1; done
workflows:
deploy:
jobs:
- marmot:
filters:
branches:
only: main
monitors check also prints the result; add --json for machine-readable output.