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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software: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)".

cmd/vole/runner.go +73

@@ -45,6 +45,13 @@ type runnerJob struct {
45 45 Steps []runnerStep `json:"steps"`
46 46 // Artifacts are path patterns, expanded in the checkout after the steps pass.
47 47 Artifacts []string `json:"artifacts,omitempty"`
48 // Pages is the site deploy this run performs, if any.
49 Pages *runnerPages `json:"pages,omitempty"`
50 }
51
52 type runnerPages struct {
53 Path string `json:"path"`
54 Ref string `json:"ref"`
48 55 }
49 56
50 57 type runnerArtifact struct {
@@ -251,6 +258,10 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur
251 258 fail("%s", err)
252 259 return
253 260 }
261 if err := publishSite(remote, work, job, log); err != nil {
262 fail("%s", err)
263 return
264 }
254 265 _, _ = s.call(runnerRequest{Op: "finish", Run: job.ID, Exit: 0, Log: log.String(), Artifacts: arts})
255 266 }
256 267
@@ -287,6 +298,68 @@ func publishArtifacts(remote, work string, job *runnerJob, log *strings.Builder)
287 298 return out, nil
288 299 }
289 300
301 // publishSite deploys the job's declared directory as the repository's site, using
302 // the same snapshot code the pages CLI uses.
303 //
304 // The staleness check first: a site is a snapshot, so a run that finishes after a
305 // newer commit has landed would quietly revert the site to an older build. Refusing
306 // is the honest outcome — the log says why, and the job fails instead of publishing
307 // a stale tree that nobody notices for a week.
308 func publishSite(remote, work string, job *runnerJob, log *strings.Builder) error {
309 if job.Pages == nil || job.Pages.Path == "" {
310 return nil
311 }
312 url, err := repoURL(remote, job.Repo)
313 if err != nil {
314 return err
315 }
316 tip, err := lsRemote(work, url, "refs/heads/"+job.Branch)
317 if err != nil {
318 return fmt.Errorf("check %s before deploying the site: %w", job.Branch, err)
319 }
320 switch {
321 case tip == "":
322 return fmt.Errorf("%s no longer exists, so there is no site to deploy to", job.Branch)
323 case tip != job.SHA:
324 return fmt.Errorf("this run built %s but %s is at %s now; the site was not deployed",
325 shortSHA(job.SHA), job.Branch, shortSHA(tip))
326 }
327
328 gitDir, err := gitDirOf(work)
329 if err != nil {
330 return err
331 }
332 c, err := openBranchCLI(gitDir, url, job.Pages.Ref, "vole-runner")
333 if err != nil {
334 return err
335 }
336 dir := filepath.Join(work, filepath.FromSlash(job.Pages.Path))
337 msg := fmt.Sprintf("site: %s from run %s (%s)", job.Branch, shortSHA(job.SHA), job.ID)
338 if len(msg) > 200 {
339 msg = fmt.Sprintf("site: %s from run %s", job.Branch, job.ID)
340 }
341 diff, files, deployed, err := deploySite(c, job.Pages.Ref, dir, msg, false)
342 if err != nil {
343 return err
344 }
345 if deployed {
346 fmt.Fprintf(log, "\nsite: published %d file(s) to %s (%s)\n", files, job.Pages.Ref, diff)
347 } else {
348 fmt.Fprintf(log, "\nsite: %s already matches %s\n", job.Pages.Ref, job.Pages.Path)
349 }
350 return nil
351 }
352
353 // gitDirOf asks the checkout where its git directory is, so the deploy can commit
354 // into it without changing directory.
355 func gitDirOf(work string) (string, error) {
356 out, err := exec.Command("git", "-C", work, "rev-parse", "--absolute-git-dir").Output()
357 if err != nil {
358 return "", fmt.Errorf("%s is not a git checkout", work)
359 }
360 return strings.TrimSpace(string(out)), nil
361 }
362
290 363 // artifactFile is one matched output.
291 364 type artifactFile struct {
292 365 Path string // slash-separated, relative to the checkout