Deploy from GitHub or GitLab
Link an Environment or an app to a repo and branch on GitHub or GitLab, and every push deploys it automatically — no CI pipeline or deployment token required.
Connect a provider
Connections are made per Organization, in Organization Settings → Integrations. Connecting
requires being an owner or admin of the Organization. From the CLI, paas connections connect -o acme
prints the Integrations page link; connecting itself happens in the browser.
GitHub
- Click Connect GitHub. GitHub asks you to install the d2o PaaS app on an account or organization and choose which repositories it can see.
- Only an owner of that GitHub account or organization can connect it — the app requests read access to organization members to confirm this. If someone who isn’t an owner installs it, GitHub holds the installation for an owner to approve; connect again once it’s approved.
- You’re returned to the Integrations page with a “GitHub connected” message, and the connection shows as Active.
A broken connection shows Reconnect; every connection also has a Disconnect button. Disconnecting in d2o removes the connection and its repo links, but does not uninstall the app from GitHub. To remove its access there too, uninstall it on GitHub: for a personal account, Settings → Applications; for an organization, the organization’s Settings → GitHub Apps.
GitLab.com
Click Connect GitLab and authorize d2o on GitLab’s consent screen. You’re returned to the Integrations page with a “GitLab connected” message, and the connection shows as Active.
Self-managed GitLab
Self-managed instances use an OAuth application you register yourself, in this order — GitLab only issues the Application ID and Secret once the application exists, so registration comes first:
- Click Connect self-managed GitLab. The form shows the Redirect URI to register with a Copy button.
- On your GitLab instance, go to User Settings → Applications (or Admin → Applications for
an instance-wide application) and create a new application with that Redirect URI,
Confidential checked, and the
apiandread_apiscopes. - Fill in Host, Client ID (GitLab’s Application ID) and Client secret (GitLab’s Secret), then click Continue.
- Authorize on GitLab.
Your GitLab instance must be reachable over HTTPS from the internet for d2o to exchange tokens and deliver webhooks.
An application registered with only the api scope still deploys, but nobody can choose a
repository through it: add the read_api scope to the application on your GitLab instance (no
need to reconnect).
Link a repo to an Environment or app
Linking makes a repo and branch the source of truth for a deployment target. Whether you link the Environment or an app depends on how the app builds:
- An app that builds from its own remote — link the app, in the app’s Settings.
- An app that builds from the Environment’s shared remote — link the Environment instead, in the Environment’s Settings → Deploy from GitHub/GitLab tab.
Linking the Environment needs at least one app that builds from its shared remote (created with
paas apps create NAME --shared-source); until one exists, a repo linked there would never deploy,
so linking it is refused with “No app in this environment builds from its shared source”. Link the
app instead, or create the shared-source app first. Don’t link the same repo to both the
Environment and an app that builds from its own remote — only the app’s link can deploy it.
The same repo and branch can feed several Environments or apps at once — for example a staging and
a production Environment both deploying from main.
In the dashboard:
- Open the Environment’s Settings → Deploy from GitHub/GitLab tab (or the app’s Settings for an app’s own remote — it’s the same editor).
- Pick a Connection, then search for the repo in Repository and pick a Branch.
- Review Wait for CI (enabled by default) and Allow direct pushes (disabled by default), then click Save. The summary shows Provider, Repository, Branch, Wait for CI and Direct pushes, one per line.
Linking a GitLab repository uses your own GitLab account. The picker shows only projects where you are a Maintainer, and linking a project needs Maintainer access to it. The first time, click Choose with your GitLab account (for a target that is already linked, click Edit settings first); GitLab asks you to authorize d2o to read your projects, and the verification lasts about two hours. Deploys still run through the organization’s GitLab connection, so its account needs access to the project too. Changing only the branch or options of an existing link needs no verification.
To change the connection, repository, branch or either option, choose Edit settings. Save changes applies the form together; Cancel discards it. Saving settings does not start a deployment by itself. Disabling Wait for CI can release requests already waiting for checks when you save.
If settings save but automatic updates cannot be connected, the page says so and offers Retry. Your saved settings remain visible.
With the CLI:
paas connections list
paas links set --connection <id> --repo acme/shop --branch production -e production
paas links set --connection <id> --repo acme/shop --branch production -e production -a web
paas links get -e production
--connection <id> is the ID from paas connections list. --repo takes owner/name or a numeric
repo id. paas links get prints the same “Deploys from …” line, plus whether direct pushes are
allowed, whether it waits for CI, and a “Last push” line for the most recent push’s state.
What happens on a push
Pushing a commit to the linked branch deploys it — the same deployment that a direct push to d2o’s own remote would trigger. There’s nothing else to configure.
- Branch deletions and tags are ignored.
- A force-push deploys the branch’s new head, even if it rewrites history.
- If two pushes land close together, only the later commit deploys — the one it replaces is simply superseded, not deployed.
Wait for CI
Wait for CI is enabled by default, including on existing links. A push waits for the commit’s checks before deploying:
- On GitHub: the commit’s check runs and commit statuses. Skipped or neutral checks count as passed; cancelled, timed-out or action-required ones count as failed.
- On GitLab: the commit’s job statuses on the linked branch, plus any statuses other tools post,
using each job’s latest run. Jobs allowed to fail, and skipped jobs, never block. A manual job
that isn’t allowed to fail blocks the deployment until someone runs it; after 2 hours the push is
refused. A commit pushed with
[skip ci]has no checks, so it deploys after 10 minutes. - d2o’s own
d2o: …checks never count.
If the commit has no checks at all, d2o waits 10 minutes and then deploys it anyway, with a “No checks found; deployed without waiting” note. If checks are still running after 2 hours, the push is refused instead of deploying.
If a newer push arrives on the same branch while an earlier one is still waiting on checks, the earlier push is dropped and never deployments, even if its checks later succeed: its check on the commit shows “Replaced by a newer push”, while inside d2o the push itself shows “A newer push replaced this one.”
To change it in the dashboard, choose Edit settings, change Wait for CI, then Save changes. You can also set it explicitly with the CLI:
paas links set --connection <id> --repo acme/shop --branch production -e production --wait-for-ci true
The current CLI replaces the whole link and sends an explicit value for this option. Include --wait-for-ci true when saving through the CLI to keep it enabled.
set replaces the whole link, so --wait-for-ci must be repeated on every set to keep it on (the
same as --allow-direct-push).
Status on your commits
Every push to a linked branch gets its own check on the commit, named d2o: production for an
Environment link or d2o: production/web for an app link.
- On GitHub, it shows up among the commit’s checks, and on any pull request whose head is that commit.
- On GitLab, it’s a commit status, and it also shows in the pipeline view.
The check moves through the same stages as the deployment — queued, then in progress, then a final state (success, or failure/cancelled on GitHub and failed/canceled on GitLab) — with a summary such as “Waiting for checks”, “Building”, “Rolling out”, “Live”, a deployment failure reason, a checks-failed or timed-out detail, or “Replaced by a newer push”. Clicking it opens the deployment on d2o, or — before a deployment exists yet — the Environment’s Deployments page.
On GitHub, the check’s heading names the outcome and the deployment — for example “Live in production — deployment #12” — and its details say what happened (where each app is live, or which app failed at which step, with a link to its logs), followed by a table of the deployment, project / environment, commit, who pushed it and, once live, how long it took from push to live. GitLab shows only the short summary.
A push that ends without deploying for a reason other than a failure — replaced by a newer push or deployment, built but waiting for you to deploy it (“Built — deploy it from d2o to go live”, when the Environment’s Automatic deployment is off), or unlinked (“Unlinked from d2o”) — ends as neutral on GitHub.
GitLab counts d2o’s status as part of your pipeline, so it differs in three ways:
- No status is added while d2o waits for your checks, so it never holds your pipeline open. It appears once the build starts. A push replaced, unlinked or timed out before that gets no status at all — its entry in d2o’s Deployments page still says why.
- A build waiting for you to deploy it shows as success: the build itself succeeded.
- A deployment replaced by a newer one, or unlinked partway, ends as canceled, which also marks that pipeline canceled.
Direct pushes
Once a target is linked, direct pushes to its d2o remote are blocked by default, so what’s running always matches the linked repo. A blocked push is refused with:
This environment deploys from GitHub acme/shop@production. Push there instead, or allow direct pushes in the environment's settings.
(An app-linked target says “This app deploys from … in the app’s settings.”)
Cloning and fetching from the d2o remote are never blocked — only pushes.
To allow direct pushes alongside the linked repo, choose Edit settings, turn on Allow direct pushes, and Save changes on the same
settings tab, or re-run paas links set with --allow-direct-push (it replaces the whole link, so
the flag must be repeated):
paas links set --connection <id> --repo acme/shop --branch production -e production --allow-direct-push
Deploy latest
Deploy the branch’s current head on demand, without waiting for a new push — useful right after reconnecting a broken connection, or if a push’s webhook never arrived.
With Wait for CI on, Deploy latest waits for the commit’s checks too. It answers “Deploying abc1234”, “Waiting for checks on abc1234”, or “Already deployed abc1234” when that commit is already live.
In the dashboard: open the linked target’s settings (the same Deploy from GitHub/GitLab editor) and click Deploy latest.
With the CLI:
paas links deploy-latest -e production
Troubleshooting
Connecting fails. After GitHub or GitLab sends you back to the Integrations page, you may see:
- “Only an owner of the GitHub account or organization can connect it. If you are an owner, ask GitHub to approve the app’s updated permissions, then try again.”
- “Your GitHub account can’t access that installation.” — you don’t have access to the GitHub installation that was used; connect with an account that does, or pick a different installation.
- “This GitHub installation is already connected to another organization.” — each GitHub installation can only back one d2o Organization; disconnect it from the other one first, or install the app again for a different account/organization.
- “The connection took too long. Start again.” — the attempt expired before you finished on GitHub/GitLab’s side; click Connect GitHub/Connect GitLab again.
- “Couldn’t finish connecting. Try again.” — a generic failure; retrying usually works.
The connection shows “Broken”. Click Reconnect on it in Organization Settings → Integrations. Reconnecting repairs the connection in place — your repo links are kept, and nothing needs relinking.
A self-managed GitLab connection shows “Not finished”. The GitLab sign-in was started but never completed. Either Disconnect it, or click Reconnect: the form reopens with the host locked, and leaving Client ID and Client secret blank keeps the current ones.
A push doesn’t deploy. Check its state on the dashboard’s Deployments page or with paas links get,
or on the Environment’s Deployments page under “Pushes not yet deployed” (it lists pushes
waiting for checks and ones that failed checks, timed out or couldn’t be fetched; replaced pushes,
and failures already followed by a successful deployment, aren’t listed). If two pushes land
close together, only the later commit deploys — that’s expected, not a failure. With Wait for
CI on, the “Last push” line shows why, and on GitHub so does the commit’s check:
- “Not deployed: checks failed (…)” — one of the commit’s checks failed; fix it and push again, or, if the check was flaky, re-run it and then use Deploy latest to re-check the same commit without a new push.
- “Not deployed: checks were still running after 2 hours” — the checks never finished in time; this also covers a GitLab manual job nobody ran — run it, then push again or use Deploy latest.
- “No app in this environment builds from its shared source, so this push wasn’t deployed” — the Environment is linked, but none of its apps builds from the shared remote. Link the repository to the app instead (and unlink the Environment), or create an app that builds from the shared source.
Otherwise, try Deploy latest; if it keeps failing, reconnect the provider in Integrations.
Unlinking: click Unlink on the linked target’s settings and confirm Unlink repository, or
run paas links unset -e production. Direct pushes are allowed again immediately.
Follow a requested deployment
Deploy latest opens the deployment request immediately. You can see whether it is waiting for checks or importing the commit. When the deployment starts, the page opens its numbered details automatically. If that commit is already deployed, it opens the existing deployment instead.
Failed checks, timeouts, import errors and requests replaced by a newer request remain visible with their reason. The request keeps the repository and branch it started from, even if you later change the settings.
While direct pushes are blocked, the deployment-token page keeps existing tokens visible so you can revoke them. It hides token creation until you enable direct pushes.