Skip to content

Commit 9052d5c

Browse files
committed
🚀 Publish the current branch to its origin with <Git.Push>
`<Git.Push />` is the first concrete consumer of the shared Git-host reconciliation engine. It takes no props: the remote is the Repository's own `origin`, the branch is the one the selected checkout is on, and the commit is the one that branch points at. Which checkout that is, is decided the way every other Git operation's place is decided, so the same element inside a `<Dir>` at a linked worktree publishes that worktree's branch. The branch it publishes to belongs to a Git host, which no local transaction can enclose, so it observes before it mutates: a destination already holding this exact commit is adopted with nothing performed, a destination proven absent is published to once, and a destination holding another commit is refused. It never force-pushes and never touches upstream tracking. The Workspace transaction is opened for the export and closed before the remote is observed, so a network round trip never holds the run's database, and nothing is imported or published back — a Push changes the Git host and, locally, only the journal. The transport never runs through the selected checkout's own `.git/config`: that file is retained document data, and a push URL, a URL rewrite, a hook or a signing program written into it would otherwise choose where this run publishes and what else runs while it happens. It runs instead in a disposable repository this provider created, reading the checkout's object database through a read-only alternate, aimed at the exact private retained locator. Durable JSON carries the Repository's filtered identity without its checkout path, the remote, the branch, the full destination ref, the refspec, the local commit and the observed remote commit. The natural key is that identity with the remote and the destination ref: the external resource is the origin branch, while the complete request's fingerprint still discriminates a changed commit. A checkout whose HEAD names no branch is the one refusal of its own, decided before a durable effect exists and before anything is contacted. Refs #370.
1 parent 1948bf0 commit 9052d5c

22 files changed

Lines changed: 3319 additions & 67 deletions

‎architecture.md‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Existing documents and code get aligned to this section retroactively.
6262
| effect transaction | the single atomic SQLite transaction that publishes one Workspace-local mutation together with that effect's journal result |
6363
| external effect | an effect whose provider-owned outcome cannot participate in the Workspace SQLite transaction and therefore requires a stable identity and provider reconciliation |
6464
| XMD-mediated HTTP read | one bounded, non-mutating HTTP request an authored document performs through the contextual Fetch capability, whose detached response is retained as an ordinary durable observation. It is distinct from an Agent's own network authority, which it does not grant, and from a reconciled mutating external effect, which it does not claim: an interruption before its record commits may repeat the read |
65+
| object-source attachment | adapter-private composition data the trusted host builds for exactly one external-effect reconciliation, giving the selected provider authorized live access to local state the frozen request cannot describe — the Git objects a push publishes, and the retained locator it is authorized to reach. It is authenticated against the durable identity it belongs to, held in that provider's own closure, disposed with the invocation, and is never durable, never part of a natural key and never visible to routing middleware |
6566
| checkpoint | a completed journal boundary associated with the logical Workspace root visible after that effect |
6667
| history fork | a new workflow run that replays a compatible journal prefix and continues from its checkpoint and Workspace root under a new immutable document definition |
6768
| loaded copy | one independently evaluated instance of a package, such as the copy bundled into the binary or a separately installed dependency |
@@ -1026,6 +1027,27 @@ the result retained at the same position, and the provider's answer is parsed at
10261027
the terminal before the durable executor returns, so a raw payload becomes a
10271028
fixed protocol failure rather than journal content.
10281029

1030+
Some effects need live local state the frozen request cannot describe, and that
1031+
travels as an object-source attachment rather than as an input. A push needs the
1032+
Git objects its commit is made of and the destination it may reach; both are
1033+
built by the trusted host for one reconciliation, authenticated against the
1034+
retained identity they belong to, and held in the selected provider's closure.
1035+
Nothing about an attachment reaches the durable request, the natural key or the
1036+
routing surface, so what discriminates the effect stays the filtered identity a
1037+
journal may hold while what performs it stays where no replaceable code can
1038+
address it. The Workspace transaction such an effect opens is read-only and is
1039+
closed before the host is observed: no transaction is claimed across the
1040+
Workspace and the external service, and reconciliation rather than a distributed
1041+
transaction is what makes an interrupted attempt reach the host once.
1042+
1043+
A provider that needs a program must also not inherit configuration the run
1044+
merely stores. A checkout's `.git/config` is retained document data, and several
1045+
ordinary settings in one name a destination or a program — a push URL, a URL
1046+
rewrite, a hook, a credential helper, a signing program. The Deno push provider
1047+
therefore runs its transport in a disposable repository of its own that reads
1048+
the selected checkout's object database through a read-only alternate, and aims
1049+
it at the exact private retained locator rather than at a configured remote.
1050+
10291051
Only an answer the terminal accepted decides the effect. A conflict, a permanent
10301052
ambiguity and a temporary unavailability so accepted publish the effect's failed
10311053
result and replay as the same fixed local failure, rebuilt from its closed name
@@ -2225,7 +2247,7 @@ Status is measured against main.
22252247
| caller-owned storage transaction | publishes several changes, including journal events, in one transaction nothing else enlists in | built on main |
22262248
| live durable-operation coordinator | explicitly coordinates structured live execution with existing Yield publication while leaving replay and callback effects unchanged | built on the #365 stack |
22272249
| Workspace coordination API | fails closed by default; replaceable context routes only a one-use provider selection, while the selected provider directly invokes an execution-owned credentialed capability for execution, publication and failure activation | built on the #365 stack; the Deno provider installs an adapter-private atomic handler |
2228-
| shared Git-host reconciliation | reconciles Git push, pull-request creation and issue creation through one provider-neutral state machine journaled as `git_host_effect` — Prompt is not a consumer and keeps its own Agent-provider contract: run and expansion identity derived by the engine, observation before mutation, adoption of a proven compatible completion, one performance of a proven absence, refusal of conflict and permanent ambiguity, temporary unavailability as its own ordinary failure, and a provider free to support only some kinds; routing is one request-only contextual operation carrying no completion authority, and only an answer the invocation's own terminal accepted can author a journaled outcome | built on the #297 stack; `Git.Push`, pull request and issue components are its consumers, unbuilt (#370, #295, #296) |
2250+
| shared Git-host reconciliation | reconciles Git push, pull-request creation and issue creation through one provider-neutral state machine journaled as `git_host_effect` — Prompt is not a consumer and keeps its own Agent-provider contract: run and expansion identity derived by the engine, observation before mutation, adoption of a proven compatible completion, one performance of a proven absence, refusal of conflict and permanent ambiguity, temporary unavailability as its own ordinary failure, and a provider free to support only some kinds; routing is one request-only contextual operation carrying no completion authority, and only an answer the invocation's own terminal accepted can author a journaled outcome | built on the #297 stack; `Git.Push` is its first consumer, built on the #370 stack, while pull request and issue components remain unbuilt (#295, #296) |
22292251
| explicit WorkflowRun journal route | binds one already-filtered publication to one exact active transaction and otherwise uses ordinary serialized journal storage | built on the #365 stack |
22302252
| `API.Service` / `startService()` | creates an authenticated, supervised loopback service attachment through a provider-neutral operation | built on main |
22312253
| foreground command routing and retention | forwards a child's channels live, captures stdout for a `<Capture as>` region, and retains output only when the host asked for a record | built on the #441 stack |
@@ -2242,6 +2264,7 @@ Status is measured against main.
22422264
| implicit workflow Workspace | retains provider-neutral filesystem, repository and attachment state by run ID | document filesystem built on the #366 stack and Repository/Worktree composition on the #293 stack; process capabilities unbuilt (#218) |
22432265
| `<Repository>` / `<Worktree>` / `<Dir>` composition | names a Git repository and its linked checkouts inside the run-owned Workspace, installs each as contextual working directory, and retains creation identity beside the retained Git bytes | built on the #293 stack, Deno provider only |
22442266
| transactional Git effects (`Git.Switch` / `Git.Add` / `Git.Commit`) | publish local Git mutations with their journal result; the enclosing Repository and the contextual working directory select which retained checkout one runs in, and neither observation carries authority — the observed record is compared with the retained row and the directory with the checkouts that row holds, so a failure of authority, of retained state or of an unrecognized native condition fails the run instead of publishing a result | built on the #294 stack, Deno provider only |
2267+
| `Git.Push` | publishes the selected checkout's exact current named branch and commit to the same branch on the retained Repository's canonical `origin`, reconciled through the shared Git-host state machine rather than through a Workspace transaction: no props and no component result, no force, no upstream mutation and no implicit staging or committing; the durable request and record carry the Repository's filtered identity without its checkout path, and the transport runs in a provider-owned isolated control repository reading the checkout's objects through an authenticated object-source attachment aimed at the exact private retained locator | built on the #370 stack, Deno provider only |
22452268
| workflow lifecycle inspection and control | reads status/list/history without advancing a run, enforces the executor lock, refuses live cancellation, cancels non-live runs under that lock and deletes retained state | built on the #367 stack |
22462269
| historical authored source | retains an authored durable operation's normalized `SourcePosition` beside its identity, and history parses it or refuses the entry | built on the #367 stack |
22472270
| history fork | creates a new run from one compatible checkpoint and retained Workspace root | defined in `specs/workflow-workspace-spec.md`, unbuilt (#368) |

‎packages/workflow/mod.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,39 @@ export type {
126126
GitSwitchRequest,
127127
GitSwitchResult,
128128
} from "./src/composition/git-records.ts";
129+
export {
130+
destinationRefFor,
131+
filteredRepositoryIdentity,
132+
GIT_PUSH,
133+
gitPushInputsJson,
134+
gitPushNaturalKeyJson,
135+
gitPushObservationsJson,
136+
gitPushPreStateJson,
137+
gitPushRepositoryIdentityJson,
138+
gitPushResultJson,
139+
parseGitPushInputs,
140+
parseGitPushNaturalKey,
141+
parseGitPushObservations,
142+
parseGitPushPreState,
143+
parseGitPushRecord,
144+
parseGitPushRepositoryIdentity,
145+
parseGitPushResult,
146+
PUSH_REMOTE,
147+
pushExpectation,
148+
refspecFor,
149+
sameRepositoryIdentity,
150+
} from "./src/composition/git-push-records.ts";
151+
export type {
152+
GitPushExpectation,
153+
GitPushInputs,
154+
GitPushNaturalKey,
155+
GitPushObservations,
156+
GitPushOutcome,
157+
GitPushPreState,
158+
GitPushRepositoryIdentity,
159+
GitPushRequest,
160+
GitPushResult,
161+
} from "./src/composition/git-push-records.ts";
129162
export { useCompositionComponents } from "./src/composition/installation.ts";
130163

131164
export { GIT_HOST_API, GitHost } from "./src/git-host/api.ts";
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* `<Git.Push>` — publish the checkout's current branch to its origin
3+
* (specs/workflow-workspace-spec.md §7.4).
4+
*
5+
* ```md
6+
* <Repository name="project" url={props.repository}>
7+
* <Git.Switch branch="release/1.4" />
8+
* <Git.Add paths="release/notes.md" />
9+
* <Git.Commit message="Prepare 1.4" as="commit" />
10+
* <Git.Push />
11+
* </Repository>
12+
* ```
13+
*
14+
* It takes no props. There is nothing to name: the remote is the Repository's
15+
* own `origin`, the branch is the one the checkout is on, and the commit is the
16+
* one that branch points at. Which checkout that is, is decided the way every
17+
* Git operation's place is decided — by the enclosing `<Repository>` and the
18+
* contextual working directory — so the same element inside a `<Dir>` at a
19+
* linked worktree publishes that worktree's branch instead.
20+
*
21+
* A prop for any of that would be a second way to say what the document already
22+
* said by writing the element where it wrote it, and a `remote`, `branch`,
23+
* `refspec` or `force` prop would be a way to say something else entirely.
24+
* Publishing somewhere other than where the checkout is takes an explicit
25+
* contract this component does not have.
26+
*
27+
* It renders nothing, binds nothing and takes no content. A push produces no
28+
* value a later element reads: what the run retains is the reconciliation
29+
* record, which is journal evidence rather than something a document could use.
30+
*
31+
* ## The remote is not the local Git
32+
*
33+
* Switch, Add and Commit move state the run's own SQLite transaction encloses.
34+
* The branch this publishes to belongs to a Git host, which no local
35+
* transaction can enclose, so Push reconciles through the shared Git-host state
36+
* machine instead: it observes the destination before it mutates, adopts a
37+
* destination that already names this exact commit, publishes a proven-absent
38+
* one once, and refuses a destination that names anything else. It never
39+
* force-pushes, and it changes no upstream tracking.
40+
*
41+
* ## Failure
42+
*
43+
* `<Git.Switch>`'s policy, with one refusal of its own. A checkout whose HEAD
44+
* names no branch has nothing for this to publish, and that is something the
45+
* document can act on: it arrives as a fixed `GitOperationError` before any
46+
* remote is contacted. No Repository in scope is missing authority and travels
47+
* past every printing boundary, because a document that continued past one
48+
* would run later siblings as though a branch had been published.
49+
*/
50+
51+
import { cwd } from "@executablemd/runtime";
52+
import { hasContent } from "@executablemd/core";
53+
import type { PropsSchema } from "@executablemd/core";
54+
import type { Operation } from "effection";
55+
import type { Json } from "@executablemd/durable-streams";
56+
import { GitComposition } from "../git-api.ts";
57+
import { currentRepository } from "../context.ts";
58+
import { GitOperationAuthorityError, GitOperationError } from "../errors.ts";
59+
60+
/** The component name, as a document writes it and as a refusal names it. */
61+
export const PUSH = "<Git.Push>";
62+
63+
export const props: PropsSchema = {
64+
type: "object",
65+
properties: {},
66+
additionalProperties: false,
67+
};
68+
69+
export default function* GitPush(_props: Record<string, Json>): Operation<string> {
70+
if (yield* hasContent()) {
71+
throw new GitOperationError(
72+
PUSH,
73+
"invalid-invocation",
74+
`${PUSH} renders nothing, so it takes no content. Write it as <Git.Push />.`,
75+
);
76+
}
77+
78+
const repository = yield* currentRepository();
79+
if (repository === undefined) {
80+
throw new GitOperationAuthorityError(
81+
PUSH,
82+
"it is written outside a lexical <Repository>, so there is no repository in scope whose " +
83+
"origin a branch could be published to",
84+
);
85+
}
86+
87+
yield* GitComposition.operations.pushCurrentBranch({
88+
repository,
89+
workingDirectory: yield* cwd(),
90+
});
91+
return "";
92+
}

‎packages/workflow/src/composition/errors.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ export type GitFailureReason =
140140
| "ignored-pathspec"
141141
| "outside-checkout-pathspec"
142142
| "invalid-pathspec-magic"
143-
| "empty-index";
143+
| "empty-index"
144+
| "unnamed-branch";
144145

145146
/** A Git operation the document asked for and did not get. */
146147
export class GitOperationError extends Error {

‎packages/workflow/src/composition/git-api.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
GitSwitchRequest,
2727
GitSwitchResult,
2828
} from "./git-records.ts";
29+
import type { GitPushOutcome, GitPushRequest } from "./git-push-records.ts";
2930

3031
export interface GitCompositionApi {
3132
/**
@@ -54,6 +55,23 @@ export interface GitCompositionApi {
5455
* result: replay writes no object, reads no clock and spawns no Git.
5556
*/
5657
commitIndex(request: GitCommitRequest): Operation<GitCommitResult>;
58+
59+
/**
60+
* Publish the selected checkout's current branch to its origin.
61+
*
62+
* The one operation here whose outcome a local transaction cannot enclose.
63+
* It observes the destination ref before it mutates and performs at most
64+
* once, so an interrupted attempt that already reached the remote is adopted
65+
* on the next execution rather than repeated; a completed one restores its
66+
* retained record without contacting the remote at all.
67+
*
68+
* Routed through this Api like the other three, and for the same reason: a
69+
* document names a checkout by writing an element inside one, and what that
70+
* observation selects is the installed provider's to decide. Observation is
71+
* all this carries — the provider still authenticates the record, the
72+
* directory and the objects it publishes against what this run retained.
73+
*/
74+
pushCurrentBranch(request: GitPushRequest): Operation<GitPushOutcome>;
5775
}
5876

5977
export const GitComposition: Api<GitCompositionApi> = createApi<GitCompositionApi>(
@@ -71,5 +89,9 @@ export const GitComposition: Api<GitCompositionApi> = createApi<GitCompositionAp
7189
*commitIndex(_request: GitCommitRequest): Operation<GitCommitResult> {
7290
throw new GitCompositionProviderError("<Git.Commit>");
7391
},
92+
// deno-lint-ignore require-yield
93+
*pushCurrentBranch(_request: GitPushRequest): Operation<GitPushOutcome> {
94+
throw new GitCompositionProviderError("<Git.Push>");
95+
},
7496
},
7597
);

0 commit comments

Comments
 (0)