Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 7 slice 7: the build deploys the site

88e980b0e88b24a20605ceeb9e42fd0dacee7842 · Keenan Nemetz · 17d ago · parent 213f4d6d

Phase 7 slice 7: the build deploys the site

pages: {path: site} on a job publishes that directory as the site branch, using
the same snapshot code `vole pages deploy` uses — a site published by a workflow
and one published by a person have to be the same commit shape, or the version
that drifts is the one nobody runs by hand.

- workflow: pages.path validated at push — one relative directory, no glob (a
  site is one tree), and only one job per workflow may declare it, since two
  publishers race and the winner is whichever runner finished last
- the directory is pinned into the queued run beside the steps and the artifact
  patterns; the branch comes from the server (pages_ref), which a workflow
  cannot know
- runner: after the artifacts, checks that the run's SHA is still the tip of its
  branch and refuses otherwise. Deploy is a snapshot, so a stale build finishing
  late would silently revert the site; the failure names both commits
- the site commit says where it came from: "site: main from run <id> (<sha>)"

Fixed on the way, caught by the end-to-end test: the shared push helper treated a
URL as a missing *named* remote, so a runner's deploy committed locally and
reported success while the site 404'd. A URL is now a remote in its own right.

Measured through the binary: two pushes queued two runs; the older one was
refused ("this run built f1f415ec but main is at 6beaa530 now; the site was not
deployed") and published nothing; the newer one deployed and the site host served
the new tree, with `git log pages` showing
"site: main from run 01M5MSC3... (6beaa530)".

internal/workflow/workflow_test.go +53

@@ -160,3 +160,56 @@ jobs:
160 160 }
161 161 }
162 162 }
163
164 // TestPagesDeclarationIsValidated covers the site deploy declaration: one directory,
165 // one job per workflow, and nothing that would make "which tree is the site" a
166 // question.
167 func TestPagesDeclarationIsValidated(t *testing.T) {
168 ok := `version: 1
169 jobs:
170 site:
171 steps:
172 - run: make site
173 pages:
174 path: dist/site/
175 `
176 wf, problems := Parse("ci.yaml", ok)
177 if len(problems) > 0 {
178 t.Fatalf("a valid pages declaration was refused: %v", problems)
179 }
180 if wf.Jobs[0].Pages == nil || wf.Jobs[0].Pages.Path != "dist/site" {
181 t.Fatalf("pages = %+v, want dist/site without the trailing slash", wf.Jobs[0].Pages)
182 }
183
184 bad := []string{
185 " path: \"\"\n", // empty
186 " path: /var/www\n", // absolute
187 " path: ../outside\n", // climbs out
188 " path: dist/*\n", // a glob: a site is one tree
189 " pathx: dist\n", // unknown key
190 }
191 for _, decl := range bad {
192 body := "version: 1\njobs:\n site:\n steps:\n - run: true\n pages:\n" + decl
193 if _, problems := Parse("ci.yaml", body); len(problems) == 0 {
194 t.Errorf("pages declaration %q was accepted", strings.TrimSpace(decl))
195 }
196 }
197
198 // Two publishers would race: the winner would be whichever runner finished last.
199 two := `version: 1
200 jobs:
201 a:
202 steps:
203 - run: true
204 pages:
205 path: site
206 b:
207 steps:
208 - run: true
209 pages:
210 path: other
211 `
212 if _, problems := Parse("ci.yaml", two); len(problems) == 0 {
213 t.Fatal("two jobs publishing the site were accepted")
214 }
215 }