How CI works here
neosource runs CI from workflow files stored in your repository. The shape will be familiar if you have used GitHub Actions, but two things differ enough to cover before you write anything: where the files live, and which triggers actually fire.
Where workflows live
Section titled “Where workflows live”.neosource/workflows/├── ci.yml└── nightly.yamlThree rules, and each of them has bitten someone:
- The directory is
.neosource/workflows/, not.github/workflows/. - Files must end in
.ymlor.yaml. - Only the top level of that directory is read. Files in subdirectories are not picked up.
.github/workflows/ is discovered, but never runs
Section titled “.github/workflows/ is discovered, but never runs”A .github/workflows/ directory is read and parsed — with the same parser
that handles .neosource/workflows/ — but nothing in it is ever registered or
dispatched. Its files are recorded as an inert report, not as workflows.
You see that report on the repository’s Actions tab while the repo has no
runs of its own: each discovered file is listed, along with whether it would
run as-is and any warnings the parser raised against it. The same data is on
GET /api/repos/{owner}/{repo}/workflows as discovered[].
So a repository imported from GitHub does not silently have “no CI” with no
explanation — but it does have no running CI until you move the files into
.neosource/workflows/. See
Coming from GitHub Actions for what to change once
you move them.
How workflows get registered
Section titled “How workflows get registered”Workflows are indexed from the default branch only. When you push to it,
neosource reads .neosource/workflows/ out of the tree that push just created
and replaces the repository’s registered workflow set with what it finds.
Two consequences follow from “replaces”:
- A workflow file added in a push is registered before that same push’s triggers are evaluated — so a workflow you add can fire on the very push that adds it.
- A workflow file deleted from the tree is deregistered. There is no stale leftover definition.
Pushes to other branches do not change the registered set. A workflow that only exists on a feature branch is not registered, though it can still run against that branch once it is on the default branch — the definition comes from the default branch, the code under test does not.
What actually triggers a run
Section titled “What actually triggers a run”The parser accepts more trigger events than the server dispatches. Three events start a run today:
| Trigger | Fires when |
|---|---|
push |
You push to the repository. |
pull_request |
A PR is opened, or updated with new commits (synchronize). |
schedule |
A cron entry comes due. |
workflow_dispatch, repository_dispatch and change parse but do not
dispatch. The full status table and the filter rules are in
Writing a workflow — including the glob
semantics, which are stricter than most people expect (release/* does not
match release/v1/rc1).
To start a run by hand, call POST /api/workflow-runs with a workflow_id and
a cause. It needs Write permission on the repository.
concurrency: is enforced
Section titled “concurrency: is enforced”A concurrency: block works, at both workflow and job level, following
GitHub’s rules:
- At most one run per group is active.
- At most one run per group is pending. A newer arrival cancels the older pending run and takes its place.
cancel-in-progress: trueadditionally cancels the active run. The default isfalse.- Group names are compared case-insensitively, scoped per repository.
A run held by the gate carries the status concurrency_blocked, so the run
list says why nothing is happening. A held job simply stays ready, which
already reads as queued.
The gate runs inside the enqueue transaction, so it holds at every entry point
— push, pull request, schedule, manual POST /api/workflow-runs, and rerun
alike.
Job ordering
Section titled “Job ordering”Within a run, needs: is enforced server-side. A job waits until everything it
depends on has finished, and if a dependency fails, the jobs downstream of it are
skipped transitively rather than run against a broken state.
if: conditions are evaluated at both step and job level. A job-level if:
follows GitHub’s rules, including the two that surprise people: a skipped
dependency does not satisfy success(), so it skips its downstream jobs too;
and a condition naming no status function is implicitly &&-ed with
success(). Name always(), failure() or !cancelled() to opt out of that.
A skipped job shows a green check, not a red one.
Two divergences: cancelling a run cancels always() jobs too, and cancelled()
never evaluates true. See
Job-level if:.
Seeing diagnostics
Section titled “Seeing diagnostics”When a workflow file is indexed, its parse diagnostics are stored alongside it:
unknown keys and unsupported triggers as warning, a whole-file parse failure as
error. A file that fails to parse is still indexed — as an empty definition
that can never run — so it shows up as broken rather than vanishing from the
list.
Read them from the workflows API:
GET /api/repos/{owner}/{repo}/workflowsEach workflow in the response carries a warnings array with a severity and a
message.
The web UI renders these diagnostics only for discovered
.github/workflows/ files, not for your registered workflows. The Actions
page lists registered workflows by path and name alone, so one that parsed with
warnings — a dropped trigger, a misspelled key — looks completely normal there.
Until that gap closes, the API is the only place to see warnings on a workflow
that actually runs, and it is worth checking after any migration.
Watching a run
Section titled “Watching a run”| Purpose | Endpoint |
|---|---|
| List a repo’s runs | GET /api/repos/{owner}/{repo}/workflow-runs |
| Runs for one commit | GET /api/repos/{owner}/{repo}/commits/{sha}/workflow-runs |
| One run | GET /api/workflow-runs/{run_id} |
| Jobs in a run | GET /api/workflow-runs/{run_id}/jobs |
| Job logs | GET /api/jobs/{job_id}/logs |
| Follow logs live | GET /api/jobs/{job_id}/logs/stream |
| Test/lint annotations | GET /api/runs/{run_id}/annotations |
| Cancel a run | POST /api/workflow-runs/{run_id}/cancel |
| Re-run | POST /api/workflow-runs/{run_id}/rerun |
| Cancel one job | POST /api/jobs/{job_id}/cancel |
Full schemas are in the API reference. There is also an Actions view in the web UI covering the run list, the job graph, steps and logs.
- Writing a workflow — the schema in detail.
- The job environment — why jobs declare
packages:instead of an image. - Coming from GitHub Actions — the honest compatibility table.