Docs · Glidepath
Change management: the itsm gate
Glidepath does not run a change tool. It wraps the one a company already has: a change request is created for
every release, the itsm gate holds the release PR until the change is approved or scheduled, and the change
follows the release to its end (implementing on merge, closed successful when the release is healthy, closed
unsuccessful when it is rolled back, with the rollback’s emergency change linked). ServiceNow is the first
provider; a webhook provider fronts any other tool with one endpoint. With no provider configured the gate
stays the loud stub it was (governance-stubs.md, ADR-0003) and every hook is a no-op.
Whether the gate blocks is the registry’s business, not the provider’s: releaseGuardrails[itsm].required
(release-guardrails.md, “Required and advisory gates”), true by default once a
provider is set. On GitHub Free a red check still cannot block a merge (GATE-03); a merge past a red itsm
check is a detected bypass (GATE-04).
The lifecycle
| When | Who | What happens to the change request | On the release record |
|---|---|---|---|
| The release PR opens | open-release-pr |
Created: a normal change for a promote, an emergency change for a rollback (its parent is the change of the release it rolls back). Carries app, environment, image, release id (correlation_id) and, once the PR exists, the PR link (a work note). Or linked: the caller’s change-request-id param, or the id a reused open PR’s body already names. The PR body gets **Change request**: CHG... (provider, state ...) and a hidden <!-- glidepath:change-request:<id> --> marker. |
changeRequestId, changeRequestNumber, changeRequestUrl, changeRequestState |
| Every gate run on the PR | itsm-gate (Pipeline itsm-check) |
Read: the change must exist and be approved or scheduled. Fails naming the change, its state and its approval; the PR comment says how to fix it and /retest itsm-. The record first, the PR body’s marker as the fallback (a swept record, or a PR made by hand with a change someone linked). |
changeRequestState, changeRequestApproval as last seen |
| The PR merges | itsm-change implement (Pipeline bypass-merge-check, after mark-release-merged) |
State Implement (-1), work note with the PR and what is deploying where. |
changeRequestState: implementing |
The relay reports environment.deployed success |
itsm-change close-outcome (Pipeline release-outcome-notify) |
Closed, close_code: successful. |
changeRequestState: closed-successful |
The relay reports environment.deployed failure |
same | A work note; the change stays open. A degraded release can still recover (the success then closes it) or be rolled back (below). | changeRequestState: implementing-failed |
The relay reports service.rolledback |
itsm-change close-rolled-back (Pipeline release-rollback-notify) |
The change of the release that was rolled back is closed unsuccessful, naming the reason and the rollback’s emergency change as the related change. The rollback release’s own change closes successful through the row above when it turns healthy. |
on the rolled-back release’s record: changeRequestState: closed-unsuccessful, rolledBackByChange |
A release that was healthy (its change closed successful) and is rolled back later gets its change re-closed
unsuccessful by the rollback: the close is a PATCH on an already-closed change. A ServiceNow instance whose
rules forbid updating a closed change fails that step visibly (onError: continue, the TaskRun is red); the
rollback’s emergency change still names the original as its parent, so the relationship is recorded either
way.
A change tool that is down fails open-release-pr before a PR exists (a release without its change is what
change management exists to prevent), fails the gate (loudly, with the HTTP status), and is tolerated by the
later hooks (onError: continue: the failed TaskRun shows, the bypass check and the outcome notifications
still run). resolve-release-tracking exposes the change on its results (change-request-id,
change-request-number, change-request-url) for anything that reads the release outcome, and the record
fields are what Tower’s Release Record can show (Tower work is separate).
Configuration
Catalog values (charts/glidepath-catalog/values.yaml, overridden per cluster in the catalog Application):
itsm:
provider: servicenow # none | servicenow | webhook
credentialsSecret: itsm-credentials
servicenow:
instanceUrl: https://acme.service-now.com
approvedStates: ["-2", "-1"] # Scheduled, Implement (ServiceNow's default change model)
webhook:
url: https://change-adapter.internal/glidepath
releaseGuardrailOverrides: { itsm: { required: false } } makes an unapproved change advisory (the check
stays green and the PR comment says what it found) while a company gets used to the flow.
Credentials come from a platform Secret in each app’s -cicd namespace, never a value: the control plane’s
itsm.credentials renders a ClusterExternalSecret that fans itsm-credentials out of the platform’s
Infisical project into every managed namespace (secrets-management.md), from the
keys itsm-token (a bearer token) or itsm-username + itsm-password (basic auth), planted by hand in
Infisical once per cluster. The Tasks mount the Secret optional and warn when it is missing, which is what a
lab’s throwaway endpoint wants.
# glidepath-control-plane values
itsm:
credentials:
enabled: true
keys: [username, password]
The provider contract
charts/glidepath-catalog/files/lib/itsm.sh is the whole client, spliced into each Task’s script by the chart
(a Pod cannot mount a ConfigMap from the catalog’s namespace) and sourced as-is by the tests. Every call prints
one JSON object and nothing else:
{"id": "<provider id>", "number": "CHG0012345", "url": "<link>", "state": "-2", "approval": "approved", "approved": true}
| Function | ServiceNow (Table API, change_request) |
Webhook (POST the url, action in the body) |
|---|---|---|
itsm_create kind app env image release-id pr-url [rollback-of-change] |
POST /api/now/table/change_request with type, short_description, description, category, correlation_id (the release id), correlation_display: glidepath, parent for a rollback |
create with release: {kind, app, env, image, release_id, pr_url, rollback_of_change} |
itsm_get id |
GET .../change_request/<sys_id>, or ?sysparm_query=number=CHG... for a number |
get with change_id |
itsm_link_pr id pr-url |
PATCH work_notes |
link with release.pr_url |
itsm_implement id note |
PATCH state: -1, work_notes |
implement with note |
itsm_note id note |
PATCH work_notes |
note |
itsm_close id successful|unsuccessful note [related] |
PATCH state: 3, close_code, close_notes, work_notes |
close with close_code, note, related_change_id |
itsm_approved json |
approval == approved, or state in approvedStates |
the adapter’s own approved |
approved is the one field a gate decides on. A ServiceNow instance with a customised change model (states,
mandatory fields, an approval that must be requested first) is adapted here, in one place: the states through
approvedStates, anything else by editing the provider’s few lines or by putting a webhook adapter in front
of the instance.
Webhook adapter: one endpoint, one JSON body per call as in the table, the same Authorization header
(bearer or basic from the Secret), answering the object above. hack/lab/itsm_mock_server.py is a reference
adapter and a ServiceNow look-alike in one file, and the lab’s throwaway endpoint.
Verifying it
charts/glidepath-catalog/tests/itsm_test.sh(catalog CI, jobchange-management) runs the client for both providers against the mock, then the rendereditsm-gateanditsm-changescripts with a stub kubectl holding the release records, through the whole lifecycle: create, gate fails naming the change and its state, approve, gate passes, implement, failed outcome leaves it open, healthy closes it successful, a rollback closes the original unsuccessful with the emergency change related. It also checks the renderings:noneis the stub with no lifecycle tasks,servicenowrenders the gate and the hooks,required: falserenders the advisory copy, and a mock that wants a bearer token refuses an unauthenticated call loudly.- Live, in a lab: run the mock in the cluster.
hack/labis a kustomization (the Deployment, its Service, and a ConfigMap generated from the script), so a cluster repo points an Argo CD Application at it and a cluster rebuild brings the mock back (hangar-dev:50-glidepath/itsm-mock); pointitsm.webhook.urlat its Service, release, approve withPOST /_mock/approve/<id>,/retest itsm-, merge, and readchangeRequestStateon the record as the relay moves it. The 2026-10-19 run (promote, gate red then green, implementing, closed successful; rollback with an emergency change, original re-closed unsuccessful) is recorded in the comments on glidepath#140.
What a change request does not do
It does not pick the gate set, the environment or the image, and the gate never creates or moves a change: the release flow decides what is released and the change tool decides whether it may be. A person who wants a release held opens nothing in Glidepath; they leave the change unapproved. A person who wants a release past an unapproved change is a break-glass merge, detected and announced (GATE-04).