Export to GitLab
The plugin creates a branch, commits your exported icons, and opens a merge request. To do this via the GitLab REST API, your token must have API write access and your user/token must have at least Developer role on the repository.
Important:
write_repositoryalone is not enough for the REST API (it only covers Git-over-HTTP). Use theapiscope to call endpoints like “create branch”, “create commit”, and “create MR”.
Option A — Personal Access Token (user-scoped)
- In GitLab, go to User menu → Edit profile → Access Tokens
(on gitlab.com: Settings → Access Tokens). - Create a token:
- Scopes: check
api(required). - (Optional) also check
read_repository/write_repositoryif you plan to use Git-over-HTTP, butapiis the key scope for the REST calls this plugin makes. - Set an expiry you’re comfortable with.
- Scopes: check
- Copy the token.
Option B — Project Access Token (least privilege for a single repo)
- In your project, go to Settings → Access Tokens.
- Create a token:
- Role: Developer (or higher).
- Scopes:
api(required).
(read_repository/write_repositoryoptional if you’ll use Git-over-HTTP.)
- Copy the token.
You can also use a Group Access Token in Group → Settings → Access tokens with Role: Developer and Scope:
apiif the repo lives in a group.
Add token in the plugin
Open Settings in the plugin and fill:
- GitLab domain (e.g.,
https://gitlab.comor your self-hosted URL) - Project ID (numeric ID). Find it on the project’s Overview → Details page (shown as “Project ID”).
- Token (paste the PAT or Project token from above)
Click Save, then run your export.
Troubleshooting
403 Forbiddenorinsufficient_scope- Token doesn’t include
apiscope → regenerate withapi. - Token isn’t associated with a member that has Developer (or higher) access to the project.
- Token doesn’t include
- Branch creation fails / push blocked
- Protected branches: pushing to
mainis blocked by default—create a feature branch offmain.
Ensure your role (Developer+) is allowed to create branches (check Settings → Repository → Protected branches).
- Protected branches: pushing to
- Project ID not found / 404
- Double-check the GitLab domain and Project ID. The ID is a number (not the path).
If the project is private, the token must have access to it.
- Double-check the GitLab domain and Project ID. The ID is a number (not the path).
- Self-managed GitLab
- Ensure your instance permits API access and your token isn’t expired (some instances enforce max expirations).
Minimal API calls used by the plugin (for reference)
- Create a branch
POST /projects/:id/repository/brancheswith paramsbranch=<new-branch>&ref=<base-ref> - Commit files (single or batch)
POST /projects/:id/repository/commitswithactions[]payload - Open a merge request
POST /projects/:id/merge_requestswithsource_branch,target_branch,title
All of the above require the
apiscope and a Developer (or higher) access level.
Example Design System repository
Jump‑start your setup
We provide a complete example Design System repository you can copy for a production‑ready icon workflow:
Icons directory:
src/icons/*CI job (GitHub Action) that automatically optimizes/normalizes icons and pushes updates to the MR
Storybook 9 Docs with examples and guidelines
Example flexible
Iconcomponent with propsExample Icons Gallery with advanced filters and click‑to‑copy
Repository: https://github.com/Sofian-Design/design-system-repository-demo
Storybook: https://sofian-design.github.io/design-system-repository-demo
See also: Design System Demo Repository
scripts/optimize-icons.mjs
import { readdir, readFile, writeFile } from 'fs/promises';
import path from 'path';
import { optimize } from 'svgo';
const ICONS_DIR = path.resolve('src', 'icons');
async function* getSvgs(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = path.join(dir, dirent.name);
if (dirent.isDirectory()) {
yield* getSvgs(res);
} else if (res.endsWith('.svg')) {
yield res;
}
}
}
for await (const file of getSvgs(ICONS_DIR)) {
const source = await readFile(file, 'utf8');
const { data } = optimize(source, {
path: file,
multipass: true,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
});
const colorized = data
.replace(/stroke="#([0-9a-fA-F]{3,6})"/g, 'stroke="currentColor"')
.replace(/fill="#([0-9a-fA-F]{3,6})"/g, 'fill="currentColor"');
await writeFile(file, colorized, 'utf8');
}