Skip to main content

Gitlab Token

@sntxrr/gitlab-tokenv2026.08.29.3· 1mo agoMODELSWORKFLOWS
01README

Create, rotate, revoke and inventory GitLab personal, project and group access tokens through one model, with a threshold-gated rotation workflow and the one-time token value written to a vaulted sensitive field

02Release Notes

Fixes the valid-target preflight, which failed any instance that relied on a documented default. This closes the known issue published with 2026.08.29.2.

The minimal valid instance failed its own check. An instance setting only token — taking every documented default — produced two errors: tokenScope "undefined" requires namespace and baseUrl "undefined" must be an absolute http(s) URL. The tokenScope one sent you in a circle, because setting a namespace then trips namespace is set but tokenScope is personal. Neither message was about the real problem: neither field had been read.

A swamp check runs against the instance YAML as written, BEFORE the schema applies its defaults, so an omitted field arrives as undefined. This check compared those against literals, and its parameter was typed as the parsed globals type, in which both fields are required and present — a lie the compiler could not catch. Every existing test spread a fixture that supplied both fields by hand, so a green suite coexisted with a check that failed the minimal real instance.

The three defaults are now exported constants used by both the schema and the check, so the two cannot drift, and the check's parameter is typed to mark every defaulted field optional — the compiler now insists on a fallback at each read. The new tests pass raw objects rather than spreading the fixture, and one asserts that the schema's parsed defaults ARE those constants, which is the invariant rather than a restatement of it.

Verified by execution: { token } now yields an empty error list, { token, tokenScope: "project" } still yields exactly one, and a malformed baseUrl that was actually supplied still yields its own — the fix must not turn the check into a no-op.

Nothing to migrate. If you wrote baseUrl or tokenScope out explicitly to work around this, you can drop them or keep them; both are correct.

Also in this release: three new exported constants (DEFAULT_BASE_URL, DEFAULT_TOKEN_SCOPE, DEFAULT_TIMEOUT_MS), and a README Pre-flight section that states the check reads the instance as written and lists the tokenId/tokenName rejection it has enforced since 2026.08.29.1 without documenting.

03Models1
@sntxrr/gitlab-tokenv2026.08.29.3gitlab_token.ts

Global Arguments

ArgumentTypeDescription
tokenstringGitLab token used to authenticate these API calls, sent as PRIVATE-TOKEN.
baseUrlstringGitLab instance base URL, without the /api/v4 suffix. Override for self-managed.
tokenScopeenumWhich token family this instance manages: personal, project, or group.
namespace?stringProject path (group/repo) or group path owning the token, URL-encoded
tokenId?stringID of the token this model manages. Also accepts the literal `self` to
tokenName?stringName of the token this model manages, resolved to its live ID on every
userId?stringUser ID to create a personal access token for. Requires instance
timeoutMsnumberAbort any single API request after this long.
fn sync()
Fetch one token's current metadata.
fn create(name: string, scopes: array, expiresAt?: string, description?: string, accessLevel: enum)
Provision a new token; writes its metadata and its one-time value.
ArgumentTypeDescription
namestringName of the new token.
scopesarrayScopes to grant, e.g. [api] or [read_repository, read_registry]. A
expiresAt?stringExpiry date as YYYY-MM-DD. Omit to take the instance's maximum allowable
description?stringFree text carried on the token, up to 255 characters.
accessLevelenumRole the token acts as. Project and group tokens only; ignored for personal.
fn rotate(expiresAt?: string, when: boolean)
Rotate the token: GitLab revokes the current value and issues a new one under a new ID.
ArgumentTypeDescription
expiresAt?stringExpiry date for the replacement token as YYYY-MM-DD. Omit to take
whenbooleanRotate only when true. Exists because swamp workflows cannot express
fn delete(when: boolean)
Revoke the token. Already-revoked is treated as success, not an error.
ArgumentTypeDescription
whenbooleanRevoke only when true. Same rationale as the argument of the same name
fn list(state: enum, search?: string, expiresBefore?: string)
Discover every token in the configured scope (factory); writes one snapshot each.
ArgumentTypeDescription
stateenumFilter by token state. `all` omits the filter and returns both.
search?stringFilter by token name.
expiresBefore?stringOnly tokens expiring before this date (YYYY-MM-DD). Use to find what is

Resources

token(infinite)— Snapshot of a token's metadata — scopes, expiry, days remaining, last use. Never the value.
secret(infinite)— A token value as disclosed once by create or rotate, in a vaulted sensitive field
ci-variable(infinite)— Record that a rotated value was written into a project's CI/CD variable. Never the value.
04Workflows1
@sntxrr/gitlab-token-rotationec6eda6a-da94-4f00-8671-349695c7ebc7

Keep one GitLab access token ahead of its own expiry: check what it has left every day, and rotate it only when a human has said to and it is actually close to lapsing. The default posture is deliberate. `rotate` defaults to FALSE, so the scheduled run is a read-only check that keeps the token's `daysRemaining` current and does nothing else. Rotation is irreversible the instant it lands — GitLab revokes the outgoing value immediately, and replaying it trips reuse detection which revokes the whol

checkRead the managed token's current expiry
1.sync-tokengitlab-token.sync— Read-only. Writes a `token` snapshot carrying `daysRemaining` twice: keyed by the token's real GitLab ID for the audit trail, and under the stable alias `managed`, which the rotate step below reads back. This is also where `tokenName` earns its keep. If the instance is configured by name rather than by ID, this step resolves it to the live ID first — so the run after a rotation addresses the NEW token rather than the revoked one it replaced.
rotateRotate, but only when told to and only when close to expiry
1.rotate-tokengitlab-token.rotate— Gated on BOTH `inputs.rotate` and the days-remaining threshold, in the model's own `when` argument. A false predicate costs nothing: the model returns before it touches the API and writes no resource, so the existing snapshot stays true rather than gaining a generation that was never issued. The threshold reads the `managed` alias, NOT `findBySpec`. This model keys each snapshot by the token's real GitLab ID so a retired generation is never overwritten, which means `findBySpec` returns EVERY gen
propagateCarry the rotated value through to the consumer that reads it
1.update-ci-variablegitlab-token.update-ci-variable— The step that turns a rotation into a completed handover rather than an outage. GitLab discloses a token value exactly once; if nothing writes it to the consumer, the previous value is already revoked and the pipeline that depended on it fails on its next run. Reads the value from the `current` alias, NOT from a snapshot keyed by token ID. Rotation mints a new ID every time, so an ID-keyed instance cannot be named in a file written beforehand, and `findBySpec` returns every generation with no wa
05Previous Versions4
2026.08.29.2

Workflow-only release. No model code changed; two defects in the shipped workflow did.

It no longer schedules itself. 2026.08.29.1 shipped a trigger:, so installing the extension registered a daily 06:40 job on every host — at a time this author picked, against whatever model instance matched the hardcoded name gitlab-token. On the first host to install it, that instance existed and was managing a production token, and the vendored job landed ten minutes before the operator's own rotation schedule. The cadence belongs to the operator. Set one with swamp workflow trigger set @sntxrr/gitlab-token-rotation --schedule "40 6 * * *" --input rotate=true --input ciVariableProject=group/repo --input ciVariableKey=GITLAB_PUSH_TOKEN, and read the README's new Scheduling section first: both defaults are safe rather than useful, and passing only some of the inputs fails quietly in two different directions.

Propagation is no longer skipped exactly when it is needed. The propagate step gated on the same days-remaining threshold as rotate — but by the time it runs, rotate has already overwritten the managed snapshot with the NEW token, which has a full lifetime ahead of it. The threshold was therefore false precisely when the handover was required: the outgoing value revoked, the replacement never published, and the run reporting success. The gate is now current.observedAt == managed.observedAt, which is true only for the run that produced the secret, since rotate writes the snapshot and the secret in one call with one timestamp while a plain sync refreshes the snapshot alone.

Data references in step inputs are safe-navigated. A step's inputs are evaluated even when its when is false — when gates execution, not evaluation — so data.latest(model,'current').attributes.token killed the run with "No such key: attributes" before any rotation had ever happened. The placeholder that replaces it contains spaces, which GitLab's masking rules forbid, so a masked write of it is refused with a 400 rather than landing a junk value in a live CI variable.

Guarded by tests over the workflow file itself, because no model test could see any of this: trigger is absent, propagate's when mentions observedAt and does NOT mention daysRemaining, and both data inputs are safe-navigated.

Known issue, recorded rather than carried silently: the valid-target preflight reads the raw globalArguments rather than the zod-parsed ones, so an instance relying on the documented baseUrl default fails its own check with baseUrl "undefined". Write baseUrl: https://gitlab.com explicitly. The fix belongs in the check and would have made this release's no-op upgrade entry untrue, so it is deferred.

Upgrading is a no-op for model instances. If you VENDORED a copy of the workflow, no upgrade can reach it — take this one.

2026.08.29.1

Rotation now completes the handover, addresses the right token, and the workflow actually loads.

update-ci-variable — a new method that writes the rotated value into a GitLab CI/CD variable. Without it a scheduled rotation was a timed outage: GitLab discloses a token value exactly once and revokes the outgoing one immediately, so nothing downstream ever received the replacement. It updates and deliberately will not create, because upserting a mistyped key produces a variable nothing reads while the real consumer keeps presenting a revoked value — a silent outage reported as a green run. masked and protected are re-asserted on every write, since GitLab does not carry them forward and a silently unmasked secret prints in the next job log.

tokenName — identify the managed token by name rather than by id. Rotation mints a new id every time, so a configured tokenId names the revoked predecessor from the second run onward: the schedule authenticates fine and then operates on a dead token forever. A rotation schedule configured with tokenId worked exactly once. Names survive rotation. Resolution is exact-match over active tokens only (GitLab's search is a substring match), duplicates are refused rather than guessed between, and setting both identifiers is an error. tokenId remains correct for pinning one specific generation.

Stable aliases — current on the secret spec and managed on the token spec. Every instance was previously keyed by a token id that does not exist until the rotation creates it, so nothing authored beforehand could name one, and findBySpec returns every generation with no CEL construct for "the newest". Read data.latest(model, 'current').attributes.token. managed also fixes a latent workflow bug: the threshold predicate used findBySpec, which still returns the generation just rotated away sitting at zero days remaining, so it stayed true permanently once any token neared expiry and would have rotated on every run.

The rotation workflow now loads. It requires a top-level UUID id and had none, so swamp dropped it with a single warning: swamp workflow list omitted it and swamp workflow run answered "Workflow not found" for a file in the repo. The manifest records that a workflow file exists, not that it parses, so 2026.08.19.2 packaged, published and scored 100% around a workflow that could never be invoked. Fixed, and guarded by a test over the file itself — no preflight check can catch this, since checks run against a model instance and the broken artifact was a workflow.

The workflow is now three jobs: check, then rotate, then propagate. Both rotation gates are unchanged — rotate defaults to false, and the threshold must also hold — so the scheduled run stays read-only. New inputs ciVariableProject and ciVariableKey; leaving either empty skips propagation.

No upgrade action required. tokenName is optional with no default and no persisted resource shape changed. Existing instances keep working on tokenId, including its rotation weakness; moving to tokenName changes which token a rotation acts on and is therefore an operator's edit rather than something an upgrade should do unasked.

Note for GitLab.com Free namespaces: project and group access tokens cannot be created at all — both endpoints refuse even a group Owner, naming a permission no role can grant. Fine-grained personal tokens are the least-privilege option available there.

Modified 1 models. Added 1, removed 1 workflows

2026.08.19.2

Added 1 workflows

2026.08.19.1
06Stats
A
100 / 100
Downloads
18
Archive size
45.1 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License declared1/1earned
  • Verified public repository2/2earned
07Platforms
08Labels