Git repository builds
Use these routes to trigger Git repository builds manually, inspect history, and read incremental logs. The older route names remain available for existing GitHub projects.
Required permissions
Use project.deploy.trigger on project:<project-id> to trigger a deploy and project.logs.read to read builds and logs. Source and command configuration requires project.source.update. owner has full access; assistant, member, and organization API Keys need the corresponding grant.
Update GitHub commands and root directory
PATCH /v1/project/:id/github/commandsUpdates the commands and root directory of a GitHub project and starts a new build. Requires project.source.update. Send at least one of the fields below; omitted fields keep their current value.
| Field | Type | Description |
|---|---|---|
start_command | string | Start command, 1 to 256 characters. |
pre_build_command | string | null | Command run before the build, 1 to 256 characters. null removes the command. |
build_command | string | null | Build command, 1 to 256 characters. null removes the command. |
context_path | string | Build root directory, relative to the repository root. Default ".". Follows the rules in Build root directory. |
{
"start_command": "npm start",
"build_command": "npm run build",
"context_path": "apps/api"
}Response
{
"status": "success",
"message": "GitHub commands updated and build triggered successfully",
"data": {
"build_id": "6650f1a2b3c4d5e6f7a8b9c1"
}
}The 202 response includes the build_id of the started build; follow it with the build routes below.
Errors
| HTTP | When it happens |
|---|---|
400 | Empty body, command outside the limits, invalid context_path (absolute path, .., backslash, or more than 256 characters), or project without a connected GitHub repository. |
401 | Missing or invalid credential. |
403 | The credential does not have project.source.update for the project. |
404 | The project does not exist or is not available to the current organization. |
500 | The API could not save the commands or start the build. |
Update GitHub deployment settings
PATCH /v1/project/:id/github/deploy-settingsSend at least one of the fields below. Supplied properties are applied to the current settings, then the resulting mode combination is validated.
| Field | Type | Description |
|---|---|---|
auto_deploy | boolean | true selects Automatic by branch. It cannot be enabled while version_deploy.enabled is true. |
version_deploy | object | Configures By Tag and By Release modes. With enabled: false, defaults for the other fields are event: "tag", tag_pattern: "*", and include_prereleases: false. Older settings may omit this object; its absence keeps tag and release triggers disabled. |
version_deploy.enabled | boolean | Enables or disables version-based mode. |
version_deploy.event | tag | release | tag updates on a newly created tag; release updates when a release is published. |
version_deploy.tag_pattern | string | Required when version mode is enabled. Matches the full tag name, is case-sensitive, and accepts 1 to 255 characters. |
version_deploy.include_prereleases | boolean | For event: "release", includes prereleases. The default is false; drafts do not start publications. |
The pattern supports only * (any sequence of characters) and ? (one character). For example, v* matches v1.2.0; character classes such as [12], control characters, and other glob syntax are not accepted. In tag mode, only tag creation starts a publication; tag updates and deletions do not.
The modes are exclusive. When switching from the current mode, send the properties needed to leave the final state in exactly one mode. These examples show a complete body for each option:
| Mode | Body |
|---|---|
| Manual | {"auto_deploy":false,"version_deploy":{"enabled":false,"event":"tag","tag_pattern":"*","include_prereleases":false}} |
| Automatic by branch | {"auto_deploy":true,"version_deploy":{"enabled":false,"event":"tag","tag_pattern":"*","include_prereleases":false}} |
| By Tag | {"auto_deploy":false,"version_deploy":{"enabled":true,"event":"tag","tag_pattern":"v*","include_prereleases":false}} |
| By Release | {"auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":false}} |
Example request to publish when a new v* tag is created:
PATCH /v1/project/507f1f77bcf86cd799439011/github/deploy-settings
Content-Type: application/json{
"auto_deploy": false,
"version_deploy": {
"enabled": true,
"event": "tag",
"tag_pattern": "v*",
"include_prereleases": false
}
}Response
For brevity, this example shows the public fields related to the update mode; the response can also include the project's configured runtime and commands.
{
"status": "success",
"message": "GitHub deploy settings updated successfully",
"data": {
"repository_owner": "example-org",
"repository_name": "web-app",
"branch": "main",
"auto_deploy": false,
"version_deploy": {
"enabled": true,
"event": "tag",
"tag_pattern": "v*",
"include_prereleases": false
}
}
}data contains the public GitHub configuration. Older settings may omit version_deploy; without that object, tag and release publications are disabled, and auto_deploy indicates whether the project uses manual or automatic-by-branch mode.
Errors
| HTTP | When it occurs |
|---|---|
400 | Empty body, invalid pattern, contradictory modes (for example, auto_deploy: true with version_deploy.enabled: true), or a GitHub repository that must be reconnected before enabling tags or releases. |
401 | Missing or invalid credentials. |
403 | The credential does not have project.source.update on the project. |
404 | The project does not exist or is not available to the current organization. |
409 | The GitHub source changed while settings were being updated; read the current project settings and retry. |
429 | The limit of 10 updates per minute was exceeded. |
500 | The API could not save the settings. |
These logs are different from
GET /v1/project/:id/logs, which returns logs from the running application. Build routes return publication history and logs.
Neutral routes for Git builds
Use these routes for projects with a Git source. Response envelopes retain the existing public build and log fields.
Trigger a manual deploy
POST /v1/project/:id/deployRate limit: 10 per minute (counted per source IP).
The body accepts optional branch and commit_sha; commit_sha must be 40 or 64 hexadecimal characters. Without an override, the project uses its configured branch. For GitHub projects in By Tag and By Release modes, send an empty body: the build publishes the latest version matching the pattern. In these modes, branch or commit_sha return 400 (GIT_VERSION_REF_OVERRIDE), and no matching version returns 409 (GIT_VERSION_NOT_FOUND). The response is 202:
{
"status": "success",
"message": "Build triggered successfully",
"data": {
"build_id": "665f1f77bcf86cd799439011"
}
}List builds
GET /v1/project/:id/buildsRate limit: 60 per minute (counted per source IP).
Optional query parameters are page, limit, branch, and status.
{
"status": "success",
"data": [],
"pagination": {
"total": 0,
"page": 1,
"limit": 10
}
}Get a build
GET /v1/project/:id/builds/:buildIdRate limit: 60 per minute (counted per source IP).
The response uses { "status": "success", "data": Build }. Build fields remain the existing public fields; responses do not include credentials or internal source identifiers.
Read incremental logs
GET /v1/project/:id/builds/:buildId/logs?cursor=0&limit=200Rate limit: 120 per minute (counted per source IP).
cursor and limit are numeric. The response contains new chunks, the next cursor, and current status:
{
"status": "success",
"data": {
"logs": [],
"next_cursor": 0,
"status": "building",
"finished": false,
"truncated": false
}
}Use next_cursor in the next request to fetch only new lines. finished says the build reached a final state (success, failed, or superseded, when a newer build of the project replaced it and it will not be published); truncated says the public log was limited.
GitHub compatibility routes
The /github/* routes below preserve compatibility with existing GitHub projects. For a generic Git source, use the neutral routes on this page.
Trigger a manual build
POST /v1/project/:id/github/deployRate limit: 10 per minute (counted per source IP).
Body
| Field | Type | Required | Description |
|---|---|---|---|
branch | string | No | Branch used for the build. When omitted, the API uses the branch configured on the project |
commit_sha | string | No | Specific commit used to trigger the build (40 hexadecimal characters) |
In By Tag and By Release modes, send an empty body: the build publishes the latest version matching the pattern. branch or commit_sha return 400, and no matching version returns 409.
Response
The response is 202:
{
"status": "success",
"message": "Build triggered successfully",
"data": {
"build_id": "665f1f77bcf86cd799439011"
}
}List project builds
GET /v1/project/:id/github/buildsRate limit: 50 per minute (counted per source IP).
Query
| Field | Type | Required | Description |
|---|---|---|---|
page | number | No | Current page. Default 1 |
limit | number | No | Page size. Default 10, maximum 50 |
branch | string | No | Filters by branch |
status | string | No | Filters by pending, building, success, failed, or superseded |
Response
{
"status": "success",
"message": "builds listed successfully",
"data": [
{
"id": "665f1f77bcf86cd799439011",
"commit_sha": "abc123def456789012345678901234567890abcd",
"branch": "main",
"status": "failed",
"started_at": "2026-06-03T14:10:00.000Z",
"finished_at": "2026-06-03T14:11:09.000Z",
"error_code": "NPM_LOCKFILE_REQUIRED",
"error_message": "O package-lock.json não foi encontrado no diretório raiz da aplicação.",
"triggered_by": "manual",
"created_at": "2026-06-03T14:10:00.000Z",
"updated_at": "2026-06-03T14:11:09.000Z"
}
],
"pagination": {
"total": 12,
"page": 1,
"limit": 10
}
}Get a specific build
GET /v1/project/:id/github/builds/:buildIdRate limit: 50 per minute (counted per source IP).
Response
{
"status": "success",
"message": "build retrieved successfully",
"data": {
"id": "665f1f77bcf86cd799439011",
"commit_sha": "abc123def456789012345678901234567890abcd",
"branch": "main",
"status": "failed",
"started_at": "2026-06-03T14:10:00.000Z",
"finished_at": "2026-06-03T14:11:09.000Z",
"error_code": "NPM_LOCKFILE_REQUIRED",
"error_message": "O package-lock.json não foi encontrado no diretório raiz da aplicação.",
"triggered_by": "manual",
"created_at": "2026-06-03T14:10:00.000Z",
"updated_at": "2026-06-03T14:10:00.000Z"
}
}Failure fields
The codes below document GitHub compatibility routes. Neutral routes may add generic build codes; clients should keep error_message as a fallback.
List and detail responses can include these fields when a build fails:
| Field | Type | Description |
|---|---|---|
error_code | string | Optional code that identifies a known cause |
error_message | string | Public message with the available diagnosis |
diagnosis_status | string | Legacy field. New builds return not_requested; older builds may carry available or unavailable |
diagnosis_code | string | Legacy field, present only on older builds |
Legacy builds may not have an error_code. API clients should use error_message as the fallback when error_code is absent or unrecognized. The text and language of error_message are not a stable localization contract; clients should prefer error_code for their own localized presentation.
The supported codes are:
| Code | Meaning |
|---|---|
NPM_LOCKFILE_REQUIRED | The Node.js project does not contain a usable package-lock.json in the expected directory |
NPM_LOCKFILE_OUT_OF_SYNC | package-lock.json is not synchronized with package.json |
GITHUB_COMMIT_UNAVAILABLE | The requested commit is unavailable in the selected repository and branch |
GITHUB_REPOSITORY_AUTH_FAILED | Zenifra could not access the repository with the current GitHub connection |
NODE_BUILD_SCRIPT_MISSING | The build expects a Node.js script that is not declared in package.json |
BUILD_FAILED_UNCLASSIFIED | The cause could not be classified automatically; review the message and Build Logs |
GITHUB_REPOSITORY_REAUTHORIZATION_REQUIRED | Zenifra could not renew access to the same repository; reconnect it in the project |
BUILD_COMMAND_FAILED | A project command (install, pre-build, or build) exited with an error; its output is in Build Logs |
ROOT_DIRECTORY_NOT_FOUND | The configured root directory does not exist in the build commit |
ROOT_DIRECTORY_INVALID | The configured root directory is not a valid relative path inside the repository |
GITHUB_ACCESS_TOKEN_MISSING | The GitHub connection did not authorize access to the repository to start the build |
PROJECT_PUBLISH_TIMEOUT | The build finished, but publishing did not respond in time; try again |
PROJECT_PUBLISH_FAILED | The build finished, but publishing failed; try again |
diagnosis_status and diagnosis_code are kept for compatibility. Use error_code and error_message to present the failure cause.
For lockfile fixes and Node.js command validation, see Node.js dependencies.
Get incremental build logs
GET /v1/project/:id/github/builds/:buildId/logsRate limit: 200 per minute (counted per source IP).
Query
| Field | Type | Required | Description |
|---|---|---|---|
cursor | number | No | Last consumed sequence. Default 0 |
limit | number | No | Maximum number of chunks. Default 200, maximum 500 |
Response
{
"status": "success",
"message": "build logs retrieved successfully",
"data": {
"logs": [
{
"sequence": 1,
"timestamp": "2026-06-03T14:10:01.000Z",
"level": "info",
"step": "build",
"message": "npm ci",
"final": false
}
],
"next_cursor": 1,
"status": "building",
"finished": false,
"truncated": false
}
}Use next_cursor in the next request to fetch only new lines.
How to interpret it
| Field | Description |
|---|---|
logs | Incremental list of public build lines |
next_cursor | Last returned sequence |
status | Current build state |
finished | true when the build ended with success, failed, or superseded |
truncated | true when the public log was truncated to protect platform stability |
Retention
GitHub build history is retained for up to:
- 30 builds per project
- 30 days of age
Whichever limit comes first removes the oldest records, together with their log chunks.
Polling example
curl -X GET "https://api.zenifra.com/v1/project/507f1f77bcf86cd799439011/github/builds/665f1f77bcf86cd799439011/logs?cursor=0&limit=200" \
-H "x-api-key: your-api-key" \
-H "x-organization-id: your-organization-id"Next steps
Last updated on