vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 4 slice 1: workflow format, validated at the push that introduces it
8fe238d48181201272a44d9ba7b8d97342afa4ca · Keenan Nemetz · 17d ago · parent 1e955bfb
Phase 4 slice 1: workflow format, validated at the push that introduces it
.vole/workflows/*.yaml is parsed in pre-receive for any push that touches it, so a
broken workflow fails the push that added it, naming the file and every problem in
it. The alternative — accepting the file and discovering months later that it
never ran — is the failure this exists to prevent.
version: 1
on: {push: {branches: [main, "release/*"]}} # globs, default: every branch
jobs: {test: {steps: [{run: go test ./..., timeout: 20m}]}}
Strict by construction:
- Unknown keys are rejected rather than ignored, so `runs-on:` or a typo'd `setps:`
cannot silently do nothing.
- Every problem is reported in one rejection, not the first one only: a person
fixes the file once instead of once per push.
- Job names, step bodies, timeouts (default 10m, cap 1h) and the directory layout
are all checked, including files that are not .yaml/.yml and nested directories.
- The validator is in internal/workflow and `vole workflow check` runs the same
code, so client and server cannot disagree about what is valid.
Validation is scoped by a diff: a push that does not touch the workflow directory
costs one `git diff` and nothing else.
Adds gopkg.in/yaml.v3 (already in the module cache). Nothing executes yet — the
queue, runner and status reporting are the next slice, and the README says so
plainly rather than implying CI runs.
Verified on a live instance: pushing a workflow with a step that runs nothing and a
9h timeout is rejected with both problems named; the corrected file pushes and
`vole workflow list` reports `ci.yaml on main,release/* test(2 steps)`.
internal/server/integration_test.go +29
| @@ -1664,6 +1664,35 @@ func TestRetiredNameCannotBeReissued(t *testing.T) { | |||
| 1664 | 1664 | } | |
| 1665 | 1665 | } | |
| 1666 | 1666 | ||
| 1667 | // TestInvalidWorkflowRejectedAtPush: a broken workflow fails the push that | ||
| 1668 | // introduced it, naming the file and the reason, instead of sitting in the tree | ||
| 1669 | // and never running. | ||
| 1670 | func TestInvalidWorkflowRejectedAtPush(t *testing.T) { | ||
| 1671 | f := newFixture(t) | ||
| 1672 | f.publish(f.repo) | ||
| 1673 | |||
| 1674 | broken := workRepo(t, map[string]string{ | ||
| 1675 | ".vole/workflows/ci.yaml": "version: 1\njobs:\n test:\n steps:\n - name: forgot the command\n", | ||
| 1676 | }) | ||
| 1677 | _, errs, err := f.git(broken, "push", f.sshURL(f.repo), "main:refs/heads/main") | ||
| 1678 | if err == nil { | ||
| 1679 | t.Fatal("a workflow with a step that runs nothing was accepted") | ||
| 1680 | } | ||
| 1681 | for _, want := range []string{".vole/workflows/ci.yaml", "run is required"} { | ||
| 1682 | if !strings.Contains(errs, want) { | ||
| 1683 | t.Fatalf("rejection does not mention %q:\n%s", want, errs) | ||
| 1684 | } | ||
| 1685 | } | ||
| 1686 | |||
| 1687 | // The same push with a valid file is accepted. | ||
| 1688 | good := workRepo(t, map[string]string{ | ||
| 1689 | ".vole/workflows/ci.yaml": "version: 1\non:\n push:\n branches: [main]\njobs:\n test:\n steps:\n - run: go test ./...\n", | ||
| 1690 | }) | ||
| 1691 | if _, errs, err := f.git(good, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 1692 | t.Fatalf("valid workflow rejected: %s", errs) | ||
| 1693 | } | ||
| 1694 | } | ||
| 1695 | |||
| 1667 | 1696 | func mustRepo(t *testing.T, f *fixture) *repo.Meta { | |
| 1668 | 1697 | t.Helper() | |
| 1669 | 1698 | m, err := f.srv.Repos.Open(f.repo) | |