Git push from a session

How the edits of a remote session leave it as a git commit: ciresk dev push turns the session's workspace into one commit on the session's own branch and, with --pr, opens a pull request for it. A local session never needs this: its edits are your worktree's, and you commit and push them with your own git.

Pushing from a session needs the installation's forge to be turned on. It is not on in eu1 yet, so in Ciresk's cloud every ciresk dev push is refused today with push_unavailable. Everything on this page that does not push (checkpoints, stopping and continuing a remote session, pushing from your own checkout) works without it.

Which sessions push

SessionStarted withWhere its edits areHow they reach git
Localciresk dev upyour worktree, synced into the sessiongit commit and git push in your worktree, as always
Remoteciresk dev up --remote, ciresk dev startthe session's workspace, no local copyciresk dev push

A push from a local session is refused with not_cloud_writer; the answer tells you to push from your own checkout.

Push

Run it in the worktree whose remote session you want to push:

Terminal
ciresk dev push
ciresk dev push --pr
ciresk dev push --pr --base main --title "fix the invoice rounding"
FlagDefaultWhat it does
--base <branch>the branch checked out in this worktreethe branch the pull request targets
--proffalso open the session's pull request into --base, or find the one already open
--title <text>ciresk: <session label>the commit message's first line and the pull request's title: one line, 1 to 256 bytes
--idempotency-key <key>a new keya retried call with the same key acts once

--pr needs a base: with no branch checked out and no --base, it is refused before anything is sent.

What a push does, in one step on the host that runs the session:

  1. Takes a checkpoint of the workspace, exactly as ciresk dev checkpoint does.
  2. Builds one commit from what changed since the session's base: the commit the session started from, or the last commit it pushed. Only the changed files are applied, so changes that landed on the base branch since the session started are never reverted.
  3. Checks the commit for workflows that would run on the session's branch (see Workflows).
  4. Pushes it to refs/heads/ciresk/<session>, fast-forward only.
  5. With --pr, opens the pull request from that branch into --base, or finds the one already open.

When nothing changed since the base, the first push still creates the session's branch, at the base commit, so a pull request asked for with it has a branch to come from.

The commit:

  • Branch: always refs/heads/ciresk/<session>. No flag names another branch, a tag, a remote or a force. A session continued from a stopped or ended one keeps writing the first session's branch.
  • Parent: always the session's base. If the branch moved to a commit the session did not push, the push is refused with non_fast_forward and the branch is left as it is.
  • Author: the session's owner, with the name and email your organization's directory gives them; without them, the owner's id at an address that receives no mail. Committer: Ciresk <[email protected]>.
  • Message: the title, then these trailers:
TrailerValue
Ciresk-Sessionthe session id, devs_...
Ciresk-Pushed-Bywho asked for the push, user:<id>: the person, or the person an agent acts for
Ciresk-Agentthe agent that asked, agent:<id>; only when an agent pushed
Code
fix the invoice rounding

Ciresk-Session: devs_...
Ciresk-Pushed-By: user:01H...
Ciresk-Agent: agent:01H...
  • Pull request body: the session id and the checkpoint's tree root.

Output:

Code
pushed 3f9c2a1 to refs/heads/ciresk/devs_...
files: 4
pull request: https://github.com/acme/app/pull/812 (opened)

A push with nothing new since the last one prints already pushed <sha>. A pull request that was already open prints (existing). --json returns the result as data:

FieldWhat it is
referencerefs/heads/ciresk/<session>
oldthe branch's commit before the push, null when the push created it; equal to new when nothing changed
newthe commit the branch now points at
tree_rootthe checkpoint the commit holds
files_changedhow many files the commit changed
change_requestwith --pr: {id, url, created}, created is false when it was already open

A push may take up to about two minutes. If it is still running when the command stops waiting, the command prints still pushing, exits with code 7 and names the call to make: run ciresk dev push --idempotency-key <key> with the same key to get the result, without pushing twice. A push that is cut off because the session stopped or was killed is refused with session_ended, and nothing that had not reached the forge was pushed.

ciresk dev status shows the session's last push as pushed: <short sha> <pull request url>.

Workflows

A session never pushes a commit that would start a GitHub Actions workflow on its branch, because such a workflow would run the session's code with your repository's secrets. The push is refused with push_unsafe_workflows, and nothing is pushed, when:

  • the change touches anything under .github/workflows/; or
  • a workflow in the commit (any .yml or .yaml file directly under .github/workflows/, usually your base branch's own) runs on a push to ciresk/<session> or on the creation of a branch. That is a workflow with a create trigger, or with a push trigger that has no branch filter, a paths filter alone, or a branches or branches-ignore list that does not leave ciresk/<session> out. A workflow file that does not parse counts as running, and so does a branch pattern with ?, +, [ or \.

To fix it, change the workflow on your base branch, from your own checkout, then push again:

.github/workflows/ci.yml
on:
  push:
    branches-ignore: ['ciresk/**']
  pull_request:

Give every push trigger branches-ignore: ['ciresk/**'] (or a branches list that does not match ciresk/..., such as [main]), and drop any create trigger. A change to .github/workflows/ itself is always made from your own checkout.

A change whose path has a .git component, in any case or in a spelling some file system reads as .git, is refused with push_unsafe_path. Delete that path in the session, then push again.

The push credential

A push uses an organization secret of purpose git, created by an admin:

Terminal
ciresk dev secrets set github-push --purpose git --host github.com --repo github.com/acme/app < token.txt
  • For a GitHub forge the value is a token; for an ssh forge it is an openssh private key (a deploy key).
  • --host binds it to the forge's git host. When several git secrets for that host reach the session, the first by name is used.
  • --repo, --template and --agent-sessions select which sessions get it, as for every secret. A session an agent, a service or a key started gets it only when the secret was set with --agent-sessions.
  • The session itself never sees it: a git secret gets no placeholder in the session and never goes through the session's egress proxy. It is fetched for each push on the host and kept on no disk, and it is never in an answer, a log or an audit record.
  • No secret of purpose proxy may name the forge's host: the session's own requests to that host would carry it, so the session could push with it around its branch. While one does, every push is refused with push_credential_proxied; set that secret again with --purpose git, or remove it.
  • Give the token only what a push needs: contents and pull requests, write.

See secrets.md for creating, listing and removing secrets.

Who may push

The session's owner, or a member the owner shared the session with as use (see sessions.md). People, agents and services may call it, with the dev:sessions.attach scope. Agents push through the MCP tool dev_session_push, with the same base, pr and title arguments and an idempotency_key (see mcp.md). Only a person can create the git secret.

Refusals

CodeWhyWhat to do
push_unavailablethe installation has no forge for sessions (what every push answers in eu1 today), or the session was created before push existed, or its branch is not refs/heads/ciresk/<session>; the detail names whichpush from your own checkout, or ask your installation's operator; for an old session, start a new one with ciresk dev up --remote
not_cloud_writerthe session is local: its edits are your worktree'sgit push from this worktree
push_credential_missingno secret of purpose git for the forge's host reaches this sessionan admin runs ciresk dev secrets set <name> --purpose git --host <host> --repo <repo>, with --agent-sessions when agents start the session
push_credential_proxieda secret of purpose proxy names the forge's hostset it again with --purpose git; ciresk dev secrets list shows which
push_unsafe_workflowsa workflow would run on the session's branch, or the change touches .github/workflows/see Workflows
push_unsafe_paththe change holds a .git pathdelete it in the session, then push again
change_requests_unsupported--pr on a forge that opens no pull requests (a plain ssh forge); nothing was pushedpush without --pr, then open the pull request on the git host
no_base_committhe session started from no commit (a project without git, or a tree uploaded without a base)ciresk dev checkpoint; this session cannot push
non_fast_forwardsomeone else pushed to the session's branch; nothing was pushedfetch the branch the answer names and look at what landed; the session pushes again only once the branch is back at its last push, or deleted
session_endedthe session stopped, was killed or moved to another host during the pushciresk dev up --remote, then push again
validation_failedthe worktree's remote is not the session's repository or not on the forge's host, the base is not a valid branch name, --pr came without a base, or the title is not one line of 1 to 256 bytesfix the argument
push_unauthorizedthe forge refused the credential: expired, or without contents and pull requests writeset the secret again with a new value
push_refusedthe forge refused the push for another reasonread the detail
forge_unavailablethe forge did not answer: down, or the installation lets no traffic out to ittry again in a minute; if it persists, ask the operator
secrets_unavailablethe session's credential could not be fetchedtry again
push_failedthe commit could not be builttry again; the detail says which step failed

A worktree with no git remote is refused before anything is sent: add one with git remote add origin <url>.

Without the forge

These work today, in eu1 too:

  • A local session. ciresk dev up syncs your worktree into the session, and you pull, commit and push in your worktree with your own git. This is the way to get edits into git from Ciresk's cloud today.
  • Checkpoints. ciresk dev checkpoint saves a remote session's workspace as a tree root and prints it, with the next step to run Ciresk CI on it (ciresk ci check --tree <root>).
  • Stopping and continuing. ciresk dev stop checkpoints a remote session and releases its compute; ciresk dev start, or ciresk dev up --remote after the session ended, continues it from its last checkpoint.

For operators of an installation

The forge is a setting of the installation, not of your organization. It is off until the operator sets it on the hosts that run sessions:

VariableValues
CIRESK_DEV_FORGEgithub: GitHub with each organization's token; ssh: a git server reached with each organization's deploy key. Unset: no push
CIRESK_DEV_FORGE_GIT_URLthe git URL, ending in /. github: https://github.com/ by default. ssh: required, an ssh:// URL
CIRESK_DEV_FORGE_API_URLgithub only: https://api.github.com by default; set both URLs for GitHub Enterprise Server
CIRESK_DEV_FORGE_KNOWN_HOSTSssh only: the server's host keys

A value that does not configure is logged at start and leaves pushes off.

The installation's egress.yaml names its rows by product, and three products concern dev:

ProductWhat it covers
dev-sessionwhat a session itself may reach, narrowed by its template's egress rows (see egress.md)
devthe control plane's own calls; never a session's traffic
dev-pushthe push alone: the host's fetch of the base commit, the push of the session's branch and the pull request calls

Turning push on means the variables above plus dev-push rows for the forge's hosts: with GitHub, github.com and api.github.com; with an ssh forge, the git server's host. Without the API row the pull request calls are refused and answer forge_unavailable. Never list the forge for dev-session instead: that opens it to every session's own traffic.