detached 0039612

feat(ci): disable, enable and list workflows per repository

commit
00396122d39c11f2d7ecd211a94f823937ff76c0
author
brice · · 2026-10-18 19:44 UTC
parent
dfd6f0b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
tag: v0.27.0
40 files · +2840 -966
M docs/ci-events.md +1 -1
@@ -39,7 +39,7 @@ Its cache is read-only and restores only default-branch entries.
3939 ermite workflow run <file> --repo <repo> [--ref <branch|tag>] [-f name=value …]
4040 ```
4141
42-- Needs `ci:write`. `<file>` is a path or a name under `.github/workflows`. `--ref` defaults to the default branch.
42+- Needs `ci:write`. `<file>` is a path or a name under `.github/workflows`. `--ref` defaults to the default branch. A disabled workflow (`ermite workflow disable`) is refused with `failed_precondition`.
4343 - Inputs are checked against the workflow (`required`, `boolean`, `number`, `choice`, `environment`). Up to 25 inputs.
4444 - A branch-limited token can dispatch only on its branches, and never gets a trusted run.
4545 - A job can dispatch with its own token when its `permissions:` give `actions: write`.
M docs/ci.md +16 -0
@@ -28,6 +28,22 @@ ermite run cancel <repo> <id> # needs ci:write
2828 ermite run rerun <repo> <id> [--failed]
2929 ```
3030
31+### Disable a workflow
32+
33+```sh
34+ermite workflow list <repo> [--json]
35+ermite workflow disable <repo> <file> # needs repo:admin
36+ermite workflow enable <repo> <file>
37+```
38+
39+Like `gh workflow disable`: a disabled workflow starts no run for any event (`push`, `pull_request`, `pull_request_target`, `schedule`, `workflow_run`, `create`, `delete`, `workflow_dispatch`). Runs already started go on, and events missed while it was disabled are not replayed.
40+
41+- `<file>` is `release.yml` or `.github/workflows/release.yml`. The state belongs to the repository and the path, not to a branch, and the file need not exist: disable a release workflow right after `ermite repo create`, before the first push, so pushing tags starts nothing. The files stay identical to their GitHub copies.
42+- `list` shows the files of the default branch with their name and state, and the disabled files that branch lacks. Needs `repo:read` for a private repository.
43+- The Actions tab names the disabled workflows, and the page of a run says when its workflow is disabled.
44+- A disabled **reusable** workflow (`on: workflow_call`) can still be called with `uses:` from an enabled workflow: it then runs inside the caller's run, not as a run of its own, as on GitHub. Disable the caller to stop it.
45+- The state is checked again in the transaction that stores a run, so a disable takes effect for every run not yet stored, even one being planned at that moment.
46+
3147 The **Actions** tab of a repository shows the same. With `failing_checks_block` on (default), a merge waits for every check to pass. Turn it off with `ermite repo edit <repo> --failing-checks-block=false`.
3248
3349 ## Trust
M docs/cli.md +3 -0
@@ -63,6 +63,9 @@ ermite run view <repo> <id> [--attempt n] [--log [--job …]] [--summary]
6363 ermite run watch <repo> <id> [--log] | cancel <repo> <id> | rerun <repo> <id> [--failed]
6464 ermite run artifacts <repo> <id> | download <repo> <id> [--name N]… [--dir D]
6565 ermite workflow run <file> --repo <repo> [--ref …] [-f name=value …]
66+ermite workflow list <repo> [--json]
67+ermite workflow disable <repo> <file> # no event starts a run of it; the file need not exist yet
68+ermite workflow enable <repo> <file>
6669 ermite runner list | remove <id>
6770 ermite secret set|list|delete … # CI secrets (needs ERMITE_SECRETS_KEY)
6871 ermite variable set|get|list|delete …
M gen/ermite/storage/ci.pb.go +89 -37 generated file: collapsed
M go.mod +1 -1
@@ -13,7 +13,7 @@ tool (
1313
1414 require (
1515 bnema.dev/ermite-actions v0.2.0
16- bnema.dev/ermite-api v0.6.0
16+ bnema.dev/ermite-api v0.7.0
1717 bnema.dev/ermite-git v0.1.0
1818 bnema.dev/ermite-kv v0.1.0
1919 connectrpc.com/connect v1.21.0
M go.sum +2 -2 generated file: collapsed
M internal/adapters/client/client_test.go +3 -3
@@ -139,16 +139,16 @@ func TestDescribeBoundsLongMessages(t *testing.T) {
139139 func TestMissingFeatures(t *testing.T) {
140140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))
141141 assert.Equal(t,
142- []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIArtifacts, client.FeatureCommitSignatures},
142+ []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures},
143143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))
144144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{
145145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSession,
146- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch,
146+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,
147147 client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures,
148148 }))
149149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{
150150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSigningKey, client.FeatureSession,
151- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIArtifacts, client.FeatureCommitSignatures,
151+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures,
152152 }))
153153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))
154154 }
M internal/adapters/client/features.go +4 -1
@@ -42,6 +42,9 @@ const (
4242 FeatureCISecrets = "ci.secrets"
4343 // FeatureCIDispatch is RunService.DispatchWorkflow (`ermite workflow run`).
4444 FeatureCIDispatch = "ci.dispatch"
45+ // FeatureCIWorkflows is RunService.ListWorkflows, DisableWorkflow and
46+ // EnableWorkflow (`ermite workflow list|disable|enable`).
47+ FeatureCIWorkflows = "ci.workflows"
4548 // FeatureCIArtifacts is ArtifactService (`ermite run artifacts`,
4649 // `run download`).
4750 FeatureCIArtifacts = "ci.artifacts"
@@ -52,7 +55,7 @@ const (
5255
5356 // UsedFeatures lists the features the CLI uses, in the order they are reported.
5457 func UsedFeatures() []string {
55- return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}
58+ return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}
5659 }
5760
5861 // MissingFeatures returns the features the CLI uses that have is lacking.
M internal/adapters/kvstore/ci.go +9 -1
@@ -580,7 +580,14 @@ const createRunTries = 20
580580 //
581581 //hexcheck:ignore suspicious-business-logic-in-adapter takes the next numbers under version preconditions: a storage concern, no policy
582582 func (c *CI) CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) ports.CIRunDraft) (ports.CIRunDraft, error) {
583+ offKey := workflowOffKey(repo, workflowPath)
583584 for range createRunTries {
585+ // A workflow the repository disabled gets no run, whatever started it.
586+ // The key is also required absent by the batch below, so a disable that
587+ // lands after this read fails the batch, and the next try sees it.
588+ if _, off := c.db.Get(ctx, offKey); off {
589+ return ports.CIRunDraft{}, domain.ErrWorkflowDisabled
590+ }
584591 lastID, idItem, idOK, err := c.readSeq(ctx, []byte(runSeqKey))
585592 if err != nil {
586593 return ports.CIRunDraft{}, err
@@ -605,6 +612,7 @@ func (c *CI) CreateRun(ctx context.Context, repo domain.RepoName, workflowPath s
605612 })
606613 b.RequireAbsent(runSrcKey(draft.Run.ID))
607614 b.Put(runSrcKey(draft.Run.ID), src)
615+ b.RequireAbsent(offKey)
608616 for r := range guarded {
609617 if err := c.guardRepo(ctx, &b, r); err != nil {
610618 return ports.CIRunDraft{}, err
@@ -887,7 +895,7 @@ func ciKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]by
887895 }
888896 keys = append(keys, repoReaderKeys(ctx, snap, repo)...)
889897 keys = append(keys, artifactKeys(ctx, snap, repo)...)
890- for _, p := range []string{checkPrefix + repo.String() + "/", runNumPrefix + repo.String() + "/", concurPrefix + repo.String() + "/", concurMarkPrefixAll + repo.String() + "/", refTipsPrefix(repo)} {
898+ for _, p := range []string{checkPrefix + repo.String() + "/", runNumPrefix + repo.String() + "/", concurPrefix + repo.String() + "/", concurMarkPrefixAll + repo.String() + "/", refTipsPrefix(repo), workflowOffPrefixOf(repo)} {
891899 for k := range snap.Prefix(ctx, []byte(p)) {
892900 keys = append(keys, slices.Clone(k))
893901 }
A internal/adapters/kvstore/ci_workflow.go +91 -0
@@ -0,0 +1,91 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package kvstore
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "sort"
10+ "strings"
11+ "time"
12+
13+ kv "bnema.dev/ermite-kv"
14+ "google.golang.org/protobuf/proto"
15+ "google.golang.org/protobuf/types/known/timestamppb"
16+
17+ storagepb "bnema.dev/ermite/gen/ermite/storage"
18+ "bnema.dev/ermite/internal/domain"
19+ "bnema.dev/ermite/internal/ports"
20+)
21+
22+// The workflows a repository disabled (`ermite workflow disable`):
23+//
24+// ciwfoff/<repo>/<workflow path> CIWorkflowDisabled
25+//
26+// <workflow path> is ".github/workflows/<file>", whether or not the file exists.
27+// Written under the guard of the repository key, deleted with the repository
28+// (ciKeys).
29+const workflowOffPrefix = "ciwfoff/"
30+
31+var _ ports.CIWorkflowStore = (*CI)(nil)
32+
33+func workflowOffKey(repo domain.RepoName, path string) []byte {
34+ return []byte(workflowOffPrefix + repo.String() + "/" + path)
35+}
36+
37+func workflowOffPrefixOf(repo domain.RepoName) string { return workflowOffPrefix + repo.String() + "/" }
38+
39+// DisableWorkflow records the path as disabled. One already disabled keeps its
40+// date. A repository that does not exist is domain.ErrNotFound.
41+func (c *CI) DisableWorkflow(ctx context.Context, repo domain.RepoName, path string, at time.Time) error {
42+ val, err := proto.Marshal(&storagepb.CIWorkflowDisabled{DisabledAt: timestamppb.New(at)})
43+ if err != nil {
44+ return fmt.Errorf("encode disabled workflow: %w", err)
45+ }
46+ key := workflowOffKey(repo, path)
47+ for range updateAttemptsTip {
48+ if _, ok := c.db.Get(ctx, key); ok {
49+ if _, ok := c.db.Get(ctx, repoKey(repo)); !ok {
50+ return domain.ErrNotFound
51+ }
52+ return nil
53+ }
54+ var b kv.Batch
55+ if err := c.guardRepo(ctx, &b, repo); err != nil {
56+ return err
57+ }
58+ b.RequireAbsent(key)
59+ b.Put(key, val)
60+ _, err := c.db.Apply(ctx, &b)
61+ if errors.Is(err, kv.ErrConflict) {
62+ continue
63+ }
64+ if err != nil {
65+ return fmt.Errorf("store disabled workflow: %w", err)
66+ }
67+ return nil
68+ }
69+ return fmt.Errorf("store disabled workflow: %w", domain.ErrConflict)
70+}
71+
72+// EnableWorkflow removes the record of the path.
73+func (c *CI) EnableWorkflow(ctx context.Context, repo domain.RepoName, path string) error {
74+ var b kv.Batch
75+ b.Delete(workflowOffKey(repo, path))
76+ if _, err := c.db.Apply(ctx, &b); err != nil {
77+ return fmt.Errorf("delete disabled workflow: %w", err)
78+ }
79+ return nil
80+}
81+
82+// DisabledWorkflows returns the disabled paths of the repository, sorted.
83+func (c *CI) DisabledWorkflows(ctx context.Context, repo domain.RepoName) ([]string, error) {
84+ prefix := workflowOffPrefixOf(repo)
85+ var out []string
86+ for k := range c.db.Snapshot(ctx).Prefix(ctx, []byte(prefix)) {
87+ out = append(out, strings.TrimPrefix(string(k), prefix))
88+ }
89+ sort.Strings(out)
90+ return out, nil
91+}
A internal/adapters/kvstore/ci_workflow_test.go +92 -0
@@ -0,0 +1,92 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package kvstore_test
4+
5+import (
6+ "context"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
11+
12+ "bnema.dev/ermite/internal/domain"
13+ "bnema.dev/ermite/internal/ports"
14+)
15+
16+const wfRelease = ".github/workflows/release.yml"
17+
18+func TestCIWorkflowDisabledState(t *testing.T) {
19+ ctx := context.Background()
20+ s := openStore(t, t.TempDir())
21+ ci := s.CI()
22+ a, b := name(t, "a"), name(t, "b")
23+ for _, n := range []domain.RepoName{a, b} {
24+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
25+ }
26+
27+ got, err := ci.DisabledWorkflows(ctx, a)
28+ require.NoError(t, err)
29+ assert.Empty(t, got)
30+
31+ require.NoError(t, ci.DisableWorkflow(ctx, a, wfRelease, created))
32+ require.NoError(t, ci.DisableWorkflow(ctx, a, wfRelease, created), "idempotent")
33+ require.NoError(t, ci.DisableWorkflow(ctx, a, ".github/workflows/aur.yml", created))
34+ got, err = ci.DisabledWorkflows(ctx, a)
35+ require.NoError(t, err)
36+ assert.Equal(t, []string{".github/workflows/aur.yml", wfRelease}, got)
37+ got, _ = ci.DisabledWorkflows(ctx, b)
38+ assert.Empty(t, got, "per repository")
39+
40+ require.NoError(t, ci.EnableWorkflow(ctx, a, wfRelease))
41+ require.NoError(t, ci.EnableWorkflow(ctx, a, wfRelease), "idempotent")
42+ got, _ = ci.DisabledWorkflows(ctx, a)
43+ assert.Equal(t, []string{".github/workflows/aur.yml"}, got)
44+
45+ assert.ErrorIs(t, ci.DisableWorkflow(ctx, name(t, "gone"), wfRelease, created), domain.ErrNotFound)
46+
47+ // The state goes with the repository: a new one under the same name starts enabled.
48+ require.NoError(t, s.Delete(ctx, a))
49+ require.NoError(t, s.Create(ctx, domain.Repo{Name: a, Visibility: domain.VisibilityPrivate, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
50+ got, _ = ci.DisabledWorkflows(ctx, a)
51+ assert.Empty(t, got)
52+}
53+
54+// A run is refused for a disabled workflow in the very transaction that stores
55+// it: a disable that lands between the read of the state and the write of the
56+// run (here, from inside build) cannot be overtaken, and no run is left.
57+func TestCreateRunRefusesAWorkflowDisabledMeanwhile(t *testing.T) {
58+ ctx := context.Background()
59+ s := openStore(t, t.TempDir())
60+ ci := s.CI()
61+ n := name(t, "a")
62+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
63+ draft := func(id, number int64) ports.CIRunDraft {
64+ return ports.CIRunDraft{Run: domain.CIRun{Repo: n, ID: id, Number: number, Attempt: 1, WorkflowPath: wfRelease, Status: domain.CIQueued, CreatedAt: created}}
65+ }
66+
67+ // Enabled: a run is created.
68+ _, err := ci.CreateRun(ctx, n, wfRelease, draft)
69+ require.NoError(t, err)
70+
71+ // Disabled from inside build, after the state was read.
72+ calls := 0
73+ _, err = ci.CreateRun(ctx, n, wfRelease, func(id, number int64) ports.CIRunDraft {
74+ calls++
75+ if calls == 1 {
76+ require.NoError(t, ci.DisableWorkflow(ctx, n, wfRelease, created))
77+ }
78+ return draft(id, number)
79+ })
80+ assert.ErrorIs(t, err, domain.ErrWorkflowDisabled)
81+ assert.Equal(t, 1, calls, "refused on the retry, before build")
82+ runs, err := ci.ListRuns(ctx, n, 0, 10)
83+ require.NoError(t, err)
84+ assert.Len(t, runs, 1, "no run was stored by the refused call")
85+
86+ // Other workflows are not affected, and enabling restores it.
87+ _, err = ci.CreateRun(ctx, n, ".github/workflows/other.yml", draft)
88+ require.NoError(t, err)
89+ require.NoError(t, ci.EnableWorkflow(ctx, n, wfRelease))
90+ _, err = ci.CreateRun(ctx, n, wfRelease, draft)
91+ require.NoError(t, err)
92+}
M internal/adapters/rpc/api.go +1 -1
@@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10
3535 // always implements, as GetVersion lists them. FeaturePasskeys comes on top
3636 // when Config.Passkeys is set, and FeatureCISecrets when the settings services
3737 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.
38-var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache"}
38+var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows"}
3939
4040 // FeaturePasskeys is listed by GetVersion when passkeys are available.
4141 // FeatureCIArtifacts (artifact.go) when the artifact services are served.
M internal/adapters/rpc/ci.go +48 -0
@@ -334,6 +334,54 @@ func (s *runService) DispatchWorkflow(ctx context.Context, req *connect.Request[
334334 return connect.NewResponse(&pb.DispatchWorkflowResponse{Run: runToPB(run)}), nil
335335 }
336336
337+func workflowToPB(w ports.WorkflowInfo) *pb.Workflow {
338+ state := pb.WorkflowState_WORKFLOW_STATE_ENABLED
339+ if w.Disabled {
340+ state = pb.WorkflowState_WORKFLOW_STATE_DISABLED
341+ }
342+ return &pb.Workflow{Path: w.Path, Name: w.Name, State: state}
343+}
344+
345+func (s *runService) ListWorkflows(ctx context.Context, req *connect.Request[pb.ListWorkflowsRequest]) (*connect.Response[pb.ListWorkflowsResponse], error) {
346+ name, err := repoOf(req.Msg.GetRepo())
347+ if err != nil {
348+ return nil, ciError(ctx, err)
349+ }
350+ list, err := s.runs.ListWorkflows(ctx, caller(ctx), name)
351+ if err != nil {
352+ return nil, ciError(ctx, err)
353+ }
354+ out := &pb.ListWorkflowsResponse{Workflows: make([]*pb.Workflow, 0, len(list))}
355+ for _, w := range list {
356+ out.Workflows = append(out.Workflows, workflowToPB(w))
357+ }
358+ return connect.NewResponse(out), nil
359+}
360+
361+func (s *runService) DisableWorkflow(ctx context.Context, req *connect.Request[pb.DisableWorkflowRequest]) (*connect.Response[pb.DisableWorkflowResponse], error) {
362+ name, err := repoOf(req.Msg.GetRepo())
363+ if err != nil {
364+ return nil, ciError(ctx, err)
365+ }
366+ w, err := s.runs.DisableWorkflow(ctx, caller(ctx), name, req.Msg.GetWorkflow())
367+ if err != nil {
368+ return nil, ciError(ctx, err)
369+ }
370+ return connect.NewResponse(&pb.DisableWorkflowResponse{Workflow: workflowToPB(w)}), nil
371+}
372+
373+func (s *runService) EnableWorkflow(ctx context.Context, req *connect.Request[pb.EnableWorkflowRequest]) (*connect.Response[pb.EnableWorkflowResponse], error) {
374+ name, err := repoOf(req.Msg.GetRepo())
375+ if err != nil {
376+ return nil, ciError(ctx, err)
377+ }
378+ w, err := s.runs.EnableWorkflow(ctx, caller(ctx), name, req.Msg.GetWorkflow())
379+ if err != nil {
380+ return nil, ciError(ctx, err)
381+ }
382+ return connect.NewResponse(&pb.EnableWorkflowResponse{Workflow: workflowToPB(w)}), nil
383+}
384+
337385 func (s *runService) ListChecks(ctx context.Context, req *connect.Request[pb.ListChecksRequest]) (*connect.Response[pb.ListChecksResponse], error) {
338386 name, err := repoOf(req.Msg.GetRepo())
339387 if err != nil {
M internal/adapters/web/actions.templ +10 -0
@@ -12,6 +12,13 @@ templ CheckIcon(kind, state string) {
1212
1313 templ Actions(l Layout, p ActionsPage) {
1414 @Shell(l, nil) {
15+ if len(p.Disabled) > 0 {
16+ <p class="dim wf-off">disabled workflows (they start no run):
17+ for _, f := range p.Disabled {
18+ <span class="pill">{ f }</span>{ " " }
19+ }
20+ </p>
21+ }
1522 if len(p.Runs) == 0 {
1623 <p class="dim">no runs yet: a push that matches a workflow of .github/workflows starts one.</p>
1724 } else {
@@ -83,6 +90,9 @@ templ RecentActionsBlock(a RecentActions) {
8390 templ RunDetail(l Layout, p RunPage) {
8491 @Shell(l, runRail(p)) {
8592 @LiveSlot("run-notice")
93+ if p.WorkflowDisabled {
94+ <p class="dim wf-off"><span class="pill">disabled</span> this workflow is disabled: no event starts a run of it (<code>ermite workflow enable</code>).</p>
95+ }
8696 <div id="run-body">
8797 @RunBody(p)
8898 </div>
M internal/adapters/web/actions_artifacts_test.go +1 -0
@@ -108,6 +108,7 @@ func artifactsServer(t *testing.T, arts *fakeArtifacts) *passkeyFixture {
108108 Jobs: []ports.JobView{job}, LatestAttempt: 1,
109109 }, nil).Maybe()
110110 runs.EXPECT().ListAnnotations(mock.Anything, mock.Anything, repo, int64(1), int32(1), "").Return(ports.AnnotationList{}, nil).Maybe()
111+ runs.EXPECT().DisabledWorkflows(mock.Anything, mock.Anything, repo).Return(nil, nil).Maybe()
111112 runs.EXPECT().GetStepSummaries(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, mock.Anything).Return(ports.SummaryList{}, nil).Maybe()
112113 browse := mocks.NewBrowseService(t)
113114 browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{}, nil).Maybe()
M internal/adapters/web/actions_pages.go +19 -0
@@ -6,6 +6,7 @@ import (
66 "context"
77 "fmt"
88 "net/http"
9+ "slices"
910 "strconv"
1011 "strings"
1112 "time"
@@ -143,6 +144,9 @@ type ActionsPage struct {
143144 More string
144145 // NoRunner says that CI has no runner to run on yet.
145146 NoRunner bool
147+ // Disabled are the workflow files that start no run (`ermite workflow
148+ // disable`), as paths.
149+ Disabled []string
146150 }
147151
148152 // StepView is a step of a job.
@@ -193,6 +197,9 @@ type RunPage struct {
193197 Active bool
194198 // detail is what the page was built from.
195199 detail ports.RunDetail
200+ // WorkflowDisabled: the workflow file of the run is disabled now, so a new
201+ // event starts no run of it.
202+ WorkflowDisabled bool
196203 // Forms are the owner's actions; nil/empty when the visitor cannot.
197204 CancelAction, RerunAction, RerunFailedAction string
198205 CanRerunFailed bool
@@ -223,6 +230,7 @@ func (s *Server) actions(ctx context.Context, r *http.Request) (templ.Component,
223230 if page.NextPageToken != "" {
224231 p.More = actionsBase(d.name()) + "?page=" + page.NextPageToken
225232 }
233+ p.Disabled = s.disabledWorkflows(ctx, d)
226234 l := s.repoLayout(ctx, d, "actions")
227235 l.Title = "actions · " + d.name() + " · " + s.instance
228236 l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=actions")
@@ -231,6 +239,16 @@ func (s *Server) actions(ctx context.Context, r *http.Request) (templ.Component,
231239 return Actions(l, p), http.StatusOK, nil
232240 }
233241
242+// disabledWorkflows are the disabled workflow paths of the repository; the
243+// pages show them as a note, so a failure to read them is not one of the page.
244+func (s *Server) disabledWorkflows(ctx context.Context, d *repoData) []string {
245+ off, err := s.cfg.Runs.DisabledWorkflows(ctx, principal(ctx), d.repo.Name)
246+ if err != nil {
247+ return nil
248+ }
249+ return off
250+}
251+
234252 func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow {
235253 row := RunRow{
236254 ID: strconv.FormatInt(run.ID, 10), Href: runHref(repo, run.ID),
@@ -318,6 +336,7 @@ func (s *Server) run(ctx context.Context, r *http.Request) (templ.Component, int
318336 return nil, 0, err
319337 }
320338 p.Artifacts = s.runArtifacts(ctx, d.repo.Name, p)
339+ p.WorkflowDisabled = slices.Contains(s.disabledWorkflows(ctx, d), p.detail.Run.WorkflowPath)
321340 l := s.repoLayout(ctx, d, "actions")
322341 l.Title = "run " + p.ID + " · " + d.name() + " · " + s.instance
323342 l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=run&n="+p.ID)
M internal/adapters/web/actions_results_test.go +33 -0
@@ -43,6 +43,7 @@ func TestSummariesAreRenderedSafely(t *testing.T) {
4343 }, "\n")
4444
4545 runs := mocks.NewRunService(t)
46+ runs.EXPECT().DisabledWorkflows(mock.Anything, mock.Anything, repo).Return(nil, nil).Maybe()
4647 runs.EXPECT().GetRun(mock.Anything, mock.Anything, repo, int64(1), int32(0)).Return(ports.RunDetail{
4748 Run: domain.CIRun{Repo: repo, ID: 1, Attempt: 1, Number: 1, WorkflowName: "ci", Sha: strings.Repeat("a", 40), Status: domain.CICompleted, Conclusion: domain.CISuccess},
4849 Jobs: []ports.JobView{job}, LatestAttempt: 1,
@@ -143,6 +144,7 @@ func runPageServer(t *testing.T, repo domain.RepoName, jobs []ports.JobView, run
143144 Run: domain.CIRun{Repo: repo, ID: 1, Attempt: 1, Number: 1, WorkflowName: "ci", Sha: strings.Repeat("a", 40), Status: domain.CICompleted, Conclusion: domain.CISuccess},
144145 Jobs: jobs, LatestAttempt: 1,
145146 }, nil)
147+ runs.EXPECT().DisabledWorkflows(mock.Anything, mock.Anything, repo).Return(nil, nil).Maybe()
146148 runs.EXPECT().ListAnnotations(mock.Anything, mock.Anything, repo, int64(1), int32(1), "").Return(ports.AnnotationList{}, nil)
147149 browse := mocks.NewBrowseService(t)
148150 browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{}, nil).Maybe()
@@ -235,3 +237,34 @@ func TestJobStateDoesNotReadTheRun(t *testing.T) {
235237 require.NoError(t, err)
236238 assert.Equal(t, gap, frags)
237239 }
240+
241+// A disabled workflow is said on the list of runs and on the page of its runs
242+// (`ermite workflow disable`).
243+func TestRunPagesSayWhenAWorkflowIsDisabled(t *testing.T) {
244+ t.Parallel()
245+ repo := repoName(t, "proj")
246+ const path = ".github/workflows/release.yml"
247+ runs := mocks.NewRunService(t)
248+ runs.EXPECT().DisabledWorkflows(mock.Anything, mock.Anything, repo).Return([]string{path}, nil)
249+ runs.EXPECT().ListRuns(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RunPage{}, nil)
250+ runs.EXPECT().GetRun(mock.Anything, mock.Anything, repo, int64(1), int32(0)).Return(ports.RunDetail{
251+ Run: domain.CIRun{Repo: repo, ID: 1, Attempt: 1, Number: 1, WorkflowName: "release", WorkflowPath: path, Sha: strings.Repeat("a", 40), Status: domain.CICompleted, Conclusion: domain.CISuccess},
252+ LatestAttempt: 1,
253+ }, nil)
254+ runs.EXPECT().ListAnnotations(mock.Anything, mock.Anything, repo, int64(1), int32(1), "").Return(ports.AnnotationList{}, nil)
255+ browse := mocks.NewBrowseService(t)
256+ browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{}, nil).Maybe()
257+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse, Runs: runs})
258+ get := func(target string) string {
259+ rec := httptest.NewRecorder()
260+ srv.ServeHTTP(rec, httptest.NewRequestWithContext(context.Background(), http.MethodGet, target, nil))
261+ require.Equal(t, http.StatusOK, rec.Code)
262+ return rec.Body.String()
263+ }
264+ list := get("/proj/actions")
265+ assert.Contains(t, list, "disabled workflows")
266+ assert.Contains(t, list, path)
267+ run := get("/proj/actions/runs/1")
268+ assert.Contains(t, run, "this workflow is disabled")
269+ assert.NotContains(t, run, "onclick", "no inline script")
270+}
M internal/adapters/web/actions_templ.go +932 -880
@@ -120,13 +120,55 @@ func Actions(l Layout, p ActionsPage) templ.Component {
120120 }()
121121 }
122122 ctx = templ.InitializeContext(ctx)
123+ if len(p.Disabled) > 0 {
124+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"dim wf-off\">disabled workflows (they start no run): ")
125+ if templ_7745c5c3_Err != nil {
126+ return templ_7745c5c3_Err
127+ }
128+ for _, f := range p.Disabled {
129+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"pill\">")
130+ if templ_7745c5c3_Err != nil {
131+ return templ_7745c5c3_Err
132+ }
133+ var templ_7745c5c3_Var8 string
134+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(f)
135+ if templ_7745c5c3_Err != nil {
136+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 18, Col: 27}
137+ }
138+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
139+ if templ_7745c5c3_Err != nil {
140+ return templ_7745c5c3_Err
141+ }
142+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span>")
143+ if templ_7745c5c3_Err != nil {
144+ return templ_7745c5c3_Err
145+ }
146+ var templ_7745c5c3_Var9 string
147+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
148+ if templ_7745c5c3_Err != nil {
149+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 18, Col: 41}
150+ }
151+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
152+ if templ_7745c5c3_Err != nil {
153+ return templ_7745c5c3_Err
154+ }
155+ }
156+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>")
157+ if templ_7745c5c3_Err != nil {
158+ return templ_7745c5c3_Err
159+ }
160+ }
161+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ")
162+ if templ_7745c5c3_Err != nil {
163+ return templ_7745c5c3_Err
164+ }
123165 if len(p.Runs) == 0 {
124- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"dim\">no runs yet: a push that matches a workflow of .github/workflows starts one.</p>")
166+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"dim\">no runs yet: a push that matches a workflow of .github/workflows starts one.</p>")
125167 if templ_7745c5c3_Err != nil {
126168 return templ_7745c5c3_Err
127169 }
128170 } else {
129- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"list runs\" aria-label=\"Runs\"><thead><tr><th class=\"c-st\"></th><th class=\"c-name\">workflow</th><th class=\"c-sub\">commit</th><th class=\"c-ref\">ref</th><th class=\"c-who\">actor</th><th class=\"c-dur\">time</th><th class=\"c-when\">created</th></tr></thead> <tbody id=\"run-rows\" data-rows>")
171+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<table class=\"list runs\" aria-label=\"Runs\"><thead><tr><th class=\"c-st\"></th><th class=\"c-name\">workflow</th><th class=\"c-sub\">commit</th><th class=\"c-ref\">ref</th><th class=\"c-who\">actor</th><th class=\"c-dur\">time</th><th class=\"c-when\">created</th></tr></thead> <tbody id=\"run-rows\" data-rows>")
130172 if templ_7745c5c3_Err != nil {
131173 return templ_7745c5c3_Err
132174 }
@@ -136,30 +178,30 @@ func Actions(l Layout, p ActionsPage) templ.Component {
136178 return templ_7745c5c3_Err
137179 }
138180 }
139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</tbody></table>")
181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table>")
140182 if templ_7745c5c3_Err != nil {
141183 return templ_7745c5c3_Err
142184 }
143185 }
144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
186+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
145187 if templ_7745c5c3_Err != nil {
146188 return templ_7745c5c3_Err
147189 }
148190 if p.More != "" {
149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"more\"><a href=\"")
191+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"more\"><a href=\"")
150192 if templ_7745c5c3_Err != nil {
151193 return templ_7745c5c3_Err
152194 }
153- var templ_7745c5c3_Var8 templ.SafeURL
154- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.More))
195+ var templ_7745c5c3_Var10 templ.SafeURL
196+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.More))
155197 if templ_7745c5c3_Err != nil {
156- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 30, Col: 50}
198+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 37, Col: 50}
157199 }
158- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
200+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
159201 if templ_7745c5c3_Err != nil {
160202 return templ_7745c5c3_Err
161203 }
162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" rel=\"next\">older runs</a></p>")
204+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" rel=\"next\">older runs</a></p>")
163205 if templ_7745c5c3_Err != nil {
164206 return templ_7745c5c3_Err
165207 }
@@ -192,25 +234,25 @@ func RunListRow(r RunRow) templ.Component {
192234 }()
193235 }
194236 ctx = templ.InitializeContext(ctx)
195- templ_7745c5c3_Var9 := templ.GetChildren(ctx)
196- if templ_7745c5c3_Var9 == nil {
197- templ_7745c5c3_Var9 = templ.NopComponent
237+ templ_7745c5c3_Var11 := templ.GetChildren(ctx)
238+ if templ_7745c5c3_Var11 == nil {
239+ templ_7745c5c3_Var11 = templ.NopComponent
198240 }
199241 ctx = templ.ClearChildren(ctx)
200- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<tr id=\"")
242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<tr id=\"")
201243 if templ_7745c5c3_Err != nil {
202244 return templ_7745c5c3_Err
203245 }
204- var templ_7745c5c3_Var10 string
205- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("run-" + r.ID)
246+ var templ_7745c5c3_Var12 string
247+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue("run-" + r.ID)
206248 if templ_7745c5c3_Err != nil {
207- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 38, Col: 23}
249+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 45, Col: 23}
208250 }
209- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
251+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
210252 if templ_7745c5c3_Err != nil {
211253 return templ_7745c5c3_Err
212254 }
213- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><td class=\"c-st\">")
255+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"><td class=\"c-st\">")
214256 if templ_7745c5c3_Err != nil {
215257 return templ_7745c5c3_Err
216258 }
@@ -218,246 +260,246 @@ func RunListRow(r RunRow) templ.Component {
218260 if templ_7745c5c3_Err != nil {
219261 return templ_7745c5c3_Err
220262 }
221- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td><td class=\"c-name\"><a href=\"")
263+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td class=\"c-name\"><a href=\"")
222264 if templ_7745c5c3_Err != nil {
223265 return templ_7745c5c3_Err
224266 }
225- var templ_7745c5c3_Var11 templ.SafeURL
226- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
267+ var templ_7745c5c3_Var13 templ.SafeURL
268+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
227269 if templ_7745c5c3_Err != nil {
228- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 41, Col: 34}
270+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 48, Col: 34}
229271 }
230- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
272+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
231273 if templ_7745c5c3_Err != nil {
232274 return templ_7745c5c3_Err
233275 }
234- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
235277 if templ_7745c5c3_Err != nil {
236278 return templ_7745c5c3_Err
237279 }
238- var templ_7745c5c3_Var12 string
239- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
280+ var templ_7745c5c3_Var14 string
281+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
240282 if templ_7745c5c3_Err != nil {
241- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 41, Col: 49}
283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 48, Col: 49}
242284 }
243- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
285+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
244286 if templ_7745c5c3_Err != nil {
245287 return templ_7745c5c3_Err
246288 }
247- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a> <span class=\"mute\">")
289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a> <span class=\"mute\">")
248290 if templ_7745c5c3_Err != nil {
249291 return templ_7745c5c3_Err
250292 }
251- var templ_7745c5c3_Var13 string
252- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
293+ var templ_7745c5c3_Var15 string
294+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
253295 if templ_7745c5c3_Err != nil {
254- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 42, Col: 32}
296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 49, Col: 32}
255297 }
256- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
298+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
257299 if templ_7745c5c3_Err != nil {
258300 return templ_7745c5c3_Err
259301 }
260- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> ")
302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> ")
261303 if templ_7745c5c3_Err != nil {
262304 return templ_7745c5c3_Err
263305 }
264306 if r.Event != "" {
265- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"pill\">")
307+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"pill\">")
266308 if templ_7745c5c3_Err != nil {
267309 return templ_7745c5c3_Err
268310 }
269- var templ_7745c5c3_Var14 string
270- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Event)
311+ var templ_7745c5c3_Var16 string
312+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(r.Event)
271313 if templ_7745c5c3_Err != nil {
272- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 44, Col: 32}
314+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 51, Col: 32}
273315 }
274- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
316+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
275317 if templ_7745c5c3_Err != nil {
276318 return templ_7745c5c3_Err
277319 }
278- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span> ")
320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span> ")
279321 if templ_7745c5c3_Err != nil {
280322 return templ_7745c5c3_Err
281323 }
282324 }
283325 if r.Attempt > 1 {
284- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"pill\">attempt ")
326+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span class=\"pill\">attempt ")
285327 if templ_7745c5c3_Err != nil {
286328 return templ_7745c5c3_Err
287329 }
288- var templ_7745c5c3_Var15 string
289- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(r.Attempt)))
330+ var templ_7745c5c3_Var17 string
331+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(r.Attempt)))
290332 if templ_7745c5c3_Err != nil {
291- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 47, Col: 53}
333+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 54, Col: 53}
292334 }
293- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
335+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
294336 if templ_7745c5c3_Err != nil {
295337 return templ_7745c5c3_Err
296338 }
297- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span>")
339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span>")
298340 if templ_7745c5c3_Err != nil {
299341 return templ_7745c5c3_Err
300342 }
301343 }
302- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td class=\"c-sub\"><a href=\"")
344+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td class=\"c-sub\"><a href=\"")
303345 if templ_7745c5c3_Err != nil {
304346 return templ_7745c5c3_Err
305347 }
306- var templ_7745c5c3_Var16 templ.SafeURL
307- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
348+ var templ_7745c5c3_Var18 templ.SafeURL
349+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.CHref))
308350 if templ_7745c5c3_Err != nil {
309- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 52}
351+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 57, Col: 52}
310352 }
311- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
353+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
312354 if templ_7745c5c3_Err != nil {
313355 return templ_7745c5c3_Err
314356 }
315- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
357+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
316358 if templ_7745c5c3_Err != nil {
317359 return templ_7745c5c3_Err
318360 }
319- var templ_7745c5c3_Var17 string
320- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
361+ var templ_7745c5c3_Var19 string
362+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.Short)
321363 if templ_7745c5c3_Err != nil {
322- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 64}
364+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 57, Col: 64}
323365 }
324- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
366+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
325367 if templ_7745c5c3_Err != nil {
326368 return templ_7745c5c3_Err
327369 }
328- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</a> ")
370+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a> ")
329371 if templ_7745c5c3_Err != nil {
330372 return templ_7745c5c3_Err
331373 }
332- var templ_7745c5c3_Var18 string
333- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title)
374+ var templ_7745c5c3_Var20 string
375+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title)
334376 if templ_7745c5c3_Err != nil {
335- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 50, Col: 80}
377+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 57, Col: 80}
336378 }
337- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
379+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
338380 if templ_7745c5c3_Err != nil {
339381 return templ_7745c5c3_Err
340382 }
341- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td class=\"c-ref\"><a href=\"")
383+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-ref\"><a href=\"")
342384 if templ_7745c5c3_Err != nil {
343385 return templ_7745c5c3_Err
344386 }
345- var templ_7745c5c3_Var19 templ.SafeURL
346- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RefHref))
387+ var templ_7745c5c3_Var21 templ.SafeURL
388+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RefHref))
347389 if templ_7745c5c3_Err != nil {
348- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 51, Col: 54}
390+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 58, Col: 54}
349391 }
350- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
392+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
351393 if templ_7745c5c3_Err != nil {
352394 return templ_7745c5c3_Err
353395 }
354- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">")
355397 if templ_7745c5c3_Err != nil {
356398 return templ_7745c5c3_Err
357399 }
358- var templ_7745c5c3_Var20 string
359- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
400+ var templ_7745c5c3_Var22 string
401+ templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
360402 if templ_7745c5c3_Err != nil {
361- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 51, Col: 64}
403+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 58, Col: 64}
362404 }
363- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
405+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
364406 if templ_7745c5c3_Err != nil {
365407 return templ_7745c5c3_Err
366408 }
367- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</a></td><td class=\"c-who dim\">")
409+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a></td><td class=\"c-who dim\">")
368410 if templ_7745c5c3_Err != nil {
369411 return templ_7745c5c3_Err
370412 }
371- var templ_7745c5c3_Var21 string
372- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(r.Actor)
413+ var templ_7745c5c3_Var23 string
414+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(r.Actor)
373415 if templ_7745c5c3_Err != nil {
374- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 52, Col: 33}
416+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 59, Col: 33}
375417 }
376- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
418+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
377419 if templ_7745c5c3_Err != nil {
378420 return templ_7745c5c3_Err
379421 }
380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td class=\"c-dur dim\">")
422+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td class=\"c-dur dim\">")
381423 if templ_7745c5c3_Err != nil {
382424 return templ_7745c5c3_Err
383425 }
384- var templ_7745c5c3_Var22 string
385- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
426+ var templ_7745c5c3_Var24 string
427+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
386428 if templ_7745c5c3_Err != nil {
387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 33}
429+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 60, Col: 33}
388430 }
389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
431+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
390432 if templ_7745c5c3_Err != nil {
391433 return templ_7745c5c3_Err
392434 }
393- var templ_7745c5c3_Var23 string
394- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
435+ var templ_7745c5c3_Var25 string
436+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
395437 if templ_7745c5c3_Err != nil {
396- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 40}
438+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 60, Col: 40}
397439 }
398- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
440+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
399441 if templ_7745c5c3_Err != nil {
400442 return templ_7745c5c3_Err
401443 }
402- var templ_7745c5c3_Var24 string
403- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(r.Duration)
444+ var templ_7745c5c3_Var26 string
445+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(r.Duration)
404446 if templ_7745c5c3_Err != nil {
405- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 53, Col: 54}
447+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 60, Col: 54}
406448 }
407- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
449+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
408450 if templ_7745c5c3_Err != nil {
409451 return templ_7745c5c3_Err
410452 }
411- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</td><td class=\"c-when dim\">")
453+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td><td class=\"c-when dim\">")
412454 if templ_7745c5c3_Err != nil {
413455 return templ_7745c5c3_Err
414456 }
415457 if r.WhenAt != "" {
416- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<time datetime=\"")
458+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<time datetime=\"")
417459 if templ_7745c5c3_Err != nil {
418460 return templ_7745c5c3_Err
419461 }
420- var templ_7745c5c3_Var25 string
421- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
462+ var templ_7745c5c3_Var27 string
463+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
422464 if templ_7745c5c3_Err != nil {
423- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 56, Col: 29}
465+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 63, Col: 29}
424466 }
425- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
467+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
426468 if templ_7745c5c3_Err != nil {
427469 return templ_7745c5c3_Err
428470 }
429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" title=\"")
471+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" title=\"")
430472 if templ_7745c5c3_Err != nil {
431473 return templ_7745c5c3_Err
432474 }
433- var templ_7745c5c3_Var26 string
434- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
475+ var templ_7745c5c3_Var28 string
476+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
435477 if templ_7745c5c3_Err != nil {
436- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 56, Col: 48}
478+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 63, Col: 48}
437479 }
438- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
480+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
439481 if templ_7745c5c3_Err != nil {
440482 return templ_7745c5c3_Err
441483 }
442- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
443485 if templ_7745c5c3_Err != nil {
444486 return templ_7745c5c3_Err
445487 }
446- var templ_7745c5c3_Var27 string
447- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
488+ var templ_7745c5c3_Var29 string
489+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
448490 if templ_7745c5c3_Err != nil {
449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 56, Col: 59}
491+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 63, Col: 59}
450492 }
451- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
493+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
452494 if templ_7745c5c3_Err != nil {
453495 return templ_7745c5c3_Err
454496 }
455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</time>")
497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</time>")
456498 if templ_7745c5c3_Err != nil {
457499 return templ_7745c5c3_Err
458500 }
459501 }
460- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td></tr>")
502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td></tr>")
461503 if templ_7745c5c3_Err != nil {
462504 return templ_7745c5c3_Err
463505 }
@@ -483,40 +525,40 @@ func RecentActionsBlock(a RecentActions) templ.Component {
483525 }()
484526 }
485527 ctx = templ.InitializeContext(ctx)
486- templ_7745c5c3_Var28 := templ.GetChildren(ctx)
487- if templ_7745c5c3_Var28 == nil {
488- templ_7745c5c3_Var28 = templ.NopComponent
528+ templ_7745c5c3_Var30 := templ.GetChildren(ctx)
529+ if templ_7745c5c3_Var30 == nil {
530+ templ_7745c5c3_Var30 = templ.NopComponent
489531 }
490532 ctx = templ.ClearChildren(ctx)
491- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<section id=\"recent-actions\" aria-labelledby=\"r-actions\"")
533+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<section id=\"recent-actions\" aria-labelledby=\"r-actions\"")
492534 if templ_7745c5c3_Err != nil {
493535 return templ_7745c5c3_Err
494536 }
495537 if len(a.Runs) == 0 {
496- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " hidden")
538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " hidden")
497539 if templ_7745c5c3_Err != nil {
498540 return templ_7745c5c3_Err
499541 }
500542 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "><h2 id=\"r-actions\">Recent actions <a href=\"")
543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "><h2 id=\"r-actions\">Recent actions <a href=\"")
502544 if templ_7745c5c3_Err != nil {
503545 return templ_7745c5c3_Err
504546 }
505- var templ_7745c5c3_Var29 templ.SafeURL
506- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.AllHref))
547+ var templ_7745c5c3_Var31 templ.SafeURL
548+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.AllHref))
507549 if templ_7745c5c3_Err != nil {
508- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 66, Col: 70}
550+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 73, Col: 70}
509551 }
510- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
552+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
511553 if templ_7745c5c3_Err != nil {
512554 return templ_7745c5c3_Err
513555 }
514- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">View all</a></h2><ul class=\"rruns\">")
556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">View all</a></h2><ul class=\"rruns\">")
515557 if templ_7745c5c3_Err != nil {
516558 return templ_7745c5c3_Err
517559 }
518560 for _, r := range a.Runs {
519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<li>")
561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<li>")
520562 if templ_7745c5c3_Err != nil {
521563 return templ_7745c5c3_Err
522564 }
@@ -524,139 +566,139 @@ func RecentActionsBlock(a RecentActions) templ.Component {
524566 if templ_7745c5c3_Err != nil {
525567 return templ_7745c5c3_Err
526568 }
527- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<a class=\"rr-name\" href=\"")
569+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a class=\"rr-name\" href=\"")
528570 if templ_7745c5c3_Err != nil {
529571 return templ_7745c5c3_Err
530572 }
531- var templ_7745c5c3_Var30 templ.SafeURL
532- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
573+ var templ_7745c5c3_Var32 templ.SafeURL
574+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
533575 if templ_7745c5c3_Err != nil {
534- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 71, Col: 52}
576+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 78, Col: 52}
535577 }
536- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
578+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
537579 if templ_7745c5c3_Err != nil {
538580 return templ_7745c5c3_Err
539581 }
540- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" title=\"")
582+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" title=\"")
541583 if templ_7745c5c3_Err != nil {
542584 return templ_7745c5c3_Err
543585 }
544- var templ_7745c5c3_Var31 string
545- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Workflow + " " + r.Number + " · " + r.State)
586+ var templ_7745c5c3_Var33 string
587+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Workflow + " " + r.Number + " · " + r.State)
546588 if templ_7745c5c3_Err != nil {
547- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 71, Col: 109}
589+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 78, Col: 109}
548590 }
549- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
591+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
550592 if templ_7745c5c3_Err != nil {
551593 return templ_7745c5c3_Err
552594 }
553- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
595+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
554596 if templ_7745c5c3_Err != nil {
555597 return templ_7745c5c3_Err
556598 }
557- var templ_7745c5c3_Var32 string
558- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
599+ var templ_7745c5c3_Var34 string
600+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(r.Workflow)
559601 if templ_7745c5c3_Err != nil {
560- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 71, Col: 124}
602+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 78, Col: 124}
561603 }
562- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
604+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
563605 if templ_7745c5c3_Err != nil {
564606 return templ_7745c5c3_Err
565607 }
566- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a> <span class=\"mute\">")
608+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</a> <span class=\"mute\">")
567609 if templ_7745c5c3_Err != nil {
568610 return templ_7745c5c3_Err
569611 }
570- var templ_7745c5c3_Var33 string
571- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
612+ var templ_7745c5c3_Var35 string
613+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.Number)
572614 if templ_7745c5c3_Err != nil {
573- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 72, Col: 34}
615+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 79, Col: 34}
574616 }
575- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
617+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
576618 if templ_7745c5c3_Err != nil {
577619 return templ_7745c5c3_Err
578620 }
579- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span> <a class=\"rr-ref\" href=\"")
621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span> <a class=\"rr-ref\" href=\"")
580622 if templ_7745c5c3_Err != nil {
581623 return templ_7745c5c3_Err
582624 }
583- var templ_7745c5c3_Var34 templ.SafeURL
584- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RefHref))
625+ var templ_7745c5c3_Var36 templ.SafeURL
626+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.RefHref))
585627 if templ_7745c5c3_Err != nil {
586- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 73, Col: 54}
628+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 80, Col: 54}
587629 }
588- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
630+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
589631 if templ_7745c5c3_Err != nil {
590632 return templ_7745c5c3_Err
591633 }
592- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">")
634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">")
593635 if templ_7745c5c3_Err != nil {
594636 return templ_7745c5c3_Err
595637 }
596- var templ_7745c5c3_Var35 string
597- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
638+ var templ_7745c5c3_Var37 string
639+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(r.Ref)
598640 if templ_7745c5c3_Err != nil {
599- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 73, Col: 64}
641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 80, Col: 64}
600642 }
601- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
602644 if templ_7745c5c3_Err != nil {
603645 return templ_7745c5c3_Err
604646 }
605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</a> ")
647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</a> ")
606648 if templ_7745c5c3_Err != nil {
607649 return templ_7745c5c3_Err
608650 }
609651 if r.WhenAt != "" {
610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<time class=\"dim\" datetime=\"")
652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<time class=\"dim\" datetime=\"")
611653 if templ_7745c5c3_Err != nil {
612654 return templ_7745c5c3_Err
613655 }
614- var templ_7745c5c3_Var36 string
615- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
656+ var templ_7745c5c3_Var38 string
657+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
616658 if templ_7745c5c3_Err != nil {
617- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 75, Col: 43}
659+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 82, Col: 43}
618660 }
619- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
661+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
620662 if templ_7745c5c3_Err != nil {
621663 return templ_7745c5c3_Err
622664 }
623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" title=\"")
665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" title=\"")
624666 if templ_7745c5c3_Err != nil {
625667 return templ_7745c5c3_Err
626668 }
627- var templ_7745c5c3_Var37 string
628- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
669+ var templ_7745c5c3_Var39 string
670+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.WhenAt)
629671 if templ_7745c5c3_Err != nil {
630- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 75, Col: 62}
672+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 82, Col: 62}
631673 }
632- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
674+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
633675 if templ_7745c5c3_Err != nil {
634676 return templ_7745c5c3_Err
635677 }
636- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\">")
678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
637679 if templ_7745c5c3_Err != nil {
638680 return templ_7745c5c3_Err
639681 }
640- var templ_7745c5c3_Var38 string
641- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
682+ var templ_7745c5c3_Var40 string
683+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(r.When)
642684 if templ_7745c5c3_Err != nil {
643- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 75, Col: 73}
685+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 82, Col: 73}
644686 }
645- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
687+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
646688 if templ_7745c5c3_Err != nil {
647689 return templ_7745c5c3_Err
648690 }
649- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</time>")
691+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</time>")
650692 if templ_7745c5c3_Err != nil {
651693 return templ_7745c5c3_Err
652694 }
653695 }
654- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</li>")
696+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</li>")
655697 if templ_7745c5c3_Err != nil {
656698 return templ_7745c5c3_Err
657699 }
658700 }
659- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</ul></section>")
701+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</ul></section>")
660702 if templ_7745c5c3_Err != nil {
661703 return templ_7745c5c3_Err
662704 }
@@ -680,12 +722,12 @@ func RunDetail(l Layout, p RunPage) templ.Component {
680722 }()
681723 }
682724 ctx = templ.InitializeContext(ctx)
683- templ_7745c5c3_Var39 := templ.GetChildren(ctx)
684- if templ_7745c5c3_Var39 == nil {
685- templ_7745c5c3_Var39 = templ.NopComponent
725+ templ_7745c5c3_Var41 := templ.GetChildren(ctx)
726+ if templ_7745c5c3_Var41 == nil {
727+ templ_7745c5c3_Var41 = templ.NopComponent
686728 }
687729 ctx = templ.ClearChildren(ctx)
688- templ_7745c5c3_Var40 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
730+ templ_7745c5c3_Var42 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
689731 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
690732 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
691733 if !templ_7745c5c3_IsBuffer {
@@ -701,7 +743,17 @@ func RunDetail(l Layout, p RunPage) templ.Component {
701743 if templ_7745c5c3_Err != nil {
702744 return templ_7745c5c3_Err
703745 }
704- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " <div id=\"run-body\">")
746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ")
747+ if templ_7745c5c3_Err != nil {
748+ return templ_7745c5c3_Err
749+ }
750+ if p.WorkflowDisabled {
751+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"dim wf-off\"><span class=\"pill\">disabled</span> this workflow is disabled: no event starts a run of it (<code>ermite workflow enable</code>).</p>")
752+ if templ_7745c5c3_Err != nil {
753+ return templ_7745c5c3_Err
754+ }
755+ }
756+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " <div id=\"run-body\">")
705757 if templ_7745c5c3_Err != nil {
706758 return templ_7745c5c3_Err
707759 }
@@ -709,7 +761,7 @@ func RunDetail(l Layout, p RunPage) templ.Component {
709761 if templ_7745c5c3_Err != nil {
710762 return templ_7745c5c3_Err
711763 }
712- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div>")
764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div>")
713765 if templ_7745c5c3_Err != nil {
714766 return templ_7745c5c3_Err
715767 }
@@ -719,7 +771,7 @@ func RunDetail(l Layout, p RunPage) templ.Component {
719771 return templ_7745c5c3_Err
720772 }
721773 }
722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " ")
774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " ")
723775 if templ_7745c5c3_Err != nil {
724776 return templ_7745c5c3_Err
725777 }
@@ -729,7 +781,7 @@ func RunDetail(l Layout, p RunPage) templ.Component {
729781 }
730782 return nil
731783 })
732- templ_7745c5c3_Err = Shell(l, runRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var40), templ_7745c5c3_Buffer)
784+ templ_7745c5c3_Err = Shell(l, runRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var42), templ_7745c5c3_Buffer)
733785 if templ_7745c5c3_Err != nil {
734786 return templ_7745c5c3_Err
735787 }
@@ -754,12 +806,12 @@ func runBodyFragment(p RunPage) templ.Component {
754806 }()
755807 }
756808 ctx = templ.InitializeContext(ctx)
757- templ_7745c5c3_Var41 := templ.GetChildren(ctx)
758- if templ_7745c5c3_Var41 == nil {
759- templ_7745c5c3_Var41 = templ.NopComponent
809+ templ_7745c5c3_Var43 := templ.GetChildren(ctx)
810+ if templ_7745c5c3_Var43 == nil {
811+ templ_7745c5c3_Var43 = templ.NopComponent
760812 }
761813 ctx = templ.ClearChildren(ctx)
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<div id=\"run-body\">")
814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<div id=\"run-body\">")
763815 if templ_7745c5c3_Err != nil {
764816 return templ_7745c5c3_Err
765817 }
@@ -767,7 +819,7 @@ func runBodyFragment(p RunPage) templ.Component {
767819 if templ_7745c5c3_Err != nil {
768820 return templ_7745c5c3_Err
769821 }
770- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</div>")
822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</div>")
771823 if templ_7745c5c3_Err != nil {
772824 return templ_7745c5c3_Err
773825 }
@@ -792,12 +844,12 @@ func RunBody(p RunPage) templ.Component {
792844 }()
793845 }
794846 ctx = templ.InitializeContext(ctx)
795- templ_7745c5c3_Var42 := templ.GetChildren(ctx)
796- if templ_7745c5c3_Var42 == nil {
797- templ_7745c5c3_Var42 = templ.NopComponent
847+ templ_7745c5c3_Var44 := templ.GetChildren(ctx)
848+ if templ_7745c5c3_Var44 == nil {
849+ templ_7745c5c3_Var44 = templ.NopComponent
798850 }
799851 ctx = templ.ClearChildren(ctx)
800- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<h2 class=\"run-h\">")
852+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<h2 class=\"run-h\">")
801853 if templ_7745c5c3_Err != nil {
802854 return templ_7745c5c3_Err
803855 }
@@ -805,70 +857,70 @@ func RunBody(p RunPage) templ.Component {
805857 if templ_7745c5c3_Err != nil {
806858 return templ_7745c5c3_Err
807859 }
808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span>")
860+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<span>")
809861 if templ_7745c5c3_Err != nil {
810862 return templ_7745c5c3_Err
811863 }
812- var templ_7745c5c3_Var43 string
813- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Workflow)
864+ var templ_7745c5c3_Var45 string
865+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Workflow)
814866 if templ_7745c5c3_Err != nil {
815- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 105, Col: 64}
867+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 115, Col: 64}
816868 }
817- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
869+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
818870 if templ_7745c5c3_Err != nil {
819871 return templ_7745c5c3_Err
820872 }
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span> <span class=\"mute\">")
873+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span> <span class=\"mute\">")
822874 if templ_7745c5c3_Err != nil {
823875 return templ_7745c5c3_Err
824876 }
825- var templ_7745c5c3_Var44 string
826- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
877+ var templ_7745c5c3_Var46 string
878+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
827879 if templ_7745c5c3_Err != nil {
828- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 105, Col: 103}
880+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 115, Col: 103}
829881 }
830- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
882+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
831883 if templ_7745c5c3_Err != nil {
832884 return templ_7745c5c3_Err
833885 }
834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></h2>")
886+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span></h2>")
835887 if templ_7745c5c3_Err != nil {
836888 return templ_7745c5c3_Err
837889 }
838890 if p.Error != "" {
839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<pre class=\"run-err\">")
891+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<pre class=\"run-err\">")
840892 if templ_7745c5c3_Err != nil {
841893 return templ_7745c5c3_Err
842894 }
843- var templ_7745c5c3_Var45 string
844- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Error)
895+ var templ_7745c5c3_Var47 string
896+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.Error)
845897 if templ_7745c5c3_Err != nil {
846- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 107, Col: 32}
898+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 117, Col: 32}
847899 }
848- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
900+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
849901 if templ_7745c5c3_Err != nil {
850902 return templ_7745c5c3_Err
851903 }
852- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</pre>")
904+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</pre>")
853905 if templ_7745c5c3_Err != nil {
854906 return templ_7745c5c3_Err
855907 }
856908 }
857909 if len(p.Annotations) > 0 {
858- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<section class=\"annots\" aria-labelledby=\"run-annots\"><h3 id=\"run-annots\">Annotations <span class=\"count\">")
910+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<section class=\"annots\" aria-labelledby=\"run-annots\"><h3 id=\"run-annots\">Annotations <span class=\"count\">")
859911 if templ_7745c5c3_Err != nil {
860912 return templ_7745c5c3_Err
861913 }
862- var templ_7745c5c3_Var46 string
863- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Annotations)))
914+ var templ_7745c5c3_Var48 string
915+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Annotations)))
864916 if templ_7745c5c3_Err != nil {
865- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 111, Col: 81}
917+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 121, Col: 81}
866918 }
867- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
919+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
868920 if templ_7745c5c3_Err != nil {
869921 return templ_7745c5c3_Err
870922 }
871- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</span></h3>")
923+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</span></h3>")
872924 if templ_7745c5c3_Err != nil {
873925 return templ_7745c5c3_Err
874926 }
@@ -876,43 +928,43 @@ func RunBody(p RunPage) templ.Component {
876928 if templ_7745c5c3_Err != nil {
877929 return templ_7745c5c3_Err
878930 }
879- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</section>")
931+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</section>")
880932 if templ_7745c5c3_Err != nil {
881933 return templ_7745c5c3_Err
882934 }
883935 }
884- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<ul class=\"jobs\">")
936+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<ul class=\"jobs\">")
885937 if templ_7745c5c3_Err != nil {
886938 return templ_7745c5c3_Err
887939 }
888940 for _, j := range p.Jobs {
889- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<li id=\"")
941+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<li id=\"")
890942 if templ_7745c5c3_Err != nil {
891943 return templ_7745c5c3_Err
892944 }
893- var templ_7745c5c3_Var47 string
894- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue("job-" + j.ID)
945+ var templ_7745c5c3_Var49 string
946+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue("job-" + j.ID)
895947 if templ_7745c5c3_Err != nil {
896- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 117, Col: 25}
948+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 127, Col: 25}
897949 }
898- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
950+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
899951 if templ_7745c5c3_Err != nil {
900952 return templ_7745c5c3_Err
901953 }
902- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"><a class=\"job-h\" href=\"")
954+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\"><a class=\"job-h\" href=\"")
903955 if templ_7745c5c3_Err != nil {
904956 return templ_7745c5c3_Err
905957 }
906- var templ_7745c5c3_Var48 templ.SafeURL
907- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(j.Href))
958+ var templ_7745c5c3_Var50 templ.SafeURL
959+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(j.Href))
908960 if templ_7745c5c3_Err != nil {
909- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 118, Col: 49}
961+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 128, Col: 49}
910962 }
911- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
963+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
912964 if templ_7745c5c3_Err != nil {
913965 return templ_7745c5c3_Err
914966 }
915- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\">")
967+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\">")
916968 if templ_7745c5c3_Err != nil {
917969 return templ_7745c5c3_Err
918970 }
@@ -920,118 +972,118 @@ func RunBody(p RunPage) templ.Component {
920972 if templ_7745c5c3_Err != nil {
921973 return templ_7745c5c3_Err
922974 }
923- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<span class=\"j-name\">")
975+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<span class=\"j-name\">")
924976 if templ_7745c5c3_Err != nil {
925977 return templ_7745c5c3_Err
926978 }
927- var templ_7745c5c3_Var49 string
928- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name)
979+ var templ_7745c5c3_Var51 string
980+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name)
929981 if templ_7745c5c3_Err != nil {
930- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 120, Col: 34}
982+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 130, Col: 34}
931983 }
932- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
984+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
933985 if templ_7745c5c3_Err != nil {
934986 return templ_7745c5c3_Err
935987 }
936- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</span> <span class=\"dim\">")
988+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</span> <span class=\"dim\">")
937989 if templ_7745c5c3_Err != nil {
938990 return templ_7745c5c3_Err
939991 }
940- var templ_7745c5c3_Var50 string
941- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(j.State)
992+ var templ_7745c5c3_Var52 string
993+ templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(j.State)
942994 if templ_7745c5c3_Err != nil {
943- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 121, Col: 32}
995+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 131, Col: 32}
944996 }
945- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
997+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
946998 if templ_7745c5c3_Err != nil {
947999 return templ_7745c5c3_Err
9481000 }
949- var templ_7745c5c3_Var51 string
950- templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1001+ var templ_7745c5c3_Var53 string
1002+ templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
9511003 if templ_7745c5c3_Err != nil {
952- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 121, Col: 39}
1004+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 131, Col: 39}
9531005 }
954- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
1006+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
9551007 if templ_7745c5c3_Err != nil {
9561008 return templ_7745c5c3_Err
9571009 }
958- var templ_7745c5c3_Var52 string
959- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration)
1010+ var templ_7745c5c3_Var54 string
1011+ templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration)
9601012 if templ_7745c5c3_Err != nil {
961- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 121, Col: 53}
1013+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 131, Col: 53}
9621014 }
963- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1015+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
9641016 if templ_7745c5c3_Err != nil {
9651017 return templ_7745c5c3_Err
9661018 }
967- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</span></a> ")
1019+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span></a> ")
9681020 if templ_7745c5c3_Err != nil {
9691021 return templ_7745c5c3_Err
9701022 }
9711023 if j.Runner != "" {
972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<span class=\"mute\">on ")
1024+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"mute\">on ")
9731025 if templ_7745c5c3_Err != nil {
9741026 return templ_7745c5c3_Err
9751027 }
976- var templ_7745c5c3_Var53 string
977- templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(runnerText(j))
1028+ var templ_7745c5c3_Var55 string
1029+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(runnerText(j))
9781030 if templ_7745c5c3_Err != nil {
979- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 124, Col: 42}
1031+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 134, Col: 42}
9801032 }
981- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
1033+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
9821034 if templ_7745c5c3_Err != nil {
9831035 return templ_7745c5c3_Err
9841036 }
985- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</span> ")
1037+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</span> ")
9861038 if templ_7745c5c3_Err != nil {
9871039 return templ_7745c5c3_Err
9881040 }
9891041 }
9901042 if len(j.Needs) > 0 {
991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<span class=\"mute\">needs ")
1043+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<span class=\"mute\">needs ")
9921044 if templ_7745c5c3_Err != nil {
9931045 return templ_7745c5c3_Err
9941046 }
995- var templ_7745c5c3_Var54 string
996- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(joinWords(j.Needs))
1047+ var templ_7745c5c3_Var56 string
1048+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(joinWords(j.Needs))
9971049 if templ_7745c5c3_Err != nil {
998- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 127, Col: 50}
1050+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 137, Col: 50}
9991051 }
1000- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
1052+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
10011053 if templ_7745c5c3_Err != nil {
10021054 return templ_7745c5c3_Err
10031055 }
1004- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</span> ")
1056+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</span> ")
10051057 if templ_7745c5c3_Err != nil {
10061058 return templ_7745c5c3_Err
10071059 }
10081060 }
10091061 if j.Error != "" {
1010- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<p class=\"job-err\">")
1062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "<p class=\"job-err\">")
10111063 if templ_7745c5c3_Err != nil {
10121064 return templ_7745c5c3_Err
10131065 }
1014- var templ_7745c5c3_Var55 string
1015- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error)
1066+ var templ_7745c5c3_Var57 string
1067+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error)
10161068 if templ_7745c5c3_Err != nil {
1017- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 130, Col: 33}
1069+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 140, Col: 33}
10181070 }
1019- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
1071+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
10201072 if templ_7745c5c3_Err != nil {
10211073 return templ_7745c5c3_Err
10221074 }
1023- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</p>")
1075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</p>")
10241076 if templ_7745c5c3_Err != nil {
10251077 return templ_7745c5c3_Err
10261078 }
10271079 }
10281080 if len(j.Steps) > 0 {
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<ol class=\"steps\">")
1081+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<ol class=\"steps\">")
10301082 if templ_7745c5c3_Err != nil {
10311083 return templ_7745c5c3_Err
10321084 }
10331085 for _, s := range j.Steps {
1034- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<li>")
1086+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<li>")
10351087 if templ_7745c5c3_Err != nil {
10361088 return templ_7745c5c3_Err
10371089 }
@@ -1039,48 +1091,48 @@ func RunBody(p RunPage) templ.Component {
10391091 if templ_7745c5c3_Err != nil {
10401092 return templ_7745c5c3_Err
10411093 }
1042- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<span>")
1094+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<span>")
10431095 if templ_7745c5c3_Err != nil {
10441096 return templ_7745c5c3_Err
10451097 }
1046- var templ_7745c5c3_Var56 string
1047- templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
1098+ var templ_7745c5c3_Var58 string
1099+ templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
10481100 if templ_7745c5c3_Err != nil {
1049- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 135, Col: 53}
1101+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 145, Col: 53}
10501102 }
1051- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
1103+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
10521104 if templ_7745c5c3_Err != nil {
10531105 return templ_7745c5c3_Err
10541106 }
1055- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span> <span class=\"dim\">")
1107+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</span> <span class=\"dim\">")
10561108 if templ_7745c5c3_Err != nil {
10571109 return templ_7745c5c3_Err
10581110 }
1059- var templ_7745c5c3_Var57 string
1060- templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration)
1111+ var templ_7745c5c3_Var59 string
1112+ templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration)
10611113 if templ_7745c5c3_Err != nil {
1062- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 135, Col: 93}
1114+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 145, Col: 93}
10631115 }
1064- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
1116+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
10651117 if templ_7745c5c3_Err != nil {
10661118 return templ_7745c5c3_Err
10671119 }
1068- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</span></li>")
1120+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</span></li>")
10691121 if templ_7745c5c3_Err != nil {
10701122 return templ_7745c5c3_Err
10711123 }
10721124 }
1073- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</ol>")
1125+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</ol>")
10741126 if templ_7745c5c3_Err != nil {
10751127 return templ_7745c5c3_Err
10761128 }
10771129 }
1078- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</li>")
1130+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</li>")
10791131 if templ_7745c5c3_Err != nil {
10801132 return templ_7745c5c3_Err
10811133 }
10821134 }
1083- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</ul>")
1135+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</ul>")
10841136 if templ_7745c5c3_Err != nil {
10851137 return templ_7745c5c3_Err
10861138 }
@@ -1108,327 +1160,327 @@ func AnnotationList(anns []AnnotationView, more bool, grouped bool) templ.Compon
11081160 }()
11091161 }
11101162 ctx = templ.InitializeContext(ctx)
1111- templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1112- if templ_7745c5c3_Var58 == nil {
1113- templ_7745c5c3_Var58 = templ.NopComponent
1163+ templ_7745c5c3_Var60 := templ.GetChildren(ctx)
1164+ if templ_7745c5c3_Var60 == nil {
1165+ templ_7745c5c3_Var60 = templ.NopComponent
11141166 }
11151167 ctx = templ.ClearChildren(ctx)
1116- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<ul class=\"annot-list\">")
1168+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<ul class=\"annot-list\">")
11171169 if templ_7745c5c3_Err != nil {
11181170 return templ_7745c5c3_Err
11191171 }
11201172 for _, a := range anns {
1121- var templ_7745c5c3_Var59 = []any{"annot", "annot-" + a.Kind}
1122- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var59...)
1173+ var templ_7745c5c3_Var61 = []any{"annot", "annot-" + a.Kind}
1174+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var61...)
11231175 if templ_7745c5c3_Err != nil {
11241176 return templ_7745c5c3_Err
11251177 }
1126- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<li class=\"")
1178+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<li class=\"")
11271179 if templ_7745c5c3_Err != nil {
11281180 return templ_7745c5c3_Err
11291181 }
1130- var templ_7745c5c3_Var60 string
1131- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var59).String())
1182+ var templ_7745c5c3_Var62 string
1183+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var61).String())
11321184 if templ_7745c5c3_Err != nil {
11331185 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 1, Col: 0}
11341186 }
1135- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
1187+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
11361188 if templ_7745c5c3_Err != nil {
11371189 return templ_7745c5c3_Err
11381190 }
1139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\">")
1191+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\">")
11401192 if templ_7745c5c3_Err != nil {
11411193 return templ_7745c5c3_Err
11421194 }
11431195 if grouped && a.StepHead {
1144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<p class=\"annot-step\">")
1196+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<p class=\"annot-step\">")
11451197 if templ_7745c5c3_Err != nil {
11461198 return templ_7745c5c3_Err
11471199 }
11481200 if a.StepHref != "" {
1149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<a href=\"")
1201+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<a href=\"")
11501202 if templ_7745c5c3_Err != nil {
11511203 return templ_7745c5c3_Err
11521204 }
1153- var templ_7745c5c3_Var61 templ.SafeURL
1154- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.StepHref))
1205+ var templ_7745c5c3_Var63 templ.SafeURL
1206+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.StepHref))
11551207 if templ_7745c5c3_Err != nil {
1156- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 155, Col: 42}
1208+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 165, Col: 42}
11571209 }
1158- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1210+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
11591211 if templ_7745c5c3_Err != nil {
11601212 return templ_7745c5c3_Err
11611213 }
1162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\">")
1214+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\">")
11631215 if templ_7745c5c3_Err != nil {
11641216 return templ_7745c5c3_Err
11651217 }
1166- var templ_7745c5c3_Var62 string
1167- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
1218+ var templ_7745c5c3_Var64 string
1219+ templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
11681220 if templ_7745c5c3_Err != nil {
1169- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 155, Col: 53}
1221+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 165, Col: 53}
11701222 }
1171- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1223+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
11721224 if templ_7745c5c3_Err != nil {
11731225 return templ_7745c5c3_Err
11741226 }
1175- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</a>")
1227+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</a>")
11761228 if templ_7745c5c3_Err != nil {
11771229 return templ_7745c5c3_Err
11781230 }
11791231 } else {
1180- var templ_7745c5c3_Var63 string
1181- templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
1232+ var templ_7745c5c3_Var65 string
1233+ templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
11821234 if templ_7745c5c3_Err != nil {
1183- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 157, Col: 15}
1235+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 167, Col: 15}
11841236 }
1185- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
1237+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
11861238 if templ_7745c5c3_Err != nil {
11871239 return templ_7745c5c3_Err
11881240 }
11891241 }
1190- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</p>")
1242+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</p>")
11911243 if templ_7745c5c3_Err != nil {
11921244 return templ_7745c5c3_Err
11931245 }
11941246 }
1195- var templ_7745c5c3_Var64 = []any{"an", "an-" + a.Kind}
1196- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var64...)
1247+ var templ_7745c5c3_Var66 = []any{"an", "an-" + a.Kind}
1248+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var66...)
11971249 if templ_7745c5c3_Err != nil {
11981250 return templ_7745c5c3_Err
11991251 }
1200- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<span class=\"")
1252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<span class=\"")
12011253 if templ_7745c5c3_Err != nil {
12021254 return templ_7745c5c3_Err
12031255 }
1204- var templ_7745c5c3_Var65 string
1205- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var64).String())
1256+ var templ_7745c5c3_Var67 string
1257+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var66).String())
12061258 if templ_7745c5c3_Err != nil {
12071259 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 1, Col: 0}
12081260 }
1209- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65)
1261+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
12101262 if templ_7745c5c3_Err != nil {
12111263 return templ_7745c5c3_Err
12121264 }
1213- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\" role=\"img\" aria-label=\"")
1265+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\" role=\"img\" aria-label=\"")
12141266 if templ_7745c5c3_Err != nil {
12151267 return templ_7745c5c3_Err
12161268 }
1217- var templ_7745c5c3_Var66 string
1218- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(a.Level)
1269+ var templ_7745c5c3_Var68 string
1270+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(a.Level)
12191271 if templ_7745c5c3_Err != nil {
1220- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 72}
1272+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 171, Col: 72}
12211273 }
1222- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
1274+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68)
12231275 if templ_7745c5c3_Err != nil {
12241276 return templ_7745c5c3_Err
12251277 }
1226- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" title=\"")
1278+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\" title=\"")
12271279 if templ_7745c5c3_Err != nil {
12281280 return templ_7745c5c3_Err
12291281 }
1230- var templ_7745c5c3_Var67 string
1231- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(a.Level)
1282+ var templ_7745c5c3_Var69 string
1283+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(a.Level)
12321284 if templ_7745c5c3_Err != nil {
1233- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 161, Col: 90}
1285+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 171, Col: 90}
12341286 }
1235- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
1287+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
12361288 if templ_7745c5c3_Err != nil {
12371289 return templ_7745c5c3_Err
12381290 }
1239- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\"></span><div class=\"annot-body\"><p class=\"annot-head\"><span class=\"annot-level\">")
1291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\"></span><div class=\"annot-body\"><p class=\"annot-head\"><span class=\"annot-level\">")
12401292 if templ_7745c5c3_Err != nil {
12411293 return templ_7745c5c3_Err
12421294 }
1243- var templ_7745c5c3_Var68 string
1244- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(a.Level)
1295+ var templ_7745c5c3_Var70 string
1296+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(a.Level)
12451297 if templ_7745c5c3_Err != nil {
1246- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 164, Col: 41}
1298+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 174, Col: 41}
12471299 }
1248- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1300+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
12491301 if templ_7745c5c3_Err != nil {
12501302 return templ_7745c5c3_Err
12511303 }
1252- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</span> ")
1304+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span> ")
12531305 if templ_7745c5c3_Err != nil {
12541306 return templ_7745c5c3_Err
12551307 }
12561308 if !grouped {
12571309 if a.JobHref != "" {
1258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<a href=\"")
1310+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<a href=\"")
12591311 if templ_7745c5c3_Err != nil {
12601312 return templ_7745c5c3_Err
12611313 }
1262- var templ_7745c5c3_Var69 templ.SafeURL
1263- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.JobHref))
1314+ var templ_7745c5c3_Var71 templ.SafeURL
1315+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.JobHref))
12641316 if templ_7745c5c3_Err != nil {
1265- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 167, Col: 42}
1317+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 177, Col: 42}
12661318 }
1267- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
1319+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
12681320 if templ_7745c5c3_Err != nil {
12691321 return templ_7745c5c3_Err
12701322 }
1271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\">")
1323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "\">")
12721324 if templ_7745c5c3_Err != nil {
12731325 return templ_7745c5c3_Err
12741326 }
1275- var templ_7745c5c3_Var70 string
1276- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(a.Job)
1327+ var templ_7745c5c3_Var72 string
1328+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(a.Job)
12771329 if templ_7745c5c3_Err != nil {
1278- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 167, Col: 52}
1330+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 177, Col: 52}
12791331 }
1280- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1332+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
12811333 if templ_7745c5c3_Err != nil {
12821334 return templ_7745c5c3_Err
12831335 }
1284- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</a>")
1336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</a>")
12851337 if templ_7745c5c3_Err != nil {
12861338 return templ_7745c5c3_Err
12871339 }
12881340 } else {
1289- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<span>")
1341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<span>")
12901342 if templ_7745c5c3_Err != nil {
12911343 return templ_7745c5c3_Err
12921344 }
1293- var templ_7745c5c3_Var71 string
1294- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(a.Job)
1345+ var templ_7745c5c3_Var73 string
1346+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(a.Job)
12951347 if templ_7745c5c3_Err != nil {
1296- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 169, Col: 21}
1348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 179, Col: 21}
12971349 }
1298- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
12991351 if templ_7745c5c3_Err != nil {
13001352 return templ_7745c5c3_Err
13011353 }
1302- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "</span>")
1354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</span>")
13031355 if templ_7745c5c3_Err != nil {
13041356 return templ_7745c5c3_Err
13051357 }
13061358 }
1307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " <span class=\"mute\">")
1359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " <span class=\"mute\">")
13081360 if templ_7745c5c3_Err != nil {
13091361 return templ_7745c5c3_Err
13101362 }
1311- var templ_7745c5c3_Var72 string
1312- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
1363+ var templ_7745c5c3_Var74 string
1364+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(a.Step)
13131365 if templ_7745c5c3_Err != nil {
1314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 171, Col: 34}
1366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 181, Col: 34}
13151367 }
1316- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
1368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
13171369 if templ_7745c5c3_Err != nil {
13181370 return templ_7745c5c3_Err
13191371 }
1320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</span> ")
1372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</span> ")
13211373 if templ_7745c5c3_Err != nil {
13221374 return templ_7745c5c3_Err
13231375 }
13241376 }
13251377 if a.Title != "" {
1326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<strong>")
1378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "<strong>")
13271379 if templ_7745c5c3_Err != nil {
13281380 return templ_7745c5c3_Err
13291381 }
1330- var templ_7745c5c3_Var73 string
1331- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(a.Title)
1382+ var templ_7745c5c3_Var75 string
1383+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(a.Title)
13321384 if templ_7745c5c3_Err != nil {
1333- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 174, Col: 24}
1385+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 184, Col: 24}
13341386 }
1335- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1387+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
13361388 if templ_7745c5c3_Err != nil {
13371389 return templ_7745c5c3_Err
13381390 }
1339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</strong>")
1391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</strong>")
13401392 if templ_7745c5c3_Err != nil {
13411393 return templ_7745c5c3_Err
13421394 }
13431395 }
1344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</p><p class=\"annot-msg\">")
1396+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</p><p class=\"annot-msg\">")
13451397 if templ_7745c5c3_Err != nil {
13461398 return templ_7745c5c3_Err
13471399 }
1348- var templ_7745c5c3_Var74 string
1349- templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(a.Message)
1400+ var templ_7745c5c3_Var76 string
1401+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(a.Message)
13501402 if templ_7745c5c3_Err != nil {
1351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 177, Col: 37}
1403+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 187, Col: 37}
13521404 }
1353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
1405+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
13541406 if templ_7745c5c3_Err != nil {
13551407 return templ_7745c5c3_Err
13561408 }
1357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "</p>")
1409+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</p>")
13581410 if templ_7745c5c3_Err != nil {
13591411 return templ_7745c5c3_Err
13601412 }
13611413 if a.Where != "" {
1362- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "<p class=\"annot-where\">")
1414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<p class=\"annot-where\">")
13631415 if templ_7745c5c3_Err != nil {
13641416 return templ_7745c5c3_Err
13651417 }
13661418 if a.WhereHref != "" {
1367- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<a href=\"")
1419+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<a href=\"")
13681420 if templ_7745c5c3_Err != nil {
13691421 return templ_7745c5c3_Err
13701422 }
1371- var templ_7745c5c3_Var75 templ.SafeURL
1372- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.WhereHref))
1423+ var templ_7745c5c3_Var77 templ.SafeURL
1424+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.WhereHref))
13731425 if templ_7745c5c3_Err != nil {
1374- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 181, Col: 44}
1426+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 191, Col: 44}
13751427 }
1376- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1428+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
13771429 if templ_7745c5c3_Err != nil {
13781430 return templ_7745c5c3_Err
13791431 }
1380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">")
1432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\">")
13811433 if templ_7745c5c3_Err != nil {
13821434 return templ_7745c5c3_Err
13831435 }
1384- var templ_7745c5c3_Var76 string
1385- templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(a.Where)
1436+ var templ_7745c5c3_Var78 string
1437+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(a.Where)
13861438 if templ_7745c5c3_Err != nil {
1387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 181, Col: 56}
1439+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 191, Col: 56}
13881440 }
1389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1441+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
13901442 if templ_7745c5c3_Err != nil {
13911443 return templ_7745c5c3_Err
13921444 }
1393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</a>")
1445+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</a>")
13941446 if templ_7745c5c3_Err != nil {
13951447 return templ_7745c5c3_Err
13961448 }
13971449 } else {
1398- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<span>")
1450+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span>")
13991451 if templ_7745c5c3_Err != nil {
14001452 return templ_7745c5c3_Err
14011453 }
1402- var templ_7745c5c3_Var77 string
1403- templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(a.Where)
1454+ var templ_7745c5c3_Var79 string
1455+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(a.Where)
14041456 if templ_7745c5c3_Err != nil {
1405- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 183, Col: 23}
1457+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 193, Col: 23}
14061458 }
1407- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
1459+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
14081460 if templ_7745c5c3_Err != nil {
14091461 return templ_7745c5c3_Err
14101462 }
1411- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</span>")
1463+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</span>")
14121464 if templ_7745c5c3_Err != nil {
14131465 return templ_7745c5c3_Err
14141466 }
14151467 }
1416- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</p>")
1468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</p>")
14171469 if templ_7745c5c3_Err != nil {
14181470 return templ_7745c5c3_Err
14191471 }
14201472 }
1421- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</div></li>")
1473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</div></li>")
14221474 if templ_7745c5c3_Err != nil {
14231475 return templ_7745c5c3_Err
14241476 }
14251477 }
1426- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "</ul>")
1478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</ul>")
14271479 if templ_7745c5c3_Err != nil {
14281480 return templ_7745c5c3_Err
14291481 }
14301482 if more {
1431- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<p class=\"mute\">more annotations exist than are shown.</p>")
1483+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<p class=\"mute\">more annotations exist than are shown.</p>")
14321484 if templ_7745c5c3_Err != nil {
14331485 return templ_7745c5c3_Err
14341486 }
@@ -1456,30 +1508,30 @@ func JobAnnotations(anns []AnnotationView, more bool) templ.Component {
14561508 }()
14571509 }
14581510 ctx = templ.InitializeContext(ctx)
1459- templ_7745c5c3_Var78 := templ.GetChildren(ctx)
1460- if templ_7745c5c3_Var78 == nil {
1461- templ_7745c5c3_Var78 = templ.NopComponent
1511+ templ_7745c5c3_Var80 := templ.GetChildren(ctx)
1512+ if templ_7745c5c3_Var80 == nil {
1513+ templ_7745c5c3_Var80 = templ.NopComponent
14621514 }
14631515 ctx = templ.ClearChildren(ctx)
1464- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<section id=\"job-annotations\" class=\"annots\" aria-label=\"Annotations\">")
1516+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<section id=\"job-annotations\" class=\"annots\" aria-label=\"Annotations\">")
14651517 if templ_7745c5c3_Err != nil {
14661518 return templ_7745c5c3_Err
14671519 }
14681520 if len(anns) > 0 {
1469- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<h3>Annotations <span class=\"count\">")
1521+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<h3>Annotations <span class=\"count\">")
14701522 if templ_7745c5c3_Err != nil {
14711523 return templ_7745c5c3_Err
14721524 }
1473- var templ_7745c5c3_Var79 string
1474- templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(anns)))
1525+ var templ_7745c5c3_Var81 string
1526+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(anns)))
14751527 if templ_7745c5c3_Err != nil {
1476- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 202, Col: 56}
1528+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 212, Col: 56}
14771529 }
1478- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
1530+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
14791531 if templ_7745c5c3_Err != nil {
14801532 return templ_7745c5c3_Err
14811533 }
1482- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</span></h3>")
1534+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span></h3>")
14831535 if templ_7745c5c3_Err != nil {
14841536 return templ_7745c5c3_Err
14851537 }
@@ -1488,7 +1540,7 @@ func JobAnnotations(anns []AnnotationView, more bool) templ.Component {
14881540 return templ_7745c5c3_Err
14891541 }
14901542 }
1491- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</section>")
1543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</section>")
14921544 if templ_7745c5c3_Err != nil {
14931545 return templ_7745c5c3_Err
14941546 }
@@ -1514,223 +1566,223 @@ func RunArtifacts(a ArtifactsView) templ.Component {
15141566 }()
15151567 }
15161568 ctx = templ.InitializeContext(ctx)
1517- templ_7745c5c3_Var80 := templ.GetChildren(ctx)
1518- if templ_7745c5c3_Var80 == nil {
1519- templ_7745c5c3_Var80 = templ.NopComponent
1569+ templ_7745c5c3_Var82 := templ.GetChildren(ctx)
1570+ if templ_7745c5c3_Var82 == nil {
1571+ templ_7745c5c3_Var82 = templ.NopComponent
15201572 }
15211573 ctx = templ.ClearChildren(ctx)
1522- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<section class=\"artifacts\" aria-labelledby=\"run-artifacts\"><h3 id=\"run-artifacts\">Artifacts <span class=\"count\">")
1574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "<section class=\"artifacts\" aria-labelledby=\"run-artifacts\"><h3 id=\"run-artifacts\">Artifacts <span class=\"count\">")
15231575 if templ_7745c5c3_Err != nil {
15241576 return templ_7745c5c3_Err
15251577 }
1526- var templ_7745c5c3_Var81 string
1527- templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(a.Rows)))
1578+ var templ_7745c5c3_Var83 string
1579+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(a.Rows)))
15281580 if templ_7745c5c3_Err != nil {
1529- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 212, Col: 74}
1581+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 222, Col: 74}
15301582 }
1531- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
1583+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
15321584 if templ_7745c5c3_Err != nil {
15331585 return templ_7745c5c3_Err
15341586 }
1535- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</span></h3><table class=\"list\" aria-label=\"Artifacts\"><thead><tr><th class=\"c-name\">name</th><th class=\"c-sub\">size</th><th class=\"c-who\">job</th><th class=\"c-when\">expires</th><th></th></tr></thead> <tbody>")
1587+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</span></h3><table class=\"list\" aria-label=\"Artifacts\"><thead><tr><th class=\"c-name\">name</th><th class=\"c-sub\">size</th><th class=\"c-who\">job</th><th class=\"c-when\">expires</th><th></th></tr></thead> <tbody>")
15361588 if templ_7745c5c3_Err != nil {
15371589 return templ_7745c5c3_Err
15381590 }
15391591 for _, r := range a.Rows {
1540- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<tr><td class=\"c-name\"><a href=\"")
1592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<tr><td class=\"c-name\"><a href=\"")
15411593 if templ_7745c5c3_Err != nil {
15421594 return templ_7745c5c3_Err
15431595 }
1544- var templ_7745c5c3_Var82 templ.SafeURL
1545- templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
1596+ var templ_7745c5c3_Var84 templ.SafeURL
1597+ templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
15461598 if templ_7745c5c3_Err != nil {
1547- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 220, Col: 56}
1599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 230, Col: 56}
15481600 }
1549- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var82))
1601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84))
15501602 if templ_7745c5c3_Err != nil {
15511603 return templ_7745c5c3_Err
15521604 }
1553- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\" download>")
1605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" download>")
15541606 if templ_7745c5c3_Err != nil {
15551607 return templ_7745c5c3_Err
15561608 }
1557- var templ_7745c5c3_Var83 string
1558- templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
1609+ var templ_7745c5c3_Var85 string
1610+ templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
15591611 if templ_7745c5c3_Err != nil {
1560- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 220, Col: 76}
1612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 230, Col: 76}
15611613 }
1562- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
1614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
15631615 if templ_7745c5c3_Err != nil {
15641616 return templ_7745c5c3_Err
15651617 }
1566- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</a></td><td class=\"c-sub dim\">")
1618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "</a></td><td class=\"c-sub dim\">")
15671619 if templ_7745c5c3_Err != nil {
15681620 return templ_7745c5c3_Err
15691621 }
1570- var templ_7745c5c3_Var84 string
1571- templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)
1622+ var templ_7745c5c3_Var86 string
1623+ templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(r.Size)
15721624 if templ_7745c5c3_Err != nil {
1573- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 221, Col: 36}
1625+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 231, Col: 36}
15741626 }
1575- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84))
1627+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
15761628 if templ_7745c5c3_Err != nil {
15771629 return templ_7745c5c3_Err
15781630 }
1579- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</td><td class=\"c-who\">")
1631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</td><td class=\"c-who\">")
15801632 if templ_7745c5c3_Err != nil {
15811633 return templ_7745c5c3_Err
15821634 }
15831635 if r.JobHref != "" {
1584- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<a href=\"")
1636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<a href=\"")
15851637 if templ_7745c5c3_Err != nil {
15861638 return templ_7745c5c3_Err
15871639 }
1588- var templ_7745c5c3_Var85 templ.SafeURL
1589- templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.JobHref))
1640+ var templ_7745c5c3_Var87 templ.SafeURL
1641+ templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.JobHref))
15901642 if templ_7745c5c3_Err != nil {
1591- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 224, Col: 42}
1643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 234, Col: 42}
15921644 }
1593- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
1645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87))
15941646 if templ_7745c5c3_Err != nil {
15951647 return templ_7745c5c3_Err
15961648 }
1597- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\">")
1649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\">")
15981650 if templ_7745c5c3_Err != nil {
15991651 return templ_7745c5c3_Err
16001652 }
1601- var templ_7745c5c3_Var86 string
1602- templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(r.Job)
1653+ var templ_7745c5c3_Var88 string
1654+ templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(r.Job)
16031655 if templ_7745c5c3_Err != nil {
1604- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 224, Col: 52}
1656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 234, Col: 52}
16051657 }
1606- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
1658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88))
16071659 if templ_7745c5c3_Err != nil {
16081660 return templ_7745c5c3_Err
16091661 }
1610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</a>")
1662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</a>")
16111663 if templ_7745c5c3_Err != nil {
16121664 return templ_7745c5c3_Err
16131665 }
16141666 } else {
1615- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<span class=\"dim\">")
1667+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<span class=\"dim\">")
16161668 if templ_7745c5c3_Err != nil {
16171669 return templ_7745c5c3_Err
16181670 }
1619- var templ_7745c5c3_Var87 string
1620- templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(r.Job)
1671+ var templ_7745c5c3_Var89 string
1672+ templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(r.Job)
16211673 if templ_7745c5c3_Err != nil {
1622- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 226, Col: 33}
1674+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 236, Col: 33}
16231675 }
1624- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87))
1676+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89))
16251677 if templ_7745c5c3_Err != nil {
16261678 return templ_7745c5c3_Err
16271679 }
1628- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "</span>")
1680+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "</span>")
16291681 if templ_7745c5c3_Err != nil {
16301682 return templ_7745c5c3_Err
16311683 }
16321684 }
1633- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "</td><td class=\"c-when dim\">")
1685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "</td><td class=\"c-when dim\">")
16341686 if templ_7745c5c3_Err != nil {
16351687 return templ_7745c5c3_Err
16361688 }
16371689 if r.ExpiresAt != "" {
1638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<time datetime=\"")
1690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<time datetime=\"")
16391691 if templ_7745c5c3_Err != nil {
16401692 return templ_7745c5c3_Err
16411693 }
1642- var templ_7745c5c3_Var88 string
1643- templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ExpiresAt)
1694+ var templ_7745c5c3_Var90 string
1695+ templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ExpiresAt)
16441696 if templ_7745c5c3_Err != nil {
1645- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 231, Col: 36}
1697+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 241, Col: 36}
16461698 }
1647- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
1699+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
16481700 if templ_7745c5c3_Err != nil {
16491701 return templ_7745c5c3_Err
16501702 }
1651- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "\">")
1703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "\">")
16521704 if templ_7745c5c3_Err != nil {
16531705 return templ_7745c5c3_Err
16541706 }
1655- var templ_7745c5c3_Var89 string
1656- templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(r.Expires)
1707+ var templ_7745c5c3_Var91 string
1708+ templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(r.Expires)
16571709 if templ_7745c5c3_Err != nil {
1658- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 231, Col: 50}
1710+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 241, Col: 50}
16591711 }
1660- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89))
1712+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91))
16611713 if templ_7745c5c3_Err != nil {
16621714 return templ_7745c5c3_Err
16631715 }
1664- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "</time>")
1716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "</time>")
16651717 if templ_7745c5c3_Err != nil {
16661718 return templ_7745c5c3_Err
16671719 }
16681720 } else {
1669- var templ_7745c5c3_Var90 string
1670- templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(r.Expires)
1721+ var templ_7745c5c3_Var92 string
1722+ templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(r.Expires)
16711723 if templ_7745c5c3_Err != nil {
1672- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 233, Col: 19}
1724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 243, Col: 19}
16731725 }
1674- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90))
1726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92))
16751727 if templ_7745c5c3_Err != nil {
16761728 return templ_7745c5c3_Err
16771729 }
16781730 }
1679- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</td><td>")
1731+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</td><td>")
16801732 if templ_7745c5c3_Err != nil {
16811733 return templ_7745c5c3_Err
16821734 }
16831735 if r.DeleteAction != "" {
1684- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<form method=\"post\" action=\"")
1736+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "<form method=\"post\" action=\"")
16851737 if templ_7745c5c3_Err != nil {
16861738 return templ_7745c5c3_Err
16871739 }
1688- var templ_7745c5c3_Var91 templ.SafeURL
1689- templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.DeleteAction))
1740+ var templ_7745c5c3_Var93 templ.SafeURL
1741+ templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.DeleteAction))
16901742 if templ_7745c5c3_Err != nil {
1691- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 238, Col: 66}
1743+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 248, Col: 66}
16921744 }
1693- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91))
1745+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
16941746 if templ_7745c5c3_Err != nil {
16951747 return templ_7745c5c3_Err
16961748 }
1697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "\"><button type=\"submit\" class=\"btn\" aria-label=\"")
1749+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "\"><button type=\"submit\" class=\"btn\" aria-label=\"")
16981750 if templ_7745c5c3_Err != nil {
16991751 return templ_7745c5c3_Err
17001752 }
1701- var templ_7745c5c3_Var92 string
1702- templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + r.Name)
1753+ var templ_7745c5c3_Var94 string
1754+ templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + r.Name)
17031755 if templ_7745c5c3_Err != nil {
1704- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 238, Col: 134}
1756+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 248, Col: 134}
17051757 }
1706- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
1758+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var94)
17071759 if templ_7745c5c3_Err != nil {
17081760 return templ_7745c5c3_Err
17091761 }
1710- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\">Delete</button></form>")
1762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\">Delete</button></form>")
17111763 if templ_7745c5c3_Err != nil {
17121764 return templ_7745c5c3_Err
17131765 }
17141766 }
1715- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "</td></tr>")
1767+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</td></tr>")
17161768 if templ_7745c5c3_Err != nil {
17171769 return templ_7745c5c3_Err
17181770 }
17191771 }
1720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</tbody></table><p class=\"mute\">")
1772+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</tbody></table><p class=\"mute\">")
17211773 if templ_7745c5c3_Err != nil {
17221774 return templ_7745c5c3_Err
17231775 }
1724- var templ_7745c5c3_Var93 string
1725- templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(a.Usage)
1776+ var templ_7745c5c3_Var95 string
1777+ templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(a.Usage)
17261778 if templ_7745c5c3_Err != nil {
1727- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 245, Col: 27}
1779+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 255, Col: 27}
17281780 }
1729- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
1781+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
17301782 if templ_7745c5c3_Err != nil {
17311783 return templ_7745c5c3_Err
17321784 }
1733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "</p></section>")
1785+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</p></section>")
17341786 if templ_7745c5c3_Err != nil {
17351787 return templ_7745c5c3_Err
17361788 }
@@ -1757,125 +1809,125 @@ func RunSummaries(sums []JobSummaries, more bool) templ.Component {
17571809 }()
17581810 }
17591811 ctx = templ.InitializeContext(ctx)
1760- templ_7745c5c3_Var94 := templ.GetChildren(ctx)
1761- if templ_7745c5c3_Var94 == nil {
1762- templ_7745c5c3_Var94 = templ.NopComponent
1812+ templ_7745c5c3_Var96 := templ.GetChildren(ctx)
1813+ if templ_7745c5c3_Var96 == nil {
1814+ templ_7745c5c3_Var96 = templ.NopComponent
17631815 }
17641816 ctx = templ.ClearChildren(ctx)
17651817 for _, j := range sums {
1766- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<section class=\"summaries\" aria-label=\"")
1818+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<section class=\"summaries\" aria-label=\"")
17671819 if templ_7745c5c3_Err != nil {
17681820 return templ_7745c5c3_Err
17691821 }
1770- var templ_7745c5c3_Var95 string
1771- templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.ResolveAttributeValue("Summary of " + j.Job)
1822+ var templ_7745c5c3_Var97 string
1823+ templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.ResolveAttributeValue("Summary of " + j.Job)
17721824 if templ_7745c5c3_Err != nil {
1773- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 254, Col: 63}
1825+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 264, Col: 63}
17741826 }
1775- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var95)
1827+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var97)
17761828 if templ_7745c5c3_Err != nil {
17771829 return templ_7745c5c3_Err
17781830 }
1779- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"><h3>")
1831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\"><h3>")
17801832 if templ_7745c5c3_Err != nil {
17811833 return templ_7745c5c3_Err
17821834 }
17831835 if j.JobHref != "" {
1784- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<a href=\"")
1836+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "<a href=\"")
17851837 if templ_7745c5c3_Err != nil {
17861838 return templ_7745c5c3_Err
17871839 }
1788- var templ_7745c5c3_Var96 templ.SafeURL
1789- templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(j.JobHref))
1840+ var templ_7745c5c3_Var98 templ.SafeURL
1841+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(j.JobHref))
17901842 if templ_7745c5c3_Err != nil {
1791- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 257, Col: 39}
1843+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 267, Col: 39}
17921844 }
1793- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
1845+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
17941846 if templ_7745c5c3_Err != nil {
17951847 return templ_7745c5c3_Err
17961848 }
1797- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\">")
1849+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "\">")
17981850 if templ_7745c5c3_Err != nil {
17991851 return templ_7745c5c3_Err
18001852 }
1801- var templ_7745c5c3_Var97 string
1802- templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(j.Job)
1853+ var templ_7745c5c3_Var99 string
1854+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(j.Job)
18031855 if templ_7745c5c3_Err != nil {
1804- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 257, Col: 49}
1856+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 267, Col: 49}
18051857 }
1806- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
1858+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
18071859 if templ_7745c5c3_Err != nil {
18081860 return templ_7745c5c3_Err
18091861 }
1810- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</a> ")
1862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</a> ")
18111863 if templ_7745c5c3_Err != nil {
18121864 return templ_7745c5c3_Err
18131865 }
18141866 } else {
1815- var templ_7745c5c3_Var98 string
1816- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(j.Job)
1867+ var templ_7745c5c3_Var100 string
1868+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(j.Job)
18171869 if templ_7745c5c3_Err != nil {
1818- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 259, Col: 12}
1870+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 269, Col: 12}
18191871 }
1820- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
1872+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
18211873 if templ_7745c5c3_Err != nil {
18221874 return templ_7745c5c3_Err
18231875 }
1824- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, " ")
1876+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, " ")
18251877 if templ_7745c5c3_Err != nil {
18261878 return templ_7745c5c3_Err
18271879 }
18281880 }
1829- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "<span class=\"mute\">summary</span></h3>")
1881+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "<span class=\"mute\">summary</span></h3>")
18301882 if templ_7745c5c3_Err != nil {
18311883 return templ_7745c5c3_Err
18321884 }
18331885 for _, s := range j.Steps {
1834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<article class=\"prose summary\" aria-label=\"")
1886+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "<article class=\"prose summary\" aria-label=\"")
18351887 if templ_7745c5c3_Err != nil {
18361888 return templ_7745c5c3_Err
18371889 }
1838- var templ_7745c5c3_Var99 string
1839- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Step)
1890+ var templ_7745c5c3_Var101 string
1891+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Step)
18401892 if templ_7745c5c3_Err != nil {
1841- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 264, Col: 54}
1893+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 274, Col: 54}
18421894 }
1843- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var99)
1895+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var101)
18441896 if templ_7745c5c3_Err != nil {
18451897 return templ_7745c5c3_Err
18461898 }
1847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\" hx-ignore><h4 class=\"summary-step\">")
1899+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\" hx-ignore><h4 class=\"summary-step\">")
18481900 if templ_7745c5c3_Err != nil {
18491901 return templ_7745c5c3_Err
18501902 }
1851- var templ_7745c5c3_Var100 string
1852- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(s.Step)
1903+ var templ_7745c5c3_Var102 string
1904+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(s.Step)
18531905 if templ_7745c5c3_Err != nil {
1854- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 265, Col: 38}
1906+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 275, Col: 38}
18551907 }
1856- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
1908+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
18571909 if templ_7745c5c3_Err != nil {
18581910 return templ_7745c5c3_Err
18591911 }
1860- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "</h4>")
1912+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</h4>")
18611913 if templ_7745c5c3_Err != nil {
18621914 return templ_7745c5c3_Err
18631915 }
18641916 if s.Note != "" {
1865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<p class=\"dim\">")
1917+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<p class=\"dim\">")
18661918 if templ_7745c5c3_Err != nil {
18671919 return templ_7745c5c3_Err
18681920 }
1869- var templ_7745c5c3_Var101 string
1870- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(s.Note)
1921+ var templ_7745c5c3_Var103 string
1922+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(s.Note)
18711923 if templ_7745c5c3_Err != nil {
1872- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 267, Col: 29}
1924+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 277, Col: 29}
18731925 }
1874- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
1926+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
18751927 if templ_7745c5c3_Err != nil {
18761928 return templ_7745c5c3_Err
18771929 }
1878- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</p>")
1930+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</p>")
18791931 if templ_7745c5c3_Err != nil {
18801932 return templ_7745c5c3_Err
18811933 }
@@ -1885,37 +1937,37 @@ func RunSummaries(sums []JobSummaries, more bool) templ.Component {
18851937 return templ_7745c5c3_Err
18861938 }
18871939 }
1888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "</article>")
1940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "</article>")
18891941 if templ_7745c5c3_Err != nil {
18901942 return templ_7745c5c3_Err
18911943 }
18921944 }
18931945 if j.More {
1894- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "<p class=\"mute\">the job has more summaries than the first 20, which are shown.</p>")
1946+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "<p class=\"mute\">the job has more summaries than the first 20, which are shown.</p>")
18951947 if templ_7745c5c3_Err != nil {
18961948 return templ_7745c5c3_Err
18971949 }
18981950 }
1899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</section>")
1951+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</section>")
19001952 if templ_7745c5c3_Err != nil {
19011953 return templ_7745c5c3_Err
19021954 }
19031955 }
19041956 if more {
1905- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<p class=\"mute\">more summaries are not shown: the summaries of the page are over ")
1957+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<p class=\"mute\">more summaries are not shown: the summaries of the page are over ")
19061958 if templ_7745c5c3_Err != nil {
19071959 return templ_7745c5c3_Err
19081960 }
1909- var templ_7745c5c3_Var102 string
1910- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(humanSize(maxSummaryPageBytes))
1961+ var templ_7745c5c3_Var104 string
1962+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(humanSize(maxSummaryPageBytes))
19111963 if templ_7745c5c3_Err != nil {
1912- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 279, Col: 115}
1964+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 289, Col: 115}
19131965 }
1914- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
1966+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
19151967 if templ_7745c5c3_Err != nil {
19161968 return templ_7745c5c3_Err
19171969 }
1918- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, ".</p>")
1970+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, ".</p>")
19191971 if templ_7745c5c3_Err != nil {
19201972 return templ_7745c5c3_Err
19211973 }
@@ -1940,269 +1992,269 @@ func runRail(p RunPage) templ.Component {
19401992 }()
19411993 }
19421994 ctx = templ.InitializeContext(ctx)
1943- templ_7745c5c3_Var103 := templ.GetChildren(ctx)
1944- if templ_7745c5c3_Var103 == nil {
1945- templ_7745c5c3_Var103 = templ.NopComponent
1995+ templ_7745c5c3_Var105 := templ.GetChildren(ctx)
1996+ if templ_7745c5c3_Var105 == nil {
1997+ templ_7745c5c3_Var105 = templ.NopComponent
19461998 }
19471999 ctx = templ.ClearChildren(ctx)
1948- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<section aria-labelledby=\"r-run\"><h2 id=\"r-run\">Run</h2><dl class=\"kv\"><dt>result</dt><dd>")
2000+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "<section aria-labelledby=\"r-run\"><h2 id=\"r-run\">Run</h2><dl class=\"kv\"><dt>result</dt><dd>")
19492001 if templ_7745c5c3_Err != nil {
19502002 return templ_7745c5c3_Err
19512003 }
1952- var templ_7745c5c3_Var104 string
1953- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
2004+ var templ_7745c5c3_Var106 string
2005+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
19542006 if templ_7745c5c3_Err != nil {
1955- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 288, Col: 16}
2007+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 298, Col: 16}
19562008 }
1957- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2009+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
19582010 if templ_7745c5c3_Err != nil {
19592011 return templ_7745c5c3_Err
19602012 }
1961- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</dd><dt>event</dt><dd>")
2013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</dd><dt>event</dt><dd>")
19622014 if templ_7745c5c3_Err != nil {
19632015 return templ_7745c5c3_Err
19642016 }
1965- var templ_7745c5c3_Var105 string
1966- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Event)
2017+ var templ_7745c5c3_Var107 string
2018+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(p.Event)
19672019 if templ_7745c5c3_Err != nil {
1968- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 290, Col: 16}
2020+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 300, Col: 16}
19692021 }
1970- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
2022+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
19712023 if templ_7745c5c3_Err != nil {
19722024 return templ_7745c5c3_Err
19732025 }
1974- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "</dd><dt>commit</dt><dd><a href=\"")
2026+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd><dt>commit</dt><dd><a href=\"")
19752027 if templ_7745c5c3_Err != nil {
19762028 return templ_7745c5c3_Err
19772029 }
1978- var templ_7745c5c3_Var106 templ.SafeURL
1979- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CHref))
2030+ var templ_7745c5c3_Var108 templ.SafeURL
2031+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CHref))
19802032 if templ_7745c5c3_Err != nil {
1981- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 292, Col: 39}
2033+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 302, Col: 39}
19822034 }
1983- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2035+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
19842036 if templ_7745c5c3_Err != nil {
19852037 return templ_7745c5c3_Err
19862038 }
1987- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "\">")
2039+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "\">")
19882040 if templ_7745c5c3_Err != nil {
19892041 return templ_7745c5c3_Err
19902042 }
1991- var templ_7745c5c3_Var107 string
1992- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(p.Short)
2043+ var templ_7745c5c3_Var109 string
2044+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(p.Short)
19932045 if templ_7745c5c3_Err != nil {
1994- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 292, Col: 51}
2046+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 302, Col: 51}
19952047 }
1996- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
2048+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
19972049 if templ_7745c5c3_Err != nil {
19982050 return templ_7745c5c3_Err
19992051 }
2000- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</a></dd><dt>ref</dt><dd><a href=\"")
2052+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "</a></dd><dt>ref</dt><dd><a href=\"")
20012053 if templ_7745c5c3_Err != nil {
20022054 return templ_7745c5c3_Err
20032055 }
2004- var templ_7745c5c3_Var108 templ.SafeURL
2005- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RefHref))
2056+ var templ_7745c5c3_Var110 templ.SafeURL
2057+ templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RefHref))
20062058 if templ_7745c5c3_Err != nil {
2007- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 294, Col: 41}
2059+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 304, Col: 41}
20082060 }
2009- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
2061+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
20102062 if templ_7745c5c3_Err != nil {
20112063 return templ_7745c5c3_Err
20122064 }
2013- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "\">")
2065+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "\">")
20142066 if templ_7745c5c3_Err != nil {
20152067 return templ_7745c5c3_Err
20162068 }
2017- var templ_7745c5c3_Var109 string
2018- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(p.Ref)
2069+ var templ_7745c5c3_Var111 string
2070+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Ref)
20192071 if templ_7745c5c3_Err != nil {
2020- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 294, Col: 51}
2072+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 304, Col: 51}
20212073 }
2022- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2074+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
20232075 if templ_7745c5c3_Err != nil {
20242076 return templ_7745c5c3_Err
20252077 }
2026- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "</a></dd><dt>actor</dt><dd>")
2078+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "</a></dd><dt>actor</dt><dd>")
20272079 if templ_7745c5c3_Err != nil {
20282080 return templ_7745c5c3_Err
20292081 }
2030- var templ_7745c5c3_Var110 string
2031- templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Actor)
2082+ var templ_7745c5c3_Var112 string
2083+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Actor)
20322084 if templ_7745c5c3_Err != nil {
2033- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 296, Col: 16}
2085+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 306, Col: 16}
20342086 }
2035- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
2087+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
20362088 if templ_7745c5c3_Err != nil {
20372089 return templ_7745c5c3_Err
20382090 }
2039- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "</dd><dt>attempt</dt><dd>")
2091+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "</dd><dt>attempt</dt><dd>")
20402092 if templ_7745c5c3_Err != nil {
20412093 return templ_7745c5c3_Err
20422094 }
2043- var templ_7745c5c3_Var111 string
2044- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Attempt)))
2095+ var templ_7745c5c3_Var113 string
2096+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Attempt)))
20452097 if templ_7745c5c3_Err != nil {
2046- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 298, Col: 29}
2098+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 308, Col: 29}
20472099 }
2048- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2100+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
20492101 if templ_7745c5c3_Err != nil {
20502102 return templ_7745c5c3_Err
20512103 }
2052- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, " of ")
2104+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, " of ")
20532105 if templ_7745c5c3_Err != nil {
20542106 return templ_7745c5c3_Err
20552107 }
2056- var templ_7745c5c3_Var112 string
2057- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Latest)))
2108+ var templ_7745c5c3_Var114 string
2109+ templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(int(p.Latest)))
20582110 if templ_7745c5c3_Err != nil {
2059- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 298, Col: 56}
2111+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 308, Col: 56}
20602112 }
2061- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2113+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
20622114 if templ_7745c5c3_Err != nil {
20632115 return templ_7745c5c3_Err
20642116 }
2065- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd></dl>")
2117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</dd></dl>")
20662118 if templ_7745c5c3_Err != nil {
20672119 return templ_7745c5c3_Err
20682120 }
20692121 if len(p.Attempts) > 0 {
2070- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<ul class=\"attempts\">")
2122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<ul class=\"attempts\">")
20712123 if templ_7745c5c3_Err != nil {
20722124 return templ_7745c5c3_Err
20732125 }
20742126 for _, a := range p.Attempts {
2075- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "<li>")
2127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<li>")
20762128 if templ_7745c5c3_Err != nil {
20772129 return templ_7745c5c3_Err
20782130 }
20792131 if a.Current {
2080- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<span aria-current=\"page\">")
2132+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<span aria-current=\"page\">")
20812133 if templ_7745c5c3_Err != nil {
20822134 return templ_7745c5c3_Err
20832135 }
2084- var templ_7745c5c3_Var113 string
2085- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label)
2136+ var templ_7745c5c3_Var115 string
2137+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label)
20862138 if templ_7745c5c3_Err != nil {
2087- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 305, Col: 42}
2139+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 315, Col: 42}
20882140 }
2089- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2141+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
20902142 if templ_7745c5c3_Err != nil {
20912143 return templ_7745c5c3_Err
20922144 }
2093- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "</span>")
2145+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</span>")
20942146 if templ_7745c5c3_Err != nil {
20952147 return templ_7745c5c3_Err
20962148 }
20972149 } else {
2098- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<a href=\"")
2150+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<a href=\"")
20992151 if templ_7745c5c3_Err != nil {
21002152 return templ_7745c5c3_Err
21012153 }
2102- var templ_7745c5c3_Var114 templ.SafeURL
2103- templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href))
2154+ var templ_7745c5c3_Var116 templ.SafeURL
2155+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(a.Href))
21042156 if templ_7745c5c3_Err != nil {
2105- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 307, Col: 38}
2157+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 317, Col: 38}
21062158 }
2107- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
2159+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
21082160 if templ_7745c5c3_Err != nil {
21092161 return templ_7745c5c3_Err
21102162 }
2111- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "\">")
2163+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\">")
21122164 if templ_7745c5c3_Err != nil {
21132165 return templ_7745c5c3_Err
21142166 }
2115- var templ_7745c5c3_Var115 string
2116- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label)
2167+ var templ_7745c5c3_Var117 string
2168+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(a.Label)
21172169 if templ_7745c5c3_Err != nil {
2118- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 307, Col: 50}
2170+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 317, Col: 50}
21192171 }
2120- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2172+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
21212173 if templ_7745c5c3_Err != nil {
21222174 return templ_7745c5c3_Err
21232175 }
2124- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</a>")
2176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</a>")
21252177 if templ_7745c5c3_Err != nil {
21262178 return templ_7745c5c3_Err
21272179 }
21282180 }
2129- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</li>")
2181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "</li>")
21302182 if templ_7745c5c3_Err != nil {
21312183 return templ_7745c5c3_Err
21322184 }
21332185 }
2134- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</ul>")
2186+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "</ul>")
21352187 if templ_7745c5c3_Err != nil {
21362188 return templ_7745c5c3_Err
21372189 }
21382190 }
2139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "</section>")
2191+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "</section>")
21402192 if templ_7745c5c3_Err != nil {
21412193 return templ_7745c5c3_Err
21422194 }
21432195 if p.CancelAction != "" || p.RerunAction != "" {
2144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<section aria-labelledby=\"r-act\" class=\"decide\"><h2 id=\"r-act\">Actions</h2><div class=\"d-btns\">")
2196+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "<section aria-labelledby=\"r-act\" class=\"decide\"><h2 id=\"r-act\">Actions</h2><div class=\"d-btns\">")
21452197 if templ_7745c5c3_Err != nil {
21462198 return templ_7745c5c3_Err
21472199 }
21482200 if p.CancelAction != "" {
2149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"")
2201+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<form method=\"post\" action=\"")
21502202 if templ_7745c5c3_Err != nil {
21512203 return templ_7745c5c3_Err
21522204 }
2153- var templ_7745c5c3_Var116 templ.SafeURL
2154- templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CancelAction))
2205+ var templ_7745c5c3_Var118 templ.SafeURL
2206+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.CancelAction))
21552207 if templ_7745c5c3_Err != nil {
2156- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 319, Col: 63}
2208+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 329, Col: 63}
21572209 }
2158- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
2210+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
21592211 if templ_7745c5c3_Err != nil {
21602212 return templ_7745c5c3_Err
21612213 }
2162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><button type=\"submit\" class=\"btn\">Cancel run</button></form>")
2214+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "\"><button type=\"submit\" class=\"btn\">Cancel run</button></form>")
21632215 if templ_7745c5c3_Err != nil {
21642216 return templ_7745c5c3_Err
21652217 }
21662218 }
21672219 if p.RerunAction != "" {
2168- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<form method=\"post\" action=\"")
2220+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "<form method=\"post\" action=\"")
21692221 if templ_7745c5c3_Err != nil {
21702222 return templ_7745c5c3_Err
21712223 }
2172- var templ_7745c5c3_Var117 templ.SafeURL
2173- templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunAction))
2224+ var templ_7745c5c3_Var119 templ.SafeURL
2225+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunAction))
21742226 if templ_7745c5c3_Err != nil {
2175- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 322, Col: 62}
2227+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 332, Col: 62}
21762228 }
2177- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
2229+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
21782230 if templ_7745c5c3_Err != nil {
21792231 return templ_7745c5c3_Err
21802232 }
2181- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\"><button type=\"submit\" class=\"btn primary\">Re-run all jobs</button></form>")
2233+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\"><button type=\"submit\" class=\"btn primary\">Re-run all jobs</button></form>")
21822234 if templ_7745c5c3_Err != nil {
21832235 return templ_7745c5c3_Err
21842236 }
21852237 }
21862238 if p.CanRerunFailed {
2187- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<form method=\"post\" action=\"")
2239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<form method=\"post\" action=\"")
21882240 if templ_7745c5c3_Err != nil {
21892241 return templ_7745c5c3_Err
21902242 }
2191- var templ_7745c5c3_Var118 templ.SafeURL
2192- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunFailedAction))
2243+ var templ_7745c5c3_Var120 templ.SafeURL
2244+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RerunFailedAction))
21932245 if templ_7745c5c3_Err != nil {
2194- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 325, Col: 68}
2246+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 335, Col: 68}
21952247 }
2196- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2248+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
21972249 if templ_7745c5c3_Err != nil {
21982250 return templ_7745c5c3_Err
21992251 }
2200- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "\"><button type=\"submit\" class=\"btn\">Re-run failed jobs</button></form>")
2252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "\"><button type=\"submit\" class=\"btn\">Re-run failed jobs</button></form>")
22012253 if templ_7745c5c3_Err != nil {
22022254 return templ_7745c5c3_Err
22032255 }
22042256 }
2205- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</div></section>")
2257+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</div></section>")
22062258 if templ_7745c5c3_Err != nil {
22072259 return templ_7745c5c3_Err
22082260 }
@@ -2229,25 +2281,25 @@ func JobHead(j JobRow, total int64) templ.Component {
22292281 }()
22302282 }
22312283 ctx = templ.InitializeContext(ctx)
2232- templ_7745c5c3_Var119 := templ.GetChildren(ctx)
2233- if templ_7745c5c3_Var119 == nil {
2234- templ_7745c5c3_Var119 = templ.NopComponent
2284+ templ_7745c5c3_Var121 := templ.GetChildren(ctx)
2285+ if templ_7745c5c3_Var121 == nil {
2286+ templ_7745c5c3_Var121 = templ.NopComponent
22352287 }
22362288 ctx = templ.ClearChildren(ctx)
2237- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<div id=\"job-head\" data-total=\"")
2289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "<div id=\"job-head\" data-total=\"")
22382290 if templ_7745c5c3_Err != nil {
22392291 return templ_7745c5c3_Err
22402292 }
2241- var templ_7745c5c3_Var120 string
2242- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(total))
2293+ var templ_7745c5c3_Var122 string
2294+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(total))
22432295 if templ_7745c5c3_Err != nil {
2244- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 335, Col: 46}
2296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 345, Col: 46}
22452297 }
2246- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var120)
2298+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)
22472299 if templ_7745c5c3_Err != nil {
22482300 return templ_7745c5c3_Err
22492301 }
2250- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "\"><h2 class=\"run-h\">")
2302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "\"><h2 class=\"run-h\">")
22512303 if templ_7745c5c3_Err != nil {
22522304 return templ_7745c5c3_Err
22532305 }
@@ -2255,74 +2307,74 @@ func JobHead(j JobRow, total int64) templ.Component {
22552307 if templ_7745c5c3_Err != nil {
22562308 return templ_7745c5c3_Err
22572309 }
2258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "<span>")
2310+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "<span>")
22592311 if templ_7745c5c3_Err != nil {
22602312 return templ_7745c5c3_Err
22612313 }
2262- var templ_7745c5c3_Var121 string
2263- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name)
2314+ var templ_7745c5c3_Var123 string
2315+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(j.Name)
22642316 if templ_7745c5c3_Err != nil {
2265- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 336, Col: 61}
2317+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 346, Col: 61}
22662318 }
2267- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2319+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
22682320 if templ_7745c5c3_Err != nil {
22692321 return templ_7745c5c3_Err
22702322 }
2271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</span> <span class=\"dim\">")
2323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</span> <span class=\"dim\">")
22722324 if templ_7745c5c3_Err != nil {
22732325 return templ_7745c5c3_Err
22742326 }
2275- var templ_7745c5c3_Var122 string
2276- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(j.State)
2327+ var templ_7745c5c3_Var124 string
2328+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(j.State)
22772329 if templ_7745c5c3_Err != nil {
2278- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 336, Col: 98}
2330+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 346, Col: 98}
22792331 }
2280- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2332+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
22812333 if templ_7745c5c3_Err != nil {
22822334 return templ_7745c5c3_Err
22832335 }
2284- var templ_7745c5c3_Var123 string
2285- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2336+ var templ_7745c5c3_Var125 string
2337+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
22862338 if templ_7745c5c3_Err != nil {
2287- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 336, Col: 105}
2339+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 346, Col: 105}
22882340 }
2289- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2341+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
22902342 if templ_7745c5c3_Err != nil {
22912343 return templ_7745c5c3_Err
22922344 }
2293- var templ_7745c5c3_Var124 string
2294- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration)
2345+ var templ_7745c5c3_Var126 string
2346+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(j.Duration)
22952347 if templ_7745c5c3_Err != nil {
2296- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 336, Col: 119}
2348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 346, Col: 119}
22972349 }
2298- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2350+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
22992351 if templ_7745c5c3_Err != nil {
23002352 return templ_7745c5c3_Err
23012353 }
2302- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</span></h2>")
2354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</span></h2>")
23032355 if templ_7745c5c3_Err != nil {
23042356 return templ_7745c5c3_Err
23052357 }
23062358 if j.Error != "" {
2307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "<p class=\"job-err\">")
2359+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<p class=\"job-err\">")
23082360 if templ_7745c5c3_Err != nil {
23092361 return templ_7745c5c3_Err
23102362 }
2311- var templ_7745c5c3_Var125 string
2312- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error)
2363+ var templ_7745c5c3_Var127 string
2364+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(j.Error)
23132365 if templ_7745c5c3_Err != nil {
2314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 338, Col: 31}
2366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 348, Col: 31}
23152367 }
2316- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2368+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
23172369 if templ_7745c5c3_Err != nil {
23182370 return templ_7745c5c3_Err
23192371 }
2320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</p>")
2372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</p>")
23212373 if templ_7745c5c3_Err != nil {
23222374 return templ_7745c5c3_Err
23232375 }
23242376 }
2325- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</div>")
2377+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</div>")
23262378 if templ_7745c5c3_Err != nil {
23272379 return templ_7745c5c3_Err
23282380 }
@@ -2347,17 +2399,17 @@ func JobSteps(steps []StepView) templ.Component {
23472399 }()
23482400 }
23492401 ctx = templ.InitializeContext(ctx)
2350- templ_7745c5c3_Var126 := templ.GetChildren(ctx)
2351- if templ_7745c5c3_Var126 == nil {
2352- templ_7745c5c3_Var126 = templ.NopComponent
2402+ templ_7745c5c3_Var128 := templ.GetChildren(ctx)
2403+ if templ_7745c5c3_Var128 == nil {
2404+ templ_7745c5c3_Var128 = templ.NopComponent
23532405 }
23542406 ctx = templ.ClearChildren(ctx)
2355- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<ol class=\"steps\" id=\"job-steps\">")
2407+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<ol class=\"steps\" id=\"job-steps\">")
23562408 if templ_7745c5c3_Err != nil {
23572409 return templ_7745c5c3_Err
23582410 }
23592411 for _, s := range steps {
2360- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "<li>")
2412+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "<li>")
23612413 if templ_7745c5c3_Err != nil {
23622414 return templ_7745c5c3_Err
23632415 }
@@ -2366,74 +2418,74 @@ func JobSteps(steps []StepView) templ.Component {
23662418 return templ_7745c5c3_Err
23672419 }
23682420 if s.LogEnd > s.LogStart {
2369- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "<a href=\"")
2421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "<a href=\"")
23702422 if templ_7745c5c3_Err != nil {
23712423 return templ_7745c5c3_Err
23722424 }
2373- var templ_7745c5c3_Var127 templ.SafeURL
2374- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#L" + itoa64(s.LogStart)))
2425+ var templ_7745c5c3_Var129 templ.SafeURL
2426+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("#L" + itoa64(s.LogStart)))
23752427 if templ_7745c5c3_Err != nil {
2376- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 350, Col: 55}
2428+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 360, Col: 55}
23772429 }
2378- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2430+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129))
23792431 if templ_7745c5c3_Err != nil {
23802432 return templ_7745c5c3_Err
23812433 }
2382- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "\">")
2434+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\">")
23832435 if templ_7745c5c3_Err != nil {
23842436 return templ_7745c5c3_Err
23852437 }
2386- var templ_7745c5c3_Var128 string
2387- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
2438+ var templ_7745c5c3_Var130 string
2439+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
23882440 if templ_7745c5c3_Err != nil {
2389- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 350, Col: 66}
2441+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 360, Col: 66}
23902442 }
2391- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2443+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130))
23922444 if templ_7745c5c3_Err != nil {
23932445 return templ_7745c5c3_Err
23942446 }
2395- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "</a> ")
2447+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "</a> ")
23962448 if templ_7745c5c3_Err != nil {
23972449 return templ_7745c5c3_Err
23982450 }
23992451 } else {
2400- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "<span>")
2452+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "<span>")
24012453 if templ_7745c5c3_Err != nil {
24022454 return templ_7745c5c3_Err
24032455 }
2404- var templ_7745c5c3_Var129 string
2405- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
2456+ var templ_7745c5c3_Var131 string
2457+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(s.Name)
24062458 if templ_7745c5c3_Err != nil {
2407- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 352, Col: 19}
2459+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 362, Col: 19}
24082460 }
2409- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129))
2461+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
24102462 if templ_7745c5c3_Err != nil {
24112463 return templ_7745c5c3_Err
24122464 }
2413- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</span> ")
2465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span> ")
24142466 if templ_7745c5c3_Err != nil {
24152467 return templ_7745c5c3_Err
24162468 }
24172469 }
2418- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<span class=\"dim\">")
2470+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<span class=\"dim\">")
24192471 if templ_7745c5c3_Err != nil {
24202472 return templ_7745c5c3_Err
24212473 }
2422- var templ_7745c5c3_Var130 string
2423- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration)
2474+ var templ_7745c5c3_Var132 string
2475+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(s.Duration)
24242476 if templ_7745c5c3_Err != nil {
2425- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 354, Col: 34}
2477+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 364, Col: 34}
24262478 }
2427- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130))
2479+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
24282480 if templ_7745c5c3_Err != nil {
24292481 return templ_7745c5c3_Err
24302482 }
2431- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</span></li>")
2483+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</span></li>")
24322484 if templ_7745c5c3_Err != nil {
24332485 return templ_7745c5c3_Err
24342486 }
24352487 }
2436- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</ol>")
2488+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</ol>")
24372489 if templ_7745c5c3_Err != nil {
24382490 return templ_7745c5c3_Err
24392491 }
@@ -2457,12 +2509,12 @@ func Job(l Layout, p JobPage) templ.Component {
24572509 }()
24582510 }
24592511 ctx = templ.InitializeContext(ctx)
2460- templ_7745c5c3_Var131 := templ.GetChildren(ctx)
2461- if templ_7745c5c3_Var131 == nil {
2462- templ_7745c5c3_Var131 = templ.NopComponent
2512+ templ_7745c5c3_Var133 := templ.GetChildren(ctx)
2513+ if templ_7745c5c3_Var133 == nil {
2514+ templ_7745c5c3_Var133 = templ.NopComponent
24632515 }
24642516 ctx = templ.ClearChildren(ctx)
2465- templ_7745c5c3_Var132 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2517+ templ_7745c5c3_Var134 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
24662518 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
24672519 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
24682520 if !templ_7745c5c3_IsBuffer {
@@ -2478,7 +2530,7 @@ func Job(l Layout, p JobPage) templ.Component {
24782530 if templ_7745c5c3_Err != nil {
24792531 return templ_7745c5c3_Err
24802532 }
2481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, " ")
2533+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " ")
24822534 if templ_7745c5c3_Err != nil {
24832535 return templ_7745c5c3_Err
24842536 }
@@ -2486,7 +2538,7 @@ func Job(l Layout, p JobPage) templ.Component {
24862538 if templ_7745c5c3_Err != nil {
24872539 return templ_7745c5c3_Err
24882540 }
2489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, " ")
2541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, " ")
24902542 if templ_7745c5c3_Err != nil {
24912543 return templ_7745c5c3_Err
24922544 }
@@ -2494,111 +2546,111 @@ func Job(l Layout, p JobPage) templ.Component {
24942546 if templ_7745c5c3_Err != nil {
24952547 return templ_7745c5c3_Err
24962548 }
2497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " <div id=\"job-log\" class=\"joblog\" data-total=\"")
2549+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " <div id=\"job-log\" class=\"joblog\" data-total=\"")
24982550 if templ_7745c5c3_Err != nil {
24992551 return templ_7745c5c3_Err
25002552 }
2501- var templ_7745c5c3_Var133 string
2502- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.Total))
2553+ var templ_7745c5c3_Var135 string
2554+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.Total))
25032555 if templ_7745c5c3_Err != nil {
2504- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 365, Col: 63}
2556+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 375, Col: 63}
25052557 }
2506- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)
2558+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
25072559 if templ_7745c5c3_Err != nil {
25082560 return templ_7745c5c3_Err
25092561 }
2510- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "\"")
2562+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "\"")
25112563 if templ_7745c5c3_Err != nil {
25122564 return templ_7745c5c3_Err
25132565 }
25142566 if p.Follow {
2515- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " data-follow")
2567+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, " data-follow")
25162568 if templ_7745c5c3_Err != nil {
25172569 return templ_7745c5c3_Err
25182570 }
25192571 }
2520- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, " data-stream=\"")
2572+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " data-stream=\"")
25212573 if templ_7745c5c3_Err != nil {
25222574 return templ_7745c5c3_Err
25232575 }
2524- var templ_7745c5c3_Var134 string
2525- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.StreamHref)
2576+ var templ_7745c5c3_Var136 string
2577+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.StreamHref)
25262578 if templ_7745c5c3_Err != nil {
2527- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 365, Col: 118}
2579+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 375, Col: 118}
25282580 }
2529- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
2581+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)
25302582 if templ_7745c5c3_Err != nil {
25312583 return templ_7745c5c3_Err
25322584 }
2533- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "\" data-next=\"")
2585+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\" data-next=\"")
25342586 if templ_7745c5c3_Err != nil {
25352587 return templ_7745c5c3_Err
25362588 }
2537- var templ_7745c5c3_Var135 string
2538- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + int64(len(p.Lines))))
2589+ var templ_7745c5c3_Var137 string
2590+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + int64(len(p.Lines))))
25392591 if templ_7745c5c3_Err != nil {
2540- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 365, Col: 169}
2592+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 375, Col: 169}
25412593 }
2542- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2594+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)
25432595 if templ_7745c5c3_Err != nil {
25442596 return templ_7745c5c3_Err
25452597 }
2546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "\">")
2598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\">")
25472599 if templ_7745c5c3_Err != nil {
25482600 return templ_7745c5c3_Err
25492601 }
25502602 if p.Note != "" {
2551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<p class=\"dim\">")
2603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "<p class=\"dim\">")
25522604 if templ_7745c5c3_Err != nil {
25532605 return templ_7745c5c3_Err
25542606 }
2555- var templ_7745c5c3_Var136 string
2556- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
2607+ var templ_7745c5c3_Var138 string
2608+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
25572609 if templ_7745c5c3_Err != nil {
2558- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 367, Col: 27}
2610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 377, Col: 27}
25592611 }
2560- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
25612613 if templ_7745c5c3_Err != nil {
25622614 return templ_7745c5c3_Err
25632615 }
2564- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</p>")
2616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</p>")
25652617 if templ_7745c5c3_Err != nil {
25662618 return templ_7745c5c3_Err
25672619 }
25682620 }
25692621 if p.Prev != "" {
2570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "<p class=\"more\"><a href=\"")
2622+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<p class=\"more\"><a href=\"")
25712623 if templ_7745c5c3_Err != nil {
25722624 return templ_7745c5c3_Err
25732625 }
2574- var templ_7745c5c3_Var137 templ.SafeURL
2575- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Prev))
2626+ var templ_7745c5c3_Var139 templ.SafeURL
2627+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Prev))
25762628 if templ_7745c5c3_Err != nil {
2577- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 370, Col: 51}
2629+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 380, Col: 51}
25782630 }
2579- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2631+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
25802632 if templ_7745c5c3_Err != nil {
25812633 return templ_7745c5c3_Err
25822634 }
2583- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\" rel=\"prev\">earlier lines</a></p>")
2635+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "\" rel=\"prev\">earlier lines</a></p>")
25842636 if templ_7745c5c3_Err != nil {
25852637 return templ_7745c5c3_Err
25862638 }
25872639 }
2588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "<ol class=\"log\" start=\"")
2640+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<ol class=\"log\" start=\"")
25892641 if templ_7745c5c3_Err != nil {
25902642 return templ_7745c5c3_Err
25912643 }
2592- var templ_7745c5c3_Var138 string
2593- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + 1))
2644+ var templ_7745c5c3_Var140 string
2645+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa64(p.From + 1))
25942646 if templ_7745c5c3_Err != nil {
2595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 372, Col: 45}
2647+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 382, Col: 45}
25962648 }
2597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2649+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var140)
25982650 if templ_7745c5c3_Err != nil {
25992651 return templ_7745c5c3_Err
26002652 }
2601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\">")
2653+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\">")
26022654 if templ_7745c5c3_Err != nil {
26032655 return templ_7745c5c3_Err
26042656 }
@@ -2608,36 +2660,36 @@ func Job(l Layout, p JobPage) templ.Component {
26082660 return templ_7745c5c3_Err
26092661 }
26102662 }
2611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "</ol>")
2663+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</ol>")
26122664 if templ_7745c5c3_Err != nil {
26132665 return templ_7745c5c3_Err
26142666 }
26152667 if p.Next != "" {
2616- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "<p class=\"more\"><a href=\"")
2668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "<p class=\"more\"><a href=\"")
26172669 if templ_7745c5c3_Err != nil {
26182670 return templ_7745c5c3_Err
26192671 }
2620- var templ_7745c5c3_Var139 templ.SafeURL
2621- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Next))
2672+ var templ_7745c5c3_Var141 templ.SafeURL
2673+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Next))
26222674 if templ_7745c5c3_Err != nil {
2623- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 378, Col: 51}
2675+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 388, Col: 51}
26242676 }
2625- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2677+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
26262678 if templ_7745c5c3_Err != nil {
26272679 return templ_7745c5c3_Err
26282680 }
2629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "\" rel=\"next\">more lines</a></p>")
2681+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\" rel=\"next\">more lines</a></p>")
26302682 if templ_7745c5c3_Err != nil {
26312683 return templ_7745c5c3_Err
26322684 }
26332685 }
2634- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "</div>")
2686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</div>")
26352687 if templ_7745c5c3_Err != nil {
26362688 return templ_7745c5c3_Err
26372689 }
26382690 return nil
26392691 })
2640- templ_7745c5c3_Err = Shell(l, jobRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var132), templ_7745c5c3_Buffer)
2692+ templ_7745c5c3_Err = Shell(l, jobRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var134), templ_7745c5c3_Buffer)
26412693 if templ_7745c5c3_Err != nil {
26422694 return templ_7745c5c3_Err
26432695 }
@@ -2663,92 +2715,92 @@ func LogLine(ln LogLineView) templ.Component {
26632715 }()
26642716 }
26652717 ctx = templ.InitializeContext(ctx)
2666- templ_7745c5c3_Var140 := templ.GetChildren(ctx)
2667- if templ_7745c5c3_Var140 == nil {
2668- templ_7745c5c3_Var140 = templ.NopComponent
2718+ templ_7745c5c3_Var142 := templ.GetChildren(ctx)
2719+ if templ_7745c5c3_Var142 == nil {
2720+ templ_7745c5c3_Var142 = templ.NopComponent
26692721 }
26702722 ctx = templ.ClearChildren(ctx)
2671- var templ_7745c5c3_Var141 = []any{logClass(ln)}
2672- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var141...)
2723+ var templ_7745c5c3_Var143 = []any{logClass(ln)}
2724+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var143...)
26732725 if templ_7745c5c3_Err != nil {
26742726 return templ_7745c5c3_Err
26752727 }
2676- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "<li id=\"")
2728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<li id=\"")
26772729 if templ_7745c5c3_Err != nil {
26782730 return templ_7745c5c3_Err
26792731 }
2680- var templ_7745c5c3_Var142 string
2681- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue("L" + itoa64(ln.N))
2732+ var templ_7745c5c3_Var144 string
2733+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue("L" + itoa64(ln.N))
26822734 if templ_7745c5c3_Err != nil {
2683- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 387, Col: 28}
2735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 397, Col: 28}
26842736 }
2685- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
2737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
26862738 if templ_7745c5c3_Err != nil {
26872739 return templ_7745c5c3_Err
26882740 }
2689- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "\" class=\"")
2741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "\" class=\"")
26902742 if templ_7745c5c3_Err != nil {
26912743 return templ_7745c5c3_Err
26922744 }
2693- var templ_7745c5c3_Var143 string
2694- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var141).String())
2745+ var templ_7745c5c3_Var145 string
2746+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var143).String())
26952747 if templ_7745c5c3_Err != nil {
26962748 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 1, Col: 0}
26972749 }
2698- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
2750+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)
26992751 if templ_7745c5c3_Err != nil {
27002752 return templ_7745c5c3_Err
27012753 }
2702- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\" data-step=\"")
2754+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "\" data-step=\"")
27032755 if templ_7745c5c3_Err != nil {
27042756 return templ_7745c5c3_Err
27052757 }
2706- var templ_7745c5c3_Var144 string
2707- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(int(ln.Step)))
2758+ var templ_7745c5c3_Var146 string
2759+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(int(ln.Step)))
27082760 if templ_7745c5c3_Err != nil {
2709- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 387, Col: 84}
2761+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 397, Col: 84}
27102762 }
2711- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
2763+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)
27122764 if templ_7745c5c3_Err != nil {
27132765 return templ_7745c5c3_Err
27142766 }
2715- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "\"")
2767+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\"")
27162768 if templ_7745c5c3_Err != nil {
27172769 return templ_7745c5c3_Err
27182770 }
27192771 if ln.Continued {
2720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, " data-cont")
2772+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, " data-cont")
27212773 if templ_7745c5c3_Err != nil {
27222774 return templ_7745c5c3_Err
27232775 }
27242776 }
2725- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "><time>")
2777+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "><time>")
27262778 if templ_7745c5c3_Err != nil {
27272779 return templ_7745c5c3_Err
27282780 }
2729- var templ_7745c5c3_Var145 string
2730- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Time)
2781+ var templ_7745c5c3_Var147 string
2782+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Time)
27312783 if templ_7745c5c3_Err != nil {
2732- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 387, Col: 130}
2784+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 397, Col: 130}
27332785 }
2734- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
2786+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
27352787 if templ_7745c5c3_Err != nil {
27362788 return templ_7745c5c3_Err
27372789 }
2738- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</time><span class=\"t\">")
2790+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</time><span class=\"t\">")
27392791 if templ_7745c5c3_Err != nil {
27402792 return templ_7745c5c3_Err
27412793 }
2742- var templ_7745c5c3_Var146 string
2743- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Text)
2794+ var templ_7745c5c3_Var148 string
2795+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(ln.Text)
27442796 if templ_7745c5c3_Err != nil {
2745- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 387, Col: 164}
2797+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 397, Col: 164}
27462798 }
2747- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
2799+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
27482800 if templ_7745c5c3_Err != nil {
27492801 return templ_7745c5c3_Err
27502802 }
2751- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</span></li>")
2803+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "</span></li>")
27522804 if templ_7745c5c3_Err != nil {
27532805 return templ_7745c5c3_Err
27542806 }
@@ -2772,12 +2824,12 @@ func jobRail(p JobPage) templ.Component {
27722824 }()
27732825 }
27742826 ctx = templ.InitializeContext(ctx)
2775- templ_7745c5c3_Var147 := templ.GetChildren(ctx)
2776- if templ_7745c5c3_Var147 == nil {
2777- templ_7745c5c3_Var147 = templ.NopComponent
2827+ templ_7745c5c3_Var149 := templ.GetChildren(ctx)
2828+ if templ_7745c5c3_Var149 == nil {
2829+ templ_7745c5c3_Var149 = templ.NopComponent
27782830 }
27792831 ctx = templ.ClearChildren(ctx)
2780- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<section aria-labelledby=\"r-job\"><h2 id=\"r-job\">Steps</h2>")
2832+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "<section aria-labelledby=\"r-job\"><h2 id=\"r-job\">Steps</h2>")
27812833 if templ_7745c5c3_Err != nil {
27822834 return templ_7745c5c3_Err
27832835 }
@@ -2785,101 +2837,101 @@ func jobRail(p JobPage) templ.Component {
27852837 if templ_7745c5c3_Err != nil {
27862838 return templ_7745c5c3_Err
27872839 }
2788- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</section><section aria-labelledby=\"r-jobf\"><h2 id=\"r-jobf\">Job</h2><dl class=\"kv\"><dt>run</dt><dd><a href=\"")
2840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section><section aria-labelledby=\"r-jobf\"><h2 id=\"r-jobf\">Job</h2><dl class=\"kv\"><dt>run</dt><dd><a href=\"")
27892841 if templ_7745c5c3_Err != nil {
27902842 return templ_7745c5c3_Err
27912843 }
2792- var templ_7745c5c3_Var148 templ.SafeURL
2793- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RunHref))
2844+ var templ_7745c5c3_Var150 templ.SafeURL
2845+ templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RunHref))
27942846 if templ_7745c5c3_Err != nil {
2795- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 399, Col: 41}
2847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 409, Col: 41}
27962848 }
2797- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
2849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
27982850 if templ_7745c5c3_Err != nil {
27992851 return templ_7745c5c3_Err
28002852 }
2801- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\">")
2853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\">")
28022854 if templ_7745c5c3_Err != nil {
28032855 return templ_7745c5c3_Err
28042856 }
2805- var templ_7745c5c3_Var149 string
2806- templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(p.RunName)
2857+ var templ_7745c5c3_Var151 string
2858+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(p.RunName)
28072859 if templ_7745c5c3_Err != nil {
2808- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 399, Col: 55}
2860+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 409, Col: 55}
28092861 }
2810- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
2862+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
28112863 if templ_7745c5c3_Err != nil {
28122864 return templ_7745c5c3_Err
28132865 }
2814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</a></dd>")
2866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</a></dd>")
28152867 if templ_7745c5c3_Err != nil {
28162868 return templ_7745c5c3_Err
28172869 }
28182870 if p.Runner != "" {
2819- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "<dt>runner</dt><dd>")
2871+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "<dt>runner</dt><dd>")
28202872 if templ_7745c5c3_Err != nil {
28212873 return templ_7745c5c3_Err
28222874 }
2823- var templ_7745c5c3_Var150 string
2824- templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(p.Runner)
2875+ var templ_7745c5c3_Var152 string
2876+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Runner)
28252877 if templ_7745c5c3_Err != nil {
2826- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 402, Col: 18}
2878+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 412, Col: 18}
28272879 }
2828- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
2880+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
28292881 if templ_7745c5c3_Err != nil {
28302882 return templ_7745c5c3_Err
28312883 }
2832- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</dd>")
2884+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "</dd>")
28332885 if templ_7745c5c3_Err != nil {
28342886 return templ_7745c5c3_Err
28352887 }
28362888 }
28372889 if p.Isolation != "" {
2838- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<dt>isolation</dt><dd>")
2890+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "<dt>isolation</dt><dd>")
28392891 if templ_7745c5c3_Err != nil {
28402892 return templ_7745c5c3_Err
28412893 }
2842- var templ_7745c5c3_Var151 string
2843- templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(p.Isolation)
2894+ var templ_7745c5c3_Var153 string
2895+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(p.Isolation)
28442896 if templ_7745c5c3_Err != nil {
2845- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 406, Col: 21}
2897+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 416, Col: 21}
28462898 }
2847- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
2899+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
28482900 if templ_7745c5c3_Err != nil {
28492901 return templ_7745c5c3_Err
28502902 }
2851- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</dd>")
2903+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</dd>")
28522904 if templ_7745c5c3_Err != nil {
28532905 return templ_7745c5c3_Err
28542906 }
28552907 }
2856- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "<dt>lines</dt><dd>")
2908+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "<dt>lines</dt><dd>")
28572909 if templ_7745c5c3_Err != nil {
28582910 return templ_7745c5c3_Err
28592911 }
2860- var templ_7745c5c3_Var152 string
2861- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(itoa64(p.Total))
2912+ var templ_7745c5c3_Var154 string
2913+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(itoa64(p.Total))
28622914 if templ_7745c5c3_Err != nil {
2863- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 409, Col: 24}
2915+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 419, Col: 24}
28642916 }
2865- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
2917+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
28662918 if templ_7745c5c3_Err != nil {
28672919 return templ_7745c5c3_Err
28682920 }
2869- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "</dd></dl><p><a href=\"")
2921+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</dd></dl><p><a href=\"")
28702922 if templ_7745c5c3_Err != nil {
28712923 return templ_7745c5c3_Err
28722924 }
2873- var templ_7745c5c3_Var153 templ.SafeURL
2874- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RawHref))
2925+ var templ_7745c5c3_Var155 templ.SafeURL
2926+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.RawHref))
28752927 if templ_7745c5c3_Err != nil {
2876- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 411, Col: 39}
2928+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 421, Col: 39}
28772929 }
2878- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
2930+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
28792931 if templ_7745c5c3_Err != nil {
28802932 return templ_7745c5c3_Err
28812933 }
2882- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\">raw log</a></p></section>")
2934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "\">raw log</a></p></section>")
28832935 if templ_7745c5c3_Err != nil {
28842936 return templ_7745c5c3_Err
28852937 }
@@ -2905,39 +2957,39 @@ func CommitChecks(c ChecksSummary) templ.Component {
29052957 }()
29062958 }
29072959 ctx = templ.InitializeContext(ctx)
2908- templ_7745c5c3_Var154 := templ.GetChildren(ctx)
2909- if templ_7745c5c3_Var154 == nil {
2910- templ_7745c5c3_Var154 = templ.NopComponent
2960+ templ_7745c5c3_Var156 := templ.GetChildren(ctx)
2961+ if templ_7745c5c3_Var156 == nil {
2962+ templ_7745c5c3_Var156 = templ.NopComponent
29112963 }
29122964 ctx = templ.ClearChildren(ctx)
29132965 if c.Total > 0 {
2914- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "<a class=\"ck-sum\" href=\"")
2966+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "<a class=\"ck-sum\" href=\"")
29152967 if templ_7745c5c3_Err != nil {
29162968 return templ_7745c5c3_Err
29172969 }
2918- var templ_7745c5c3_Var155 templ.SafeURL
2919- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
2970+ var templ_7745c5c3_Var157 templ.SafeURL
2971+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
29202972 if templ_7745c5c3_Err != nil {
2921- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 419, Col: 48}
2973+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 429, Col: 48}
29222974 }
2923- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
2975+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
29242976 if templ_7745c5c3_Err != nil {
29252977 return templ_7745c5c3_Err
29262978 }
2927- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "\" title=\"")
2979+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "\" title=\"")
29282980 if templ_7745c5c3_Err != nil {
29292981 return templ_7745c5c3_Err
29302982 }
2931- var templ_7745c5c3_Var156 string
2932- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Title)
2983+ var templ_7745c5c3_Var158 string
2984+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Title)
29332985 if templ_7745c5c3_Err != nil {
2934- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 419, Col: 66}
2986+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 429, Col: 66}
29352987 }
2936- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var156)
2988+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var158)
29372989 if templ_7745c5c3_Err != nil {
29382990 return templ_7745c5c3_Err
29392991 }
2940- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "\">")
2992+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "\">")
29412993 if templ_7745c5c3_Err != nil {
29422994 return templ_7745c5c3_Err
29432995 }
@@ -2945,20 +2997,20 @@ func CommitChecks(c ChecksSummary) templ.Component {
29452997 if templ_7745c5c3_Err != nil {
29462998 return templ_7745c5c3_Err
29472999 }
2948- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<span class=\"mute\">")
3000+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<span class=\"mute\">")
29493001 if templ_7745c5c3_Err != nil {
29503002 return templ_7745c5c3_Err
29513003 }
2952- var templ_7745c5c3_Var157 string
2953- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
3004+ var templ_7745c5c3_Var159 string
3005+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
29543006 if templ_7745c5c3_Err != nil {
2955- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 421, Col: 31}
3007+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 431, Col: 31}
29563008 }
2957- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
3009+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
29583010 if templ_7745c5c3_Err != nil {
29593011 return templ_7745c5c3_Err
29603012 }
2961- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</span></a>")
3013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "</span></a>")
29623014 if templ_7745c5c3_Err != nil {
29633015 return templ_7745c5c3_Err
29643016 }
@@ -2986,25 +3038,25 @@ func MRChecksSum(c ChecksSummary) templ.Component {
29863038 }()
29873039 }
29883040 ctx = templ.InitializeContext(ctx)
2989- templ_7745c5c3_Var158 := templ.GetChildren(ctx)
2990- if templ_7745c5c3_Var158 == nil {
2991- templ_7745c5c3_Var158 = templ.NopComponent
3041+ templ_7745c5c3_Var160 := templ.GetChildren(ctx)
3042+ if templ_7745c5c3_Var160 == nil {
3043+ templ_7745c5c3_Var160 = templ.NopComponent
29923044 }
29933045 ctx = templ.ClearChildren(ctx)
2994- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "<a class=\"ck-sum mr-ck\" id=\"mr-ck\" href=\"#mr-checks\" title=\"")
3046+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "<a class=\"ck-sum mr-ck\" id=\"mr-ck\" href=\"#mr-checks\" title=\"")
29953047 if templ_7745c5c3_Err != nil {
29963048 return templ_7745c5c3_Err
29973049 }
2998- var templ_7745c5c3_Var159 string
2999- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Title)
3050+ var templ_7745c5c3_Var161 string
3051+ templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Title)
30003052 if templ_7745c5c3_Err != nil {
3001- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 430, Col: 69}
3053+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 440, Col: 69}
30023054 }
3003- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)
3055+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var161)
30043056 if templ_7745c5c3_Err != nil {
30053057 return templ_7745c5c3_Err
30063058 }
3007- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "\">")
3059+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "\">")
30083060 if templ_7745c5c3_Err != nil {
30093061 return templ_7745c5c3_Err
30103062 }
@@ -3013,30 +3065,30 @@ func MRChecksSum(c ChecksSummary) templ.Component {
30133065 if templ_7745c5c3_Err != nil {
30143066 return templ_7745c5c3_Err
30153067 }
3016- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, " <span>")
3068+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, " <span>")
30173069 if templ_7745c5c3_Err != nil {
30183070 return templ_7745c5c3_Err
30193071 }
3020- var templ_7745c5c3_Var160 string
3021- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
3072+ var templ_7745c5c3_Var162 string
3073+ templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
30223074 if templ_7745c5c3_Err != nil {
3023- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 433, Col: 18}
3075+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 443, Col: 18}
30243076 }
3025- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3077+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var162))
30263078 if templ_7745c5c3_Err != nil {
30273079 return templ_7745c5c3_Err
30283080 }
3029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</span>")
3081+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</span>")
30303082 if templ_7745c5c3_Err != nil {
30313083 return templ_7745c5c3_Err
30323084 }
30333085 } else {
3034- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "<span class=\"mute\">no checks</span>")
3086+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "<span class=\"mute\">no checks</span>")
30353087 if templ_7745c5c3_Err != nil {
30363088 return templ_7745c5c3_Err
30373089 }
30383090 }
3039- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "</a>")
3091+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "</a>")
30403092 if templ_7745c5c3_Err != nil {
30413093 return templ_7745c5c3_Err
30423094 }
@@ -3061,40 +3113,40 @@ func MRChecks(c ChecksSection) templ.Component {
30613113 }()
30623114 }
30633115 ctx = templ.InitializeContext(ctx)
3064- templ_7745c5c3_Var161 := templ.GetChildren(ctx)
3065- if templ_7745c5c3_Var161 == nil {
3066- templ_7745c5c3_Var161 = templ.NopComponent
3116+ templ_7745c5c3_Var163 := templ.GetChildren(ctx)
3117+ if templ_7745c5c3_Var163 == nil {
3118+ templ_7745c5c3_Var163 = templ.NopComponent
30673119 }
30683120 ctx = templ.ClearChildren(ctx)
3069- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "<section aria-labelledby=\"r-checks\" id=\"mr-checks\"><h2 id=\"r-checks\">Checks <span class=\"count\">")
3121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "<section aria-labelledby=\"r-checks\" id=\"mr-checks\"><h2 id=\"r-checks\">Checks <span class=\"count\">")
30703122 if templ_7745c5c3_Err != nil {
30713123 return templ_7745c5c3_Err
30723124 }
3073- var templ_7745c5c3_Var162 string
3074- templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(c.Rows)))
3125+ var templ_7745c5c3_Var164 string
3126+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(c.Rows)))
30753127 if templ_7745c5c3_Err != nil {
3076- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 443, Col: 66}
3128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 453, Col: 66}
30773129 }
3078- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var162))
3130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
30793131 if templ_7745c5c3_Err != nil {
30803132 return templ_7745c5c3_Err
30813133 }
3082- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "</span></h2>")
3134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "</span></h2>")
30833135 if templ_7745c5c3_Err != nil {
30843136 return templ_7745c5c3_Err
30853137 }
30863138 if len(c.Rows) == 0 {
3087- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "<p class=\"dim\">no checks</p>")
3139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "<p class=\"dim\">no checks</p>")
30883140 if templ_7745c5c3_Err != nil {
30893141 return templ_7745c5c3_Err
30903142 }
30913143 } else {
3092- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "<ul class=\"checks\">")
3144+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "<ul class=\"checks\">")
30933145 if templ_7745c5c3_Err != nil {
30943146 return templ_7745c5c3_Err
30953147 }
30963148 for _, r := range c.Rows {
3097- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "<li>")
3149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li>")
30983150 if templ_7745c5c3_Err != nil {
30993151 return templ_7745c5c3_Err
31003152 }
@@ -3103,108 +3155,108 @@ func MRChecks(c ChecksSection) templ.Component {
31033155 return templ_7745c5c3_Err
31043156 }
31053157 if r.Href != "" {
3106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "<a href=\"")
3158+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<a href=\"")
31073159 if templ_7745c5c3_Err != nil {
31083160 return templ_7745c5c3_Err
31093161 }
3110- var templ_7745c5c3_Var163 templ.SafeURL
3111- templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
3162+ var templ_7745c5c3_Var165 templ.SafeURL
3163+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
31123164 if templ_7745c5c3_Err != nil {
3113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 452, Col: 38}
3165+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 38}
31143166 }
3115- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
3167+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
31163168 if templ_7745c5c3_Err != nil {
31173169 return templ_7745c5c3_Err
31183170 }
3119- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "\">")
3171+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">")
31203172 if templ_7745c5c3_Err != nil {
31213173 return templ_7745c5c3_Err
31223174 }
3123- var templ_7745c5c3_Var164 string
3124- templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3175+ var templ_7745c5c3_Var166 string
3176+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
31253177 if templ_7745c5c3_Err != nil {
3126- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 452, Col: 49}
3178+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 49}
31273179 }
3128- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
3180+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
31293181 if templ_7745c5c3_Err != nil {
31303182 return templ_7745c5c3_Err
31313183 }
3132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "</a> ")
3184+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</a> ")
31333185 if templ_7745c5c3_Err != nil {
31343186 return templ_7745c5c3_Err
31353187 }
31363188 } else {
3137- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "<span>")
3189+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span>")
31383190 if templ_7745c5c3_Err != nil {
31393191 return templ_7745c5c3_Err
31403192 }
3141- var templ_7745c5c3_Var165 string
3142- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3193+ var templ_7745c5c3_Var167 string
3194+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
31433195 if templ_7745c5c3_Err != nil {
3144- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 454, Col: 21}
3196+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 464, Col: 21}
31453197 }
3146- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3198+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
31473199 if templ_7745c5c3_Err != nil {
31483200 return templ_7745c5c3_Err
31493201 }
3150- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "</span> ")
3202+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span> ")
31513203 if templ_7745c5c3_Err != nil {
31523204 return templ_7745c5c3_Err
31533205 }
31543206 }
3155- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "<span class=\"dim\">")
3207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<span class=\"dim\">")
31563208 if templ_7745c5c3_Err != nil {
31573209 return templ_7745c5c3_Err
31583210 }
3159- var templ_7745c5c3_Var166 string
3160- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
3211+ var templ_7745c5c3_Var168 string
3212+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
31613213 if templ_7745c5c3_Err != nil {
3162- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 456, Col: 33}
3214+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 466, Col: 33}
31633215 }
3164- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3216+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
31653217 if templ_7745c5c3_Err != nil {
31663218 return templ_7745c5c3_Err
31673219 }
3168- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "</span> ")
3220+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> ")
31693221 if templ_7745c5c3_Err != nil {
31703222 return templ_7745c5c3_Err
31713223 }
31723224 if r.Waiting {
3173- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<span class=\"mute\">waiting for a runner</span>")
3225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<span class=\"mute\">waiting for a runner</span>")
31743226 if templ_7745c5c3_Err != nil {
31753227 return templ_7745c5c3_Err
31763228 }
31773229 }
3178- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</li>")
3230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</li>")
31793231 if templ_7745c5c3_Err != nil {
31803232 return templ_7745c5c3_Err
31813233 }
31823234 }
3183- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</ul>")
3235+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>")
31843236 if templ_7745c5c3_Err != nil {
31853237 return templ_7745c5c3_Err
31863238 }
31873239 }
31883240 if c.Note != "" {
3189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")
3241+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")
31903242 if templ_7745c5c3_Err != nil {
31913243 return templ_7745c5c3_Err
31923244 }
3193- var templ_7745c5c3_Var167 string
3194- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)
3245+ var templ_7745c5c3_Var169 string
3246+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)
31953247 if templ_7745c5c3_Err != nil {
3196- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 62}
3248+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 475, Col: 62}
31973249 }
3198- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3250+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
31993251 if templ_7745c5c3_Err != nil {
32003252 return templ_7745c5c3_Err
32013253 }
3202- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</p>")
3254+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>")
32033255 if templ_7745c5c3_Err != nil {
32043256 return templ_7745c5c3_Err
32053257 }
32063258 }
3207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</section>")
3259+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</section>")
32083260 if templ_7745c5c3_Err != nil {
32093261 return templ_7745c5c3_Err
32103262 }
M internal/app/api_test.go +1 -1
@@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) {
196196 for _, who := range allPrincipals {
197197 resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{}))
198198 require.NoError(t, err, who)
199- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, resp.Msg.GetFeatures(), who)
199+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, resp.Msg.GetFeatures(), who)
200200 assert.NotEmpty(t, resp.Msg.GetVersion())
201201 }
202202 })
A internal/app/ci_workflow_test.go +359 -0
@@ -0,0 +1,359 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package app
4+
5+import (
6+ "testing"
7+ "time"
8+
9+ "connectrpc.com/connect"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ pb "bnema.dev/ermite-api/gen/ermite"
14+ "bnema.dev/ermite/internal/domain"
15+ "bnema.dev/ermite/internal/usecase"
16+)
17+
18+// `ermite workflow disable|enable|list` (P8, D5): a disabled workflow starts no
19+// run for any event, and enabling it restores it.
20+
21+const releaseWF = ".github/workflows/release.yml"
22+
23+// wfOn is a workflow file with the given "on:" block (indented under on:).
24+func wfOn(name, on string) string {
25+ return "name: " + name + "\non:\n" + on + "jobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"
26+}
27+
28+func (c *ciHarness) setWorkflow(api *clients, disable bool, file string) (*pb.Workflow, error) {
29+ c.t.Helper()
30+ if disable {
31+ res, err := api.run.DisableWorkflow(c.ctx, connect.NewRequest(&pb.DisableWorkflowRequest{Repo: c.repo, Workflow: file}))
32+ if err != nil {
33+ return nil, err
34+ }
35+ return res.Msg.GetWorkflow(), nil
36+ }
37+ res, err := api.run.EnableWorkflow(c.ctx, connect.NewRequest(&pb.EnableWorkflowRequest{Repo: c.repo, Workflow: file}))
38+ if err != nil {
39+ return nil, err
40+ }
41+ return res.Msg.GetWorkflow(), nil
42+}
43+
44+func (c *ciHarness) listWorkflows(api *clients) map[string]*pb.Workflow {
45+ c.t.Helper()
46+ res, err := api.run.ListWorkflows(c.ctx, connect.NewRequest(&pb.ListWorkflowsRequest{Repo: c.repo}))
47+ require.NoError(c.t, err)
48+ out := map[string]*pb.Workflow{}
49+ for _, w := range res.Msg.GetWorkflows() {
50+ out[w.GetPath()] = w
51+ }
52+ return out
53+}
54+
55+func runsOfWorkflow(runs []*pb.Run, path string) []*pb.Run {
56+ var out []*pb.Run
57+ for _, r := range runs {
58+ if r.GetWorkflowPath() == path {
59+ out = append(out, r)
60+ }
61+ }
62+ return out
63+}
64+
65+// Disabled before the first push, on an empty repository where the file does
66+// not exist yet: the push of many refs starts nothing of it, the other
67+// workflows still run, and enabling restores it.
68+func TestWorkflowDisabledBeforeFirstPushStartsNoRun(t *testing.T) {
69+ c := newCIHarness(t, nil)
70+ runner := c.registerRunner("alpha", "ubuntu-latest")
71+
72+ w, err := c.setWorkflow(c.root, true, "release.yml")
73+ require.NoError(t, err, "the repository is empty and has no such file")
74+ assert.Equal(t, releaseWF, w.GetPath(), "stored by its normalized path")
75+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_DISABLED, w.GetState())
76+ _, err = c.setWorkflow(c.root, true, releaseWF)
77+ require.NoError(t, err, "idempotent, by path too")
78+
79+ // The list shows it though no branch holds the file.
80+ listed := c.listWorkflows(c.root)
81+ require.Len(t, listed, 1)
82+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_DISABLED, listed[releaseWF].GetState())
83+ assert.Empty(t, listed[releaseWF].GetName(), "no file, no name")
84+
85+ push := wfOn("release", " push:\n tags: ['v*']\n branches: [main]\n")
86+ c.push("main", map[string]string{
87+ releaseWF: push,
88+ ".github/workflows/z-ctrl.yml": wfOn("control", " push:\n branches: [main]\n"),
89+ }, "")
90+ c.waitFor("the control run", func() bool { return len(runsOfWorkflow(c.runs(), ".github/workflows/z-ctrl.yml")) == 1 })
91+ assert.Empty(t, runsOfWorkflow(c.runs(), releaseWF), "a disabled workflow starts no push run")
92+ runner.runJob(runner.await(), pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "ctrl")
93+
94+ // Tags: many at once.
95+ for _, tag := range []string{"v1", "v2", "v3"} {
96+ c.mustGit(c.wt, "tag", tag)
97+ }
98+ c.mustGit(c.wt, "push", "-q", c.urlAuth(adminToken, "/"+c.repo+".git"), "v1", "v2", "v3")
99+ c.push("main", map[string]string{"x.txt": "x"}, "")
100+ c.waitFor("the control run of the next push", func() bool { return len(runsOfWorkflow(c.runs(), ".github/workflows/z-ctrl.yml")) == 2 })
101+ assert.Empty(t, runsOfWorkflow(c.runs(), releaseWF))
102+
103+ listed = c.listWorkflows(c.root)
104+ assert.Equal(t, "release", listed[releaseWF].GetName(), "the file now exists and has a name")
105+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_DISABLED, listed[releaseWF].GetState())
106+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_ENABLED, listed[".github/workflows/z-ctrl.yml"].GetState())
107+
108+ // Enabling restores it.
109+ w, err = c.setWorkflow(c.root, false, "release.yml")
110+ require.NoError(t, err)
111+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_ENABLED, w.GetState())
112+ _, err = c.setWorkflow(c.root, false, "release.yml")
113+ require.NoError(t, err, "idempotent")
114+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_ENABLED, c.listWorkflows(c.root)[releaseWF].GetState())
115+ c.push("main", map[string]string{"y.txt": "y"}, "")
116+ c.waitFor("the release run", func() bool { return len(runsOfWorkflow(c.runs(), releaseWF)) == 1 })
117+}
118+
119+// A disabled workflow does not fire on its schedule, and fires again once
120+// enabled, with no reload of the schedules.
121+func TestWorkflowDisabledStartsNoScheduledRun(t *testing.T) {
122+ c, clk, _ := newScheduleHarness(t, "2026-03-04 10:00:30")
123+ c.push("main", scheduleWorkflow("*/5 * * * *"), "")
124+ ci := c.forge.CI
125+ tick := func(at string) {
126+ clk.set(at)
127+ ci.ScheduleNow(c.ctx)
128+ }
129+ tick("2026-03-04 10:05:00")
130+ require.Len(t, c.scheduled(), 1, "enabled: fires")
131+ assert.Equal(t, 1, ci.CachedSchedules(c.name("proj")))
132+
133+ _, err := c.setWorkflow(c.root, true, "nightly.yml")
134+ require.NoError(t, err)
135+ assert.Equal(t, 1, ci.CachedSchedules(c.name("proj")), "the cache holds every workflow: the state is read when it fires")
136+ tick("2026-03-04 10:10:00")
137+ tick("2026-03-04 10:15:00")
138+ assert.Len(t, c.scheduled(), 1, "disabled: no run for the minutes 10:10 and 10:15")
139+
140+ // A push of the file while disabled changes nothing.
141+ c.push("main", scheduleWorkflow("*/5 * * * *", "0 0 1 1 *"), "again")
142+ tick("2026-03-04 10:20:00")
143+ assert.Len(t, c.scheduled(), 1)
144+
145+ _, err = c.setWorkflow(c.root, false, "nightly.yml")
146+ require.NoError(t, err)
147+ tick("2026-03-04 10:25:00")
148+ assert.Len(t, c.scheduled(), 2, "enabled again: fires, without catching up the missed minutes")
149+}
150+
151+// workflow_dispatch and pull_request are refused/skipped too, per file.
152+func TestWorkflowDisabledStartsNoDispatchOrPullRequestRun(t *testing.T) {
153+ c := newCIHarness(t, nil)
154+ runner := c.registerRunner("alpha", "ubuntu-latest")
155+ c.push("main", map[string]string{
156+ ".github/workflows/deploy.yml": dispatchWorkflow,
157+ ".github/workflows/pr.yml": wfOn("pr", " pull_request:\n"),
158+ ".github/workflows/other.yml": wfOn("other", " pull_request:\n"),
159+ }, "")
160+ c.mustGit(c.wt, "checkout", "-q", "-b", "feat")
161+ c.push("feat", map[string]string{"f.txt": "f"}, "")
162+
163+ _, err := c.setWorkflow(c.root, true, "deploy.yml")
164+ require.NoError(t, err)
165+ _, err = c.setWorkflow(c.root, true, ".github/workflows/pr.yml")
166+ require.NoError(t, err)
167+
168+ _, err = c.dispatch(c.root, "deploy.yml", "", map[string]string{"version": "1"})
169+ assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err), "a disabled workflow is not dispatched")
170+ assert.Contains(t, err.Error(), "disabled")
171+
172+ c.openMR(c.root, "feat")
173+ c.waitFor("the other workflow's run", func() bool { return len(runsOfWorkflow(c.runs(), ".github/workflows/other.yml")) == 1 })
174+ assert.Empty(t, runsOfWorkflow(c.runs(), ".github/workflows/pr.yml"), "no pull_request run")
175+ runner.runJob(runner.await(), pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "other")
176+
177+ _, err = c.setWorkflow(c.root, false, "deploy.yml")
178+ require.NoError(t, err)
179+ run, err := c.dispatch(c.root, "deploy.yml", "", map[string]string{"version": "1"})
180+ require.NoError(t, err)
181+ assert.Equal(t, "workflow_dispatch", run.GetEvent())
182+}
183+
184+// A disabled workflow does not start a workflow_run workflow.
185+func TestWorkflowDisabledStartsNoWorkflowRunRun(t *testing.T) {
186+ c := newCIHarness(t, nil)
187+ runner := c.registerRunner("alpha", "ubuntu-latest")
188+ c.push("main", map[string]string{
189+ ".github/workflows/ci.yml": wfOn("ci", " push:\n"),
190+ ".github/workflows/after.yml": wfOn("after", " workflow_run:\n workflows: [ci]\n types: [completed]\n"),
191+ ".github/workflows/after2.yml": wfOn("after2", " workflow_run:\n workflows: [ci]\n types: [completed]\n"),
192+ }, "")
193+ _, err := c.setWorkflow(c.root, true, "after.yml")
194+ require.NoError(t, err)
195+ // Pushed before the disable took effect? The first push ran ci: finish it.
196+ job := runner.await()
197+ runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "ci")
198+ c.waitFor("after2 runs", func() bool { return len(runsOfWorkflow(c.runs(), ".github/workflows/after2.yml")) == 1 })
199+ assert.Empty(t, runsOfWorkflow(c.runs(), ".github/workflows/after.yml"))
200+}
201+
202+// Scopes: repo:admin changes the state, repo:read lists, ci:write and
203+// repo:write do not change it; the shape is checked, not the existence.
204+func TestWorkflowStateAccessAndValidation(t *testing.T) {
205+ c := newCIHarness(t, nil)
206+ admin := c.api(c.token(scopes(domain.ScopeRepoRead, domain.ScopeRepoAdmin), "proj"))
207+ writer := c.api(c.token(scopes(domain.ScopeRepoRead, domain.ScopeRepoWrite, domain.ScopeCIWrite), "proj"))
208+ reader := c.api(c.token(scopes(domain.ScopeRepoRead), "proj"))
209+ stranger := c.api(c.token(scopes(domain.ScopeRepoAdmin), "other"))
210+ limited, _ := c.branchToken(scopes(domain.ScopeRepoRead, domain.ScopeRepoAdmin), []string{"agent/"}, "proj")
211+
212+ _, err := c.setWorkflow(admin, true, "release.yml")
213+ require.NoError(t, err)
214+ _, err = c.setWorkflow(writer, true, "aur.yml")
215+ assert.Equal(t, connect.CodePermissionDenied, ciErrCode(err), "repo:write and ci:write are not enough")
216+ _, err = c.setWorkflow(writer, false, "release.yml")
217+ assert.Equal(t, connect.CodePermissionDenied, ciErrCode(err))
218+ _, err = c.setWorkflow(reader, true, "aur.yml")
219+ assert.Equal(t, connect.CodePermissionDenied, ciErrCode(err))
220+ _, err = c.setWorkflow(c.api(limited), true, "aur.yml")
221+ assert.Equal(t, connect.CodePermissionDenied, ciErrCode(err), "a token limited to branches may not")
222+ _, err = c.setWorkflow(stranger, true, "aur.yml")
223+ assert.Equal(t, connect.CodeNotFound, ciErrCode(err), "a repository the token cannot read")
224+
225+ for _, bad := range []string{"", "release", "release.txt", "sub/release.yml", ".github/workflows/sub/release.yml", "../release.yml", ".github/other/release.yml"} {
226+ _, err = c.setWorkflow(admin, true, bad)
227+ assert.Equal(t, connect.CodeInvalidArgument, ciErrCode(err), "%q", bad)
228+ }
229+
230+ assert.Equal(t, pb.WorkflowState_WORKFLOW_STATE_DISABLED, c.listWorkflows(reader)[releaseWF].GetState(), "repo:read lists")
231+ assert.Len(t, c.listWorkflows(reader), 1, "the refused changes left nothing")
232+ _, err = stranger.run.ListWorkflows(c.ctx, connect.NewRequest(&pb.ListWorkflowsRequest{Repo: c.repo}))
233+ assert.Equal(t, connect.CodeNotFound, ciErrCode(err))
234+ _, err = c.setWorkflow(admin, true, ".github/workflows/Odd Name.YAML")
235+ require.NoError(t, err, "only the shape is checked")
236+
237+ _, err = admin.run.DisableWorkflow(c.ctx, connect.NewRequest(&pb.DisableWorkflowRequest{Repo: "nope", Workflow: "a.yml"}))
238+ assert.Equal(t, connect.CodeNotFound, ciErrCode(err))
239+}
240+
241+// GO-001: the state is enforced where the run is stored. A dispatch that passed
242+// the check and is paused after resolving its ref (DispatchResolved) while the
243+// workflow is disabled creates no run.
244+func TestWorkflowDisabledBetweenDispatchCheckAndCreation(t *testing.T) {
245+ var c *ciHarness
246+ var disable func()
247+ c = newCIHarness(t, func(cfg *usecase.CIConfig) {
248+ cfg.DispatchResolved = func() {
249+ if disable != nil {
250+ disable()
251+ }
252+ }
253+ })
254+ c.push("main", map[string]string{".github/workflows/deploy.yml": dispatchWorkflow}, "")
255+ before := len(c.runs())
256+
257+ disable = func() {
258+ disable = nil
259+ _, err := c.setWorkflow(c.root, true, "deploy.yml")
260+ require.NoError(t, err)
261+ }
262+ _, err := c.dispatch(c.root, "deploy.yml", "", map[string]string{"version": "1"})
263+ assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err), "the disable landed after the check")
264+ assert.Contains(t, err.Error(), "disabled")
265+ assert.Len(t, c.runs(), before, "no run was created")
266+
267+ _, err = c.setWorkflow(c.root, false, "deploy.yml")
268+ require.NoError(t, err)
269+ _, err = c.dispatch(c.root, "deploy.yml", "", map[string]string{"version": "1"})
270+ require.NoError(t, err, "enabled again")
271+}
272+
273+// GO-001: an invalid workflow file gets no failed run either while it is
274+// disabled, on a push and on the failed-run path of a disable that races.
275+func TestWorkflowDisabledInvalidFileStartsNoFailedRun(t *testing.T) {
276+ c := newCIHarness(t, nil)
277+ c.registerRunner("alpha", "ubuntu-latest")
278+ _, err := c.setWorkflow(c.root, true, "broken.yml")
279+ require.NoError(t, err)
280+ c.push("main", map[string]string{
281+ ".github/workflows/broken.yml": "on: push\njobs: [this is not a workflow\n",
282+ ".github/workflows/bad2.yml": "on: push\njobs: [this is not a workflow\n",
283+ }, "")
284+ c.waitFor("the invalid enabled file fails", func() bool { return len(runsOfWorkflow(c.runs(), ".github/workflows/bad2.yml")) == 1 })
285+ assert.Empty(t, runsOfWorkflow(c.runs(), ".github/workflows/broken.yml"), "a disabled invalid file reports nothing")
286+}
287+
288+// GO-002: a schedule load that is overtaken by an enable (or a disable) neither
289+// loses the workflow nor fires a disabled one: the cache holds every workflow
290+// and the state is read when the schedule fires.
291+func TestScheduleLoadRacingWithEnableAndDisable(t *testing.T) {
292+ clk := &testClock{}
293+ clk.set("2026-03-04 10:00:30")
294+ var c *ciHarness
295+ var inLoad func()
296+ c = newCIHarness(t, func(cfg *usecase.CIConfig) {
297+ cfg.Clock = clk
298+ cfg.ScheduleTick = time.Hour
299+ cfg.ScheduleLoaded = func(domain.RepoName) {
300+ if f := inLoad; f != nil {
301+ inLoad = nil
302+ f()
303+ }
304+ }
305+ })
306+ c.registerRunner("alpha", "ubuntu-latest")
307+ ci := c.forge.CI
308+ tick := func(at string) {
309+ clk.set(at)
310+ ci.ScheduleNow(c.ctx)
311+ }
312+ // Disabled before the first push; the load of the schedules sees the file,
313+ // and an enable lands while it is being cached.
314+ _, err := c.setWorkflow(c.root, true, "nightly.yml")
315+ require.NoError(t, err)
316+ c.push("main", scheduleWorkflow("*/5 * * * *"), "")
317+ inLoad = func() {
318+ _, err := c.setWorkflow(c.root, false, "nightly.yml")
319+ require.NoError(t, err)
320+ }
321+ tick("2026-03-04 10:05:00")
322+ require.Len(t, c.scheduled(), 1, "enabled while the load was running: the workflow is there")
323+ assert.Equal(t, 1, ci.CachedSchedules(c.name("proj")))
324+
325+ // And the other way: disabled while a load is running.
326+ require.NoError(t, func() error { _, err := c.setWorkflow(c.root, false, "nightly.yml"); return err }())
327+ c.push("main", scheduleWorkflow("*/5 * * * *", "0 0 1 1 *"), "second") // the tip moves: a new load
328+ inLoad = func() {
329+ _, err := c.setWorkflow(c.root, true, "nightly.yml")
330+ require.NoError(t, err)
331+ }
332+ tick("2026-03-04 10:10:00")
333+ assert.Len(t, c.scheduled(), 1, "disabled while the load was running: it does not fire")
334+ _, err = c.setWorkflow(c.root, false, "nightly.yml")
335+ require.NoError(t, err)
336+ tick("2026-03-04 10:15:00")
337+ assert.Len(t, c.scheduled(), 2, "enabled again: fires with the cached entry, nothing missing")
338+}
339+
340+// GO-003: one file has one key. Non-canonical spellings are refused by the API,
341+// for disable, enable and dispatch alike.
342+func TestWorkflowNonCanonicalPathsAreRefused(t *testing.T) {
343+ c := newCIHarness(t, nil)
344+ c.push("main", map[string]string{".github/workflows/deploy.yml": dispatchWorkflow}, "")
345+ for _, bad := range []string{
346+ ".github/workflows//release.yml", "./.github/workflows/release.yml", ".github/workflows/sub/../release.yml",
347+ ".github/workflows/./release.yml",
348+ } {
349+ _, err := c.setWorkflow(c.root, true, bad)
350+ assert.Equal(t, connect.CodeInvalidArgument, ciErrCode(err), "disable %q", bad)
351+ _, err = c.setWorkflow(c.root, false, bad)
352+ assert.Equal(t, connect.CodeInvalidArgument, ciErrCode(err), "enable %q", bad)
353+ _, err = c.dispatch(c.root, bad, "", nil)
354+ assert.Equal(t, connect.CodeInvalidArgument, ciErrCode(err), "dispatch %q", bad)
355+ }
356+ assert.Empty(t, c.listWorkflows(c.root)[".github/workflows/release.yml"], "nothing was stored")
357+ _, err := c.dispatch(c.root, ".github/workflows/deploy.yml", "", map[string]string{"version": "1"})
358+ require.NoError(t, err, "the canonical path still dispatches")
359+}
M internal/app/cli_e2e_test.go +1 -1
@@ -338,7 +338,7 @@ func TestAC6CLIEndToEnd(t *testing.T) {
338338 ver := decode[versionOut](t, res.stdout)
339339 assert.Equal(t, c.url, ver.Server.URL)
340340 assert.NotEmpty(t, ver.Server.Version)
341- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, ver.Server.Features)
341+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, ver.Server.Features)
342342 assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures)
343343 stdout, stderr, code = c.bin(t, secret, "version")
344344 require.Equal(t, 0, code, stderr)
M internal/app/cli_run_test.go +51 -0
@@ -136,3 +136,54 @@ func TestRefTextShowsMergeRequests(t *testing.T) {
136136 assert.Equal(t, "main", refText("refs/heads/main"))
137137 assert.Equal(t, "v1", refText("refs/tags/v1"))
138138 }
139+
140+// `ermite workflow disable|enable|list`, over a real server: right after
141+// `repo create`, before any push, then once the file exists.
142+func TestCLIWorkflowDisableEnableList(t *testing.T) {
143+ ci := newCIHarness(t, nil)
144+ h := &cliHarness{harness: ci.harness, url: ci.srv.URL}
145+
146+ dis := h.cli(t, adminToken, "workflow", "disable", "proj", "release.yml")
147+ require.NoError(t, dis.err, dis.stderr)
148+ assert.Contains(t, dis.stderr, "disabled .github/workflows/release.yml in proj")
149+
150+ empty := h.cli(t, adminToken, "workflow", "list", "proj")
151+ require.NoError(t, empty.err, empty.stderr)
152+ assert.Contains(t, empty.stdout, "FILE")
153+ assert.Contains(t, empty.stdout, ".github/workflows/release.yml")
154+ assert.Contains(t, empty.stdout, "disabled")
155+
156+ ci.push("main", map[string]string{".github/workflows/ci.yml": ciWorkflow, releaseWF: wfOn("release", " push:\n")}, "")
157+ js := h.cli(t, adminToken, "workflow", "list", "proj", "--json")
158+ require.NoError(t, js.err, js.stderr)
159+ list := decode[[]jsonWorkflow](t, js.stdout)
160+ assert.Equal(t, []jsonWorkflow{
161+ {Repo: "proj", File: ".github/workflows/ci.yml", Name: "ci", State: "enabled"},
162+ {Repo: "proj", File: releaseWF, Name: "release", State: "disabled"},
163+ }, list)
164+ raw := decode[[]map[string]any](t, js.stdout)
165+ assert.Len(t, raw[0], 4, "repo, file, name, state")
166+ for _, k := range []string{"repo", "file", "name", "state"} {
167+ assert.Contains(t, raw[0], k)
168+ }
169+
170+ en := h.cli(t, adminToken, "workflow", "enable", "proj", "release.yml", "--json")
171+ require.NoError(t, en.err, en.stderr)
172+ assert.Equal(t, jsonWorkflow{Repo: "proj", File: releaseWF, State: "enabled"}, decode[jsonWorkflow](t, en.stdout))
173+ after := decode[[]jsonWorkflow](t, h.cli(t, adminToken, "workflow", "list", "proj", "--json").stdout)
174+ assert.Equal(t, "enabled", after[1].State)
175+
176+ bad := h.cli(t, adminToken, "workflow", "disable", "proj", "sub/x.yml")
177+ require.Error(t, bad.err)
178+ missing := h.cli(t, adminToken, "workflow", "disable", "nope", "x.yml")
179+ require.Error(t, missing.err)
180+
181+ reader := h.cli(t, ci.token(scopes(domain.ScopeRepoRead), "proj"), "workflow", "disable", "proj", "release.yml")
182+ require.Error(t, reader.err)
183+ assert.Contains(t, reader.err.Error(), "permission denied")
184+
185+ help := h.cli(t, adminToken, "workflow", "disable", "--help")
186+ require.NoError(t, help.err)
187+ assert.Contains(t, help.stdout, "JSON fields of a workflow (--json): repo, file")
188+ assert.Contains(t, help.stdout, "repo:admin")
189+}
M internal/app/cli_version.go +1 -0
@@ -120,6 +120,7 @@ var featureUse = map[string]string{
120120 client.FeatureCIRunners: "`ermite runner` needs: the commands will fail on this server",
121121 client.FeatureCISecrets: "`ermite secret`, `variable` and `environment` need: the commands will fail on this server",
122122 client.FeatureCIDispatch: "`ermite workflow run` needs: the command will fail on this server",
123+ client.FeatureCIWorkflows: "`ermite workflow list`, `disable` and `enable` need: the commands will fail on this server",
123124 client.FeatureCIArtifacts: "`ermite run artifacts` and `run download` need: the commands will fail on this server",
124125 client.FeaturePasskeys: "`ermite web passkey` needs: the commands will fail on this server (passkeys need an https public URL with a DNS host name)",
125126 client.FeatureCommitSignatures: "`ermite signing-key trust` needs: the commands will fail on this server",
M internal/app/cli_version_test.go +5 -5
@@ -17,7 +17,7 @@ import (
1717 )
1818
1919 // allFeatures are the features this client uses.
20-var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.artifacts", "commit_signatures", "passkeys"}
20+var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys"}
2121
2222 // Every feature the client uses needs a sentence for the version warning.
2323 func TestEveryUsedFeatureExplainsWhatBreaks(t *testing.T) {
@@ -45,15 +45,15 @@ func TestVersionWithServer(t *testing.T) {
4545 f := versionFake(allFeatures...)
4646 url := f.serve(t)
4747 res := runCLI(t, envFor(url), "", "version").ok(t)
48- assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.artifacts, commit_signatures, passkeys\n", res.stdout)
48+ assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, passkeys\n", res.stdout)
4949 assert.Empty(t, res.stderr, "no warning when every feature is there")
5050
5151 res = runCLI(t, envFor(url), "", "version", "--json").ok(t)
52- assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.artifacts","commit_signatures","passkeys"],"missing_features":[]}}`, res.stdout)
52+ assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":[]}}`, res.stdout)
5353 }
5454
5555 func TestVersionWarnsAboutMissingFeatures(t *testing.T) {
56- f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.artifacts", "commit_signatures", "passkeys")
56+ f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys")
5757 url := f.serve(t)
5858 res := runCLI(t, envFor(url), "", "version").ok(t)
5959 assert.Contains(t, res.stdout, "v0.4.0")
@@ -61,7 +61,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) {
6161 assert.NotContains(t, res.stderr, "repo.update")
6262
6363 res = runCLI(t, envFor(url), "", "version", "--json").ok(t)
64- assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.artifacts","commit_signatures","passkeys"],"missing_features":["events.resume"]}}`, res.stdout)
64+ assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":["events.resume"]}}`, res.stdout)
6565 assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json")
6666
6767 f = versionFake()
M internal/app/cli_workflow.go +132 -3
@@ -13,14 +13,15 @@ import (
1313 )
1414
1515 // `ermite workflow run`: start a workflow that has a workflow_dispatch trigger,
16-// like `gh workflow run`.
16+// like `gh workflow run`. `workflow list|disable|enable` show and set the state
17+// of the workflow files of a repository, like `gh workflow list|disable|enable`.
1718
1819 func newWorkflowCmd(env *cliEnv) *cobra.Command {
1920 wf := &cobra.Command{
2021 Use: "workflow",
21- Short: "Start workflows by hand",
22+ Short: "Start, list, disable and enable workflows",
2223 }
23- wf.AddCommand(newWorkflowRunCmd(env))
24+ wf.AddCommand(newWorkflowRunCmd(env), newWorkflowListCmd(env), newWorkflowDisableCmd(env), newWorkflowEnableCmd(env))
2425 return wf
2526 }
2627
@@ -83,3 +84,131 @@ func newWorkflowRunCmd(env *cliEnv) *cobra.Command {
8384 }
8485 return cmd
8586 }
87+
88+type jsonWorkflow struct {
89+ Repo string `json:"repo"`
90+ File string `json:"file"`
91+ Name string `json:"name"`
92+ State string `json:"state"`
93+}
94+
95+const jsonWorkflowHelp = `
96+JSON fields of a workflow (--json): repo, file (the path, always .github/workflows/<file>), name (the
97+"name:" of the workflow on the default branch, the path when it has none, empty when the default branch
98+has no such file) and state (enabled or disabled).`
99+
100+func workflowStateText(s ermite.WorkflowState) string {
101+ if s == ermite.WorkflowState_WORKFLOW_STATE_DISABLED {
102+ return "disabled"
103+ }
104+ return "enabled"
105+}
106+
107+func toJSONWorkflow(repo string, w *ermite.Workflow) jsonWorkflow {
108+ return jsonWorkflow{Repo: repo, File: w.GetPath(), Name: w.GetName(), State: workflowStateText(w.GetState())}
109+}
110+
111+func newWorkflowListCmd(env *cliEnv) *cobra.Command {
112+ cmd := &cobra.Command{
113+ Use: "list <repo>",
114+ Short: "List the workflows of a repository and whether they are enabled",
115+ Long: "List the workflow files of .github/workflows on the default branch with their state, and the disabled\n" +
116+ "workflows whose file is absent there (a disabled workflow is listed even in an empty repository), ordered\n" +
117+ "by file. Needs the repo:read scope for a private repository." + jsonWorkflowHelp + "\n--json prints an array of workflows.",
118+ Example: " ermite workflow list app\n ermite workflow list app --json",
119+ Args: cobra.ExactArgs(1),
120+ ValidArgsFunction: completeRepoNames(env),
121+ }
122+ asJSON := addJSONFlag(cmd)
123+ cmd.RunE = func(cmd *cobra.Command, args []string) error {
124+ c, err := env.client(cmd)
125+ if err != nil {
126+ return err
127+ }
128+ res, err := c.Runs().ListWorkflows(env.ctx(cmd), connect.NewRequest(&ermite.ListWorkflowsRequest{Repo: args[0]}))
129+ if err != nil {
130+ return c.Describe(err)
131+ }
132+ list := res.Msg.GetWorkflows()
133+ if *asJSON {
134+ out := make([]jsonWorkflow, 0, len(list))
135+ for _, w := range list {
136+ out = append(out, toJSONWorkflow(args[0], w))
137+ }
138+ return writeJSON(cmd.OutOrStdout(), out)
139+ }
140+ if len(list) == 0 {
141+ noticef(cmd, "no workflows")
142+ return nil
143+ }
144+ rows := make([][]string, 0, len(list))
145+ for _, w := range list {
146+ rows = append(rows, []string{w.GetPath(), w.GetName(), workflowStateText(w.GetState())})
147+ }
148+ return table(cmd.OutOrStdout(), []string{"FILE", "NAME", "STATE"}, rows)
149+ }
150+ return cmd
151+}
152+
153+func newWorkflowDisableCmd(env *cliEnv) *cobra.Command {
154+ cmd := &cobra.Command{
155+ Use: "disable <repo> <file>",
156+ Short: "Stop a workflow from starting runs",
157+ Long: "Disable a workflow file of a repository, like `gh workflow disable`: no event (push, pull_request,\n" +
158+ "pull_request_target, schedule, workflow_run, create, delete, workflow_dispatch) starts a run of it any more.\n" +
159+ "Runs already started go on. The state belongs to the repository and to the file path, not to a branch. The\n" +
160+ "file is its path or its file name (release.yml) and does not have to exist: disable it right after\n" +
161+ "`ermite repo create`, before the first push. Disabling a disabled workflow changes nothing. Undo with\n" +
162+ "`ermite workflow enable`. Needs the repo:admin scope." + jsonWorkflowHelp,
163+ Example: " ermite workflow disable app release.yml\n ermite workflow disable app .github/workflows/codeql.yml",
164+ Args: cobra.ExactArgs(2),
165+ ValidArgsFunction: completeRepoNames(env),
166+ }
167+ asJSON := addJSONFlag(cmd)
168+ cmd.RunE = func(cmd *cobra.Command, args []string) error {
169+ c, err := env.client(cmd)
170+ if err != nil {
171+ return err
172+ }
173+ res, err := c.Runs().DisableWorkflow(env.ctx(cmd), connect.NewRequest(&ermite.DisableWorkflowRequest{Repo: args[0], Workflow: args[1]}))
174+ if err != nil {
175+ return c.Describe(err)
176+ }
177+ return printWorkflowState(cmd, *asJSON, args[0], res.Msg.GetWorkflow(), "disabled")
178+ }
179+ return cmd
180+}
181+
182+func newWorkflowEnableCmd(env *cliEnv) *cobra.Command {
183+ cmd := &cobra.Command{
184+ Use: "enable <repo> <file>",
185+ Short: "Let a disabled workflow start runs again",
186+ Long: "Enable a workflow file of a repository, like `gh workflow enable`. The events that happened while it was\n" +
187+ "disabled are not replayed. Enabling a workflow that is not disabled changes nothing. The file is its path\n" +
188+ "or its file name. Needs the repo:admin scope." + jsonWorkflowHelp,
189+ Example: " ermite workflow enable app release.yml",
190+ Args: cobra.ExactArgs(2),
191+ ValidArgsFunction: completeRepoNames(env),
192+ }
193+ asJSON := addJSONFlag(cmd)
194+ cmd.RunE = func(cmd *cobra.Command, args []string) error {
195+ c, err := env.client(cmd)
196+ if err != nil {
197+ return err
198+ }
199+ res, err := c.Runs().EnableWorkflow(env.ctx(cmd), connect.NewRequest(&ermite.EnableWorkflowRequest{Repo: args[0], Workflow: args[1]}))
200+ if err != nil {
201+ return c.Describe(err)
202+ }
203+ return printWorkflowState(cmd, *asJSON, args[0], res.Msg.GetWorkflow(), "enabled")
204+ }
205+ return cmd
206+}
207+
208+func printWorkflowState(cmd *cobra.Command, asJSON bool, repo string, w *ermite.Workflow, state string) error {
209+ if asJSON {
210+ return writeJSON(cmd.OutOrStdout(), toJSONWorkflow(repo, w))
211+ }
212+ noticef(cmd, "%s %s in %s", state, clean(w.GetPath()), clean(repo))
213+ return nil
214+}
M internal/app/forge.go +6 -5
@@ -277,11 +277,12 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
277277 Bin: cfg.Git, Path: path, Home: filepath.Join(cfg.DataDir, "run", "git-home"),
278278 Proxy: gitremote.ProxyEnv(os.Getenv),
279279 })),
280- Tokens: store.Tokens(),
281- RefTips: store.CI(),
282- Settings: f.Settings,
283- Workflows: opts.workflows(githubraw.New(githubraw.Config{})),
284- OIDC: f.OIDC,
280+ Tokens: store.Tokens(),
281+ RefTips: store.CI(),
282+ WorkflowState: store.CI(),
283+ Settings: f.Settings,
284+ Workflows: opts.workflows(githubraw.New(githubraw.Config{})),
285+ OIDC: f.OIDC,
285286 }
286287 artifacts(&ciCfg, cfg, store)
287288 f.pullRequests(&ciCfg, store, git, committer)
M internal/app/main_test.go +13 -1
@@ -116,8 +116,20 @@ func baseEnv() []string {
116116 return []string{"PATH=" + os.Getenv("PATH")}
117117 }
118118
119+// isolatedEnv is the environment of a CLI run that must not see the user's
120+// ERMITE_URL, ERMITE_TOKEN or config file: a version command with a server URL
121+// also asks the server and prints its version.
122+func isolatedEnv(t *testing.T) []string {
123+ t.Helper()
124+ cfg := filepath.Join(t.TempDir(), "config")
125+ require.NoError(t, os.WriteFile(cfg, nil, 0o600))
126+ return append(baseEnv(), "ERMITE_CONFIG="+cfg, "HOME="+t.TempDir(), "XDG_CONFIG_HOME="+t.TempDir())
127+}
128+
119129 func TestVersion(t *testing.T) {
120- out, err := exec.Command(binary(t), "version").Output()
130+ cmd := exec.Command(binary(t), "version")
131+ cmd.Env = isolatedEnv(t)
132+ out, err := cmd.Output()
121133 require.NoError(t, err)
122134 assert.True(t, strings.HasPrefix(string(out), "ermite "), string(out))
123135 assert.Equal(t, 1, strings.Count(string(out), "\n"))
A internal/domain/ci_workflow.go +55 -0
@@ -0,0 +1,55 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "path"
9+ "strings"
10+ "unicode"
11+)
12+
13+// WorkflowDir is the only directory the workflows of a repository are read
14+// from (ADR 0019: no ermite dialect).
15+const WorkflowDir = ".github/workflows"
16+
17+// maxWorkflowFileName bounds the file name of a workflow, as a file name of
18+// Git trees is bounded on the usual file systems.
19+const maxWorkflowFileName = 255
20+
21+// ErrWorkflowDisabled is returned by the store when a run is created for a
22+// workflow the repository disabled. It is checked in the very transaction that
23+// creates the run, so no disable can slip in between a check and the creation.
24+var ErrWorkflowDisabled = errors.New("workflow is disabled")
25+
26+// ParseWorkflowPath normalizes the name of a workflow file, as `gh workflow`
27+// takes it: either its file name ("release.yml") or its path
28+// (".github/workflows/release.yml"). It returns the canonical path, and
29+// refuses every other spelling of it ("./.github/workflows/x.yml",
30+// ".github/workflows//x.yml", ".github/workflows/sub/../x.yml"), so one file has
31+// one key. Only the shape is checked, never whether the file exists: the state
32+// of a workflow can be set on a repository that is empty or that does not hold
33+// the file yet. A file in a subdirectory, a name that does not end in .yml or
34+// .yaml, and a name with a control character or a backslash are ErrInvalid.
35+func ParseWorkflowPath(s string) (string, error) {
36+ s = strings.TrimSpace(s)
37+ if s == "" {
38+ return "", fmt.Errorf("%w: the workflow file is required", ErrInvalid)
39+ }
40+ if !strings.Contains(s, "/") {
41+ s = WorkflowDir + "/" + s
42+ }
43+ name, ok := strings.CutPrefix(s, WorkflowDir+"/")
44+ ext := strings.ToLower(path.Ext(name))
45+ switch {
46+ case !ok,
47+ strings.Contains(name, "/"),
48+ ext != ".yml" && ext != ".yaml",
49+ strings.TrimSuffix(name, path.Ext(name)) == "",
50+ len(name) > maxWorkflowFileName,
51+ strings.ContainsFunc(name, func(r rune) bool { return unicode.IsControl(r) || r == '\\' }):
52+ return "", fmt.Errorf("%w: %q is not a workflow file of %s (a file name or the path %s/<file>, ending in .yml or .yaml)", ErrInvalid, s, WorkflowDir, WorkflowDir)
53+ }
54+ return s, nil
55+}
A internal/domain/ci_workflow_test.go +34 -0
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestParseWorkflowPath(t *testing.T) {
13+ for in, want := range map[string]string{
14+ "release.yml": ".github/workflows/release.yml",
15+ " release.yaml ": ".github/workflows/release.yaml",
16+ ".github/workflows/release.yml": ".github/workflows/release.yml",
17+ ".github/workflows/a.b-c_d.yaml": ".github/workflows/a.b-c_d.yaml",
18+ } {
19+ got, err := ParseWorkflowPath(in)
20+ require.NoError(t, err, in)
21+ assert.Equal(t, want, got, in)
22+ }
23+ for _, in := range []string{
24+ "", " ", "release", "release.txt", ".yml", ".github/workflows/.yml",
25+ "sub/release.yml", ".github/workflows/sub/release.yml", ".github/workflows/../release.yml",
26+ "/.github/workflows/release.yml", ".github/other/release.yml",
27+ ".github/workflows//release.yml", "./.github/workflows/release.yml", ".github/workflows/sub/../release.yml",
28+ ".github/workflows/./release.yml", ".github//workflows/release.yml", "github/workflows/release.yml",
29+ ".github/workflows/release.yml/", "..", "../release.yml", ".github/workflows/..", ".github/workflows/.", "x\\y.yml", "a\x00.yml", ".github/workflows/",
30+ } {
31+ _, err := ParseWorkflowPath(in)
32+ assert.ErrorIs(t, err, ErrInvalid, "%q", in)
33+ }
34+}
M internal/ports/ci.go +43 -2
@@ -95,6 +95,21 @@ type CIRefTipStore interface {
9595 DeleteRefTip(ctx context.Context, repo domain.RepoName, ref string) error
9696 }
9797
98+// CIWorkflowStore keeps, per repository, the workflow files that are disabled
99+// (`ermite workflow disable`). Paths are normalized (domain.ParseWorkflowPath);
100+// the file does not have to exist. The records go with the repository.
101+// *kvstore.CI implements it.
102+type CIWorkflowStore interface {
103+ // DisableWorkflow records the path as disabled; one already disabled is
104+ // left as it is. A repository that does not exist is domain.ErrNotFound.
105+ DisableWorkflow(ctx context.Context, repo domain.RepoName, path string, at time.Time) error
106+ // EnableWorkflow removes the record; a path that is not disabled is not an
107+ // error.
108+ EnableWorkflow(ctx context.Context, repo domain.RepoName, path string) error
109+ // DisabledWorkflows returns the disabled paths of the repository, sorted.
110+ DisabledWorkflows(ctx context.Context, repo domain.RepoName) ([]string, error)
111+}
112+
98113 // CIStore keeps CI runs, jobs and checks. The store derives its own indexes
99114 // (the queue of ready jobs, the active jobs, the open runs, the job tokens)
100115 // from the records it writes.
@@ -102,8 +117,12 @@ type CIStore interface {
102117 // CreateRun takes the next run id and the next number of the workflow in
103118 // the repository (both never reused), calls build with them and stores
104119 // the draft it returns, in one batch that fails with domain.ErrNotFound
105- // once the repository is deleted. The records come back with their
106- // versions.
120+ // once the repository is deleted, and with domain.ErrWorkflowDisabled when
121+ // the repository disabled workflowPath (CIWorkflowStore) when the batch
122+ // applies: the state is checked in that transaction, not before it, so a
123+ // disable can never be overtaken, whatever event started the run and
124+ // whether the run is a failed one for an invalid file or not. The records
125+ // come back with their versions.
107126 CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) CIRunDraft) (CIRunDraft, error)
108127 // Apply writes the records in one batch and returns the version they all
109128 // have afterwards.
@@ -285,6 +304,16 @@ type DispatchParams struct {
285304 Inputs map[string]string
286305 }
287306
307+// WorkflowInfo is a workflow file of a repository and whether it starts runs.
308+type WorkflowInfo struct {
309+ // Path is ".github/workflows/<file>".
310+ Path string
311+ // Name is the "name:" of the workflow at the tip of the default branch (the
312+ // path when it has none); empty when that file is absent or not a workflow.
313+ Name string
314+ Disabled bool
315+}
316+
288317 // WorkflowTrigger starts the workflows that match pushed refs. OnPush only
289318 // queues the work and returns: planning happens in the background and its
290319 // errors never reach the push.
@@ -721,6 +750,18 @@ type RunService interface {
721750 // permission actions: write. The run is returned queued.
722751 DispatchWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, params DispatchParams) (domain.CIRun, error)
723752 ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
753+ // ListWorkflows returns the workflow files at the tip of the default branch
754+ // and the disabled ones whatever the branch holds, ordered by path. Reading
755+ // the repository is enough.
756+ ListWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]WorkflowInfo, error)
757+ // DisabledWorkflows returns the paths of the disabled workflows, sorted,
758+ // without reading any file: the cheap form of ListWorkflows for the pages.
759+ DisabledWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]string, error)
760+ // DisableWorkflow stops the workflow (its file name or path; it does not
761+ // have to exist) from starting runs for any event. EnableWorkflow undoes it.
762+ // Both need repo:admin on the repository and are idempotent.
763+ DisableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
764+ EnableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
724765 // ListAnnotations returns the annotations of a run attempt (0 for the
725766 // latest), of one job when jobID is not empty. Each job gives those of the
726767 // attempt in which it executed.
A internal/ports/mocks/CIWorkflowStore.go +249 -0
@@ -0,0 +1,249 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+ "time"
10+
11+ "bnema.dev/ermite/internal/domain"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewCIWorkflowStore creates a new instance of CIWorkflowStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewCIWorkflowStore(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *CIWorkflowStore {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &CIWorkflowStore{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// CIWorkflowStore is an autogenerated mock type for the CIWorkflowStore type
39+type CIWorkflowStore struct {
40+ mock.Mock
41+}
42+
43+type CIWorkflowStore_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *CIWorkflowStore) EXPECT() *CIWorkflowStore_Expecter {
48+ return &CIWorkflowStore_Expecter{mock: &_m.Mock}
49+}
50+
51+// DisableWorkflow provides a mock function for the type CIWorkflowStore
52+func (_mock *CIWorkflowStore) DisableWorkflow(ctx context.Context, repo domain.RepoName, path string, at time.Time) error {
53+ ret := _mock.Called(ctx, repo, path, at)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for DisableWorkflow")
57+ }
58+
59+ var r0 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, time.Time) error); ok {
61+ r0 = returnFunc(ctx, repo, path, at)
62+ } else {
63+ r0 = ret.Error(0)
64+ }
65+ return r0
66+}
67+
68+// CIWorkflowStore_DisableWorkflow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableWorkflow'
69+type CIWorkflowStore_DisableWorkflow_Call struct {
70+ *mock.Call
71+}
72+
73+// DisableWorkflow is a helper method to define mock.On call
74+// - ctx context.Context
75+// - repo domain.RepoName
76+// - path string
77+// - at time.Time
78+func (_e *CIWorkflowStore_Expecter) DisableWorkflow(ctx any, repo any, path any, at any) *CIWorkflowStore_DisableWorkflow_Call {
79+ return &CIWorkflowStore_DisableWorkflow_Call{Call: _e.mock.On("DisableWorkflow", ctx, repo, path, at)}
80+}
81+
82+func (_c *CIWorkflowStore_DisableWorkflow_Call) Run(run func(ctx context.Context, repo domain.RepoName, path string, at time.Time)) *CIWorkflowStore_DisableWorkflow_Call {
83+ _c.Call.Run(func(args mock.Arguments) {
84+ var arg0 context.Context
85+ if args[0] != nil {
86+ arg0 = args[0].(context.Context)
87+ }
88+ var arg1 domain.RepoName
89+ if args[1] != nil {
90+ arg1 = args[1].(domain.RepoName)
91+ }
92+ var arg2 string
93+ if args[2] != nil {
94+ arg2 = args[2].(string)
95+ }
96+ var arg3 time.Time
97+ if args[3] != nil {
98+ arg3 = args[3].(time.Time)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ arg2,
104+ arg3,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *CIWorkflowStore_DisableWorkflow_Call) Return(err error) *CIWorkflowStore_DisableWorkflow_Call {
111+ _c.Call.Return(err)
112+ return _c
113+}
114+
115+func (_c *CIWorkflowStore_DisableWorkflow_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, path string, at time.Time) error) *CIWorkflowStore_DisableWorkflow_Call {
116+ _c.Call.Return(run)
117+ return _c
118+}
119+
120+// DisabledWorkflows provides a mock function for the type CIWorkflowStore
121+func (_mock *CIWorkflowStore) DisabledWorkflows(ctx context.Context, repo domain.RepoName) ([]string, error) {
122+ ret := _mock.Called(ctx, repo)
123+
124+ if len(ret) == 0 {
125+ panic("no return value specified for DisabledWorkflows")
126+ }
127+
128+ var r0 []string
129+ var r1 error
130+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]string, error)); ok {
131+ return returnFunc(ctx, repo)
132+ }
133+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []string); ok {
134+ r0 = returnFunc(ctx, repo)
135+ } else {
136+ if ret.Get(0) != nil {
137+ r0 = ret.Get(0).([]string)
138+ }
139+ }
140+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
141+ r1 = returnFunc(ctx, repo)
142+ } else {
143+ r1 = ret.Error(1)
144+ }
145+ return r0, r1
146+}
147+
148+// CIWorkflowStore_DisabledWorkflows_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisabledWorkflows'
149+type CIWorkflowStore_DisabledWorkflows_Call struct {
150+ *mock.Call
151+}
152+
153+// DisabledWorkflows is a helper method to define mock.On call
154+// - ctx context.Context
155+// - repo domain.RepoName
156+func (_e *CIWorkflowStore_Expecter) DisabledWorkflows(ctx any, repo any) *CIWorkflowStore_DisabledWorkflows_Call {
157+ return &CIWorkflowStore_DisabledWorkflows_Call{Call: _e.mock.On("DisabledWorkflows", ctx, repo)}
158+}
159+
160+func (_c *CIWorkflowStore_DisabledWorkflows_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *CIWorkflowStore_DisabledWorkflows_Call {
161+ _c.Call.Run(func(args mock.Arguments) {
162+ var arg0 context.Context
163+ if args[0] != nil {
164+ arg0 = args[0].(context.Context)
165+ }
166+ var arg1 domain.RepoName
167+ if args[1] != nil {
168+ arg1 = args[1].(domain.RepoName)
169+ }
170+ run(
171+ arg0,
172+ arg1,
173+ )
174+ })
175+ return _c
176+}
177+
178+func (_c *CIWorkflowStore_DisabledWorkflows_Call) Return(strings []string, err error) *CIWorkflowStore_DisabledWorkflows_Call {
179+ _c.Call.Return(strings, err)
180+ return _c
181+}
182+
183+func (_c *CIWorkflowStore_DisabledWorkflows_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]string, error)) *CIWorkflowStore_DisabledWorkflows_Call {
184+ _c.Call.Return(run)
185+ return _c
186+}
187+
188+// EnableWorkflow provides a mock function for the type CIWorkflowStore
189+func (_mock *CIWorkflowStore) EnableWorkflow(ctx context.Context, repo domain.RepoName, path string) error {
190+ ret := _mock.Called(ctx, repo, path)
191+
192+ if len(ret) == 0 {
193+ panic("no return value specified for EnableWorkflow")
194+ }
195+
196+ var r0 error
197+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) error); ok {
198+ r0 = returnFunc(ctx, repo, path)
199+ } else {
200+ r0 = ret.Error(0)
201+ }
202+ return r0
203+}
204+
205+// CIWorkflowStore_EnableWorkflow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableWorkflow'
206+type CIWorkflowStore_EnableWorkflow_Call struct {
207+ *mock.Call
208+}
209+
210+// EnableWorkflow is a helper method to define mock.On call
211+// - ctx context.Context
212+// - repo domain.RepoName
213+// - path string
214+func (_e *CIWorkflowStore_Expecter) EnableWorkflow(ctx any, repo any, path any) *CIWorkflowStore_EnableWorkflow_Call {
215+ return &CIWorkflowStore_EnableWorkflow_Call{Call: _e.mock.On("EnableWorkflow", ctx, repo, path)}
216+}
217+
218+func (_c *CIWorkflowStore_EnableWorkflow_Call) Run(run func(ctx context.Context, repo domain.RepoName, path string)) *CIWorkflowStore_EnableWorkflow_Call {
219+ _c.Call.Run(func(args mock.Arguments) {
220+ var arg0 context.Context
221+ if args[0] != nil {
222+ arg0 = args[0].(context.Context)
223+ }
224+ var arg1 domain.RepoName
225+ if args[1] != nil {
226+ arg1 = args[1].(domain.RepoName)
227+ }
228+ var arg2 string
229+ if args[2] != nil {
230+ arg2 = args[2].(string)
231+ }
232+ run(
233+ arg0,
234+ arg1,
235+ arg2,
236+ )
237+ })
238+ return _c
239+}
240+
241+func (_c *CIWorkflowStore_EnableWorkflow_Call) Return(err error) *CIWorkflowStore_EnableWorkflow_Call {
242+ _c.Call.Return(err)
243+ return _c
244+}
245+
246+func (_c *CIWorkflowStore_EnableWorkflow_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, path string) error) *CIWorkflowStore_EnableWorkflow_Call {
247+ _c.Call.Return(run)
248+ return _c
249+}
M internal/ports/mocks/RunService.go +304 -0
@@ -126,6 +126,158 @@ func (_c *RunService_CancelRun_Call) RunAndReturn(run func(ctx context.Context,
126126 return _c
127127 }
128128
129+// DisableWorkflow provides a mock function for the type RunService
130+func (_mock *RunService) DisableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (ports.WorkflowInfo, error) {
131+ ret := _mock.Called(ctx, p, repo, file)
132+
133+ if len(ret) == 0 {
134+ panic("no return value specified for DisableWorkflow")
135+ }
136+
137+ var r0 ports.WorkflowInfo
138+ var r1 error
139+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) (ports.WorkflowInfo, error)); ok {
140+ return returnFunc(ctx, p, repo, file)
141+ }
142+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) ports.WorkflowInfo); ok {
143+ r0 = returnFunc(ctx, p, repo, file)
144+ } else {
145+ r0 = ret.Get(0).(ports.WorkflowInfo)
146+ }
147+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, string) error); ok {
148+ r1 = returnFunc(ctx, p, repo, file)
149+ } else {
150+ r1 = ret.Error(1)
151+ }
152+ return r0, r1
153+}
154+
155+// RunService_DisableWorkflow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableWorkflow'
156+type RunService_DisableWorkflow_Call struct {
157+ *mock.Call
158+}
159+
160+// DisableWorkflow is a helper method to define mock.On call
161+// - ctx context.Context
162+// - p domain.Principal
163+// - repo domain.RepoName
164+// - file string
165+func (_e *RunService_Expecter) DisableWorkflow(ctx any, p any, repo any, file any) *RunService_DisableWorkflow_Call {
166+ return &RunService_DisableWorkflow_Call{Call: _e.mock.On("DisableWorkflow", ctx, p, repo, file)}
167+}
168+
169+func (_c *RunService_DisableWorkflow_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, file string)) *RunService_DisableWorkflow_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 context.Context
172+ if args[0] != nil {
173+ arg0 = args[0].(context.Context)
174+ }
175+ var arg1 domain.Principal
176+ if args[1] != nil {
177+ arg1 = args[1].(domain.Principal)
178+ }
179+ var arg2 domain.RepoName
180+ if args[2] != nil {
181+ arg2 = args[2].(domain.RepoName)
182+ }
183+ var arg3 string
184+ if args[3] != nil {
185+ arg3 = args[3].(string)
186+ }
187+ run(
188+ arg0,
189+ arg1,
190+ arg2,
191+ arg3,
192+ )
193+ })
194+ return _c
195+}
196+
197+func (_c *RunService_DisableWorkflow_Call) Return(workflowInfo ports.WorkflowInfo, err error) *RunService_DisableWorkflow_Call {
198+ _c.Call.Return(workflowInfo, err)
199+ return _c
200+}
201+
202+func (_c *RunService_DisableWorkflow_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (ports.WorkflowInfo, error)) *RunService_DisableWorkflow_Call {
203+ _c.Call.Return(run)
204+ return _c
205+}
206+
207+// DisabledWorkflows provides a mock function for the type RunService
208+func (_mock *RunService) DisabledWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]string, error) {
209+ ret := _mock.Called(ctx, p, repo)
210+
211+ if len(ret) == 0 {
212+ panic("no return value specified for DisabledWorkflows")
213+ }
214+
215+ var r0 []string
216+ var r1 error
217+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) ([]string, error)); ok {
218+ return returnFunc(ctx, p, repo)
219+ }
220+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) []string); ok {
221+ r0 = returnFunc(ctx, p, repo)
222+ } else {
223+ if ret.Get(0) != nil {
224+ r0 = ret.Get(0).([]string)
225+ }
226+ }
227+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
228+ r1 = returnFunc(ctx, p, repo)
229+ } else {
230+ r1 = ret.Error(1)
231+ }
232+ return r0, r1
233+}
234+
235+// RunService_DisabledWorkflows_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisabledWorkflows'
236+type RunService_DisabledWorkflows_Call struct {
237+ *mock.Call
238+}
239+
240+// DisabledWorkflows is a helper method to define mock.On call
241+// - ctx context.Context
242+// - p domain.Principal
243+// - repo domain.RepoName
244+func (_e *RunService_Expecter) DisabledWorkflows(ctx any, p any, repo any) *RunService_DisabledWorkflows_Call {
245+ return &RunService_DisabledWorkflows_Call{Call: _e.mock.On("DisabledWorkflows", ctx, p, repo)}
246+}
247+
248+func (_c *RunService_DisabledWorkflows_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *RunService_DisabledWorkflows_Call {
249+ _c.Call.Run(func(args mock.Arguments) {
250+ var arg0 context.Context
251+ if args[0] != nil {
252+ arg0 = args[0].(context.Context)
253+ }
254+ var arg1 domain.Principal
255+ if args[1] != nil {
256+ arg1 = args[1].(domain.Principal)
257+ }
258+ var arg2 domain.RepoName
259+ if args[2] != nil {
260+ arg2 = args[2].(domain.RepoName)
261+ }
262+ run(
263+ arg0,
264+ arg1,
265+ arg2,
266+ )
267+ })
268+ return _c
269+}
270+
271+func (_c *RunService_DisabledWorkflows_Call) Return(strings []string, err error) *RunService_DisabledWorkflows_Call {
272+ _c.Call.Return(strings, err)
273+ return _c
274+}
275+
276+func (_c *RunService_DisabledWorkflows_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]string, error)) *RunService_DisabledWorkflows_Call {
277+ _c.Call.Return(run)
278+ return _c
279+}
280+
129281 // DispatchWorkflow provides a mock function for the type RunService
130282 func (_mock *RunService) DispatchWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.DispatchParams) (domain.CIRun, error) {
131283 ret := _mock.Called(ctx, p, repo, params)
@@ -204,6 +356,84 @@ func (_c *RunService_DispatchWorkflow_Call) RunAndReturn(run func(ctx context.Co
204356 return _c
205357 }
206358
359+// EnableWorkflow provides a mock function for the type RunService
360+func (_mock *RunService) EnableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (ports.WorkflowInfo, error) {
361+ ret := _mock.Called(ctx, p, repo, file)
362+
363+ if len(ret) == 0 {
364+ panic("no return value specified for EnableWorkflow")
365+ }
366+
367+ var r0 ports.WorkflowInfo
368+ var r1 error
369+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) (ports.WorkflowInfo, error)); ok {
370+ return returnFunc(ctx, p, repo, file)
371+ }
372+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) ports.WorkflowInfo); ok {
373+ r0 = returnFunc(ctx, p, repo, file)
374+ } else {
375+ r0 = ret.Get(0).(ports.WorkflowInfo)
376+ }
377+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, string) error); ok {
378+ r1 = returnFunc(ctx, p, repo, file)
379+ } else {
380+ r1 = ret.Error(1)
381+ }
382+ return r0, r1
383+}
384+
385+// RunService_EnableWorkflow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableWorkflow'
386+type RunService_EnableWorkflow_Call struct {
387+ *mock.Call
388+}
389+
390+// EnableWorkflow is a helper method to define mock.On call
391+// - ctx context.Context
392+// - p domain.Principal
393+// - repo domain.RepoName
394+// - file string
395+func (_e *RunService_Expecter) EnableWorkflow(ctx any, p any, repo any, file any) *RunService_EnableWorkflow_Call {
396+ return &RunService_EnableWorkflow_Call{Call: _e.mock.On("EnableWorkflow", ctx, p, repo, file)}
397+}
398+
399+func (_c *RunService_EnableWorkflow_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, file string)) *RunService_EnableWorkflow_Call {
400+ _c.Call.Run(func(args mock.Arguments) {
401+ var arg0 context.Context
402+ if args[0] != nil {
403+ arg0 = args[0].(context.Context)
404+ }
405+ var arg1 domain.Principal
406+ if args[1] != nil {
407+ arg1 = args[1].(domain.Principal)
408+ }
409+ var arg2 domain.RepoName
410+ if args[2] != nil {
411+ arg2 = args[2].(domain.RepoName)
412+ }
413+ var arg3 string
414+ if args[3] != nil {
415+ arg3 = args[3].(string)
416+ }
417+ run(
418+ arg0,
419+ arg1,
420+ arg2,
421+ arg3,
422+ )
423+ })
424+ return _c
425+}
426+
427+func (_c *RunService_EnableWorkflow_Call) Return(workflowInfo ports.WorkflowInfo, err error) *RunService_EnableWorkflow_Call {
428+ _c.Call.Return(workflowInfo, err)
429+ return _c
430+}
431+
432+func (_c *RunService_EnableWorkflow_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (ports.WorkflowInfo, error)) *RunService_EnableWorkflow_Call {
433+ _c.Call.Return(run)
434+ return _c
435+}
436+
207437 // GetJob provides a mock function for the type RunService
208438 func (_mock *RunService) GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (ports.JobView, error) {
209439 ret := _mock.Called(ctx, p, repo, jobID, attempt)
@@ -861,6 +1091,80 @@ func (_c *RunService_ListRuns_Call) RunAndReturn(run func(ctx context.Context, p
8611091 return _c
8621092 }
8631093
1094+// ListWorkflows provides a mock function for the type RunService
1095+func (_mock *RunService) ListWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]ports.WorkflowInfo, error) {
1096+ ret := _mock.Called(ctx, p, repo)
1097+
1098+ if len(ret) == 0 {
1099+ panic("no return value specified for ListWorkflows")
1100+ }
1101+
1102+ var r0 []ports.WorkflowInfo
1103+ var r1 error
1104+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) ([]ports.WorkflowInfo, error)); ok {
1105+ return returnFunc(ctx, p, repo)
1106+ }
1107+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) []ports.WorkflowInfo); ok {
1108+ r0 = returnFunc(ctx, p, repo)
1109+ } else {
1110+ if ret.Get(0) != nil {
1111+ r0 = ret.Get(0).([]ports.WorkflowInfo)
1112+ }
1113+ }
1114+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
1115+ r1 = returnFunc(ctx, p, repo)
1116+ } else {
1117+ r1 = ret.Error(1)
1118+ }
1119+ return r0, r1
1120+}
1121+
1122+// RunService_ListWorkflows_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListWorkflows'
1123+type RunService_ListWorkflows_Call struct {
1124+ *mock.Call
1125+}
1126+
1127+// ListWorkflows is a helper method to define mock.On call
1128+// - ctx context.Context
1129+// - p domain.Principal
1130+// - repo domain.RepoName
1131+func (_e *RunService_Expecter) ListWorkflows(ctx any, p any, repo any) *RunService_ListWorkflows_Call {
1132+ return &RunService_ListWorkflows_Call{Call: _e.mock.On("ListWorkflows", ctx, p, repo)}
1133+}
1134+
1135+func (_c *RunService_ListWorkflows_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *RunService_ListWorkflows_Call {
1136+ _c.Call.Run(func(args mock.Arguments) {
1137+ var arg0 context.Context
1138+ if args[0] != nil {
1139+ arg0 = args[0].(context.Context)
1140+ }
1141+ var arg1 domain.Principal
1142+ if args[1] != nil {
1143+ arg1 = args[1].(domain.Principal)
1144+ }
1145+ var arg2 domain.RepoName
1146+ if args[2] != nil {
1147+ arg2 = args[2].(domain.RepoName)
1148+ }
1149+ run(
1150+ arg0,
1151+ arg1,
1152+ arg2,
1153+ )
1154+ })
1155+ return _c
1156+}
1157+
1158+func (_c *RunService_ListWorkflows_Call) Return(workflowInfos []ports.WorkflowInfo, err error) *RunService_ListWorkflows_Call {
1159+ _c.Call.Return(workflowInfos, err)
1160+ return _c
1161+}
1162+
1163+func (_c *RunService_ListWorkflows_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]ports.WorkflowInfo, error)) *RunService_ListWorkflows_Call {
1164+ _c.Call.Return(run)
1165+ return _c
1166+}
1167+
8641168 // RemoveRunner provides a mock function for the type RunService
8651169 func (_mock *RunService) RemoveRunner(ctx context.Context, p domain.Principal, id string) error {
8661170 ret := _mock.Called(ctx, p, id)
M internal/usecase/ci.go +7 -0
@@ -167,6 +167,9 @@ type CIConfig struct {
167167 // runs and the trust of dispatched ones (ci_tips.go). Nil: they are never
168168 // trusted. Leave it unset rather than assigning a typed nil pointer.
169169 RefTips ports.CIRefTipStore
170+ // WorkflowState keeps the workflows a repository disabled (ci_workflows.go).
171+ // Nil: none is. Leave it unset rather than assigning a typed nil pointer.
172+ WorkflowState ports.CIWorkflowStore
170173 // DispatchResolved, when set, is called by DispatchWorkflow after it resolved
171174 // its ref and before it judges the trust of the run. Tests use it to move the
172175 // ref in between.
@@ -175,6 +178,10 @@ type CIConfig struct {
175178 // "requested" or "completed" event of a run were started (or none matched).
176179 // Tests use it as a barrier.
177180 RunEventHandled func(runID int64, action string)
181+ // ScheduleLoaded, when set, is called after the scheduler read the workflows
182+ // of a repository and before it caches and fires them. Tests use it to
183+ // change the disabled state in that window.
184+ ScheduleLoaded func(repo domain.RepoName)
178185 // ScheduleVisit, when set, is called with each repository the scheduler
179186 // looks at. Tests use it.
180187 ScheduleVisit func(repo domain.RepoName)
M internal/usecase/ci_dispatch.go +12 -16
@@ -7,7 +7,6 @@ import (
77 "errors"
88 "fmt"
99 "io"
10- "path"
1110 "strings"
1211
1312 "bnema.dev/ermite/internal/domain"
@@ -41,6 +40,11 @@ func (c *CI) DispatchWorkflow(ctx context.Context, p domain.Principal, name doma
4140 if err != nil {
4241 return domain.CIRun{}, err
4342 }
43+ if off, err := c.workflowDisabled(ctx, name, wfPath); err != nil {
44+ return domain.CIRun{}, err
45+ } else if off {
46+ return domain.CIRun{}, fmt.Errorf("%w: %s is disabled (`ermite workflow enable`)", domain.ErrPreconditionFailed, wfPath)
47+ }
4448 pin, err := c.dispatchRef(ctx, name, params.Ref)
4549 if err != nil {
4650 return domain.CIRun{}, err
@@ -91,6 +95,10 @@ func (c *CI) DispatchWorkflow(ctx context.Context, p domain.Principal, name doma
9195 return domain.CIRun{}, fmt.Errorf("%w: %s", domain.ErrInvalid, problem.text)
9296 }
9397 run, err := c.launchRun(ctx, repo, t, f, wfPath, string(content), wf)
98+ if errors.Is(err, domain.ErrWorkflowDisabled) {
99+ // Disabled after the check above: the store refused the run.
100+ return domain.CIRun{}, fmt.Errorf("%w: %s is disabled (`ermite workflow enable`)", domain.ErrPreconditionFailed, wfPath)
101+ }
94102 if err != nil {
95103 return domain.CIRun{}, err
96104 }
@@ -131,21 +139,9 @@ func (c *CI) dispatchCaller(ctx context.Context, p domain.Principal, name domain
131139 }
132140
133141 // dispatchPath checks the workflow file of a dispatch: a file of
134-// .github/workflows, named by its path or by its file name.
135-func dispatchPath(s string) (string, error) {
136- s = strings.TrimSpace(s)
137- if s == "" {
138- return "", fmt.Errorf("%w: the workflow file is required", domain.ErrInvalid)
139- }
140- if !strings.Contains(s, "/") {
141- s = workflowDir + "/" + s
142- }
143- ext := strings.ToLower(path.Ext(s))
144- if path.Dir(s) != workflowDir || (ext != ".yml" && ext != ".yaml") {
145- return "", fmt.Errorf("%w: %q is not a workflow file of %s", domain.ErrInvalid, s, workflowDir)
146- }
147- return s, nil
148-}
142+// .github/workflows, named by its path or by its file name, in its canonical
143+// spelling (the one the disabled state is keyed by).
144+func dispatchPath(s string) (string, error) { return domain.ParseWorkflowPath(s) }
149145
150146 // pinnedRef is what a dispatch resolved its ref to, once: the full name, the
151147 // commit the run executes and the object the ref points to (the commit for a
M internal/usecase/ci_schedule.go +16 -1
@@ -329,7 +329,11 @@ func (c *CI) loadSchedules(ctx context.Context, repo domain.RepoName) (*schedule
329329 return &scheduleEntry{}, nil // no repository, no default branch, nothing to schedule
330330 }
331331 e := &scheduleEntry{tip: tip}
332- files, err := c.workflowFiles(ctx, repo, tip)
332+ // Every workflow, disabled or not: the cache is of the commit, not of the
333+ // state, which is read when a schedule fires (fireSchedule). A load that
334+ // raced with a disable or an enable would otherwise keep the wrong set of
335+ // workflows for as long as the tip does not move.
336+ files, err := c.listWorkflowFiles(ctx, repo, tip)
333337 if err != nil {
334338 return nil, err
335339 }
@@ -394,6 +398,9 @@ func (c *CI) scheduleRepo(ctx context.Context, repo domain.RepoName, minute time
394398 if e, err = c.loadSchedules(ctx, repo); err != nil {
395399 return fmt.Errorf("read the schedules: %w", err)
396400 }
401+ if c.cfg.ScheduleLoaded != nil {
402+ c.cfg.ScheduleLoaded(repo)
403+ }
397404 c.sched.put(repo, e)
398405 }
399406 idx, specs := dueFor(e, minute)
@@ -422,6 +429,11 @@ func (c *CI) scheduleRepo(ctx context.Context, repo domain.RepoName, minute time
422429
423430 // fireSchedule starts the run of one scheduled workflow.
424431 func (c *CI) fireSchedule(ctx context.Context, repo domain.Repo, def string, e *scheduleEntry, head domain.Commit, sw scheduledWorkflow, spec string) error {
432+ // The cache holds the disabled workflows too; the state is read now (and the
433+ // store checks it again when the run is stored).
434+ if off, err := c.workflowDisabled(ctx, repo.Name, sw.path); err != nil || off {
435+ return err
436+ }
425437 wf, perrs := parseWorkflow(sw.path, sw.yaml)
426438 if len(perrs) > 0 {
427439 return nil
@@ -442,5 +454,8 @@ func (c *CI) fireSchedule(ctx context.Context, repo domain.Repo, def string, e *
442454 return c.startInvalid(ctx, repo, t, facts, sw.path, name, problem.text)
443455 }
444456 _, err := c.launchRun(ctx, repo, t, facts, sw.path, string(sw.yaml), wf)
457+ if errors.Is(err, domain.ErrWorkflowDisabled) {
458+ return nil
459+ }
445460 return err
446461 }
M internal/usecase/ci_trigger.go +25 -3
@@ -430,8 +430,25 @@ type workflowFile struct {
430430 problem string
431431 }
432432
433-// workflowFiles reads .github/workflows/*.yml|*.yaml at the commit.
433+// workflowFiles reads .github/workflows/*.yml|*.yaml at the commit, without the
434+// workflows the repository disabled: every event that starts a run reads its
435+// workflows here, so a disabled one starts none (ci_workflows.go).
434436 func (c *CI) workflowFiles(ctx context.Context, repo domain.RepoName, commit domain.ObjectID) ([]workflowFile, error) {
437+ files, err := c.listWorkflowFiles(ctx, repo, commit)
438+ if err != nil || len(files) == 0 {
439+ return files, err
440+ }
441+ off, err := c.disabledPaths(ctx, repo)
442+ if err != nil || len(off) == 0 {
443+ return files, err
444+ }
445+ return slices.DeleteFunc(files, func(f workflowFile) bool { return slices.Contains(off, f.path) }), nil
446+}
447+
448+// listWorkflowFiles is workflowFiles with the disabled ones. The disabled state
449+// is also enforced where a run is stored (CIStore.CreateRun), which is what
450+// holds against a change made while a push is being planned.
451+func (c *CI) listWorkflowFiles(ctx context.Context, repo domain.RepoName, commit domain.ObjectID) ([]workflowFile, error) {
435452 dir, err := domain.ParseTreePath(workflowDir)
436453 if err != nil {
437454 return nil, err
@@ -512,7 +529,7 @@ func (c *CI) startInvalid(ctx context.Context, repo domain.Repo, t ciTrigger, f
512529 }},
513530 }
514531 })
515- if errors.Is(err, domain.ErrNotFound) || errors.Is(err, errPullClosed) {
532+ if errors.Is(err, domain.ErrNotFound) || errors.Is(err, errPullClosed) || errors.Is(err, domain.ErrWorkflowDisabled) {
516533 return nil
517534 }
518535 if err != nil {
@@ -534,11 +551,16 @@ func (c *CI) publishRun(ctx context.Context, repo domain.Repo, run domain.CIRun)
534551 // job of the file, then plans the jobs that can start.
535552 func (c *CI) startRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) error {
536553 _, err := c.launchRun(ctx, repo, t, f, wfPath, yaml, wf)
554+ if errors.Is(err, domain.ErrWorkflowDisabled) {
555+ return nil // disabled since it was read: no run (the store decides, atomically)
556+ }
537557 return err
538558 }
539559
540560 // launchRun is startRun that also returns the run it created, as it was when
541-// created (the zero run when the repository or the merge request is gone).
561+// created (the zero run when the repository or the merge request is gone). A
562+// workflow that is disabled when the run is stored gets none, whatever was
563+// checked before: the error is domain.ErrWorkflowDisabled.
542564 func (c *CI) launchRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) (domain.CIRun, error) {
543565 now := c.now()
544566 name := workflowName(wf, wfPath)
M internal/usecase/ci_workflowrun.go +1 -1
@@ -244,7 +244,7 @@ func (c *CI) handleRunEvent(ctx context.Context, e runEvent) error {
244244 errs = append(errs, c.startInvalid(ctx, repo, t, facts, f.path, name, problem.text))
245245 continue
246246 }
247- if _, err := c.launchRun(ctx, repo, t, facts, f.path, string(f.content), wf); err != nil && !errors.Is(err, context.Canceled) {
247+ if _, err := c.launchRun(ctx, repo, t, facts, f.path, string(f.content), wf); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, domain.ErrWorkflowDisabled) {
248248 errs = append(errs, err)
249249 }
250250 }
A internal/usecase/ci_workflows.go +152 -0
@@ -0,0 +1,152 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package usecase
4+
5+import (
6+ "context"
7+ "fmt"
8+ "slices"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+)
13+
14+// Disabled workflows (`ermite workflow disable|enable|list`, the equivalent of
15+// `gh workflow disable`). The state is per repository and per workflow file
16+// path, kept in the store apart from Git: it holds for every branch and every
17+// commit, and the file does not have to exist, so it can be set on an empty
18+// repository before the first push.
19+//
20+// A disabled workflow starts no run for any event. The workflow files are read
21+// through workflowFiles for push, pull_request, pull_request_target, create,
22+// delete, schedule and workflow_run, and that one place leaves the disabled
23+// ones out; workflow_dispatch and the firing of a cached schedule check the
24+// state themselves (the cache holds every workflow). Whatever was checked
25+// before, the store refuses to create a run for a disabled workflow in the very
26+// transaction that creates it (CIStore.CreateRun, domain.ErrWorkflowDisabled),
27+// so a disable that lands while a push, a dispatch or a schedule is being
28+// planned cannot be overtaken. A run already started is not touched, and the events that
29+// happened while a workflow was disabled are not replayed when it is enabled.
30+
31+// workflowDisabled reports whether the path is disabled; nothing is when no
32+// store is configured.
33+func (c *CI) workflowDisabled(ctx context.Context, repo domain.RepoName, wfPath string) (bool, error) {
34+ off, err := c.disabledPaths(ctx, repo)
35+ if err != nil {
36+ return false, err
37+ }
38+ return slices.Contains(off, wfPath), nil
39+}
40+
41+// disabledPaths are the disabled paths of a repository, sorted.
42+func (c *CI) disabledPaths(ctx context.Context, repo domain.RepoName) ([]string, error) {
43+ if c.cfg.WorkflowState == nil {
44+ return nil, nil
45+ }
46+ off, err := c.cfg.WorkflowState.DisabledWorkflows(ctx, repo)
47+ if err != nil {
48+ return nil, fmt.Errorf("read the disabled workflows: %w", err)
49+ }
50+ return off, nil
51+}
52+
53+// ListWorkflows returns the workflow files at the tip of the default branch and
54+// the disabled ones whatever the branch holds, ordered by path. Reading the
55+// repository is enough.
56+func (c *CI) ListWorkflows(ctx context.Context, p domain.Principal, name domain.RepoName) ([]ports.WorkflowInfo, error) {
57+ if _, err := c.readableRepo(ctx, p, name); err != nil {
58+ return nil, err
59+ }
60+ off, err := c.disabledPaths(ctx, name)
61+ if err != nil {
62+ return nil, err
63+ }
64+ byPath := map[string]*ports.WorkflowInfo{}
65+ if tip, ok := c.defaultTip(ctx, name); ok {
66+ files, err := c.listWorkflowFiles(ctx, name, tip)
67+ if err != nil {
68+ return nil, err
69+ }
70+ for _, f := range files {
71+ info := &ports.WorkflowInfo{Path: f.path}
72+ if f.problem == "" {
73+ if wf, perrs := parseWorkflow(f.path, f.content); len(perrs) == 0 && wf != nil {
74+ info.Name = workflowName(wf, f.path)
75+ }
76+ }
77+ byPath[f.path] = info
78+ }
79+ }
80+ for _, path := range off {
81+ if byPath[path] == nil {
82+ byPath[path] = &ports.WorkflowInfo{Path: path}
83+ }
84+ byPath[path].Disabled = true
85+ }
86+ out := make([]ports.WorkflowInfo, 0, len(byPath))
87+ for _, info := range byPath {
88+ out = append(out, *info)
89+ }
90+ slices.SortFunc(out, func(a, b ports.WorkflowInfo) int {
91+ switch {
92+ case a.Path < b.Path:
93+ return -1
94+ case a.Path > b.Path:
95+ return 1
96+ }
97+ return 0
98+ })
99+ return out, nil
100+}
101+
102+// DisabledWorkflows returns the disabled paths of a repository, sorted, without
103+// reading any file.
104+func (c *CI) DisabledWorkflows(ctx context.Context, p domain.Principal, name domain.RepoName) ([]string, error) {
105+ if _, err := c.readableRepo(ctx, p, name); err != nil {
106+ return nil, err
107+ }
108+ return c.disabledPaths(ctx, name)
109+}
110+
111+// DisableWorkflow stops a workflow file from starting runs. The file is named by
112+// its path or its file name and does not have to exist. It needs repo:admin on
113+// the repository (adminAccess), and is idempotent.
114+func (c *CI) DisableWorkflow(ctx context.Context, p domain.Principal, name domain.RepoName, file string) (ports.WorkflowInfo, error) {
115+ return c.setWorkflowState(ctx, p, name, file, true)
116+}
117+
118+// EnableWorkflow undoes DisableWorkflow. Same access; idempotent.
119+func (c *CI) EnableWorkflow(ctx context.Context, p domain.Principal, name domain.RepoName, file string) (ports.WorkflowInfo, error) {
120+ return c.setWorkflowState(ctx, p, name, file, false)
121+}
122+
123+func (c *CI) setWorkflowState(ctx context.Context, p domain.Principal, name domain.RepoName, file string, disable bool) (ports.WorkflowInfo, error) {
124+ repo, err := lookupRepo(ctx, c.cfg.Meta, name)
125+ if err != nil {
126+ return ports.WorkflowInfo{}, err
127+ }
128+ if err := adminAccess(p, repo); err != nil {
129+ return ports.WorkflowInfo{}, err
130+ }
131+ wfPath, err := domain.ParseWorkflowPath(file)
132+ if err != nil {
133+ return ports.WorkflowInfo{}, err
134+ }
135+ if c.cfg.WorkflowState == nil {
136+ return ports.WorkflowInfo{}, fmt.Errorf("%w: this server keeps no workflow state", domain.ErrPreconditionFailed)
137+ }
138+ if disable {
139+ err = c.cfg.WorkflowState.DisableWorkflow(ctx, name, wfPath, c.now())
140+ } else {
141+ err = c.cfg.WorkflowState.EnableWorkflow(ctx, name, wfPath)
142+ }
143+ if err != nil {
144+ return ports.WorkflowInfo{}, err
145+ }
146+ state := "enabled"
147+ if disable {
148+ state = "disabled"
149+ }
150+ ctxLog(ctx).Info().Str("repo", name.String()).Str("workflow", wfPath).Str("principal", p.Label()).Msg("ci workflow " + state)
151+ return ports.WorkflowInfo{Path: wfPath, Disabled: disable}, nil
152+}
M proto/ermite/storage/ci.proto +7 -0
@@ -466,3 +466,10 @@ message CIConcurrencyMark {
466466 // that a binary that predates it still cancels the entry.
467467 bool queue_full = 4;
468468 }
469+
470+// CIWorkflowDisabled is stored under "ciwfoff/<repo>/<workflow path>" (the path
471+// is ".github/workflows/<file>"): the workflow is disabled and starts no run.
472+// The file does not have to exist. It goes with the repository.
473+message CIWorkflowDisabled {
474+ google.protobuf.Timestamp disabled_at = 1;
475+}
M skills/ermite/CLI.md +11 -0
@@ -62,6 +62,17 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
6262
6363 Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `target_branch`, `state`, `author` (token label), `created_at`, `updated_at`, `head_id`, `merged_commit_id`, `merge_mode`. `mr show --json` adds `commits`, `diff_base_id`, `stats`, `files` (with `hunks` under `--diff`: `kind`, `old_number`, `new_number`, `text`), `comments`, `last_read_id`, `last_read_lost`.
6464
65+## Workflows
66+
67+| Command | Scope | Notes |
68+|---|---|---|
69+| `workflow list <repo> [--json]` | `repo:read` for a private repository | the files of `.github/workflows` on the default branch with their state, and the disabled files that branch lacks |
70+| `workflow disable <repo> <file> [--json]` | `repo:admin` | like `gh workflow disable`: no event (push, pull_request, pull_request_target, schedule, workflow_run, create, delete, workflow_dispatch) starts a run of it; runs already started go on; `<file>` is `release.yml` or `.github/workflows/release.yml` and need not exist, so it works on an empty repository before the first push; doing it twice is not an error |
71+| `workflow enable <repo> <file> [--json]` | `repo:admin` | undoes `disable`; missed events are not replayed |
72+| `workflow run <file> --repo R [--ref REF] [-f name=value]` | `ci:write` | starts a `workflow_dispatch` workflow; refused while the workflow is disabled |
73+
74+Workflow JSON: `repo`, `file` (always `.github/workflows/<file>`), `name` (the `name:` on the default branch, empty when the file is absent there), `state` (`enabled` or `disabled`). A token limited to branch prefixes cannot disable or enable.
75+
6576 ## Events
6677
6778 `ermite watch [repo] [--json]` runs until interrupted, reconnects and resumes after the last event. With `--json` it prints one object per line with `id`, `time`, `type` and type-specific fields.

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