Commit 61d5525

Eric Bower  ·  2026-08-30 11:10:04 -0400 EDT
parent 25ab3ff
refactor: pici.sh instead of pico.sh
5 files changed,  +114, -31
+2, -2
......@@ -1,5 +1,5 @@
11 name: "Pici CI"
2-description: "Run parallel CI workflows with pico.sh, zmx, and interactive pipe.pico.sh remote debugging"
2+description: "Run parallel CI workflows with pici.sh, zmx, and interactive pipe.pico.sh remote debugging"
33 author: "pico.sh"
44 branding:
55 icon: "terminal"
......@@ -23,7 +23,7 @@ inputs:
2323 required: false
2424 default: "true"
2525 working_directory:
26- description: "Directory containing pico.sh"
26+ description: "Directory containing pici.sh"
2727 required: false
2828 default: "."
2929 zmx_version:
+28, -25
......@@ -230,9 +230,9 @@ func newLogger(space string, levelStr string) *slog.Logger {
230230 func printMainHelp() {
231231 fmt.Println(`pici - minimal parallel CI runner & monitor powered by zmx
232232
233-HOW IT WORKS: pici runs your project's CI pipeline defined in pico.sh:
233+HOW IT WORKS: pici runs your project's CI pipeline defined in pici.sh:
234234 1. Syncs workspace to a clean temporary directory (/tmp).
235- 2. Executes pico.sh inside zmx, running each task in its own isolated PTY.
235+ 2. Executes pici.sh inside zmx, running each task in its own isolated PTY.
236236 3. Bring your own isolation: tasks run on host unless wrapped (e.g. Docker, Podman, Nix).
237237 4. Tasks execute serially ('zmx run <name> <cmd>') or concurrently in detached mode ('zmx run <name> -d <cmd>').
238238 5. Concurrent background tasks are joined using 'zmx wait "*"' to block until all complete.
......@@ -241,7 +241,7 @@ HOW IT WORKS: pici runs your project's CI pipeline defined in pico.sh:
241241 8. Optionally rsyncs rendered logs & artifacts to a destination when specified.
242242
243243 PROJECT SETUP
244- 1. Create a pico.sh script in your project root:
244+ 1. Create a pici.sh script in your project root:
245245
246246 #!/usr/bin/env bash
247247 set -euo pipefail
......@@ -266,10 +266,10 @@ PROJECT SETUP
266266 # Wait for all background steps to finish
267267 zmx wait "*"
268268
269- 2. Make it executable: chmod +x pico.sh
269+ 2. Make it executable: chmod +x pici.sh
270270 3. Run locally: pici
271271
272-ENVIRONMENT VARIABLES: pici injects these vars into pico.sh:
272+ENVIRONMENT VARIABLES: pici injects these vars into pici.sh:
273273 PICI_JOB Unique job identifier (timestamp or commit SHA)
274274 PICI_REPO Repository name (default: directory basename)
275275 PICI_EVENT Event type ('local', 'git.push', 'git.tag')
......@@ -282,7 +282,7 @@ ENVIRONMENT VARIABLES: pici injects these vars into pico.sh:
282282 Pass custom env vars or overrides with -e / -env (e.g. pici -e CGO_ENABLED=0).
283283
284284 LOCAL DEVELOPER USAGE
285- pici [destination] [flags] Run ./pico.sh locally in /tmp & render HTML logs
285+ pici [destination] [flags] Run ./pici.sh locally in /tmp & render HTML logs
286286 pici pgs.sh:/my-site Run locally and rsync HTML logs to destination
287287 pici run [destination] Explicit alias for local run
288288 pici debug <repo/job_id | session> Attach to active zmx session for debugging
......@@ -297,7 +297,7 @@ SUBCOMMAND HELP
297297 pici <command> --help Show detailed help for a specific command (e.g. pici runner --help)
298298
299299 FLAGS
300- -e, -env <KEY=VAL> Set or override environment variable for pico.sh
300+ -e, -env <KEY=VAL> Set or override environment variable for pici.sh
301301 -in-place Execute workspace in-place without copying to /tmp
302302 -summary-file <path> Write markdown job summary table to file
303303 -debug-on-fail Open interactive pipe.pico.sh debug bridge on failure
......@@ -309,9 +309,9 @@ FLAGS
309309 }
310310
311311 func printMissingPicoHelp(cwd string) {
312- fmt.Printf(`❌ Error: no pico.sh found in %s
312+ fmt.Printf(`❌ Error: no pici.sh found in %s
313313
314-To run local CI tasks, create a pico.sh script in your project root:
314+To run local CI tasks, create a pici.sh script in your project root:
315315
316316 #!/usr/bin/env bash
317317 set -euo pipefail
......@@ -479,8 +479,8 @@ EVENT JSON FIELDS
479479 EXAMPLE
480480 echo '{"type":"push","name":"myrepo","workspace":"git@github.com:user/myrepo.git"}' | pici runner
481481
482-ENV VARS IN pico.sh
483- The runner exports these environment variables for your pico.sh script:
482+ENV VARS IN pici.sh
483+ The runner exports these environment variables for your pici.sh script:
484484 PICI_JOB Unique job identifier (e.g. "a3f2b8c1")
485485 PICI_REPO Repository name (from event "name" field)
486486 PICI_EVENT Event type (e.g. "git.push", "git.tag")
......@@ -488,10 +488,10 @@ ENV VARS IN pico.sh
488488 PICI_BRANCH Git branch name (set only for git.push events)
489489 PICI_ARTIFACTS_DIR Path to job artifacts directory for staging assets
490490
491- Note: pico.sh runs with the workspace as its working directory, so
491+ Note: pici.sh runs with the workspace as its working directory, so
492492 $(pwd) gives you the workspace path directly.
493493
494- Use them with defaults so pico.sh works standalone:
494+ Use them with defaults so pici.sh works standalone:
495495 JOB="${PICI_JOB:-local}"
496496 REPO="${PICI_REPO:-unknown}"
497497
......@@ -840,7 +840,7 @@ func (eng *JobEngine) Run(manifest string) error {
840840 domain = getDomain(eng.Ev.Type)
841841 }
842842 prefix := fmt.Sprintf("%s.%s.%s.", domain, eng.Ev.Name, eng.JobID)
843- // Child sessions use ".step." sub-prefix so zmx wait "*" inside pico.sh
843+ // Child sessions use ".step." sub-prefix so zmx wait "*" inside pici.sh
844844 // matches <domain>.<name>.<jobID>.step.* but NOT <domain>.<name>.<jobID>.runner.
845845 // This avoids a deadlock where the runner waits for itself.
846846 childPrefix := prefix + "step."
......@@ -851,7 +851,7 @@ func (eng *JobEngine) Run(manifest string) error {
851851 runnerName := prefix + "runner"
852852 // Name the runner explicitly. Do NOT set ZMX_SESSION_PREFIX for this
853853 // outer zmx run call — it would be prepended to the runner's own name.
854- // The prefix is only for child sessions spawned inside pico.sh (via the
854+ // The prefix is only for child sessions spawned inside pici.sh (via the
855855 // exported env var).
856856 artifactsDir := filepath.Join(eng.Cfg.ArtifactDir, eng.Ev.Name, eng.JobID, "artifacts")
857857 if err := os.MkdirAll(artifactsDir, 0755); err != nil {
......@@ -890,15 +890,19 @@ func (eng *JobEngine) Cleanup() error {
890890 return eng.Wk.Cleanup()
891891 }
892892
893-func (eng *JobEngine) FindManifest() (string, error) {
894- fnames := []string{"pico.sh"}
893+func findManifestInDir(dir string) (string, error) {
894+ fnames := []string{"pici.sh", "pico.sh"}
895895 for _, manifest := range fnames {
896- path := filepath.Join(eng.Wk.GetDir(), manifest)
896+ path := filepath.Join(dir, manifest)
897897 if _, err := os.Stat(path); err == nil {
898898 return path, nil
899899 }
900900 }
901- return "", fmt.Errorf("no pico.sh found in %s", eng.Wk.GetDir())
901+ return "", fmt.Errorf("no pici.sh or pico.sh found in %s", dir)
902+}
903+
904+func (eng *JobEngine) FindManifest() (string, error) {
905+ return findManifestInDir(eng.Wk.GetDir())
902906 }
903907
904908 func eventHandler(cfg *Cfg, eventData *Event) error {
......@@ -987,7 +991,7 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
987991 runErr = err
988992 fmt.Fprintf(os.Stdout, "❌ %s\n\n", err) //nolint:errcheck
989993 //nolint:errcheck
990- fmt.Fprint(os.Stdout, `Create a pico.sh script in your workspace root:
994+ fmt.Fprint(os.Stdout, `Create a pici.sh script in your workspace root:
991995
992996 #!/usr/bin/env bash
993997 set -euo pipefail
......@@ -1570,7 +1574,7 @@ func parseJobPrefix(prefix string) (name, jobID string) {
15701574
15711575 // resolveJobExitCode determines the job's exit code from its sessions.
15721576 // Defensive: if any child session failed, the job failed regardless of the
1573-// runner's exit code. This protects against bad pico.sh scripts that exit 0
1577+// runner's exit code. This protects against bad pici.sh scripts that exit 0
15741578 // without waiting for children.
15751579 func resolveJobExitCode(sessions []SessionInfo) (int, string) {
15761580 var runnerExit *int
......@@ -1837,7 +1841,7 @@ func allCompleted(sessions []SessionInfo) bool {
18371841
18381842 // isJobComplete returns true if all sessions for a job have completed
18391843 // AND a runner session exists and has completed. This ensures monitor and wait
1840-// do not mark a job complete while pico.sh (the runner) is still executing and
1844+// do not mark a job complete while pici.sh (the runner) is still executing and
18411845 // potentially spawning new tasks.
18421846 func isJobComplete(sessions []SessionInfo) bool {
18431847 if len(sessions) == 0 {
......@@ -2318,10 +2322,9 @@ func runLocal(cfg *Cfg, dest string) error {
23182322 return fmt.Errorf("get working directory: %w", err)
23192323 }
23202324
2321- picoPath := filepath.Join(cwd, "pico.sh")
2322- if _, err := os.Stat(picoPath); os.IsNotExist(err) {
2325+ if _, err := findManifestInDir(cwd); err != nil {
23232326 printMissingPicoHelp(cwd)
2324- return fmt.Errorf("no pico.sh found in %s", cwd)
2327+ return err
23252328 }
23262329
23272330 repoName := filepath.Base(cwd)
+82, -2
......@@ -818,7 +818,62 @@ func TestResolveJobExitCode(t *testing.T) {
818818 }
819819 }
820820
821-func TestRunLocal_MissingPico(t *testing.T) {
821+func TestFindManifestInDir(t *testing.T) {
822+ t.Run("finds pici.sh when present", func(t *testing.T) {
823+ tempDir := t.TempDir()
824+ if err := os.WriteFile(filepath.Join(tempDir, "pici.sh"), []byte("#!/bin/bash\n"), 0755); err != nil {
825+ t.Fatal(err)
826+ }
827+ path, err := findManifestInDir(tempDir)
828+ if err != nil {
829+ t.Fatalf("unexpected error: %v", err)
830+ }
831+ if filepath.Base(path) != "pici.sh" {
832+ t.Fatalf("expected pici.sh, got %s", filepath.Base(path))
833+ }
834+ })
835+
836+ t.Run("falls back to pico.sh when pici.sh is missing", func(t *testing.T) {
837+ tempDir := t.TempDir()
838+ if err := os.WriteFile(filepath.Join(tempDir, "pico.sh"), []byte("#!/bin/bash\n"), 0755); err != nil {
839+ t.Fatal(err)
840+ }
841+ path, err := findManifestInDir(tempDir)
842+ if err != nil {
843+ t.Fatalf("unexpected error: %v", err)
844+ }
845+ if filepath.Base(path) != "pico.sh" {
846+ t.Fatalf("expected pico.sh, got %s", filepath.Base(path))
847+ }
848+ })
849+
850+ t.Run("prefers pici.sh over pico.sh when both exist", func(t *testing.T) {
851+ tempDir := t.TempDir()
852+ if err := os.WriteFile(filepath.Join(tempDir, "pici.sh"), []byte("#!/bin/bash\n"), 0755); err != nil {
853+ t.Fatal(err)
854+ }
855+ if err := os.WriteFile(filepath.Join(tempDir, "pico.sh"), []byte("#!/bin/bash\n"), 0755); err != nil {
856+ t.Fatal(err)
857+ }
858+ path, err := findManifestInDir(tempDir)
859+ if err != nil {
860+ t.Fatalf("unexpected error: %v", err)
861+ }
862+ if filepath.Base(path) != "pici.sh" {
863+ t.Fatalf("expected pici.sh to take precedence, got %s", filepath.Base(path))
864+ }
865+ })
866+
867+ t.Run("returns error when neither exists", func(t *testing.T) {
868+ tempDir := t.TempDir()
869+ _, err := findManifestInDir(tempDir)
870+ if err == nil {
871+ t.Fatal("expected error when neither pici.sh nor pico.sh exists")
872+ }
873+ })
874+}
875+
876+func TestRunLocal_MissingManifest(t *testing.T) {
822877 tempDir := t.TempDir()
823878 origWd, _ := os.Getwd()
824879 defer func() { _ = os.Chdir(origWd) }()
......@@ -829,7 +884,32 @@ func TestRunLocal_MissingPico(t *testing.T) {
829884 }
830885 err := runLocal(cfg, "")
831886 if err == nil {
832- t.Fatal("expected error when pico.sh is missing")
887+ t.Fatal("expected error when runner script is missing")
888+ }
889+}
890+
891+func TestRunLocal_PiciSh_EnvOverrides(t *testing.T) {
892+ tempDir := t.TempDir()
893+ origWd, _ := os.Getwd()
894+ defer func() { _ = os.Chdir(origWd) }()
895+ _ = os.Chdir(tempDir)
896+
897+ piciContent := `#!/usr/bin/env bash
898+echo "hello from pici"
899+`
900+ if err := os.WriteFile("pici.sh", []byte(piciContent), 0755); err != nil {
901+ t.Fatal(err)
902+ }
903+
904+ cfg := &Cfg{
905+ ArtifactDir: t.TempDir(),
906+ EnvVars: envList{"PICI_REPO=custom-repo", "CUSTOM_VAR=hello_pici"},
907+ }
908+
909+ _ = runLocal(cfg, "")
910+
911+ if os.Getenv("CUSTOM_VAR") != "hello_pici" {
912+ t.Errorf("expected CUSTOM_VAR to be hello_pici, got %q", os.Getenv("CUSTOM_VAR"))
833913 }
834914 }
835915
R pico.sh => pici.sh
+0, -0
+2, -2
......@@ -706,7 +706,7 @@ details .faq-answer {
706706 <span class="dot red"></span>
707707 <span class="dot yellow"></span>
708708 <span class="dot green"></span>
709- <span style="margin-left: 0.5rem;">pico.sh</span>
709+ <span style="margin-left: 0.5rem;">pici.sh</span>
710710 </div>
711711 <div class="terminal-body">
712712 <div><span class="comment">#!/usr/bin/env bash</span></div>
......@@ -731,7 +731,7 @@ details .faq-answer {
731731 </div>
732732 <div>
733733 <p>
734- Your pipeline is <code>pico.sh</code>: a bash script that runs <strong>identically on your machine
734+ Your pipeline is <code>pici.sh</code>: a bash script that runs <strong>identically on your machine
735735 and on the CI runner</strong>; no yaml required.
736736 </p>
737737 <p>