Skip to content

Push directly to a branch

After this page you can deliver a session’s reviewed revision to the branch it was based on — no pull request, no merge step — and you know exactly what the server guarantees about that push, which commit message and author land, what happens when the branch moved or GitHub refuses it, and how to retry safely.

This is the sibling of Open a pull request. Same publication command, same reviewed-revision token, same authority, different delivery. If your flow should go through review on GitHub, use that guide instead.

Direct delivery writes to the base branch itself — usually the repository’s default branch — and it runs on the same authority a pull request does:

Requirement Who provides it
A live repository grant covering the session’s repository An administrator of the customer’s organization, in the WAMP Cloud web app. Grants are described in Connect a repository.
A session bound to that grant, through source.grantId You, at session creation. A session whose repository comes from a person’s own GitHub connection instead of a grant cannot use direct delivery at all.
The wamp.cloud.publications:create capability on the calling credential The installation’s consent.

A grant carries no per-delivery permission, so there is nothing to preflight: any grant that lets a session open a pull request lets it push directly. What decides whether a given push lands is the branch’s own protection on GitHub, under What the server guarantees below.

The branch is selectable, but not on this call — it is pinned when the session is created, through source.baseBranch:

{
"source": { "kind": "github", "grantId": "…", "repositoryId": "1029384756", "baseBranch": "release/2026-08" }
}

Omit baseBranch and the repository’s default branch is used. Either way the server resolves that branch to a commit at creation and stores both on the session as baseBranch and baseOid; PATCH /v1/sessions/{sessionId} cannot change them. The branch has to exist already — nothing here creates one, and a name that does not resolve makes session creation answer 400 cloud_repository_branch_not_found. That code is about your request rather than about anyone’s authority, so a branch name that arrives from a customer’s configuration is worth validating where you collect it.

A different base branch is therefore a different session. See Connect a repository for the full source contract.

The call is the same publication PUT as pull-request delivery, with delivery: "direct_branch". There is no branch or ref parameter on it — the destination is the base branch pinned above.

Terminal window
PUBLICATION_ID=$(uuidgen | tr 'A-Z' 'a-z')
REVISION=$(curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/repository/review" \
-H "Authorization: Bearer $WAMP_TOKEN" | jq -r '.review.revision')
curl -sS -X PUT \
"$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUBLICATION_ID" \
-H "Authorization: Bearer $WAMP_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg r "$REVISION" '{
revision: $r,
title: "Fix CSV export rounding",
body: "Adds a regression test for the invoice total.",
delivery: "direct_branch"
}')"
Field Required Constraint
revision yes Exactly ^[a-f0-9]{64}$, from the review
title yes 1–256 characters, no CR or LF. Trimmed, and it becomes the commit subject
body yes Up to 65 536 characters and 65 536 UTF-8 bytes. May be "". It becomes the commit message body
delivery yes here "direct_branch"
draft omit Refused for this delivery. A direct push opens no pull request to draft, so the field is rejected rather than accepted and discarded — sending it at all, with either value, is 400 invalid_request, and issues names draft

One commit is created from the reviewed tree, with the pinned base commit as its only parent. Its message is the conventional Git shape — title as the subject line, a blank line, then body:

Fix CSV export rounding
Adds a regression test for the invoice total.

An empty body leaves a bare subject. CRLF line endings and trailing blank lines in body are normalized away.

Who authored it. Both the author and the committer are the app bot, not the end user and not the organization member who triggered the run:

Field Value
Author and committer name <app-slug>[bot] — for example wamp-cloud[bot]
Author and committer email <app-slug>[bot]@users.noreply.github.com
Author and committer date The pinned base commit’s own date plus one second, in +0000
Signature None. Commits are not GPG-signed, so a repository requiring signed commits refuses the push

That identity is deliberately the same for every publication your installation makes, which means the commit alone does not say who asked for the work. (A publication a signed-in person makes through a grant does add a Requested-by: trailer to the message, for exactly that reason. One your installation makes carries no trailer — its actor is the installation itself.) The audit trail for that is the publication record and the event log, not git: the publication carries the actor that created it, and wamp.publication.created carries the same commitSha you can find in the branch history. Keep the publication id if you may have to answer “who shipped this” later — it is the join between the commit and the requester.

The fixed date also means the commit is byte-identical when the same reviewed tree is published twice from the same base, which is what lets a lost response be recovered instead of pushed again.

The push is bound to what the session actually started from, at four levels:

  1. Session-pinned base branch. Direct delivery pushes to refs/heads/<baseBranch> and to nothing else. A moved default branch does not change where an existing session delivers.
  2. A pure fast-forward. The reviewed commit must have the pinned base commit as its exact parent. The server validates this before any provider write; a commit that is not a direct child of the pinned base is refused.
  3. An exact expected-head lease. The push is performed as “update this branch only if it still points at the pinned base OID”. If the branch moved — someone pushed since the session was created — the lease fails and nothing is overwritten. There is no force mode; a divergent branch is a conflict, never a rewrite.
  4. GitHub stays authoritative. Branch protection, rulesets and required reviews apply exactly as configured. A push GitHub rejects is observed and reported, never retried around. The credential used is write-scoped to the repository’s contents only — it carries no pull-request authority and no way to bypass protection.

The publication moves through the same lifecycle as pull-request delivery — admitted ─▶ prepared ─▶ attempted ─▶ succeeded | failed — and you poll it the same way. At most one non-terminal publication per session applies here too.

On success, result carries the delivery discriminator, the branch that was updated, and the commit that landed — and nothing else:

{
"publication": {
"id": "…",
"status": "succeeded",
"delivery": "direct_branch",
"result": {
"delivery": "direct_branch",
"branch": "main",
"commitSha": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d"
},
"completedAt": "2026-08-11T09:12:07.512Z"
}
}

There is no pullRequest object — none is created. The event log records wamp.publication.created with kind: "github.branch" and the same three fields (branch, commitSha, delivery), and unlike pull-request delivery, no github.pull_request artifact is appended. See Events.

Two different things are called a refusal, and they arrive by different routes.

The PUT itself is rejected. These are HTTP statuses on the request, and no publication is created:

Status error What happened
400 invalid_request The request body is malformed — a bad revision, a title over 256 characters, a draft field this delivery does not accept
403 cloud_repository_grant_required No grant covers the repository, or it was revoked — including a session whose repository is not backed by a grant at all
409 cloud_publication_conflict This publication id already exists with a different request. See Retrying
409 cloud_publication_in_progress Another publication on this session is still non-terminal
409 cloud_workspace_expired The session’s sandbox lease has lapsed, so there is no reviewed tree to publish. PUT /v1/sessions/{sessionId}/workspace, re-read the review, publish again
503 cloud_workspace_unavailable The session holds no sandbox lease at all — it was released, or it moved. The same recovery

The publication is created and then fails. The PUT answers 2xx, and the reason is in lastError.code on the publication — the same code the event log records on wamp.publication.failed. Every code below is terminal: the server does not retry it, and the publication never leaves failed.

lastError.code What happened What to do
branch_conflict The branch no longer points at the pinned base OID — someone pushed to it — or GitHub refused the push (branch protection, rulesets, a required signature). Nothing was forced and nothing was overwritten. See below; the recovery depends on which of the two it was.
review_changed The workspace changed between the review and the push, so the approved revision no longer names the tree that would be committed. Re-read the review and publish the new revision under a new publication id.
no_changes The reviewed tree is identical to the pinned base. There is nothing to commit. Nothing to retry — the agent produced no change. Read the review first if an empty diff is a normal outcome for your flow.
workspace_lost The sandbox holding the reviewed tree was gone by the time the push was prepared. The reviewed tree cannot be reconstructed from the publication. PUT /v1/sessions/{sessionId}/workspace, re-read the review, publish again under a new publication id.
repository_read_only The GitHub App installation can no longer write the repository — its contents permission was reduced, or the repository left the installation. An administrator restores it on GitHub; see Connect a repository.
cloud_repository_grant_required Publishing authority was revoked between admission and the push. As above.

branch_conflict covers two different situations, and the recovery differs:

  • GitHub refused, the branch unmoved. Protection or a ruleset said no, but the branch still points at the pinned base. Clear the block (or get an exception), re-read the review, and create a new publication from the same session — the lease still matches.
  • The base moved. Someone pushed to the branch, so the pinned base OID is no longer the head and no publication from this session can ever fast-forward it. The work is not lost, but it needs a new session created against the current base — or switch this work to pull-request delivery, which does not fast-forward the base at all.

In both cases the answer is a fresh publication id (or a fresh session), never a changed request: there is no caller-selected ref, no force flag, and no way to move the lease. That is the design.

Retries behave the same as everywhere in this API, with two properties worth knowing.

Replaying the identical PUT is safe, and “identical” means the values, not the bytes. The server stores a hash of the request’s canonical fields — session, repository, revision, title, body, delivery, draft and the resolved destination ref — so JSON key order, indentation and whitespace around the request are all irrelevant, and so is anything the schema normalizes on the way in (title is trimmed before it is hashed, delivery is filled in from its default). Replaying the same publication id with the same values returns the existing publication. Replaying it with a different title, body or revision is 409 cloud_publication_conflict — the id is bound to the request it was minted for.

Server-side attempts keep the same lease. If an attempt fails on a transient fault, the publication is retried (up to six attempts) against the same persisted destination and expected OID; the grant is re-checked on every attempt. And if the branch already points at your commit — a push that landed but whose confirmation was lost — the retry observes that and reports success rather than pushing twice.

Once status is failed, it is terminal: mint a new publication id for the next attempt.