detached bfdf4d9

feat(ci): plan steps with uses, job containers, services and step timeouts

CI-2 runs what CI-1 refused with "not supported yet". Reusable workflows,
environment and concurrency are still refused (CI-3).
commit
bfdf4d9b9d048789f077bef70c2072b23d5011a5
author
brice · · 2026-10-13 10:34 UTC
parent
cd77819
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +9 -20
M README.md +1 -1
@@ -116,7 +116,7 @@ ermite runs the `.github/workflows/*.yml` workflows of a repository on `push` (a
116116
117117 Runs execute on [ermite-runner](https://git.bnema.dev/bnema/ermite-runner) hosts, which call ermite over its API. Create a token for the runner host with `ermite token create <label> --preset runner` (the `runner:register` scope, which also gives access to the code and the secrets of every repository: give it only to runners you operate), then register the runner with it. `ermite runner list` shows the runners and whether they are online; `ermite runner remove <id>` stops one. A job goes to a runner that has every label of its `runs-on`; a job no runner takes within 24 hours fails.
118118
119-`ermite run list|view|watch|cancel|rerun` and the `Actions` tab of a repository read and control the runs; `cancel` and `rerun` need the `ci:write` scope. With `failing_checks_block` on (the default, `ermite repo edit --failing-checks-block=false`), a merge is refused while a check of the head failed or has not finished. A workflow that uses a feature the runner does not support yet (`uses:` steps, `container:`, `services:`, `environment:`, `concurrency`, `continue-on-error`) fails with `not supported yet: <feature>`, never silently. Logs are files under `$ERMITE_DATA_DIR/ci/logs`.
119+`ermite run list|view|watch|cancel|rerun` and the `Actions` tab of a repository read and control the runs; `cancel` and `rerun` need the `ci:write` scope. With `failing_checks_block` on (the default, `ermite repo edit --failing-checks-block=false`), a merge is refused while a check of the head failed or has not finished. A workflow that uses a feature ermite does not support yet (reusable workflows, `environment:`, `concurrency`) fails with `not supported yet: <feature>`, never silently. Logs are files under `$ERMITE_DATA_DIR/ci/logs`.
120120
121121 ## Web sign-in
122122
@@ -116,7 +116,7 @@ ermite runs the `.github/workflows/*.yml` workflows of a repository on `push` (a
116 116
117 Runs execute on [ermite-runner](https://git.bnema.dev/bnema/ermite-runner) hosts, which call ermite over its API. Create a token for the runner host with `ermite token create <label> --preset runner` (the `runner:register` scope, which also gives access to the code and the secrets of every repository: give it only to runners you operate), then register the runner with it. `ermite runner list` shows the runners and whether they are online; `ermite runner remove <id>` stops one. A job goes to a runner that has every label of its `runs-on`; a job no runner takes within 24 hours fails.117 Runs execute on [ermite-runner](https://git.bnema.dev/bnema/ermite-runner) hosts, which call ermite over its API. Create a token for the runner host with `ermite token create <label> --preset runner` (the `runner:register` scope, which also gives access to the code and the secrets of every repository: give it only to runners you operate), then register the runner with it. `ermite runner list` shows the runners and whether they are online; `ermite runner remove <id>` stops one. A job goes to a runner that has every label of its `runs-on`; a job no runner takes within 24 hours fails.
118 118
119-`ermite run list|view|watch|cancel|rerun` and the `Actions` tab of a repository read and control the runs; `cancel` and `rerun` need the `ci:write` scope. With `failing_checks_block` on (the default, `ermite repo edit --failing-checks-block=false`), a merge is refused while a check of the head failed or has not finished. A workflow that uses a feature the runner does not support yet (`uses:` steps, `container:`, `services:`, `environment:`, `concurrency`, `continue-on-error`) fails with `not supported yet: <feature>`, never silently. Logs are files under `$ERMITE_DATA_DIR/ci/logs`.119+`ermite run list|view|watch|cancel|rerun` and the `Actions` tab of a repository read and control the runs; `cancel` and `rerun` need the `ci:write` scope. With `failing_checks_block` on (the default, `ermite repo edit --failing-checks-block=false`), a merge is refused while a check of the head failed or has not finished. A workflow that uses a feature ermite does not support yet (reusable workflows, `environment:`, `concurrency`) fails with `not supported yet: <feature>`, never silently. Logs are files under `$ERMITE_DATA_DIR/ci/logs`.
120 120
121 ## Web sign-in121 ## Web sign-in
122 122
M internal/app/ci_flow_test.go +3 -2
@@ -324,13 +324,14 @@ func TestCIUnsupportedFeatureFailsTheJob(t *testing.T) {
324324 jobs:
325325 j:
326326 runs-on: ubuntu-latest
327+ environment: production
327328 steps:
328- - uses: actions/checkout@v4
329+ - run: echo
329330 `}, "")
330331 c.waitFor("the run fails", func() bool { return len(c.runs()) == 1 && c.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED })
331332 j := c.getRun(c.runs()[0].GetId()).GetJobs()[0]
332333 assert.Equal(t, pb.CiConclusion_CI_CONCLUSION_FAILURE, j.GetConclusion())
333- assert.Equal(t, "not supported yet: uses: actions/checkout@v4", j.GetError())
334+ assert.Equal(t, "not supported yet: environment", j.GetError())
334335 }
335336
336337 // A job for a label no runner has is "waiting for a runner" and fails after the
@@ -324,13 +324,14 @@ func TestCIUnsupportedFeatureFailsTheJob(t *testing.T) {
324 jobs:324 jobs:
325 j:325 j:
326 runs-on: ubuntu-latest326 runs-on: ubuntu-latest
327+ environment: production
327 steps:328 steps:
328- - uses: actions/checkout@v4329+ - run: echo
329 `}, "")330 `}, "")
330 c.waitFor("the run fails", func() bool { return len(c.runs()) == 1 && c.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED })331 c.waitFor("the run fails", func() bool { return len(c.runs()) == 1 && c.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED })
331 j := c.getRun(c.runs()[0].GetId()).GetJobs()[0]332 j := c.getRun(c.runs()[0].GetId()).GetJobs()[0]
332 assert.Equal(t, pb.CiConclusion_CI_CONCLUSION_FAILURE, j.GetConclusion())333 assert.Equal(t, pb.CiConclusion_CI_CONCLUSION_FAILURE, j.GetConclusion())
333- assert.Equal(t, "not supported yet: uses: actions/checkout@v4", j.GetError())334+ assert.Equal(t, "not supported yet: environment", j.GetError())
334 }335 }
335 336
336 // A job for a label no runner has is "waiting for a runner" and fails after the337 // A job for a label no runner has is "waiting for a runner" and fails after the
M internal/usecase/ci_plan.go +5 -17
@@ -52,32 +52,19 @@ func (w *wfCache) get(runID int64, path, yaml string) (*workflow.Workflow, error
5252 return wf, nil
5353 }
5454
55-// unsupported names the feature of a job that CI-1 does not run, or "". Such
56-// a job fails with "not supported yet: <feature>" instead of running wrong.
57-// Relax it as the runner learns the features (ADR 0025, CI-2 and CI-3).
55+// unsupported names the feature of a job that ermite does not run yet, or "".
56+// Such a job fails with "not supported yet: <feature>" instead of running
57+// wrong. CI-2 runs steps with uses:, job container:, services: and step
58+// timeout-minutes (ADR 0025); what stays is CI-3.
5859 func unsupported(j *workflow.Job) string {
5960 switch {
6061 case j.IsReusable():
6162 return "reusable workflows (uses at the job level)"
62- case j.Container != nil:
63- return "container"
64- case j.Services != nil:
65- return "services"
6663 case j.Environment != nil:
6764 return "environment"
6865 case j.Concurrency != nil:
6966 return "concurrency"
7067 }
71- for _, s := range j.Steps {
72- switch {
73- case s.Uses != nil:
74- return "uses: " + s.Uses.Value
75- case s.TimeoutMinutes != nil:
76- // The runner refuses it too (executor.notSupported): it has no
77- // step timeout yet.
78- return "timeout-minutes on a step"
79- }
80- }
8168 return ""
8269 }
8370
@@ -253,6 +240,7 @@ func (rs *runState) end(j *domain.CIJob, conclusion domain.CIConclusion, msg str
253240 if j.HasToken() {
254241 j.TokenValidUntil = now.Add(domain.CITokenGrace)
255242 }
243+ rs.revokeTokens(j)
256244 rs.touch(j)
257245 rs.c.mu.Lock()
258246 delete(rs.c.heartbeats, jobRef(*j))
@@ -52,32 +52,19 @@ func (w *wfCache) get(runID int64, path, yaml string) (*workflow.Workflow, error
52 return wf, nil52 return wf, nil
53 }53 }
54 54
55-// unsupported names the feature of a job that CI-1 does not run, or "". Such55+// unsupported names the feature of a job that ermite does not run yet, or "".
56-// a job fails with "not supported yet: <feature>" instead of running wrong.56+// Such a job fails with "not supported yet: <feature>" instead of running
57-// Relax it as the runner learns the features (ADR 0025, CI-2 and CI-3).57+// wrong. CI-2 runs steps with uses:, job container:, services: and step
58+// timeout-minutes (ADR 0025); what stays is CI-3.
58 func unsupported(j *workflow.Job) string {59 func unsupported(j *workflow.Job) string {
59 switch {60 switch {
60 case j.IsReusable():61 case j.IsReusable():
61 return "reusable workflows (uses at the job level)"62 return "reusable workflows (uses at the job level)"
62- case j.Container != nil:
63- return "container"
64- case j.Services != nil:
65- return "services"
66 case j.Environment != nil:63 case j.Environment != nil:
67 return "environment"64 return "environment"
68 case j.Concurrency != nil:65 case j.Concurrency != nil:
69 return "concurrency"66 return "concurrency"
70 }67 }
71- for _, s := range j.Steps {
72- switch {
73- case s.Uses != nil:
74- return "uses: " + s.Uses.Value
75- case s.TimeoutMinutes != nil:
76- // The runner refuses it too (executor.notSupported): it has no
77- // step timeout yet.
78- return "timeout-minutes on a step"
79- }
80- }
81 return ""68 return ""
82 }69 }
83 70
@@ -253,6 +240,7 @@ func (rs *runState) end(j *domain.CIJob, conclusion domain.CIConclusion, msg str
253 if j.HasToken() {240 if j.HasToken() {
254 j.TokenValidUntil = now.Add(domain.CITokenGrace)241 j.TokenValidUntil = now.Add(domain.CITokenGrace)
255 }242 }
243+ rs.revokeTokens(j)
256 rs.touch(j)244 rs.touch(j)
257 rs.c.mu.Lock()245 rs.c.mu.Lock()
258 delete(rs.c.heartbeats, jobRef(*j))246 delete(rs.c.heartbeats, jobRef(*j))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close