Deploy from GitHub

Connect a GitHub repository to a Volcano project so every push to the production branch deploys your functions and frontend automatically.

Connect a GitHub repository to a project and Volcano deploys it on every push to the production branch — no CLI step in your pipeline.

PUT /projects/{id}/git-deploy-settings
Authorization: Bearer <platform_token>
Content-Type: application/json

{
  "auto_deploy_enabled": true,
  "deploy_functions": true,
  "frontend_name": "web"
}

With that saved and a repository connected, git push origin main builds and deploys the functions under volcano/functions/ and the frontend at the configured app root.

Repository layout

For repositories you connect directly, Volcano discovers functions from the file tree and deploys the frontend selected in Git deploy settings.

my-repo/
├── volcano/
│   ├── functions/
│   │   ├── hello.js              # single-file function -> "hello"
│   │   ├── checkout/             # directory function   -> "checkout"
│   │   │   ├── index.mjs
│   │   │   └── tax.mjs
│   │   └── _shared/              # shared code, packaged into every function
│   │       └── db.js
│   └── migrations/               # applied first when apply_migrations is on
│       └── 001_create_orders.sql
├── web/                          # frontend app root (package.json here)
│   ├── package.json
│   └── app/
└── package.json                  # dependency manifest for functions

Functions are the entries directly under volcano/functions/ in your build root:

EntryFunction nameEntry file
hello.jshellothe file itself
checkout/checkoutindex.*, else the runtime's handler file (index.js, main.py, main.rb), else the first source file by name

Node.js functions deploy .js and .mjs files. Volcano doesn't compile TypeScript yet, so compile it first and commit the output, such as checkout/index.mjs. A function that holds only TypeScript, such as a checkout.ts file or a checkout/ directory with no .js or .mjs file at its top, isn't deployed; the push deploys everything else. Commit its compiled JavaScript to deploy it. Keep shared TypeScript that isn't a function under a _-prefixed name, such as _lib/, so it isn't taken for one.

Names must be DNS-safe: lowercase letters, digits, and internal hyphens, 1–63 characters. batch, invocations, and logs are reserved. Other entries that do not qualify — an unsupported file type, a hidden entry, a name Volcano cannot deploy — are skipped silently so one stray file cannot fail the whole push. Two entries resolving to the same name (foo.js and foo/) fail the run.

Shared code is any _-prefixed file or directory under volcano/ or volcano/functions/. Each one is copied into every function archive under its own name, so volcano/functions/_shared/db.js is importable as _shared/db.js.

Dependency manifests (package.json, requirements.txt, Gemfile, and their lockfiles) are taken from the function's own directory when it has any, otherwise from the build root. The two sets are never mixed — a function with its own package.json is not paired with the build root's lockfile.

The frontend is detected by a package.json at the app root, or by the nearest package.json above it when its build script runs next build <dir> on the app root, as volcano init nextjs sets up for web/. Its archive excludes node_modules, vendor, .git, and .volcano. Function archives exclude those plus the Python artifact directories .venv, venv, site-packages, __pycache__, and python_deps — a frontend build root carrying those still packages them, which counts against the size limits below.

Frontend dependencies install from the outermost directory in the frontend's archive that has a lockfile or declares workspaces. With the layout above, a lockfile beside the root package.json makes the root the install root. Its install covers web/ only when the root is a workspace that includes it, through pnpm-workspace.yaml or a workspaces field. Otherwise the build fails because web/'s dependencies aren't installed.

Already running on Volcano and want your code in Git? Export your source can initialize a new empty repository and push the project directly to its production branch.

Exported projects with multiple frontends

Source export places each frontend in frontends/<name>/ and writes .volcano/source-layout.json with its name and app root. When that file is present, every listed frontend deploys on each production-branch push. The single frontend selection applies to repositories without that file. Functions continue to deploy from volcano/functions/ when Deploy functions is on. These pushes deploy all configured resources; they do not filter by changed paths.

Choose frontend workspaces

To move an exported frontend, move its files and update .volcano/source-layout.json in the same commit:

{
  "version": 1,
  "frontends": [
    { "name": "web", "workspace": "apps/web", "app_root": "" },
    { "name": "admin", "workspace": "apps/admin", "app_root": "" }
  ]
}

workspace is relative to the configured project root. app_root is relative to that workspace. Volcano packages the entire workspace, including shared packages and dependency manifests, subject to the normal source exclusions. Both paths must be clean relative paths contained within their parent. A missing workspace, or an app root the frontend is not detected at, fails deployment.

Omitting workspace keeps frontends/<name>. An explicit "workspace": "" selects the project root. For a shared monorepo, give both frontends an empty workspace and set their app roots to apps/web and apps/admin; both builds then receive the shared dependencies. Each retry uses the layout from the same commit.

Initial export always uses frontends/<name>; custom paths are configured in Git after export. This layout belongs to the source commit, not volcano-config.yaml.

If you do not have a repository yet

Volcano never creates a repository for you. Commit your project locally, create a repository on your own account or organization, push to it, then connect it.

Start with a local commit — both routes below push existing commits, so neither works without one:

git init && git add -A && git commit -m "Initial commit"
git branch -M main

Then create the remote and push. With the GitHub CLI:

gh repo create my-app --private --source=. --push

Or create it in the GitHub UI and add the remote by hand. Create it empty — no README, .gitignore, or license — so the first commit is the one you just made:

git remote add origin https://github.com/<owner>/my-app.git
git push -u origin main

Either way you keep your own Git credentials — Volcano never mints, stores, or asks for a push credential. With the repository pushed, continue below.

1. Connect GitHub

Start the connection flow and send the user to the returned URL to install the Volcano GitHub App:

curl -X POST "https://api.volcano.dev/user/git/connect?provider=github" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"
{ "authorization_url": "https://github.com/apps/volcano/installations/new?state=..." }

The curl command illustrates the direct API flow. To complete it interactively, the same browser client must make the start request and open the returned URL so its binding cookie is available at the API callback. The Volcano dashboard starts through its same-origin edge function instead:

const redirect = `${location.origin}/settings/git`;
const response = await fetch(`/api/web/user/git/connect?provider=github&redirect=${encodeURIComponent(redirect)}`, {
  method: 'POST',
  credentials: 'include',
});
const { authorization_url } = await response.json();
location.assign(authorization_url);

The edge function forwards the request with callback_url set to its same-origin provider callback, rewrites the binding cookie path for that route, and returns the cookie to the browser. The callback URL and redirect must be on the Volcano dashboard's origin, or http to 127.0.0.1 or [::1] with a port; any other target answers 400. The callback must share an origin with redirect and cannot contain a query string or fragment. Do not send callback_url directly from browser code to api.volcano.dev; that would store the binding cookie on the API origin instead of the application origin.

After the callback completes, the connection is stored against your user and is reusable across projects:

curl "https://api.volcano.dev/user/git/connections" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"
{
  "connections": [
    {
      "id": "9f1c2b7e-1f34-4a0d-9c7a-5b2f1d0e8a11",
      "provider": "github",
      "provider_login": "acme-dev",
      "status": "active",
      "last_authenticated_at": "2026-08-11T16:58:02Z"
    }
  ]
}

Installing from GitHub

You can also install the Volcano GitHub App from GitHub itself: from the App's page, by changing which repositories it can access in your GitHub settings, or by asking an organization owner to install it. GitHub then sends your browser to Volcano without a connect flow behind it. Volcano connects nothing: it creates no connection, binds no repository, and sets no cookie. It sends you to the Git settings page in the dashboard:

GET /github/callback?installation_id=52001234&setup_action=install
HTTP/1.1 302 Found
Location: https://volcano.dev/dashboard/project-settings/git?github_install=1
Cache-Control: no-store
Referrer-Policy: no-referrer

From there, pick the project and connect the repository. The dashboard starts the connect flow above, which finds the installation you just made. If you asked an organization owner to install the App, connect after they approve it.

2. Pick an installation and repository

volcano git repos lists every installation and repository your connection reaches. Through the API, both calls proxy GitHub live and persist nothing:

curl "https://api.volcano.dev/user/git/connections/$CONNECTION_ID/installations" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"

curl "https://api.volcano.dev/user/git/connections/$CONNECTION_ID/installations/$INSTALLATION_ID/repositories" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"
{
  "repositories": [
    { "id": 812345678, "full_name": "acme/storefront", "default_branch": "main", "private": true, "is_empty": false }
  ]
}

3. Bind the repository to a project

From a checkout, volcano git connect binds the repository its remote names; pass a URL to name one explicitly. Through the API:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-connection" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "'"$CONNECTION_ID"'",
    "installation_id": '"$INSTALLATION_ID"',
    "repository_id": 812345678,
    "root_directory": "apps/api"
  }'
{
  "repo_installation_id": 52001234,
  "repo_id": 812345678,
  "repo_full_name": "acme/storefront",
  "root_directory": "apps/api",
  "production_branch": "main",
  "production_branch_follows_default": true,
  "updated_at": "2026-08-11T17:04:22Z"
}
  • repository_id is GitHub's stable numeric repository id. It survives renames, which is why it — not repo_full_name — is the binding.

  • root_directory is the build root for a monorepo. Function discovery, dependency manifests, and the frontend app root are all resolved beneath it. Omit it for the repository root.

  • production_branch is the branch a push must land on to deploy. Omit it and the project follows the repository's GitHub default branch, which Volcano keeps in step when you rename it on GitHub — so omitting it is also how a project goes back to following the default. Any other branch deploys from that branch instead. See Deploy from a different branch.

    Sending back the branch the project already deploys from, when that is the repository's default — which is what you get by reading the connection, changing one field and sending it back — changes nothing either way, so a branch you pinned earlier stays pinned. Editing the root directory this way is safe. Sending the default branch when the project deploys from something else returns it to following the default.

    Changing repository_id and naming a branch other than the new repository's default in the same request returns 400: the branch is almost always the old repository's, sent back unchanged. Reconnect first, then set the branch. A repository that reports no default branch, as one with no commits can, accepts any branch, including on a rebind.

  • production_branch_follows_default is true while the branch tracks the repository's default and false once you pin one. Before you re-run the connect call for the same repository, read the connection; if this is false, send production_branch back, or omitting it discards the pin.

Several projects may bind the same repository. Binding never creates or deletes anything on GitHub; DELETE on the same path only unbinds the project.

A person chooses the repository. Binding or changing it needs a platform token or the dashboard. A project access token gets 403:

{
  "error": "project access tokens cannot change a project's repository; a person changes it in the dashboard or with a platform token"
}

CI and agents holding a project access token can still change the root directory and the production branch, which stay within the repository a person chose.

Change the root directory

Move a project to another directory of the same repository without touching the binding:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-connection/root-directory" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "root_directory": "apps/storefront" }'
{
  "repo_installation_id": 52001234,
  "repo_id": 812345678,
  "repo_full_name": "acme/storefront",
  "root_directory": "apps/storefront",
  "production_branch": "main",
  "production_branch_follows_default": true,
  "updated_at": "2026-08-11T17:12:40Z"
}
  • The path is relative to the repository root. "", . and / mean the root itself.
  • The response shows the path the way a deploy reads it, so ./apps/storefront/ comes back as apps/storefront.
  • An absolute path, or one that climbs out of the repository with .., returns 400.
  • A project with no repository connected returns 404.
  • Once the project's source is exported to Git, the root directory is fixed and this returns 409.

Deploy from a different branch

A project can deploy from any branch. Change it without touching the binding:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-connection/production-branch" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "production_branch": "release" }'
{
  "repo_installation_id": 52001234,
  "repo_id": 812345678,
  "repo_full_name": "acme/storefront",
  "root_directory": "apps/api",
  "production_branch": "release",
  "production_branch_follows_default": false,
  "updated_at": "2026-08-11T17:09:03Z"
}

The branch does not have to exist yet. It is validated as a Git branch name and nothing more, so you can point a project at a branch you are about to push — which is what makes this work for a repository you created empty and are about to push into.

Once you set the branch here, it is yours: renaming the repository's default branch on GitHub no longer moves it, and production_branch_follows_default reads false. This holds even when the branch you set is the current default — use this endpoint, not the connect call, when you want to pin a project to the default branch it already deploys from.

Projects that never set a branch keep following the default. To return one to following it, send an empty branch:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-connection/production-branch" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "production_branch": "" }'

The response carries the repository's current default branch with production_branch_follows_default: true, and later default-branch renames on GitHub move the project again. Volcano reads the default through the project owner's GitHub connection:

StatusWhen
400The repository has no commits yet. Push one first; until then a pin stays as it was.
400Code feature_unavailable: GitHub is not configured for this Volcano deployment.
409The owner's GitHub connection needs reconnecting or has lost access to the repository.
429GitHub rate limited the read. Try again shortly.

Volcano reads the default once more after switching the project over, to catch a rename that landed in between. If that read fails, the switch has already happened. Send the same request again to confirm the branch.

Pushes to any other branch are ignored — there is one deployment branch per project, and nothing else deploys.

4. Turn on auto-deploy

Connecting a repository already turned this on. Step 3 set auto_deploy_enabled and deploy_functions to true, so pushes deploy your functions without any call here.

Use this endpoint to deploy a frontend as well, to change the build roots, or to turn auto-deploy off. It is a full replace, so send every field you want to keep — including the two that are already on:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-deploy-settings" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_deploy_enabled": true,
    "deploy_functions": true,
    "frontend_name": "web",
    "frontend_app_root": "web"
  }'
FieldEffect
auto_deploy_enabledMaster switch, on from the first connect. A push that arrives while this is off is never deployed, and turning it on later does not deploy that push retroactively. Once you save settings here they are never defaulted over again — rebinding, disconnecting and reconnecting all leave them as you set them.
deploy_functionsDeploy every function discovered under volcano/functions/.
frontend_nameFrontend to deploy. Omit or leave empty to deploy no frontend. The frontend does not have to exist yet — it is created on the first deploy.
frontend_app_rootApp root the frontend builds from, relative to root_directory. Requires frontend_name; omit for the build root itself.
apply_migrationsApply the commit's pending migrations before anything deploys. Off until you turn it on. Omitting it keeps the saved value. See Apply migrations on push.

This is a full replace, except apply_migrations: send every other field you want to keep.

5. Push

git push origin main

Volcano receives the webhook, records a deployment run for the commit, downloads that exact commit, applies its migrations when apply_migrations is on, builds one archive per function plus one for the frontend, and dispatches the deploys. Functions and the frontend deploy concurrently; traffic stays on the live version until each new deployment is ready.

Watch the result through the project's deployment feed:

curl "https://api.volcano.dev/projects/$PROJECT_ID/deployments?limit=20" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"

Check deployment runs

Each push records a deployment run, which says whether the push deployed and, when it did not, why. List the runs, newest push first:

curl "https://api.volcano.dev/projects/$PROJECT_ID/git-deployment-runs?limit=10" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"
{
  "data": [
    {
      "id": "3c9e7b1a-4f2d-4e8a-9b6c-1d0e2f3a4b5c",
      "status": "failed",
      "branch": "main",
      "commit_sha": "4f2a9c1d8e7b6a5f4e3d2c1b0a9f8e7d6c5b4a39",
      "apply_migrations": true,
      "migration_runs": [
        {
          "id": "7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73",
          "database": { "type": "database", "id": "0b9c3f5e-2a41-4d7e-9f0a-6c8e1d2b3a45", "name": "main_db" }
        }
      ],
      "error": {
        "code": "migration_failed",
        "message": "migration run failed: run 7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73 on database main_db (migration_failed): migration 002_add_notes_pinned.sql failed at line 1 (SQLSTATE 42601); it failed in rehearsal, so production was not changed"
      },
      "created_at": "2024-01-01T00:00:00Z",
      "started_at": "2024-01-01T00:00:02Z",
      "completed_at": "2024-01-01T00:01:10Z"
    }
  ],
  "limit": 10,
  "has_more": false
}

Filter with status or branch, and page with cursor set to the previous response's next_cursor. Get one run at GET /projects/{id}/git-deployment-runs/{runId}. Project access tokens can read runs, including read-only ones.

statusMeaning
pendingWaiting for a worker, or for the project's previous run to finish
runningApplying migrations or deploying
succeededEvery deployment the run dispatched succeeded, or the commit only had migrations to apply
failedStopped; error says why
supersededA newer push replaced the run before it started
skippedNothing to deploy; error says why, such as nothing_to_deploy or auto_deploy_disabled

Other runs have no error. A failed run's error.code is one of the following. The four migration codes stop the push before anything deploys.

CodeCause
migration_failedA migration run failed for any reason, such as a file or connection_failed, or refused the commit's files. message names the run, its database, and its own code, or the refused file.
migration_invalid_layoutvolcano/migrations/ does not map onto the project's databases. message names the directory or file.
migration_timeoutThe commit's migrations had not finished two hours after the push arrived.
feature_unavailableThe run applies migrations where they are not available. Turn apply_migrations off to deploy without them.
source_invalidVolcano refused the commit's source or volcano-config.yaml, such as a repository over a limit. message says what to fix. Pushing the same commit again fails the same way.
child_deploy_failedA function, frontend, or Sandbox deployment failed. The deployment feed has a function's or frontend's error; GET /projects/{id}/sandboxes/{sandboxId}/deployments has a Sandbox's.
child_deploy_not_startedA function or frontend deploy failed before it recorded a deployment, such as a frontend whose previous deployment was still in progress, or a first deploy that failed and removed what it created. message names it and, when Volcano has one, the reason. Push again to retry.
deploy_refusedVolcano refused to deploy the commit for a reason a retry cannot fix, such as a function or frontend over a plan limit, a function whose kind changed, invocation settings a function cannot have, or a Sandbox it cannot run. message names the function, frontend, or Sandbox and the reason when Volcano has them. Push again once the cause is fixed.
typescript_not_compiledA function holds only TypeScript, which Volcano doesn't compile. The rest of the push deployed. message names the functions; commit their compiled JavaScript and push again. When a deployment also failed, the run reports child_deploy_failed instead.
deploy_failedA temporary failure outlasted Volcano's retries, or Volcano could not deploy the commit for a reason of its own. Push again to retry.
max_attempts_exhaustedThe deployment failed after repeated attempts. Push again to retry.
owner_not_activeThe project owner's account can't deploy right now, so nothing new from the push deployed. No new deploy or migration run started; what an earlier attempt already deployed or migrated stays, so the schema can be ahead of the deployed code. Reactivating the owner's account restores deployment eligibility; push again to deploy.
project_deletingThe project is being deleted, so nothing new from the push deployed. What an earlier attempt deployed or migrated stays until the deletion removes it.

A Vercel project import that fails reports its deployment run's code as its own error_code.

What a push deploys

A push deploys code: the functions under volcano/functions/ and the configured frontend. With apply_migrations on, it applies the database migrations under volcano/migrations/ first. It does not apply the rest of your project configuration.

ConcernApplied on push?How to change it
Function sourceYesPush
Frontend sourceYesPush
Function invocation settings (visibility, modes, OpenAPI metadata)Yes, from volcano-config.yamlPush, or volcano cloud config deploy
Function kind (standard or durable)Yes, from volcano-config.yaml, on the push that creates itFixed once created
Function variable scope (variable_scope, variables)Yes, from volcano-config.yamlPush, or volcano cloud config deploy
Database migrations (volcano/migrations/)With apply_migrations on, before anything deploysPush, or a tracked migration run
Variables, buckets, auth, OAuth, email templates, schedulers, databasesNovolcano cloud config deploy — see the configuration manifest

Variables are read at build and run time from the project, so a variable you change through the API or the dashboard applies to the next deployment without a code change.

A function that declares a variable the project does not have, or whose environment exceeds the size limit, fails the run on the first attempt without retrying. Define the variable or shrink the environment, then push the commit again.

volcano-config.yaml

If your repository has a volcano-config.yaml at the build root, auto-deploy reads it and applies the per-resource settings that belong to the resources it deploys: function kind, visibility, invocation/auth modes, and OpenAPI metadata:

version: 1
functions:
  - name: hello
    visibility: public
    invocation_mode: http
    http_auth_mode: none
    openapi_spec:
      openapi: 3.1.0
      info: { title: Hello webhook, version: 1.0.0 }
      paths: {}
  - name: order-workflow
    kind: durable

Deploying durable functions from a repository

Durable functions live under volcano/functions/ alongside standard ones and are declared with kind: durable. Omitting kind means standard, so a manifest written before durable functions existed keeps deploying unchanged.

A function's kind is fixed when it is created. The declared kind therefore applies on the push that creates the function, and on every later push it is checked rather than applied — a manifest that changes kind for a function that already exists fails the run instead of silently deploying the wrong kind. To change a kind, delete the function and let the next push create it again.

An unrecognised kind fails the run without retrying, the same as any other unusable manifest. That is deliberate: defaulting it would create a resource your repository does not describe, and no later push could correct it.

Invocation settings are current function state, not properties of a code version: the declared values take effect when Volcano processes the push, before any code deploys and regardless of whether that code deploy later succeeds. That is the same behavior as volcano cloud config deploy and volcano cloud functions update <name> --visibility <level>, which also flip it immediately. A function the commit creates gets its declared settings as the function is created. A push that would take a function with Frontend Function routes out of public or HTTP mode is refused before anything deploys; remove the routes first.

Precedence is fixed: the project's deploy settings decide whether a resource deploys, and the manifest fills in details for the resources that do. A function listed in the manifest but absent from volcano/functions/ is not created, and the manifest cannot switch on a frontend that frontend_name does not name.

Every other section of the manifest is ignored by auto-deploy and still requires volcano cloud config deploy — those sections sync destructively (entries absent from the file are deleted) and can carry secrets, which is not something a git push should do implicitly.

volcano-config.yaml must be a regular file at the build root. Symbolic and hard links are not followed.

A manifest that is present but unusable fails the run without retrying, rather than deploying with its settings silently dropped: malformed YAML, an empty file, an unsupported version, a function entry with no name or a duplicate one, a function variables key with no value, such as a bare variables: or variables: null, a file over 4 MiB, or a link rather than a regular file. volcano cloud config deploy rejects the same file.

Apply migrations on push

Turn on apply_migrations and each push applies the commit's pending migrations before it deploys anything, so new code never runs against the old schema.

Turning it on lets anyone who can push to the production branch run any SQL on your databases as the database owner. A migration file can read, change, or delete data as well as the schema, and row-level security does not stop it. Before you turn it on, protect the production branch and require pull request reviews, so every migration is reviewed before it reaches that branch.

The settings PUT is a full replace, except apply_migrations: a field you leave out is cleared, so leaving out frontend_name stops the frontend from deploying. Read the saved settings first:

curl "https://api.volcano.dev/projects/$PROJECT_ID/git-deploy-settings" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"

Then send them back with apply_migrations on. With the settings from step 4:

curl -X PUT "https://api.volcano.dev/projects/$PROJECT_ID/git-deploy-settings" \
  -H "Authorization: Bearer $VOLCANO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_deploy_enabled": true,
    "deploy_functions": true,
    "frontend_name": "web",
    "frontend_app_root": "web",
    "apply_migrations": true
  }'

If the database already holds these migrations, applied with the CLI or by hand, record a baseline of those files before you turn apply_migrations on. The CLI does not track the files it runs, so without a baseline the first push treats every file as pending: DDL that already ran fails its rehearsal, and a file that succeeds again, such as an INSERT, applies a second time.

Each push then:

  1. Reads the .sql files under volcano/migrations/ in your build root. Lay them out as for any tracked run: at the top level for a project with one database, or in a directory named for each database.
  2. Starts a tracked migration run for each database the commit has files for, with source: git_deploy and the commit's commit_sha. Files the database already applied are skipped; the pending ones are rehearsed, backed up, and applied.
  3. Deploys the functions and frontend once every run has succeeded. A commit that changes only migrations ends there, as a successful deploy.

If a run fails, nothing from the push deploys and the live version keeps serving. The push's deployment run fails with code migration_failed whatever the migration run's own code, which its message names, and lists the migration run it started on each database in migration_runs. Get the one that failed for the reason and, when a file failed, which one:

curl "https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/migration-runs/7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73" \
  -H "Authorization: Bearer $VOLCANO_TOKEN"
{
  "id": "7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73",
  "database": { "type": "database", "id": "0b9c3f5e-2a41-4d7e-9f0a-6c8e1d2b3a45", "name": "main_db" },
  "status": "failed",
  "phase": "rehearse",
  "source": "git_deploy",
  "commit_sha": "4f2a9c1d8e7b6a5f4e3d2c1b0a9f8e7d6c5b4a39",
  "error": {
    "code": "migration_failed",
    "message": "migration 002_add_notes_pinned.sql failed at line 1 (SQLSTATE 42601); it failed in rehearsal, so production was not changed"
  },
  "files": [{ "name": "002_add_notes_pinned.sql", "outcome": "failed" }],
  "warnings": [],
  "created_at": "2024-01-01T00:00:00Z",
  "updated_at": "2024-01-01T00:01:10Z"
}

Fix the file and push again. A push also deploys nothing when its files cannot start a run: a directory that names no database in the project, top-level files in a project with several databases, a file a run refuses, such as a malformed name, a link, a file over 1 MiB, or a directory nested more than one level, or a database over its storage allowance. No migration run is recorded for that database, and the deployment run's error names the reason:

{
  "code": "migration_invalid_layout",
  "message": "invalid migrations layout: volcano/migrations/billing/ names no database in the project"
}

A push starts its databases' runs one at a time, in name order. When a later database's files are refused, a run already started for an earlier database still applies, though nothing from the push deploys. The deployment run's migration_runs lists it.

While another run holds a database, the push waits for it. A push waits for its migrations for up to two hours from when it arrived, which can include time queued behind the project's previous push, then fails without deploying. A run it started keeps going and can still apply after that, while the live version keeps serving the previous code. Once the run has finished, push again: the next push finds the files already applied and deploys the code written for them.

A push starts a migration run only while the project owner's account is active, as starting a run through the API requires. While the account is read_only or inactive, a push starts no migration run, deploys nothing, and fails with code owner_not_active, with or without volcano/migrations/. A run an earlier attempt of the push started before the account changed fails with owner_not_active too: before it applies, or before its next file while it applies. Under the plan's limits hold, an apply under way finishes its files, as Apply migrations with a tracked run describes. Either way the push deploys none of the commit's code and fails with owner_not_active. Schema changes an earlier run applied stay, so the schema can be ahead of the deployed code; the run's message says so, and it lists the migration runs it started.

Nothing retries a push that failed with owner_not_active, and there is no redeploy route: once the account is active again, push again. That push skips the files already applied and deploys the code written for them. Plans and limits explains how a read_only account returns to active. Until then the owner cannot change Git deploy settings either: PUT .../git-deploy-settings answers 403 for a read_only account, and an inactive account's credentials answer 401. Turning apply_migrations off does not let a push deploy in the meantime.

Applying migrations needs the databases.migrations capability. Where it is unavailable, turning on apply_migrations answers 400 with code feature_unavailable, and a push that would apply migrations deploys nothing. Turn apply_migrations off to deploy without them. While the owner's account is active, turning it off, or sending back a saved true with other settings, always works. A commit without volcano/migrations/ deploys either way.

apply_migrations is not part of volcano-config.yaml. Like the other Git deploy settings, only this endpoint sets it, so a commit cannot grant itself the right to change your production schema.

Deployment behavior

Only the production branch deploys. Pushes to other branches, tag pushes, and branch deletions are ignored. Preview deployments for branches and pull requests are not available yet.

Projects that export source to GitHub pin the production branch confirmed at export time. A later GitHub default-branch change does not move production. After handover, set a different production branch explicitly before pushing it; branch changes are frozen while export is in progress.

Latest wins. While a run is pending, a newer push to the same branch supersedes it — the older run is marked superseded and never deploys a commit you have already moved past. A run that is already executing is allowed to finish.

Same commit, once. Re-delivering the same webhook, or pushing the same commit while a run for it is still pending or running, does not create a second deployment. Pushing that commit again after the earlier run reached a terminal state does deploy again, so you can retry a failed deploy by re-pushing.

Every configured resource redeploys on every push. There is no per-file change detection yet: a commit that touches only the frontend still redeploys the functions, and vice versa.

A frontend served by a promoted preview is not deployed. The push fails the frontend's deployment with an error that says to roll back to the frontend's own build first. Roll back, then push again.

A push deploys only while the project owner's account is active. Otherwise the run deploys nothing new and fails with code owner_not_active. Deploys and migration runs an earlier attempt of the run already started still finish and stay, and the run's message says so. Reactivating the owner's account restores deployment eligibility; push again to deploy. A push to a project being deleted fails the same way, with code project_deleting.

Runs are skipped, not failed, when there is nothing to do: auto-deploy is off, the project was disconnected or its binding changed before execution, the project was deleted, or the commit contains no deployable resource, no migrations to apply, and no function that holds only TypeScript.

Deploying directly while connected

Connecting a repository does not disable the CLI or the dashboard. A connected project still accepts volcano cloud functions deploy and every other direct deploy, and those deploys take effect immediately.

They are not durable, though, and the two ways they can go are opposite:

  • A resource that also exists in the repository is reverted by the next push. The push redeploys it from the commit, discarding what you uploaded. Nothing warns you at upload time, and the revert looks like an ordinary deployment.
  • A resource that does not exist in the repository survives. Pushes never delete resources that are missing from the commit, so a function you only ever deployed directly keeps running — and keeps not being in your repo.

Treat the repository as the source of truth for anything you want to keep. Use direct deploys for throwaway iteration, and commit the change once you want it to stick.

Telling them apart after the fact. Every deployment records deploy_source, so a project's history distinguishes a push (git) from a direct deploy (cli, web, api). Read it on each item in the deployment feed when a resource is not what you expect — the endpoint has no deploy_source query filter:

curl -H "Authorization: Bearer $VOLCANO_TOKEN" \
  "https://api.volcano.dev/projects/$PROJECT_ID/deployments?limit=20"

A git entry after your direct deploy is the revert.

Make Git the only source of truth

An ordinary production-branch push deploys the repository but does not disable direct platform deploys. To move source ownership to Git, follow Export your source to GitHub. Export initializes a new empty repository, observes its root push, and disables direct source deploys only after that push or a newer production push deploys successfully.

Check that the owner can push

Volcano writes to a connected repository for a project, such as opening a pull request, only where the project owner's GitHub account can push. An app or agent acting on the project can ask the same question before it plans a change:

curl -H "Authorization: Bearer $VOLCANO_TOKEN" \
  "https://api.volcano.dev/projects/$PROJECT_ID/git-connection/owner-push"
{ "repository_id": 123456789, "can_push": true }

The check is always about the project's connected repository; it takes no repository argument. A project access token can call it, including a read_only one, and so can the MCP tool get_git_owner_push. When can_push is false, reason says why:

reasonMeaningFix
owner_not_connectedThe project owner has not connected a GitHub accountThe owner connects GitHub
owner_reconnect_requiredThe owner's GitHub connection expired or was revokedThe owner connects GitHub again
repository_access_lostThe owner's GitHub account can no longer see the repositoryRestore the owner's access, or connect a repository the owner can see
no_push_permissionThe owner can read the repository but not push to itGive the owner write access to the repository on GitHub

Other responses:

  • 404: the project has no repository connected. With code feature_unavailable, this deployment has no GitHub integration, as in local mode.
  • 429 with Retry-After: checks that ask GitHub have an hourly budget for the project, and another for its owner across all their projects. Wait the seconds Retry-After gives.
  • 503 with Retry-After: GitHub did not answer, or is rate limiting the owner's account. The check never answers can_push: true when it could not ask.

An answer can be a few seconds old. A repeated check within that time is answered without asking GitHub again and does not count against the budget.

Limits

Per deployment run:

LimitValue
Downloaded repository tarball256 MiB compressed
Extracted repository512 MiB total, 256 MiB per file
Repository entries50,000
Functions per commit1,000
Each generated deploy archive256 MiB by default (operator-configured)
Total bytes packaged into archives1 GiB

Exceeding any of these fails the run with source_invalid, without retrying: they are properties of the commit, not transient conditions.

Troubleshooting

Nothing happened after a push. Find the commit in the project's deployment runs. A skipped or failed run says why in error. No run for the commit means the push was not eligible: check that it targeted the project's production_branch — read it back, don't assume it is the repository's default — that auto_deploy_enabled is true, and that the project is still bound to that repository (GET /projects/{id}/git-connection).

A push with migrations deployed nothing. With apply_migrations on, a failed migration stops the push. The commit's deployment run says why and lists the migration runs it started, as in Apply migrations on push. A run that failed with owner_not_active started no new migration run and deployed nothing new, because the project owner's account is not active. Schema changes from a run an earlier attempt started stay, so the schema can be ahead of the deployed code until you push again.

A function is missing. It is probably being skipped by discovery. Confirm it sits directly under volcano/functions/ inside root_directory, that its name is DNS-safe and not reserved, and that a directory function contains at least one supported source file. A function that holds only TypeScript isn't deployed; commit its compiled JavaScript.

The frontend did not deploy. frontend_name must be set, and root_directory + frontend_app_root must contain a package.json, or the nearest package.json above it must build it with next build <dir>. A volcano init nextjs project with frontend_app_root: web works this way. A configured frontend that is not found in the source is skipped, not failed.

Signed-in users or anon keys get 404 from a function you deployed. New functions are private, which admits only service keys and schedulers and looks missing to everyone else. An anon key on an authenticated function gets 403 when it invokes by id, and 404 when it calls by name through an SDK. Declare the level the function needs in volcano-config.yaml, such as visibility: authenticated for functions your signed-in pages call, or set it through the API. An entry that still says public: false sets authenticated on every push; use visibility: private to keep a function private. The SDKs remember a name that wasn't found for about 30 seconds, so a running client can keep getting not-found for that long after you widen the level. See who can invoke a function.

On this page