The scope check asked whether the newest merge touched code. The question is
whether what is published matches the branch, and the two differ as soon as a
build does not finish.
That happened: run 300 carried the daemon fix and was cancelled by the next
merge; the two merges after it were CI-only, so the check reported nothing to
build and skipped publishing. The fix exists on develop and in no image —
runner:develop still carries version 0.2.0-dev. The release build then ran on
the unfixed daemon and died the same way for the fourth time.
Branch pushes build unconditionally again. The job image keeps its own skip,
which is safe because it compares published content, not the last commit.
The agent declared `env!("CARGO_PKG_VERSION")`, which expands to the version of
the crate being compiled — syncode-runner-agent, still 0.2.0. Both runners on
the production host run 0.2.1 and report `syncode_runner_info{version="0.2.0"}`
to Prometheus and to the control plane.
The version now travels from the binary crate through AgentSettings. A test
pins the crate version to the VERSION file, so the two cannot drift apart the
way the release bump exposed.
The public benchmark document pointed readers at the private meta repository
for the harness, the runbook and the raw CSV samples, and named the internal
result files. None of that is reachable from here, and a public document should
not describe where our infrastructure keeps its data.
The A/B session split went with it. Each comparison is now its own table of two
builds measured together, which is what a reader needs; the session bookkeeping
belonged to the people running the benchmark, not to the published result.
The release candidate is named 0.2.1 now that the tag exists. Its source commit
and digest stay in the table, so the exact build remains identifiable.
The README called them Forgejo releases — a leak from the fj CLI used to publish
them.
A merge that only touched documentation rebuilt everything. The scope check
that used to prevent it asked whether the newest commit changed code, which a
cancelled build could make wrong, so #33 removed it.
Both images now answer the safe version of that question: they are named after
the content that goes into them and built only when that name is missing from
the registry. A documentation merge resolves to the same name, finds the image
published, and moves the branch tags onto it. The runner image name also covers
the version label, so develop and main never share a build.
Both epochs are fixed, so identical content produces an identical digest.
The single job became five: resolve, validate, runner-image, job-image,
promote. A failure now names what failed instead of pointing at a step called
"Build and push images", and each part can be re-run on its own.
Logging in, building and pushing are what docker/login-action and
docker/build-push-action do, so they do them. The build path is now a single
action call that carries every tag, instead of a build followed by a shell loop
that retags.
Bash stays where nothing off the shelf applies: resolving version and tag names
from git, asking the registry whether the content is already published, and
copying manifests. Each path uses one mechanism rather than half of each.
A change under images/, tools/ or scripts/release/ invalidated the source layer
and recompiled the workspace, although none of it reaches the runner image. The
build context now excludes them, and the content hash that names the image
excludes the same paths.
.gitea stays in the context: tests/workflow_model.rs embeds the workflow files
with include_str!, so validation reads them. Removing it broke the build, which
is why the image hash excludes .gitea while the context does not — the compiled
binary does not carry the workflows, the test does.
Move the compilation side of the workflow crates to syncode/workflow and
embed it at workflow/, leaving sandbox, worker, cache and source fetching
here. Point the checkout compatibility test at our own forge and mirrors
so the suite no longer needs a GitHub token.
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous commit removed the compilation side without recording the
submodule that replaces it, so the tree had no compiler to build against.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cargo consumes the shared compiler by git URL, so the runner tree carries
no submodule and its CI needs no submodule checkout.
Co-authored-by: Cursor <cursoragent@cursor.com>
Crates state their own dependencies, so the git pin lives in the manifests
that use it rather than in a workspace table.
Co-authored-by: Cursor <cursoragent@cursor.com>
A reference is a tag, branch or commit, not a revision expression, so look
it up by name before parsing it.
Co-authored-by: Cursor <cursoragent@cursor.com>
The model, the reference and the action.yml parser now live with the
compiler; fetching an action and choosing the JavaScript runtime stay here.
Co-authored-by: Cursor <cursoragent@cursor.com>
The declared runtime and the shipped runtime held the same values once
node12 and node16 were dropped, so the mapping between them is gone.
Co-authored-by: Cursor <cursoragent@cursor.com>
An assignment now says what to run: workflow source the runner compiles, as a
forge control plane sends it, or a plan already compiled, as the SynCode
control plane sends it. The runner refuses a plan of an unknown schema version
or a foreign dialect instead of reading it as if it understood it.
A node opens one stream to the control plane, says hello and is given work on
it. The assignment carries a compiled plan, and conclusions travel back against
the run the control plane named. Nothing polls and no task version is counted.
The forge protocol identifies a job with an i64, so the session keeps the
control plane's own identities and hands the domain a local handle.
The agent asked the forge for a backlog version and decided from the answer
whether anything was assigned. It now asks an AssignmentSource for work: the
forge adapter keeps the version counter as its own business, and a node session
simply waits on its stream. Backoff, telemetry and shutdown are unchanged.
Wiring the binary onto the SynCode source waits for enrollment, which is the
next slice: the agent still declares itself through the forge protocol.
Capacity is a port in the domain and a statvfs call at the process boundary.
The layer store size is absent rather than reported as zero, because nothing
asks the engine for it yet and a zero would be indistinguishable from an
answer.
Expanding a matrix belongs to whoever decides what runs. The forge specialises
it before sending a workflow, so a compile here yields exactly one combination;
anything else is refused with the reason rather than run as if the first
combination were the whole job.
The vendored protocol had drifted: the runner still spoke the version from
before enrolment, fencing and capacity, so the two sides did not share a schema
at all. It is resynced, and the client now spends a token for an identity, opens
the session with its credential, capabilities and capacity, states the grant on
every report, and names what it still holds on each heartbeat. A refusal reaches
the node as a reason instead of leaving it waiting for work that will not come.
Reporting a job's state and appending its logs are not properties of a forge
control plane; they are what any control plane accepts. JobReporter carries
them, ControlPlane and JobSummaryPublisher build on it, and the reporting
pipeline now asks for a reporter rather than for the whole forge protocol.
This is what a node session needs to plug in: it reports up its own stream and
has no register, declare or poll to offer.
A session had one owner and reporting needs another: the agent keeps the
receiving half while a clone of the sending half travels with the running job.
NodeReports is that clone, and it implements JobReporter, so the reporting
pipeline takes it exactly as it takes the forge client.
Step summaries are refused with a reason. This control plane has nowhere to put
them yet and does not offer the capability that asks for them, so being called
anyway is a mistake worth hearing rather than a quiet no-op.
The adapter for the SynCode node protocol existed with its own tests and
nothing used it: the daemon wired the forge client and the polling assignment
source unconditionally, so a runner could not be pointed at syncode-control at
all. The protocol is now chosen in configuration, and the choice is made once
rather than attempted and fallen back from.
The agent no longer declares the runner itself. It could not: a forge is asked
who this runner is, while a node protocol states it as the session opens, and
an agent that insisted on asking would have forced one shape of handshake onto
both. The handshake now belongs to whichever control plane is in use, and the
agent is handed the result. That also drops ControlPlane from its bounds — it
only ever needed to report, and register and poll were never its business.
Registration records which protocol its credentials belong to. A forge
registration and a node enrolment are not interchangeable, and presenting one to
the other locks the runner out with an error nobody can read.
A node still fetches sources, actions and artefacts from the forge, so config
states where the forge is when the control plane is not it. Omitting it is
refused rather than guessed.
Capacity is finally assembled and sent: the probe existed and nothing called it,
so every node would have declared nothing and been refused work for it.
A node credential lasts fifteen minutes. Nothing on this side ever took a
replacement, so a runner worked for one term and was then locked out of the
control plane with no way back but a fresh enrolment token.
The session now takes a `Rotated` credential and writes it down through a
store the daemon points at the registration file, before it reads anything
else. The credential it replaces is already dead on the control plane, so
failing to keep the new one is an error rather than something to carry on
past.
Heartbeats come back with it. Nothing else on the session speaks while a job
runs, so a busy node looked exactly like a dead one and lost its leases for
it, and a rotation offered on an open session needs the node to be saying
something to be offered at all. Room is measured on every beat rather than
repeating what was true when the session opened, and a host that cannot be
measured says it is alive anyway instead of going quiet over it.
Credential rotation landed on develop while this branch was open, and both
sides touched the same session. The resolution keeps each: an assignment
still has to state where a run came from, and a session still keeps the
credential the control plane hands it.
The test fixtures the two branches each kept locally now live once in the
shared support module.
This repository named a revision seven commits behind the one the control
plane compiles with, which D017 exists to prevent: a plan compiled there is
executed here, so the two reading the same file differently is the failure the
shared compiler was split out to make impossible.
Catching up found that the newer compiler refused this repository's own
compatibility workflow, because it read `on.schedule` as a mapping when the
dialect writes it as a sequence. That is fixed in syncode/workflow, and this
names the revision that carries the fix.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Promote the verified SynCode 0.3 implementation to the release branch.
The agent declared `env!("CARGO_PKG_VERSION")`, which expands to the version of the crate being compiled — syncode-runner-agent, still 0.2.0. Both runners on the production host run 0.2.1 and report `syncode_runner_info{version="0.2.0"}` to Prometheus and to the control plane. The version now travels from the binary crate through AgentSettings. A test pins the crate version to the VERSION file, so the two cannot drift apart the way the release bump exposed.Approved for the verified SynCode 0.3.0 release.