@laikacms/gitlab
v2.0.0
Published
GitLab-backed StorageRepository for Laika CMS. Stores content in a GitLab project via the REST v4 API; authenticates with a Personal Access Token or OAuth bearer token. Runtime-agnostic — only depends on `fetch`.
Maintainers
Readme
@laikacms/gitlab
GitLab-backed StorageRepository for Laika CMS. Stores content as commits in a GitLab project via
the REST v4 API; authenticates with a Personal Access Token, an OAuth bearer token, or a CI job
token. The runtime parallel of @laikacms/github, with one major simplification:
GitLab tokens are long-lived, so there is no App-installation flow — bring a token, point at a
project, and write.
Runtime-agnostic: only depends on fetch. Works on Node, Bun, Deno, Cloudflare Workers, and the
browser.
Why a PAT (vs an App)
GitLab Personal Access Tokens scope by project membership and permission set (read_repository,
write_repository, api). Combined with the GitLab "service account" feature (or a regular bot
user), they cover the same threat model as a GitHub App installation token but without the
JWT-mints-installation-token dance — one fewer moving part to break.
For multi-tenant hosting where end users grant you access to their own projects, prefer OAuth 2
bearer tokens (oauthToken) over PATs.
Usage
import { GitlabStorageRepository } from '@laikacms/gitlab/storage-gl';
import { markdownSerializer } from 'laikacms/storage-serializers-markdown';
const repo = new GitlabStorageRepository({
projectId: 'esstudio/content', // numeric id OR `group/subgroup/project`
branch: 'main',
auth: { token: process.env.GITLAB_PAT! },
// apiUrl: 'https://gitlab.example.com/api/v4', // self-hosted; defaults to gitlab.com
serializerRegistry: { md: markdownSerializer },
defaultFileExtension: 'md',
commitAuthor: { name: 'Laika Bot', email: '[email protected]' },
});OAuth bearer token
new GitlabStorageRepository({
projectId: 12345,
branch: 'main',
auth: { oauthToken: userOauthToken },
serializerRegistry,
defaultFileExtension: 'json',
});CI job token
Useful when the same repo also hosts a GitLab CI pipeline that writes content back:
new GitlabStorageRepository({
projectId: process.env.CI_PROJECT_ID!,
branch: process.env.CI_COMMIT_REF_NAME!,
auth: { jobToken: process.env.CI_JOB_TOKEN! },
serializerRegistry,
defaultFileExtension: 'md',
});Extra auth headers
auth.headers are merged into every request alongside the auth-method header. The extra headers are
applied first; the auth-method header (PRIVATE-TOKEN, Authorization, or JOB-TOKEN) is always
written after them, so you cannot accidentally override credentials via auth.headers. Defaults to
none. Useful for GitLab instances behind a reverse proxy that requires its own header (e.g. an
internal gateway token) alongside your normal GitLab credentials:
new GitlabStorageRepository({
projectId: 'esstudio/content',
branch: 'main',
auth: {
token: process.env.GITLAB_PAT!,
headers: { 'X-Gateway-Token': process.env.GATEWAY_TOKEN! },
},
serializerRegistry,
defaultFileExtension: 'md',
});Advanced options
ignoreList— glob patterns for files excluded from directory listings. When supplied, overrides the built-in list entirely. Default:**/.keep **/.DS_Store **/Thumbs.db **/desktop.ini **/.catalog **/.laikacmscommitAuthor—{ name: string; email: string }stamped as both the author and committer on every write call (shown in the usage examples above). Omit to let GitLab fall back to the identity of the authenticated token.determineExtension— custom resolver that picks the file extension for a new object given its key and metadata. Replaces the built-indefaultDetermineExtensionlogic when provided.
Behaviour notes
- Extension hiding. Keys are extension-free at the boundary, exactly like
@laikacms/githubandlaikacms/storage-fs. The on-server file extension is chosen from the registered serializers and looked up on read. - Upsert via POST → PUT.
createOrUpdatefirst triesPOST /repository/files/...(create); on the "already exists" path (HTTP 400 with the matching message) it transparently retries withPUT(update). TherevisionIdreturned inmetadatais the file'slast_commit_id, which you can pass back viaupdate.metadata.revisionIdfor optimistic-concurrency updates. - Empty directories. Git tracks files, not directories.
createFolderwrites a.keepfile (filtered out of listings via the same ignore list asstorage-fsand@laikacms/github). - Listings on missing folders are reported as
recoverableErrors(aNotFoundError). Note that@laikacms/githubbehaves differently: GitHub's API cannot distinguish an empty directory from a missing one (both return 404), so the GitHub backend maps 404 → empty results instead of aNotFoundError. Bitbucket matches GitLab — missing folders surface as arecoverableError. - Pagination. Cursor pagination is not supported. The directory listing pages through
X-Next-Pageuntil exhausted, then offset/page styles are applied in memory. - Self-hosted. Pass
apiUrl: 'https://gitlab.example.com/api/v4'for a self-hosted instance. - Custom
fetch. Defaults toglobalThis.fetch. Passfetchto swap in a different implementation — e.g. a mockedfetchin tests, or an instrumented/proxied one on a runtime without a globalfetch. userAgent. Sent as theUser-Agentheader on every request. Defaults to@laikacms/gitlab. Override it to identify your app in GitLab's request logs, e.g.userAgent: 'my-cms/1.4.0'.
What this does not do
- No GitLab Merge Request integration. Each write is a direct commit on the configured
branch. If you want a "draft → MR → review" workflow, do it at a layer above (e.g. wirebranchto a per-editor branch and open the MR yourself). - No webhooks. If you need to react to changes pushed from elsewhere, subscribe to GitLab webhooks separately and invalidate your caches.
- No LFS. Objects are stored as plain files; binary assets belong behind the assets API, not the storage API.
