Git Provider Setup
One-time setup for using the workspace CLI (ws) with GitHub and/or GitLab. The workspace auto-detects your provider from remote URLs — no config needed for github.com or gitlab.com. Self-hosted instances need a config entry (see Provider Detection below).
Env var quick reference
All workspace credentials live in .env at the yggdrasil root (gitignored). The shell scripts source it automatically — there's no manual source .env step required during normal ws use.
| Variable | Purpose | Required when |
|---|---|---|
GH_TOKEN |
GitHub PAT (gh reads it directly) |
Using GitHub remotes |
GITLAB_TOKEN |
Default GitLab PAT (glab fallback) |
Using GitLab remotes |
GITLAB_USER |
GitLab username — referenced in setup docs and example .env. |
Optional |
GITLAB_HOST |
Self-hosted GitLab hostname | Self-hosted GitLab only |
GITLAB_<scope>_<owner>_<role> |
Per-fork / per-group GitLab tokens | Multi-token GitLab setups (see GitLab section) |
For GitLab multi-token setups, individual env vars are mapped to URL prefixes via defaults.gitTokens in ecosystem.yaml (longest-prefix match). The script gp_set_token_for_url exports the right GITLAB_TOKEN for each operation. The full naming pattern and examples live in the GitLab section below.
Verify what's currently set without changing anything:
ws gitlab-auth --status # which token slots are set/unset
ws diagnose <comp> # which token covers each remote for a component
GitHub
Install the GitHub CLI
macOS:
brew install gh
Windows (Git Bash):
winget install GitHub.cli
PATH is picked up. Alternatively, download the MSI directly from https://cli.github.com.
Linux: See https://github.com/cli/cli/blob/trunk/docs/install_linux.md
Create a classic Personal Access Token
Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic).
Settings:
- Note: descriptive name, e.g. yggdrasil-workspace
- Expiration: set a reasonable expiry (90 days, 1 year)
- Scopes:
| Scope | Why |
|---|---|
repo |
Full repo access — PRs, issues, code, cross-org forks |
read:org |
Read org membership — needed by gh pr edit and other org-aware ops |
read:discussion |
Read discussion threads — also checked by gh pr edit |
read:project |
Read GitHub Projects — checked by gh pr edit --body-file |
workflow |
Only if you modify .github/workflows files |
The three read:* scopes are the recommended baseline alongside repo. They don't grant any mutation power — they just remove recurring papercuts where gh pr edit-shaped operations would otherwise fail with a scopes error and need the gh api PATCH workaround documented in docs/gdd/access.md § 4. If org policy or token-issuance rules block adding them, the strict-minimum (repo alone) still works — you'll just hit the fallback path more often.
Why classic and not fine-grained? Fine-grained PATs have known limitations with cross-org pull requests and workflow file access. Classic PATs are simpler and more reliable for GDD workspaces that span multiple orgs or forks.
Classic PAT vs fine-grained vs a machine account
The base setup above (a classic PAT on your own account) is the right default for a single user willing to manage one token. The trade-offs, if you outgrow it:
| Option | Pros | Cons |
|---|---|---|
| Classic PAT (default) | One token; works across every org/fork you can access; no per-repo wiring | Account-wide — it can reach everything your account can; you renew it by hand at expiry |
| Fine-grained PAT | Least privilege — scope to specific repos + specific permissions | More maintenance: shorter max lifetimes, per-repo/per-org selection to keep current, org-owner approval for org-owned repos; still hits the cross-org/workflow limits above |
| Separate machine (bot) account | Access managed through normal team/collaborator membership instead of token juggling; not tied to your personal identity; durable | One more account to create and hold a seat for |
A machine account is often the nicest long-term answer for shared or automated work: you add the bot as a collaborator (or to a team) on the specific repos, and access is maintained via team management rather than by rotating a personal token. Reach for fine-grained tokens or a machine account when least-privilege or shared ownership actually matters; until then, a classic PAT keeps setup a single step.
Store the token
Create .env in the yggdrasil root (gitignored):
export GH_TOKEN=ghp_xxxxxxxxxxxx
Load and verify
source .env
gh auth status
gh reads GH_TOKEN automatically — no gh auth login step is needed.
Test
gh issue list --repo <your-org>/<your-repo> --limit 5
If this returns a list (or an empty table), auth is working correctly.
GitLab
Install the GitLab CLI
macOS:
brew install glab
Windows (Git Bash):
winget install GLab.GLab
PATH is picked up.
Linux: See https://gitlab.com/gitlab-org/cli#installation
Create a Token or Service Account
GitLab offers several token-bearing actors. Use the narrowest one that covers your use case:
| Actor | Scope | How to create |
|---|---|---|
| Project Access Token | Single project | Project → Settings → Access Tokens |
| Group Access Token | All repos in a group | Group → Settings → Access Tokens |
| Group Service Account | A named service-account user under a top-level group | Group → Settings → Service Accounts, then create a token for the service account |
| Instance Service Account | A named service-account user that admins can invite across groups | Admin-created; too broad/heavy for normal per-user setup |
| Personal Access Token | Entire account | User Settings → Access Tokens |
Recommended: use a Project Access Token for a single test repo, a Group Access Token when your workspace components all live under one group, or a Group Service Account when your GitLab instance exposes it and you want a named fork-home actor on a Free-compatible GitLab setup. Personal Access Tokens with api scope work but give broader access than needed.
GitLab.com Free and older self-managed instances: Group and Project Access Tokens require a paid GitLab.com subscription, while group service accounts may be available when the instance exposes
Settings → Service Accountsto top-level group Owners. If that section is absent on a self-managed or Dedicated instance, first check whether an administrator has enabled top-level group owners to create service accounts and whether you are an Owner of the top-level group; absence of the UI alone does not prove the feature is unavailable. If the instance version, feature flags, permissions, or admin settings still do not expose the flow, use a Personal Access Token withapiscope as the fallback. For the multi-token setup (fork → source project), you can point multiple env vars at the same PAT (see.env.examplefor variable names) — the token routing logic still works, you just don't get the access-level separation that scoped actors provide.
Choose the narrowest role that matches the token's job: - Developer for fork/write tokens that must push branches or create MRs. - Reporter for source-project read/review/issue tokens in the split-token setup. - Maintainer may be required on a destination group for API-created forks, depending on instance settings around project creation/import. If a Developer token fails with "not allowed to import projects," raise only the fork-home token's role.
For cross-group fork creation, the fork token must be able to read the source project and create in the destination namespace. A fork-group access-token bot cannot be invited directly to a sibling/external source group, but GitLab's project/group "Invite a group" sharing can grant source-project read by inviting the fork-home group. This is a group-sharing grant, not a token-specific grant.
Set Up a GitLab Fork Group for GDD
Use a fork group when you want GDD agents to push branches and open MRs from a controlled namespace instead of pushing directly to source projects. The fork group is the destination namespace for forks; the project being forked is the source project.
- Choose a fork-home namespace. Good generic shapes are
gitlab.example.com/<team>/gdd/<user>-fork-group,gitlab.example.com/<team>/<user>-forks, orgitlab.example.com/<user>-forks. Pick a path you can administer and that makes the human owner obvious in MR source paths. - Create the group in GitLab. Go to Groups → New group, create the chosen fork-home group, and keep it empty at first. You need enough access on this group to create projects/forks and create tokens.
- Create the fork-write actor. Preferred: create a Group Access Token on the fork-home group with
apiscope and the Maintainer role to allow fork creation. - Add the token to
.envwith a descriptive variable name:
export GITLAB_FORKGROUP_ALICE_WRITE=glpat-or-token-value
- Map the fork group in
ecosystem.local.yaml. Use the full fork-home group path as thegitTokenskey so longest-prefix matching sends fork operations to the fork-write token:
defaults:
gitProviders:
gitlab.example.com: gitlab # self-hosted instances only; gitlab.com is auto-detected
gitTokens:
gitlab.example.com/my-team/gdd/alice-fork-group: GITLAB_FORKGROUP_ALICE_WRITE
identity:
human_account: alice
forkRemote: alice-fork-group # git remote name used by ws push/ws cr
homes:
fork:
namespace: gitlab.example.com/my-team/gdd/alice-fork-group
- Make source projects readable to the fork actor. For source projects under the same GitLab group tree, the fork token may already read them. For private sibling/external source projects, either add a separate Reporter token mapping for the source project/group, or ask the source project/group owner to use GitLab's Invite a group sharing to grant Reporter access to the fork-home group. The sharing option can let the fork-group token satisfy GitLab's fork API because the same caller can read the source project and create in the destination namespace.
- Declare or override the component. With
identity.homes.fork.namespaceset,ws clone-forkcreates or usesgitlab.example.com/my-team/gdd/alice-fork-group/<repo>automatically. Use an explicitforkRepoonly for one-off exceptions:
components:
example-service:
tier: supporting
repo: https://gitlab.example.com/source-team/example-service.git
# forkRepo: https://gitlab.example.com/my-team/special-forks/example-service.git
- Verify and clone:
ws gitlab-auth --status
ws clone-fork example-service
ws clone-fork creates the fork if the configured fork token can read the source project and create in the destination namespace. If not, it prints a prefilled GitLab UI fork URL; complete the fork as a human, then rerun the command.
Token naming
GitLab display name (what appears as the MR/issue author bot or service-account user):
Pattern: <scope>-<owner>-<role> where scope is the group or project slug, owner is your username, and role is the GitLab role level. For personal namespaces (no shared scope), omit the scope prefix and use <owner>-<role>.
| Token's job | GitLab display name | Why |
|---|---|---|
| Fork group write | gdd-rpraestholm-developer |
Group slug + owner + GitLab role |
| Upstream group read | gdd-reporter |
Group slug + role (shared, no owner) |
| Personal namespace write | rpraestholm-developer |
Namespace + role |
| Single-project write | aws-ops-wheel-rpraestholm-developer |
Project + owner + role |
With a well-named token or service account, an MR opened by the agent shows as authored by e.g. gdd-rpraestholm-developer — the human connection is clear even though it is not the human GitLab account. Combined with @HUMAN_ACCOUNT in the MR body, this is the primary attribution mechanism on GitLab (see docs/gdd/cr-internals.md).
Env var name (used in .env and referenced in gitTokens):
Patterns (no redundant _TOKEN suffix; all entries are tokens):
GITLAB_<SCOPE>_<ROLE>— shared scope, single owner implied (e.g.GITLAB_GDD_REPORTER)GITLAB_<SCOPE>_<OWNER>_<ROLE>— personal variant within a shared scope (e.g.GITLAB_GDD_RPRAESTHOLM_DEVELOPER)GITLAB_<OWNER>_<ROLE>— personal-namespace token, no group scope (e.g.GITLAB_RPRAESTHOLM_DEVELOPER)GITLAB_<OWNER>_PAT— full-account token;_PATsignals broad scope
| Token's job | Env var | Notes |
|---|---|---|
| GDD source group read | GITLAB_GDD_REPORTER |
Group path slug, reporter role |
| GDD fork group write | GITLAB_GDD_RPRAESTHOLM_DEVELOPER |
Full fork-group path + owner |
| Personal namespace write | GITLAB_RPRAESTHOLM_DEVELOPER |
Namespace + role |
| Personal access token | GITLAB_RPRAESTHOLM_PAT |
Full-account token; _PAT signals broad scope |
This naming makes gitTokens entries readable at a glance:
defaults:
gitTokens:
gitlab.example.com/gdd: GITLAB_GDD_REPORTER
gitlab.example.com/gdd/rpraestholm-fork-group: GITLAB_GDD_RPRAESTHOLM_DEVELOPER
gitlab.example.com/rpraestholm: GITLAB_RPRAESTHOLM_PAT
gitTokens values are environment-variable names, not arbitrary environment lookups. Use GITLAB_* names for GitLab tokens; GH_TOKEN and GITHUB_TOKEN are also accepted for GitHub. Other names are rejected before shell indirection so a realm or local mapping cannot redirect credential reads to unrelated variables such as cloud credentials.
For self-hosted instances, token URLs follow the pattern:
https://<your-host>/<group>/<project>/-/settings/access_tokens.
Regardless of token type, set the scope to:
| Scope | Why |
|---|---|
api |
Required for MRs, issues, and repo writes via glab |
Store the token
Add to .env in the yggdrasil root:
export GITLAB_TOKEN=glpat-xxxxxxxxxxxx
export GITLAB_USER=your-gitlab-username
# Self-hosted instances only:
export GITLAB_HOST=git.mycompany.com
ws parses .env as literal KEY=value or export KEY=value assignments. It does not execute command substitutions, pipelines, or other shell syntax from the file. Single- and double-quoted values are accepted as literal text. Variables that alter command lookup or shell/runtime startup, such as PATH, BASH_ENV, and dynamic-loader variables, are rejected; configure those in your shell profile instead.
Load and verify
For self-hosted instances, register the hostname with glab once after setting GITLAB_HOST:
ws gitlab-auth
For gitlab.com, glab reads GITLAB_TOKEN automatically and no auth login step is needed. For self-hosted, the one-time login registers the host for API calls. ws push does not require the OS credential helper for HTTPS remotes when a matching .env / defaults.gitTokens token is present; it injects that token into the single push process and disables credential-helper prompts.
Verify with:
glab auth status
Note:
glab auth statusexits non-zero if any configured GitLab host fails authentication — includinggitlab.comif you have no token for it. Check that your self-hosted instance shows✓ Logged in to <your-host>. Thegitlab.comfailure is harmless if you only use the self-hosted instance.
Test
# gitlab.com
glab issue list --repo <your-group>/<your-repo>
# Self-hosted (GITLAB_HOST must be set in environment)
source .env
glab issue list --repo <your-group>/<your-repo>
Verifying token coverage
Before pushing or opening a CR, confirm token routing is wired up correctly:
ws gitlab-auth --status # shows all configured token slots and set/unset status
ws diagnose <comp> # shows which token covers each remote for a specific component
ws diagnose is the fastest way to confirm auth is wired up correctly for a new component — it shows the ecosystem entry, clone status, remotes, provider detection, and whether the covering token env var is set or missing.
Provider Detection
The workspace auto-detects which provider to use from the remote URL:
- github.com → GitHub (gh CLI)
- gitlab.com → GitLab (glab CLI)
For self-hosted instances, add a mapping in ecosystem.local.yaml:
defaults:
gitProviders:
git.mycompany.com: gitlab
See ecosystem.local.yaml.example for more options (workspace-wide defaults, per-component overrides).
Shared Prerequisites
yq (YAML processor)
The workspace scripts require yq v4+ to parse ecosystem.yaml.
macOS:
brew install yq
Windows:
# Option 1: winget
winget install MikeFarah.yq
# Option 2: manual download (no admin needed)
mkdir -p "$HOME/bin"
curl -sL https://github.com/mikefarah/yq/releases/latest/download/yq_windows_amd64.exe \
-o "$HOME/bin/yq.exe"
If you use the manual download, add ~/bin to your PATH in ~/.bashrc:
export PATH="$HOME/bin:$PATH"
Verify: yq --version should report v4.x.
jq (JSON processor)
The GitLab provider uses jq to parse API responses. GitHub uses gh --jq natively, so jq is only required for GitLab.
macOS:
brew install jq
Windows:
# Option 1: winget
winget install jqlang.jq
# Option 2: chocolatey
choco install jq
After installing via chocolatey on Windows, jq may not be on PATH in Git Bash. Add it to ~/.bashrc:
export PATH="/c/ProgramData/chocolatey/bin:$PATH"
Verify: jq --version should report 1.6+.
Running Shell Scripts on Windows
All workspace scripts are Bash scripts. Windows needs Git Bash (installed with Git for Windows).
| From | How to run |
|---|---|
| Git Bash | bash scripts/ws help |
| cmd / PowerShell | bash scripts/ws help (Git Bash must be on PATH) |
| VS Code terminal | Set default shell to Git Bash, or prefix with bash |
A .gitattributes in the repo root forces LF line endings on .sh files, preventing \r: command not found errors. If you hit this on existing checkouts: git checkout -- scripts/*.sh
Git Remote Naming Convention
Name remotes after the org or service — never use the generic origin.
| Remote name | Points to |
|---|---|
siliconsaga |
github.com/SiliconSaga/* |
mygroup |
gitlab.com/mygroup/* |
local-gitea |
Homelab Gitea instance |
<orgname> |
Any org — name the remote after it |
The rule: the remote name should answer "where does this push go?" without running git remote -v.
Bootstrap scripts may add an internal Gitea remote during cluster setup. That remote is ephemeral and should not be given a permanent name like origin.
Troubleshooting
gh/glab not found after install
Open a fresh terminal session. The installer updates PATH, but existing sessions don't pick it up.
"Bad credentials" or 401 errors (GitHub)
- Token may be expired — check at GitHub → Settings → Developer settings → Personal access tokens.
- Classic PAT: ensure the
reposcope is selected. - Org-scoped fine-grained PAT: the org owner may need to approve it first.
"401 Unauthorized" (GitLab)
- Token may be expired or revoked.
- Ensure the
apiscope is selected. - For self-hosted: set
GITLAB_HOSTin.env.
Push fails with "remote: Permission denied" or "Write access not granted"
- Credential helper may not have your token stored. Run
gh auth status(GitHub) orglab auth status(GitLab) to check. - Stale cached credential (Windows): Git Credential Manager caches tokens and won't pick up a new
GH_TOKENfrom.envautomatically. If you rotated your PAT, erase the stale entry and let the next push re-cache:
# Erase the cached credential for github.com
printf 'protocol=https\nhost=github.com\n' | git credential-manager erase
# Verify the new token works
source .env
gh api user --jq .login # should print your bot account name
The next git push will prompt GCM to store the current credential.
If using a bot PAT via .env, you can pre-store it:
source .env
printf 'protocol=https\nhost=github.com\nusername=agent-refr\npassword=%s\n' \
"$GH_TOKEN" | git credential-manager store
- GitKraken users: check
~/.gitconfigforurl.insteadOfentries that redirect HTTPS to SSH. Remove or scope them if they interfere with CLI auth. GitKraken manages its own credentials separately from the system credential helper, so it is unaffected by the erase/store steps above.
"Cannot detect git provider for URL"
Self-hosted domain not recognized. Add it to defaults.gitProviders in ecosystem.local.yaml:
defaults:
gitProviders:
git.mycompany.com: gitlab
See Provider Detection above.