.. _howto-ci: Set up CI: watched repos, webhooks, statuses ============================================ Goal: every push builds itself, the instant it lands if you want, and the forge shows the verdict on the commit. 1. Register the watch --------------------- Register a repo once; saggar.dev polls its refs and a head it has not seen triggers the same fan-out a manual submission would: one group per commit, attributed to the watch's project: .. code-block:: console $ saggar watch add https://git.example.com/me/web \ --branch main 'release/*' \ --formats auto --project web Semantics worth knowing (full flags: :ref:`the CLI reference `): - **A head triggers once, ever**: dedup is per (watch, commit sha); re-polling a known head is a no-op, a force-push to a new sha triggers afresh. - **Builds and tests by default**: a triggered push produces the formats×arches group plus a test member sharing the group id. ``--no-test`` drops the test member; ``--test`` alone is a test-only watch. - The watch's flags are the defaults its triggered builds run with; there are no per-push arguments on this path. - Polling is plain git (``git ls-remote`` on the interval, first poll at startup), with no credentials attached. A repo whose refs need a credential to read cannot be polled; a repository webhook (next section) still reaches it, and github.com repositories covered by an installation get their private clone at claim time (see :doc:`github-import`). No credential flag exists on the CLI, on purpose. One watch per repo, keyed by the normalized URL. ``saggar watch trigger `` polls right now, which is how you test a fresh registration. 2. (Optional) Instant builds with forge webhooks ------------------------------------------------ Polling needs no forge-side setup. To build the instant a push lands, point the forge's webhook at the service and paste the watch's webhook secret, shown by ``saggar watch show w-…``: - URL: ``https://saggar.dev/hooks/gitea`` (Gitea and Forgejo) or ``…/hooks/github``; event type ``push`` only. - The delivery is authenticated by an HMAC-SHA256 signature of the raw body under the watch's secret: Gitea/Forgejo send it in ``X-Gitea-Signature`` (hex), GitHub in ``X-Hub-Signature-256`` (``sha256=``). Deliveries answer honestly, so the forge's delivery log reads sanely: .. list-table:: :header-rows: 1 * - Answer - Meaning * - ``200 {"triggered": […]}`` - a new commit built, same shape as the trigger endpoint * - ``204`` - understood, nothing new: redelivery, filtered ref, ref deletion, disabled watch, non-push event * - ``400`` - the body is not a push payload * - ``401`` - signature missing or wrong * - ``404`` - no watch for this repository * - ``410`` - the watch has no webhook secret configured The same rules govern a webhook as a poll: the watch's branch/tag globs decide, one commit builds exactly one group ever (the dedup gate makes redeliveries free), and a webhook cannot add refs the watch does not listen to. Rotate a leaked secret with ``PUT /api/v1/watches/:id`` and ``{"rotate_webhook_secret": true}`` (then update the forge config). 3. (Optional) Commit statuses ----------------------------- When the watch names its forge and a credential, saggar reports one commit status per triggered commit: ``pending`` the moment the group is queued, then one terminal verdict when the whole group settles: ``success``, or ``failure`` naming the first failing job's class and phase. The status carries ``context: "saggar"`` and, when the service's public address is configured, a ``target_url`` link to the group. Set it on the watch (API; the CLI holds no credential flag by design): .. code-block:: console $ curl -X PUT -H "X-Auth-Token: sgt-…" \ -d '{"forge": "gitea", "credential": ""}' \ https://saggar.dev/api/v1/watches/w-1a2b3c4d - ``forge``: ``gitea`` or ``forgejo`` (statuses API beside the repo), ``github`` (api.github.com), or ``generic`` (the default; it reports nothing). - ``credential``: the forge API token, sent as ``Authorization: token …``. Stored write-only: unlike the webhook secret, it is never served back. Status reporting is optional and never a build dependency: a ``generic`` forge, a missing credential, or a hand-submitted group reports nothing, and a failed status post is logged once and never retried. A forge outage cannot red the build it reports on. 4. Tokens for CI ---------------- CI holds the same personal tokens a laptop does: minted through :doc:`login`, one per machine, expiring when you can afford it: .. code-block:: console $ saggar token create "ci-runner" --expires-days 90 The runner then submits headlessly with the environment layering (flags > ``SAGGAR_SERVER``/``SAGGAR_TOKEN`` > ``cli.toml`` > default): .. code-block:: yaml # .gitlab-ci.yml (any CI works the same) build: script: - SAGGAR_TOKEN=$SAGGAR_TOKEN saggar submit . --formats auto --quiet - SAGGAR_TOKEN=$SAGGAR_TOKEN saggar download --group $GROUP -o out/ For a CI machine that only ever consumes artifacts, prefer a pull-only token (``{"scope": "registry"}``): it authenticates on the registries and nowhere else (see :doc:`../reference/registries`).