fun_ci 2.2.0-aarch64-linux → 2.4.0-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +19 -11
  4. data/lib/fun_ci/agent/cancel_command.rb +52 -0
  5. data/lib/fun_ci/agent/commands.rb +3 -1
  6. data/lib/fun_ci/agent/job_events.rb +26 -7
  7. data/lib/fun_ci/agent/job_json.rb +10 -1
  8. data/lib/fun_ci/agent/job_report.rb +10 -6
  9. data/lib/fun_ci/agent/job_why_text.rb +11 -3
  10. data/lib/fun_ci/agent/jobs_text.rb +8 -3
  11. data/lib/fun_ci/agent/live_pipeline.rb +7 -4
  12. data/lib/fun_ci/agent/snapshots.rb +5 -1
  13. data/lib/fun_ci/agent/starts_in.rb +14 -0
  14. data/lib/fun_ci/agent/status_json.rb +1 -1
  15. data/lib/fun_ci/agent/status_text.rb +15 -3
  16. data/lib/fun_ci/agent/trunk_reading.rb +2 -1
  17. data/lib/fun_ci/agent/wait_command.rb +2 -1
  18. data/lib/fun_ci/cli.rb +3 -1
  19. data/lib/fun_ci/cli_command_help.rb +51 -0
  20. data/lib/fun_ci/cli_help.rb +11 -10
  21. data/lib/fun_ci/console/board_data.rb +7 -2
  22. data/lib/fun_ci/console/job_message.rb +12 -6
  23. data/lib/fun_ci/console/job_order.rb +5 -4
  24. data/lib/fun_ci/console/key_handler.rb +3 -1
  25. data/lib/fun_ci/console/trunk_marks.rb +3 -1
  26. data/lib/fun_ci/evidence/about_stage.rb +1 -1
  27. data/lib/fun_ci/evidence/collector.rb +6 -3
  28. data/lib/fun_ci/evidence/outcome.rb +6 -3
  29. data/lib/fun_ci/evidence/settings.rb +1 -1
  30. data/lib/fun_ci/jobs/due.rb +4 -3
  31. data/lib/fun_ci/jobs/due_jobs.rb +26 -2
  32. data/lib/fun_ci/jobs/job_fork.rb +19 -12
  33. data/lib/fun_ci/jobs/job_run.rb +26 -7
  34. data/lib/fun_ci/jobs/locks.rb +1 -1
  35. data/lib/fun_ci/jobs/schedule.rb +25 -0
  36. data/lib/fun_ci/jobs/site.rb +10 -3
  37. data/lib/fun_ci/jobs/standings.rb +4 -0
  38. data/lib/fun_ci/jobs/state.rb +3 -2
  39. data/lib/fun_ci/jobs/wall_clock_wait.rb +29 -0
  40. data/lib/fun_ci/persistence/active_jobs.rb +15 -18
  41. data/lib/fun_ci/persistence/active_runs.rb +5 -3
  42. data/lib/fun_ci/persistence/died.rb +21 -0
  43. data/lib/fun_ci/persistence/job_recorder.rb +1 -0
  44. data/lib/fun_ci/persistence/job_runs.rb +12 -0
  45. data/lib/fun_ci/persistence/null_recorder.rb +1 -0
  46. data/lib/fun_ci/persistence/pipeline_recorder.rb +1 -0
  47. data/lib/fun_ci/persistence/pipeline_run.rb +13 -9
  48. data/lib/fun_ci/persistence/stage_job.rb +12 -0
  49. data/lib/fun_ci/pipeline/budgets.rb +9 -0
  50. data/lib/fun_ci/pipeline/group_kill.rb +38 -0
  51. data/lib/fun_ci/pipeline/pipeline_forker.rb +14 -9
  52. data/lib/fun_ci/pipeline/priorities.rb +23 -0
  53. data/lib/fun_ci/pipeline/process_runner.rb +13 -6
  54. data/lib/fun_ci/pipeline/process_tree.rb +37 -0
  55. data/lib/fun_ci/pipeline/progress_reporter.rb +5 -0
  56. data/lib/fun_ci/pipeline/run_canceller.rb +15 -3
  57. data/lib/fun_ci/pipeline/slot.rb +11 -0
  58. data/lib/fun_ci/pipeline/slot_run.rb +19 -4
  59. data/lib/fun_ci/pipeline/stage_end.rb +2 -1
  60. data/lib/fun_ci/pipeline/trigger.rb +17 -6
  61. data/lib/fun_ci/pipeline/trigger_command.rb +6 -2
  62. data/lib/fun_ci/pipeline/trigger_params.rb +20 -9
  63. data/lib/fun_ci/pipeline/workspaces.rb +2 -1
  64. data/lib/fun_ci/pipeline/worktree_pool.rb +22 -5
  65. data/lib/fun_ci/pipeline/worktree_prune.rb +9 -4
  66. data/lib/fun_ci/pipeline/worktrees.rb +39 -6
  67. data/lib/fun_ci/setup/agent_instructions.rb +21 -5
  68. data/lib/fun_ci/setup/commands.rb +19 -8
  69. data/lib/fun_ci/setup/eslint_detection.rb +38 -0
  70. data/lib/fun_ci/setup/hook_script.rb +9 -4
  71. data/lib/fun_ci/setup/hook_writer.rb +13 -5
  72. data/lib/fun_ci/setup/init_options.rb +34 -0
  73. data/lib/fun_ci/setup/installed_hooks.rb +45 -0
  74. data/lib/fun_ci/setup/installer.rb +44 -9
  75. data/lib/fun_ci/setup/lfs_hook.rb +50 -0
  76. data/lib/fun_ci/setup/placeholders.rb +30 -0
  77. data/lib/fun_ci/setup/project_config.rb +8 -1
  78. data/lib/fun_ci/setup/settings.rb +59 -10
  79. data/lib/fun_ci/setup/setup_checker.rb +16 -3
  80. data/lib/fun_ci/setup/stage_templates/javascript.rb +14 -4
  81. data/lib/fun_ci/setup/stage_templates/placeholders.rb +21 -0
  82. data/lib/fun_ci/setup/stage_templates.rb +6 -4
  83. data/lib/fun_ci/setup/template_writer.rb +3 -5
  84. data/lib/fun_ci/trunk/shown.rb +7 -4
  85. data/lib/fun_ci/version.rb +1 -1
  86. data/libexec/fun-ci-renderer +0 -0
  87. metadata +17 -1
@@ -3,6 +3,8 @@
3
3
  require_relative "project_detector"
4
4
  require_relative "template_writer"
5
5
  require_relative "lint_detection"
6
+ require_relative "eslint_detection"
7
+ require_relative "stage_templates"
6
8
  require_relative "agent_instructions"
7
9
 
8
10
  module FunCi
@@ -14,13 +16,18 @@ module FunCi
14
16
  "Then fast.sh runs beside slow.sh, both on what build.sh built: have build.sh " \
15
17
  "compile everything they need, test code included."].freeze
16
18
 
17
- def self.run(project_root:, stdout: $stdout)
18
- new(project_root: project_root, stdout: stdout).run
19
+ PLACEHOLDERS = "Could not detect the project type, so .fun-ci/ has placeholder scripts that pass and " \
20
+ "test nothing: put the project's own commands in them, or start again with --stack NAME."
21
+
22
+ # stack: a name of ProjectDetector::STACKS to write in place of the one detected.
23
+ def self.run(project_root:, stdout: $stdout, stack: nil)
24
+ new(project_root: project_root, stdout: stdout, stack: stack).run
19
25
  end
20
26
 
21
- def initialize(project_root:, stdout:)
27
+ def initialize(project_root:, stdout:, stack:)
22
28
  @project_root = project_root
23
29
  @stdout = stdout
30
+ @stack = stack
24
31
  end
25
32
 
26
33
  # A project set up for fun-ci, new or not, also tells agents what to do.
@@ -35,20 +42,37 @@ module FunCi
35
42
  def set_up
36
43
  return report(".fun-ci/ already exists, so init left it as it is.", 0) if initialised?
37
44
 
38
- detected = ProjectDetector.new(Dir.children(@project_root)).detect
39
- return report("Could not detect project type. Create .fun-ci/ manually.", 1) if detected == :unknown
45
+ stack = chosen_stack
46
+ return unknown_stack if stack.nil?
47
+ return write_placeholders if stack == :unknown
48
+
49
+ write_templates(stack)
50
+ end
51
+
52
+ def chosen_stack
53
+ return ProjectDetector.new(Dir.children(@project_root)).detect if @stack.nil?
54
+
55
+ ProjectDetector::STACKS.keys.find { |stack| stack.to_s == @stack }
56
+ end
57
+
58
+ def unknown_stack
59
+ report("fun-ci init: no stack named #{@stack}; the stacks are #{ProjectDetector::STACKS.keys.join(", ")}", 64)
60
+ end
40
61
 
41
- write_templates(detected)
62
+ def write_placeholders
63
+ TemplateWriter.new(StageTemplates::PLACEHOLDERS, @project_root).write
64
+ report(PLACEHOLDERS, 0)
42
65
  end
43
66
 
44
67
  def tell_agents
45
68
  file = AgentInstructions.file_for(Dir.children(@project_root))
46
69
  path = File.join(@project_root, file)
47
- merged = AgentInstructions.merged(File.exist?(path) ? File.read(path) : "")
70
+ told = File.exist?(path) ? File.read(path) : ""
71
+ merged = AgentInstructions.merged(told)
48
72
  return unless merged
49
73
 
50
74
  File.write(path, merged)
51
- @stdout.puts "Told agents what to do after a commit, in #{file}."
75
+ @stdout.puts AgentInstructions.said(told, file)
52
76
  end
53
77
 
54
78
  def initialised?
@@ -57,11 +81,22 @@ module FunCi
57
81
 
58
82
  def write_templates(detected)
59
83
  @stdout.puts "Detected: #{detected.to_s.tr("_", " ")}"
60
- TemplateWriter.new(detected, @project_root, lint_override: LintDetection.command(detected, @project_root)).write
84
+ TemplateWriter.new(scripts_for(detected), @project_root).write
61
85
  @stdout.puts "Created .fun-ci/ with template scripts.", *SIDE_BY_SIDE
62
86
  0
63
87
  end
64
88
 
89
+ def scripts_for(detected)
90
+ StageTemplates.scripts(detected, lint_override: LintDetection.command(detected, @project_root),
91
+ build_override: EslintDetection.build(detected, Dir.children(@project_root),
92
+ package_json))
93
+ end
94
+
95
+ def package_json
96
+ path = File.join(@project_root, "package.json")
97
+ File.exist?(path) ? File.read(path) : "{}"
98
+ end
99
+
65
100
  def report(message, exit_code)
66
101
  @stdout.puts message
67
102
  exit_code
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hook_script"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # git-lfs's own post-commit and pre-push hooks, as `git lfs install`
8
+ # writes them, and the hook fun-ci writes in their place, which runs
9
+ # fun-ci's, then git-lfs's. A push reads its ref list from git once, so
10
+ # the pre-push hook keeps it in a file and gives it to both.
11
+ module LfsHook
12
+ GUARD = %r{\Acommand -v git-lfs >/dev/null 2>&1 \|\| \{.*; exit 2; \}\z}
13
+ MISSING = %(command -v git-lfs >/dev/null 2>&1 || { echo "This repository is configured for Git LFS ) +
14
+ %(but 'git-lfs' was not found on your path." >&2; exit 2; })
15
+
16
+ RUN = {
17
+ "post-commit" => "if #{HookScript::INSTALLED}; then fun_ci; else #{HookScript::NOT_INSTALLED}; fi\n",
18
+ "pre-push" => %(if #{HookScript::INSTALLED}; then fun_ci < "$refs" || exit 1; ) +
19
+ "else #{HookScript::NOT_INSTALLED}; fi\n"
20
+ }.freeze
21
+
22
+ KEEP_REFS = { "post-commit" => "", "pre-push" => <<~SH }.freeze
23
+ refs=$(mktemp) || exit 1
24
+ trap 'rm -f "$refs"' EXIT
25
+ cat > "$refs"
26
+ SH
27
+
28
+ FUN_CI = { "post-commit" => HookScript::POST_COMMIT, "pre-push" => "#{HookScript::PRE_PUSH_WAITS}return $status\n" }
29
+ .freeze
30
+ LFS_INPUT = { "post-commit" => "", "pre-push" => %( < "$refs") }.freeze
31
+
32
+ # Whether `script` is the hook `git lfs install` writes for `hook_type`
33
+ # and nothing more.
34
+ def self.stock?(script, hook_type)
35
+ lines = script.lines(chomp: true).reject { |line| line.strip.empty? }
36
+ lines.size == 3 && lines.first == "#!/bin/sh" && GUARD.match?(lines[1]) && lines.last == call(hook_type)
37
+ end
38
+
39
+ def self.runs_lfs?(script, hook_type) = script.include?(call(hook_type))
40
+
41
+ def self.script(hook_type)
42
+ "#!/bin/sh\n#{HookScript::MARKER}\n# fun-ci's #{hook_type} hook, then git-lfs's, which it replaced.\n" \
43
+ "#{KEEP_REFS.fetch(hook_type)}fun_ci() {\n#{FUN_CI.fetch(hook_type)}}\n" \
44
+ "#{RUN.fetch(hook_type)}#{MISSING}\n#{call(hook_type)}#{LFS_INPUT.fetch(hook_type)}\n"
45
+ end
46
+
47
+ def self.call(hook_type) = %(git lfs #{hook_type} "$@")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stage_templates"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # The stage scripts in .fun-ci/ that are still init's placeholders, as
8
+ # `fun-ci check`'s warnings.
9
+ class Placeholders
10
+ STAGES = %w[lint build fast slow].freeze
11
+
12
+ def initialize(project_root)
13
+ @dir = File.join(project_root, ".fun-ci")
14
+ end
15
+
16
+ def warnings = STAGES.select { |stage| placeholder?(stage) }.map { |stage| warning(stage) }
17
+
18
+ private
19
+
20
+ def placeholder?(stage)
21
+ path = File.join(@dir, "#{stage}.sh")
22
+ File.file?(path) && File.read(path).include?(StageTemplates::MARKER)
23
+ end
24
+
25
+ def warning(stage)
26
+ ".fun-ci/#{stage}.sh is init's placeholder, so the #{stage} stage tests nothing: put the project's own in it"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -20,12 +20,17 @@ module FunCi
20
20
  Dir.exist?(@fun_ci_dir)
21
21
  end
22
22
 
23
+ # What stops a pipeline: no .fun-ci/, or a stage script it can't run.
23
24
  def validate
24
25
  return ["No .fun-ci/ folder found in #{@project_root}"] unless folder_exists?
25
26
 
26
- REQUIRED_SCRIPTS.flat_map { |script| script_errors(script) } + settings.errors
27
+ REQUIRED_SCRIPTS.flat_map { |script| script_errors(script) }
27
28
  end
28
29
 
30
+ # Mistakes in .fun-ci/config, which never stop a pipeline: the setting
31
+ # takes its default instead.
32
+ def settings_errors = settings.errors
33
+
29
34
  # Mistakes in the `evidence` key, which `fun-ci check` reports but which
30
35
  # never stop a pipeline: the entry with the mistake is left out instead.
31
36
  def evidence_errors = Evidence::Settings.load(File.join(@fun_ci_dir, "config")).errors(root: @project_root)
@@ -44,8 +49,10 @@ module FunCi
44
49
  def job_errors = Jobs::Folders.new(@project_root).errors
45
50
 
46
51
  def worktree_slots = settings.worktree_slots
52
+ def clean = settings.clean
47
53
  def trunk = settings.trunk
48
54
  def trunk_fetch = settings.trunk_fetch
55
+ def job_spacing = settings.job_spacing
49
56
 
50
57
  def script_path(stage)
51
58
  File.join(@fun_ci_dir, "#{stage}.sh")
@@ -6,7 +6,8 @@ module FunCi
6
6
  module Setup
7
7
  # .fun-ci/config: settings for this machine's runs, as YAML. Each one left
8
8
  # out takes its default; one that is wrong is reported and the default used,
9
- # as are all of them when the file isn't YAML.
9
+ # as are all of them when the file can't be read as YAML. Anchors and
10
+ # aliases are YAML, so they are read.
10
11
  class Settings
11
12
  DEFAULTS = { "worktree_slots" => 2 }.freeze
12
13
  FETCH_EVERY = 300
@@ -20,7 +21,7 @@ module FunCi
20
21
  @text = text
21
22
  end
22
23
 
23
- def worktree_slots = errors.empty? ? values.fetch("worktree_slots") : DEFAULTS.fetch("worktree_slots")
24
+ def worktree_slots = slots || DEFAULTS.fetch("worktree_slots")
24
25
 
25
26
  # The trunk's name, or nil when none is given or it is no name. A mistake
26
27
  # in the trunk settings never stops a pipeline, so none is among `errors`.
@@ -37,22 +38,71 @@ module FunCi
37
38
  seconds(given) || FETCH_EVERY
38
39
  end
39
40
 
40
- def errors
41
- return [".fun-ci/config must be a mapping such as `worktree_slots: 2`"] unless raw.is_a?(Hash)
41
+ # Seconds between the starts of the jobs a commit starts, 0 for all at
42
+ # once (Jobs::Schedule); nil when none is given, or none it can read,
43
+ # and the jobs share the day evenly (Jobs::DueJobs).
44
+ def job_spacing = seconds(setting("job_spacing"))
42
45
 
43
- slots = values["worktree_slots"]
44
- return [] if slots.is_a?(Integer) && slots.positive?
46
+ # The paths, inside the project, whose git-ignored files a slot loses
47
+ # when it moves off its line of history (Pipeline::Worktrees); none
48
+ # when none are given, or what is given is no such list.
49
+ def clean = cleaned || []
45
50
 
46
- [".fun-ci/config: worktree_slots must be a whole number above 0, not #{slots.inspect}"]
51
+ def errors
52
+ setting_errors
47
53
  rescue Psych::SyntaxError => e
48
54
  [".fun-ci/config is not YAML: #{e.problem} (line #{e.line})"]
55
+ rescue Psych::DisallowedClass => e
56
+ [".fun-ci/config can't be read: #{e.message} (quote a value such as a date to read it as text)"]
57
+ rescue Psych::Exception => e
58
+ [".fun-ci/config can't be read: #{e.message}"]
49
59
  end
50
60
 
51
61
  private
52
62
 
63
+ def setting_errors
64
+ return [".fun-ci/config must be a mapping such as `worktree_slots: 2`"] unless raw.is_a?(Hash)
65
+
66
+ [slots_error, spacing_error, clean_error].compact
67
+ end
68
+
69
+ # The slots the file asks for, when that is a whole number above 0.
70
+ def slots
71
+ given = setting("worktree_slots")
72
+ given if given.is_a?(Integer) && given.positive?
73
+ end
74
+
75
+ def slots_error
76
+ given = setting("worktree_slots")
77
+ ".fun-ci/config: worktree_slots must be a whole number above 0, not #{given.inspect}" unless given.nil? || slots
78
+ end
79
+
80
+ # Unlike the trunk's, since taking turns is the jobs' whole point (AT-13.28).
81
+ def spacing_error
82
+ given = setting("job_spacing")
83
+ return nil if given.nil? || seconds(given)
84
+
85
+ ".fun-ci/config: job_spacing must be seconds, or a number with s, m or h, not #{given.inspect}"
86
+ end
87
+
88
+ def cleaned
89
+ given = setting("clean")
90
+ given if given.is_a?(Array) && given.all? { |path| inside?(path) }
91
+ end
92
+
93
+ def inside?(path) = path.is_a?(String) && !path.start_with?("/") && !path.split("/").include?("..")
94
+
95
+ def clean_error
96
+ given = setting("clean")
97
+ return nil if given.nil? || cleaned
98
+
99
+ ".fun-ci/config: clean must be a list of paths inside the project, such as [public/assets], " \
100
+ "not #{given.inspect}"
101
+ end
102
+
53
103
  def setting(key)
54
104
  raw.is_a?(Hash) ? raw[key] : nil
55
- rescue Psych::SyntaxError
105
+ rescue Psych::Exception
56
106
  nil
57
107
  end
58
108
 
@@ -61,8 +111,7 @@ module FunCi
61
111
  match && (match[1].to_i * UNITS.fetch(match[2]))
62
112
  end
63
113
 
64
- def raw = (@text && YAML.safe_load(@text)) || {}
65
- def values = DEFAULTS.merge(raw)
114
+ def raw = (@text && YAML.safe_load(@text, aliases: true)) || {}
66
115
  end
67
116
  end
68
117
  end
@@ -2,13 +2,24 @@
2
2
 
3
3
  require_relative "project_config"
4
4
  require_relative "legacy_hooks"
5
+ require_relative "installed_hooks"
5
6
  require_relative "trunk_report"
7
+ require_relative "placeholders"
6
8
 
7
9
  module FunCi
8
10
  module Setup
9
11
  class SetupChecker
10
- def self.run(project_root:, stdout: $stdout)
11
- new(config: ProjectConfig.new(project_root), hooks: LegacyHooks.new(project_root), stdout: stdout,
12
+ # What it warns of: fun-ci 1.x's hooks, any in `hooks_dir` that doesn't
13
+ # run fun-ci, and stage scripts still init's placeholders.
14
+ Hooks = Data.define(:sources) do
15
+ def warnings = sources.flat_map(&:warnings)
16
+ end
17
+
18
+ # hooks_dir: where git runs the project's hooks from; nil outside a git repository.
19
+ def self.run(project_root:, stdout: $stdout, hooks_dir: nil)
20
+ sources = [LegacyHooks.new(project_root), InstalledHooks.new(hooks_dir), Placeholders.new(project_root)]
21
+ hooks = Hooks.new(sources)
22
+ new(config: ProjectConfig.new(project_root), hooks: hooks, stdout: stdout,
12
23
  trunk: TrunkReport.new(project_root)).run
13
24
  end
14
25
 
@@ -22,7 +33,7 @@ module FunCi
22
33
 
23
34
  # Warnings are printed but don't fail the check.
24
35
  def run
25
- errors = @config.validate + @config.evidence_errors + @config.job_errors
36
+ errors = problems
26
37
  @stdout.puts(errors.empty? ? "All OK. The project is configured." : errors)
27
38
  @stdout.puts(@trunk.lines)
28
39
  list_presets
@@ -33,6 +44,8 @@ module FunCi
33
44
 
34
45
  private
35
46
 
47
+ def problems = @config.validate + @config.settings_errors + @config.evidence_errors + @config.job_errors
48
+
36
49
  def list_presets
37
50
  lists = { "this project" => @config.presets, "any project's output" => @config.any_project_presets }
38
51
  lists.each { |what, names| @stdout.puts "Evidence presets for #{what}: #{names.join(", ")}" if names.any? }
@@ -5,13 +5,23 @@ require_relative "shell"
5
5
  module FunCi
6
6
  module Setup
7
7
  # JavaScript and TypeScript: Deno, Bun, and Node with its package managers.
8
+ # Lint runs beside the build, which installs the packages, so it checks
9
+ # the source alone: a linter that loads them, ESLint, goes in the build
10
+ # after the install (EslintDetection). Deno's lint needs no packages.
8
11
  module StageTemplates
12
+ SOURCES = "find . -name node_modules -prune -o -name .git -prune -o -type f"
13
+ NODE_CHECK = "#{SOURCES} \\( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' \\) -print | " \
14
+ 'while read -r file; do node --check "$file" || exit 1; done'.freeze
15
+ BUN_CHECK = "#{SOURCES} \\( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.ts' " \
16
+ "-o -name '*.tsx' \\) -print | " \
17
+ 'while read -r file; do bun build --no-bundle "$file" > /dev/null || exit 1; done'.freeze
18
+
9
19
  JAVASCRIPT = {
10
20
  deno: stages("deno lint", "deno check", "deno test", "deno task test:slow"),
11
- bun: stages("bunx eslint .", "bun install", "bun test", "bun run test:slow"),
12
- node_pnpm: stages("pnpm exec eslint .", "pnpm install", "pnpm test", "pnpm run test:slow"),
13
- node_yarn: stages("yarn eslint .", "yarn install", "yarn test", "yarn run test:slow"),
14
- node_npm: stages("npx eslint .", "npm install", "npm test", "npm run test:slow")
21
+ bun: stages(BUN_CHECK, "bun install", "bun test", "bun run test:slow"),
22
+ node_pnpm: stages(NODE_CHECK, "pnpm install", "pnpm test", "pnpm run test:slow"),
23
+ node_yarn: stages(NODE_CHECK, "yarn install", "yarn test", "yarn run test:slow"),
24
+ node_npm: stages(NODE_CHECK, "npm install", "npm test", "npm run test:slow")
15
25
  }.freeze
16
26
  end
17
27
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+
5
+ module FunCi
6
+ module Setup
7
+ # The scripts `fun-ci init` writes where it knows no stack: each passes,
8
+ # says it tests nothing, and carries MARKER, by which `fun-ci check` warns
9
+ # of it until the project's own command takes its place.
10
+ module StageTemplates
11
+ MARKER = "# fun-ci placeholder"
12
+
13
+ def self.placeholder(stage)
14
+ "#{MARKER}: init knew no stack here, so put the project's #{stage} in place of this.\n" \
15
+ "echo \"fun-ci: .fun-ci/#{stage}.sh is init's placeholder and tests nothing\""
16
+ end
17
+
18
+ PLACEHOLDERS = stages(*%w[lint build fast slow].map { |stage| placeholder(stage) }).freeze
19
+ end
20
+ end
21
+ end
@@ -4,6 +4,7 @@ require_relative "stage_templates/managed"
4
4
  require_relative "stage_templates/native"
5
5
  require_relative "stage_templates/scripting"
6
6
  require_relative "stage_templates/javascript"
7
+ require_relative "stage_templates/placeholders"
7
8
 
8
9
  module FunCi
9
10
  module Setup
@@ -15,10 +16,11 @@ module FunCi
15
16
  module StageTemplates
16
17
  TEMPLATES = [MANAGED, NATIVE, SCRIPTING, JAVASCRIPT].reduce(:merge).freeze
17
18
 
18
- # { "lint.sh" => script, ... }; +lint_override+ replaces the lint command.
19
- def self.scripts(template_id, lint_override: nil)
20
- scripts = TEMPLATES.fetch(template_id)
21
- lint_override ? scripts.merge("lint.sh" => script("lint.sh", lint_override)) : scripts
19
+ # { "lint.sh" => script, ... }; +lint_override+ and +build_override+
20
+ # replace the lint and build commands.
21
+ def self.scripts(template_id, lint_override: nil, build_override: nil)
22
+ overrides = { "lint.sh" => lint_override, "build.sh" => build_override }.compact
23
+ TEMPLATES.fetch(template_id).merge(overrides.to_h { |name, command| [name, script(name, command)] })
22
24
  end
23
25
  end
24
26
  end
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "stage_templates"
4
-
5
3
  module FunCi
6
4
  module Setup
7
- # Writes a project's .fun-ci/ stage scripts, executable.
5
+ # Writes a project's .fun-ci/ stage scripts, executable: scripts is { "lint.sh" => script, ... }.
8
6
  class TemplateWriter
9
- def initialize(template_id, target_dir, lint_override: nil)
10
- @scripts = StageTemplates.scripts(template_id, lint_override: lint_override)
7
+ def initialize(scripts, target_dir)
8
+ @scripts = scripts
11
9
  @target_dir = target_dir
12
10
  end
13
11
 
@@ -8,8 +8,9 @@ module FunCi
8
8
  # rather than stored, how many seconds ago its trunk tip was seen (nil
9
9
  # when it had none), the project's last fetch (a LastFetch, nil if none), and
10
10
  # the SHA the trunk has moved to since, if it has.
11
- # A run whose check hasn't been recorded is checking until its fetch and
12
- # merge are past their time, and then unknown.
11
+ # A run whose check hasn't been recorded is checking while its foreground
12
+ # stages run and until its fetch and merge are past their time, and then
13
+ # unknown.
13
14
  Shown = Data.define(:state, :check, :age, :fetch, :moved_to)
14
15
 
15
16
  class Shown
@@ -21,8 +22,10 @@ module FunCi
21
22
  fetch: fetch, moved_to: moved_to)
22
23
  end
23
24
 
24
- def self.unchecked(started:, now:)
25
- if now - started <= FETCH_DEADLINE + CHECK_BUDGET
25
+ # stages_running: whether the run's foreground stages, which the check
26
+ # is recorded after, are still going.
27
+ def self.unchecked(started:, now:, stages_running: false)
28
+ if stages_running || now - started <= FETCH_DEADLINE + CHECK_BUDGET
26
29
  return new(state: "checking", check: nil, age: nil, fetch: nil, moved_to: nil)
27
30
  end
28
31
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Loaded on its own by the gemspec, so it must not require anything.
4
4
  module FunCi
5
- VERSION = "2.2.0"
5
+ VERSION = "2.4.0"
6
6
  end
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fun_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.4.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Erik Thyge Madsen
@@ -63,6 +63,7 @@ files:
63
63
  - exe/fun-ci
64
64
  - lib/fun_ci.rb
65
65
  - lib/fun_ci/agent/age.rb
66
+ - lib/fun_ci/agent/cancel_command.rb
66
67
  - lib/fun_ci/agent/command_support.rb
67
68
  - lib/fun_ci/agent/commands.rb
68
69
  - lib/fun_ci/agent/context.rb
@@ -94,6 +95,7 @@ files:
94
95
  - lib/fun_ci/agent/snapshots.rb
95
96
  - lib/fun_ci/agent/span.rb
96
97
  - lib/fun_ci/agent/stage_summary.rb
98
+ - lib/fun_ci/agent/starts_in.rb
97
99
  - lib/fun_ci/agent/status_command.rb
98
100
  - lib/fun_ci/agent/status_json.rb
99
101
  - lib/fun_ci/agent/status_text.rb
@@ -111,6 +113,7 @@ files:
111
113
  - lib/fun_ci/agent/why_json.rb
112
114
  - lib/fun_ci/agent/why_text.rb
113
115
  - lib/fun_ci/cli.rb
116
+ - lib/fun_ci/cli_command_help.rb
114
117
  - lib/fun_ci/cli_help.rb
115
118
  - lib/fun_ci/console/board_data.rb
116
119
  - lib/fun_ci/console/board_heartbeat.rb
@@ -226,12 +229,15 @@ files:
226
229
  - lib/fun_ci/jobs/job_fork.rb
227
230
  - lib/fun_ci/jobs/job_run.rb
228
231
  - lib/fun_ci/jobs/locks.rb
232
+ - lib/fun_ci/jobs/schedule.rb
229
233
  - lib/fun_ci/jobs/site.rb
230
234
  - lib/fun_ci/jobs/standings.rb
231
235
  - lib/fun_ci/jobs/state.rb
236
+ - lib/fun_ci/jobs/wall_clock_wait.rb
232
237
  - lib/fun_ci/persistence/active_jobs.rb
233
238
  - lib/fun_ci/persistence/active_runs.rb
234
239
  - lib/fun_ci/persistence/database.rb
240
+ - lib/fun_ci/persistence/died.rb
235
241
  - lib/fun_ci/persistence/job_recorder.rb
236
242
  - lib/fun_ci/persistence/job_runs.rb
237
243
  - lib/fun_ci/persistence/null_recorder.rb
@@ -249,14 +255,18 @@ files:
249
255
  - lib/fun_ci/persistence/trunk_recording.rb
250
256
  - lib/fun_ci/persistence/write_trouble.rb
251
257
  - lib/fun_ci/pipeline/background_fork.rb
258
+ - lib/fun_ci/pipeline/budgets.rb
252
259
  - lib/fun_ci/pipeline/command_executor.rb
253
260
  - lib/fun_ci/pipeline/gate.rb
254
261
  - lib/fun_ci/pipeline/git_environment.rb
262
+ - lib/fun_ci/pipeline/group_kill.rb
255
263
  - lib/fun_ci/pipeline/head_part.rb
256
264
  - lib/fun_ci/pipeline/in_place.rb
257
265
  - lib/fun_ci/pipeline/output_window.rb
258
266
  - lib/fun_ci/pipeline/pipeline_forker.rb
267
+ - lib/fun_ci/pipeline/priorities.rb
259
268
  - lib/fun_ci/pipeline/process_runner.rb
269
+ - lib/fun_ci/pipeline/process_tree.rb
260
270
  - lib/fun_ci/pipeline/progress_reporter.rb
261
271
  - lib/fun_ci/pipeline/run_canceller.rb
262
272
  - lib/fun_ci/pipeline/slot.rb
@@ -279,14 +289,19 @@ files:
279
289
  - lib/fun_ci/pipeline/worktrees.rb
280
290
  - lib/fun_ci/setup/agent_instructions.rb
281
291
  - lib/fun_ci/setup/commands.rb
292
+ - lib/fun_ci/setup/eslint_detection.rb
282
293
  - lib/fun_ci/setup/git_hooks.rb
283
294
  - lib/fun_ci/setup/gradle_linter_detector.rb
284
295
  - lib/fun_ci/setup/hook_script.rb
285
296
  - lib/fun_ci/setup/hook_writer.rb
297
+ - lib/fun_ci/setup/init_options.rb
298
+ - lib/fun_ci/setup/installed_hooks.rb
286
299
  - lib/fun_ci/setup/installer.rb
287
300
  - lib/fun_ci/setup/legacy_hooks.rb
301
+ - lib/fun_ci/setup/lfs_hook.rb
288
302
  - lib/fun_ci/setup/lint_detection.rb
289
303
  - lib/fun_ci/setup/maven_linter_detector.rb
304
+ - lib/fun_ci/setup/placeholders.rb
290
305
  - lib/fun_ci/setup/project_config.rb
291
306
  - lib/fun_ci/setup/project_detector.rb
292
307
  - lib/fun_ci/setup/settings.rb
@@ -295,6 +310,7 @@ files:
295
310
  - lib/fun_ci/setup/stage_templates/javascript.rb
296
311
  - lib/fun_ci/setup/stage_templates/managed.rb
297
312
  - lib/fun_ci/setup/stage_templates/native.rb
313
+ - lib/fun_ci/setup/stage_templates/placeholders.rb
298
314
  - lib/fun_ci/setup/stage_templates/scripting.rb
299
315
  - lib/fun_ci/setup/stage_templates/shell.rb
300
316
  - lib/fun_ci/setup/template_writer.rb