fun_ci 2.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +294 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/exe/fun-ci +7 -0
- data/lib/fun_ci/agent/age.rb +15 -0
- data/lib/fun_ci/agent/command_support.rb +27 -0
- data/lib/fun_ci/agent/commands.rb +27 -0
- data/lib/fun_ci/agent/context.rb +10 -0
- data/lib/fun_ci/agent/digest.rb +49 -0
- data/lib/fun_ci/agent/events.rb +47 -0
- data/lib/fun_ci/agent/events_command.rb +61 -0
- data/lib/fun_ci/agent/evidence_text.rb +46 -0
- data/lib/fun_ci/agent/exit_code.rb +11 -0
- data/lib/fun_ci/agent/git.rb +31 -0
- data/lib/fun_ci/agent/live_pipeline.rb +35 -0
- data/lib/fun_ci/agent/options.rb +75 -0
- data/lib/fun_ci/agent/output.rb +81 -0
- data/lib/fun_ci/agent/report_stage.rb +73 -0
- data/lib/fun_ci/agent/reports.rb +47 -0
- data/lib/fun_ci/agent/run_report.rb +30 -0
- data/lib/fun_ci/agent/runs_command.rb +34 -0
- data/lib/fun_ci/agent/runs_text.rb +27 -0
- data/lib/fun_ci/agent/snapshots.rb +39 -0
- data/lib/fun_ci/agent/stage_summary.rb +30 -0
- data/lib/fun_ci/agent/status_command.rb +36 -0
- data/lib/fun_ci/agent/status_json.rb +27 -0
- data/lib/fun_ci/agent/status_text.rb +37 -0
- data/lib/fun_ci/agent/system_clock.rb +11 -0
- data/lib/fun_ci/agent/verdict.rb +32 -0
- data/lib/fun_ci/agent/wait_command.rb +71 -0
- data/lib/fun_ci/agent/waiting.rb +32 -0
- data/lib/fun_ci/agent/why_command.rb +50 -0
- data/lib/fun_ci/agent/why_json.rb +35 -0
- data/lib/fun_ci/agent/why_text.rb +43 -0
- data/lib/fun_ci/cli.rb +128 -0
- data/lib/fun_ci/cli_help.rb +65 -0
- data/lib/fun_ci/console/board_data.rb +66 -0
- data/lib/fun_ci/console/board_heartbeat.rb +40 -0
- data/lib/fun_ci/console/console_events.rb +20 -0
- data/lib/fun_ci/console/console_log.rb +26 -0
- data/lib/fun_ci/console/console_loop.rb +41 -0
- data/lib/fun_ci/console/console_session.rb +82 -0
- data/lib/fun_ci/console/console_state.rb +42 -0
- data/lib/fun_ci/console/key_handler.rb +73 -0
- data/lib/fun_ci/console/launcher.rb +43 -0
- data/lib/fun_ci/console/milestone_events.rb +38 -0
- data/lib/fun_ci/console/milestones.rb +25 -0
- data/lib/fun_ci/console/renderer_lookup.rb +51 -0
- data/lib/fun_ci/console/renderer_process.rb +60 -0
- data/lib/fun_ci/console/run_message.rb +33 -0
- data/lib/fun_ci/console/stage_change_detector.rb +32 -0
- data/lib/fun_ci/console/stage_events.rb +31 -0
- data/lib/fun_ci/console/stop_report.rb +39 -0
- data/lib/fun_ci/console/streak_counter.rb +19 -0
- data/lib/fun_ci/console/view.rb +47 -0
- data/lib/fun_ci/evidence/about.rb +12 -0
- data/lib/fun_ci/evidence/about_stage.rb +29 -0
- data/lib/fun_ci/evidence/caps.rb +77 -0
- data/lib/fun_ci/evidence/catalog.rb +83 -0
- data/lib/fun_ci/evidence/collector.rb +95 -0
- data/lib/fun_ci/evidence/command_output.rb +45 -0
- data/lib/fun_ci/evidence/command_reading.rb +38 -0
- data/lib/fun_ci/evidence/command_runner.rb +75 -0
- data/lib/fun_ci/evidence/context.rb +21 -0
- data/lib/fun_ci/evidence/context_document.rb +29 -0
- data/lib/fun_ci/evidence/deadline.rb +15 -0
- data/lib/fun_ci/evidence/detection.rb +82 -0
- data/lib/fun_ci/evidence/document.rb +59 -0
- data/lib/fun_ci/evidence/extract_command.rb +83 -0
- data/lib/fun_ci/evidence/extract_options.rb +45 -0
- data/lib/fun_ci/evidence/extraction.rb +54 -0
- data/lib/fun_ci/evidence/extractors/command.rb +52 -0
- data/lib/fun_ci/evidence/extractors/grep.rb +49 -0
- data/lib/fun_ci/evidence/extractors/json_log.rb +45 -0
- data/lib/fun_ci/evidence/extractors/junit_files.rb +32 -0
- data/lib/fun_ci/evidence/extractors/last_lines.rb +25 -0
- data/lib/fun_ci/evidence/extractors/log_file.rb +62 -0
- data/lib/fun_ci/evidence/extractors/process_tree.rb +51 -0
- data/lib/fun_ci/evidence/extractors/section.rb +49 -0
- data/lib/fun_ci/evidence/findings.rb +14 -0
- data/lib/fun_ci/evidence/line_ranges.rb +21 -0
- data/lib/fun_ci/evidence/line_scan.rb +22 -0
- data/lib/fun_ci/evidence/log_record.rb +58 -0
- data/lib/fun_ci/evidence/masking.rb +55 -0
- data/lib/fun_ci/evidence/option_check.rb +53 -0
- data/lib/fun_ci/evidence/outcome.rb +15 -0
- data/lib/fun_ci/evidence/patterns.rb +22 -0
- data/lib/fun_ci/evidence/presets/README.md +7 -0
- data/lib/fun_ci/evidence/presets/bun-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/cargo-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/dart-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/deno-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/dotnet-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/ecs.yml +3 -0
- data/lib/fun_ci/evidence/presets/eslint.yml +6 -0
- data/lib/fun_ci/evidence/presets/exunit.yml +6 -0
- data/lib/fun_ci/evidence/presets/gcc.yml +6 -0
- data/lib/fun_ci/evidence/presets/go-build.yml +5 -0
- data/lib/fun_ci/evidence/presets/go-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/gradle.yml +6 -0
- data/lib/fun_ci/evidence/presets/gtest.yml +6 -0
- data/lib/fun_ci/evidence/presets/jest.yml +6 -0
- data/lib/fun_ci/evidence/presets/logstash.yml +3 -0
- data/lib/fun_ci/evidence/presets/maven.yml +6 -0
- data/lib/fun_ci/evidence/presets/minitest.yml +6 -0
- data/lib/fun_ci/evidence/presets/mocha.yml +6 -0
- data/lib/fun_ci/evidence/presets/mypy.yml +6 -0
- data/lib/fun_ci/evidence/presets/node-test.yml +6 -0
- data/lib/fun_ci/evidence/presets/phpunit.yml +6 -0
- data/lib/fun_ci/evidence/presets/pino.yml +5 -0
- data/lib/fun_ci/evidence/presets/prove.yml +6 -0
- data/lib/fun_ci/evidence/presets/pytest.yml +6 -0
- data/lib/fun_ci/evidence/presets/rspec.yml +6 -0
- data/lib/fun_ci/evidence/presets/rubocop.yml +6 -0
- data/lib/fun_ci/evidence/presets/ruff.yml +6 -0
- data/lib/fun_ci/evidence/presets/rustc.yml +6 -0
- data/lib/fun_ci/evidence/presets/shellcheck.yml +5 -0
- data/lib/fun_ci/evidence/presets/structlog.yml +4 -0
- data/lib/fun_ci/evidence/presets/swift-test.yml +5 -0
- data/lib/fun_ci/evidence/presets/tsc.yml +6 -0
- data/lib/fun_ci/evidence/presets/unittest.yml +6 -0
- data/lib/fun_ci/evidence/presets/vitest.yml +6 -0
- data/lib/fun_ci/evidence/presets.rb +30 -0
- data/lib/fun_ci/evidence/problem.rb +9 -0
- data/lib/fun_ci/evidence/process_table.rb +40 -0
- data/lib/fun_ci/evidence/saved_run.rb +22 -0
- data/lib/fun_ci/evidence/section_walk.rb +49 -0
- data/lib/fun_ci/evidence/settings.rb +64 -0
- data/lib/fun_ci/evidence/settings_check.rb +78 -0
- data/lib/fun_ci/evidence/source.rb +34 -0
- data/lib/fun_ci/evidence/sources.rb +21 -0
- data/lib/fun_ci/evidence/stage_context.rb +30 -0
- data/lib/fun_ci/evidence/stamp.rb +10 -0
- data/lib/fun_ci/evidence/start.rb +36 -0
- data/lib/fun_ci/evidence/worktree.rb +62 -0
- data/lib/fun_ci/persistence/active_runs.rb +69 -0
- data/lib/fun_ci/persistence/database.rb +81 -0
- data/lib/fun_ci/persistence/null_recorder.rb +25 -0
- data/lib/fun_ci/persistence/output_tail.rb +24 -0
- data/lib/fun_ci/persistence/pipeline_recorder.rb +120 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +68 -0
- data/lib/fun_ci/persistence/project_runs.rb +52 -0
- data/lib/fun_ci/persistence/raw_outputs.rb +46 -0
- data/lib/fun_ci/persistence/retention.rb +25 -0
- data/lib/fun_ci/persistence/run_status.rb +31 -0
- data/lib/fun_ci/persistence/stage_job.rb +91 -0
- data/lib/fun_ci/persistence/state_dir.rb +26 -0
- data/lib/fun_ci/persistence/write_trouble.rb +46 -0
- data/lib/fun_ci/pipeline/background_fork.rb +42 -0
- data/lib/fun_ci/pipeline/command_executor.rb +58 -0
- data/lib/fun_ci/pipeline/gate.rb +35 -0
- data/lib/fun_ci/pipeline/git_environment.rb +16 -0
- data/lib/fun_ci/pipeline/head_part.rb +40 -0
- data/lib/fun_ci/pipeline/in_place.rb +17 -0
- data/lib/fun_ci/pipeline/output_window.rb +73 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +40 -0
- data/lib/fun_ci/pipeline/process_runner.rb +101 -0
- data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
- data/lib/fun_ci/pipeline/run_canceller.rb +75 -0
- data/lib/fun_ci/pipeline/slot.rb +30 -0
- data/lib/fun_ci/pipeline/slot_lock.rb +13 -0
- data/lib/fun_ci/pipeline/slot_run.rb +86 -0
- data/lib/fun_ci/pipeline/stage_dir.rb +50 -0
- data/lib/fun_ci/pipeline/stage_end.rb +70 -0
- data/lib/fun_ci/pipeline/stage_execution.rb +67 -0
- data/lib/fun_ci/pipeline/stage_runner.rb +63 -0
- data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +36 -0
- data/lib/fun_ci/pipeline/tail_segments.rb +60 -0
- data/lib/fun_ci/pipeline/test_report.rb +48 -0
- data/lib/fun_ci/pipeline/trigger.rb +93 -0
- data/lib/fun_ci/pipeline/trigger_command.rb +62 -0
- data/lib/fun_ci/pipeline/trigger_params.rb +48 -0
- data/lib/fun_ci/pipeline/workspaces.rb +22 -0
- data/lib/fun_ci/pipeline/worktree_pool.rb +66 -0
- data/lib/fun_ci/pipeline/worktree_prune.rb +49 -0
- data/lib/fun_ci/pipeline/worktrees.rb +45 -0
- data/lib/fun_ci/setup/agent_instructions.rb +31 -0
- data/lib/fun_ci/setup/commands.rb +49 -0
- data/lib/fun_ci/setup/git_hooks.rb +20 -0
- data/lib/fun_ci/setup/hook_script.rb +47 -0
- data/lib/fun_ci/setup/hook_writer.rb +57 -0
- data/lib/fun_ci/setup/installer.rb +71 -0
- data/lib/fun_ci/setup/legacy_hooks.rb +38 -0
- data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
- data/lib/fun_ci/setup/project_config.rb +68 -0
- data/lib/fun_ci/setup/project_detector.rb +54 -0
- data/lib/fun_ci/setup/settings.rb +36 -0
- data/lib/fun_ci/setup/setup_checker.rb +36 -0
- data/lib/fun_ci/setup/stage_templates/javascript.rb +18 -0
- data/lib/fun_ci/setup/stage_templates/managed.rb +24 -0
- data/lib/fun_ci/setup/stage_templates/native.rb +25 -0
- data/lib/fun_ci/setup/stage_templates/scripting.rb +27 -0
- data/lib/fun_ci/setup/stage_templates/shell.rb +16 -0
- data/lib/fun_ci/setup/stage_templates.rb +25 -0
- data/lib/fun_ci/setup/template_writer.rb +28 -0
- data/lib/fun_ci/version.rb +6 -0
- data/lib/fun_ci.rb +13 -0
- data/libexec/fun-ci-renderer +0 -0
- metadata +286 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../findings"
|
|
4
|
+
require_relative "../source"
|
|
5
|
+
require_relative "../line_scan"
|
|
6
|
+
require_relative "../patterns"
|
|
7
|
+
require_relative "../section_walk"
|
|
8
|
+
|
|
9
|
+
module FunCi
|
|
10
|
+
module Evidence
|
|
11
|
+
module Extractors
|
|
12
|
+
# `section`: the lines of the output, or of a file, from a line that
|
|
13
|
+
# matches any of `start` up to one that matches any of `end` (or the
|
|
14
|
+
# last line), each section at most `lines` long. A preset names a tool's
|
|
15
|
+
# sections, such as rspec's failures.
|
|
16
|
+
class Section
|
|
17
|
+
OPTIONS = { "start" => :patterns, "end" => :patterns, "path" => :path, "lines" => :count,
|
|
18
|
+
"title" => :word }.freeze
|
|
19
|
+
REQUIRED = %w[start].freeze
|
|
20
|
+
LINES = 200
|
|
21
|
+
|
|
22
|
+
def initialize(options)
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def extract(context) = found_in(Source.for(context, @options["path"]), context.deadline)
|
|
27
|
+
|
|
28
|
+
def found_in(source, deadline)
|
|
29
|
+
walk = new_walk
|
|
30
|
+
truncated = LineScan.cut_short?(source.lines, deadline) { |line, index| walk.step(line, index) }
|
|
31
|
+
Findings.new(excerpts: walk.finish(source.lines).map { |found| excerpt(source, found, truncated) })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def new_walk
|
|
37
|
+
SectionWalk.new(Patterns.compile(@options["start"]), Patterns.compile(@options["end"]),
|
|
38
|
+
@options.fetch("lines", LINES))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def excerpt(source, found, truncated)
|
|
42
|
+
{ title: @options.fetch("title") { "Lines from #{Array(@options["start"]).join(" or ")}" },
|
|
43
|
+
location: source.location(found.first, found.last), lines: source.lines[found.first..found.last],
|
|
44
|
+
truncated: truncated || found.cut }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# What an extractor found: facts ({ name:, value: }), failures ({ test:,
|
|
6
|
+
# file:, line:, message:, output: }) and excerpts ({ title:, location:,
|
|
7
|
+
# lines:, truncated: }).
|
|
8
|
+
Findings = Data.define(:facts, :failures, :excerpts)
|
|
9
|
+
|
|
10
|
+
class Findings
|
|
11
|
+
def initialize(facts: [], failures: [], excerpts: []) = super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# The runs of lines around chosen lines: `context` lines either side of
|
|
6
|
+
# each, within 0...size, joined where they meet.
|
|
7
|
+
module LineRanges
|
|
8
|
+
def self.around(indexes, context, size)
|
|
9
|
+
indexes.map { |index| [index - context, 0].max..[index + context, size - 1].min }
|
|
10
|
+
.each_with_object([]) { |range, joined| join(joined, range) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.join(joined, range)
|
|
14
|
+
return joined << range if joined.empty? || joined.last.last + 1 < range.first
|
|
15
|
+
|
|
16
|
+
joined[-1] = joined.last.first..[joined.last.last, range.last].max
|
|
17
|
+
end
|
|
18
|
+
private_class_method :join
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# Walks lines for a built-in extractor, checking the deadline every
|
|
6
|
+
# CHECK_EVERY lines, since a built-in runs in fun-ci's own process and
|
|
7
|
+
# can't be killed.
|
|
8
|
+
module LineScan
|
|
9
|
+
CHECK_EVERY = 1000
|
|
10
|
+
|
|
11
|
+
# Yields each line and its index; answers whether the deadline stopped it first.
|
|
12
|
+
def self.cut_short?(lines, deadline)
|
|
13
|
+
lines.each_with_index do |line, index|
|
|
14
|
+
return true if (index % CHECK_EVERY).zero? && deadline.passed?
|
|
15
|
+
|
|
16
|
+
yield line, index
|
|
17
|
+
end
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Evidence
|
|
7
|
+
# One JSON log line, read through the field names a logging setup uses
|
|
8
|
+
# (`fields`: time, level, logger, message, stack), each a key or a dotted
|
|
9
|
+
# path, and `levels`, the names of numeric levels.
|
|
10
|
+
class LogRecord
|
|
11
|
+
RANKS = %w[trace debug info warn error fatal].freeze
|
|
12
|
+
ALIASES = { "warning" => "warn", "critical" => "fatal", "err" => "error", "panic" => "fatal",
|
|
13
|
+
"verbose" => "debug" }.freeze
|
|
14
|
+
|
|
15
|
+
# The record on a line, or nil for one that isn't a JSON object.
|
|
16
|
+
def self.parse(line, fields, levels)
|
|
17
|
+
return nil unless line.lstrip.start_with?("{")
|
|
18
|
+
|
|
19
|
+
new(JSON.parse(line), fields, levels)
|
|
20
|
+
rescue JSON::ParserError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.rank(level) = RANKS.index(ALIASES.fetch(level.to_s.downcase, level.to_s.downcase))
|
|
25
|
+
|
|
26
|
+
def initialize(parsed, fields, levels)
|
|
27
|
+
@parsed = parsed
|
|
28
|
+
@fields = fields
|
|
29
|
+
@levels = levels
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def level = LogRecord.rank(@levels.fetch(field("level").to_s, field("level")))
|
|
33
|
+
def at_least?(rank) = !level.nil? && level >= rank
|
|
34
|
+
|
|
35
|
+
def lines
|
|
36
|
+
logger = field("logger")
|
|
37
|
+
head = [field("time"), RANKS[level].upcase, logger && "#{logger}:", field("message")].compact.join(" ")
|
|
38
|
+
[head, *stack.map { |line| " #{line}" }]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def stack
|
|
44
|
+
trace = field("stack")
|
|
45
|
+
trace.is_a?(Array) ? trace.map(&:to_s) : trace.to_s.lines(chomp: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def field(name)
|
|
49
|
+
key = @fields[name]
|
|
50
|
+
return nil unless key
|
|
51
|
+
|
|
52
|
+
@parsed.key?(key) ? @parsed[key] : @parsed.dig(*key.split("."))
|
|
53
|
+
rescue TypeError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# Masks secrets in text before fun-ci keeps it (architecture.md, "Evidence
|
|
6
|
+
# of a failed stage"): the values of the stage's secret-named variables,
|
|
7
|
+
# well-known token shapes, and a project's own patterns. A best effort, not
|
|
8
|
+
# a guarantee. Given the environment as a hash; never reads ENV.
|
|
9
|
+
class Masking
|
|
10
|
+
SECRET_NAME = /TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|CREDENTIAL/
|
|
11
|
+
SHORTEST = 8
|
|
12
|
+
SHAPES = [
|
|
13
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----/m,
|
|
14
|
+
/\bgh[pousr]_[A-Za-z0-9]{36,}/,
|
|
15
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
16
|
+
/\bxox[abpors]-[A-Za-z0-9-]{10,}/,
|
|
17
|
+
/Authorization: \K.+/i
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Masks nothing, for a project that turned masking off.
|
|
21
|
+
def self.none = new({}, shapes: [])
|
|
22
|
+
|
|
23
|
+
def initialize(environment, patterns: [], shapes: SHAPES)
|
|
24
|
+
@secrets = secrets(environment)
|
|
25
|
+
@patterns = shapes + patterns
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every string in the document masked.
|
|
29
|
+
def document(document) = document.with(**document.to_h.transform_values { |items| deep(items) })
|
|
30
|
+
|
|
31
|
+
# Bytes that aren't UTF-8 become U+FFFD first, as patterns can't read them.
|
|
32
|
+
def mask(text)
|
|
33
|
+
text = text.dup.force_encoding(Encoding::UTF_8).scrub
|
|
34
|
+
masked = @secrets.reduce(text) { |current, (name, value)| current.gsub(value, "[masked:#{name}]") }
|
|
35
|
+
@patterns.reduce(masked) { |current, pattern| current.gsub(pattern, "[masked]") }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def deep(value)
|
|
41
|
+
return mask(value) if value.is_a?(String)
|
|
42
|
+
return value.map { |item| deep(item) } if value.is_a?(Array)
|
|
43
|
+
return value.transform_values { |item| deep(item) } if value.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Longest first, so a value that contains another is masked whole.
|
|
49
|
+
def secrets(environment)
|
|
50
|
+
environment.select { |name, value| name.match?(SECRET_NAME) && value.to_s.length >= SHORTEST }
|
|
51
|
+
.sort_by { |_name, value| -value.length }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "patterns"
|
|
4
|
+
require_relative "log_record"
|
|
5
|
+
|
|
6
|
+
module FunCi
|
|
7
|
+
module Evidence
|
|
8
|
+
# Checks the options of an entry for a built-in against what the built-in
|
|
9
|
+
# declares it takes (OPTIONS, each with a kind, and REQUIRED), and
|
|
10
|
+
# answers the first mistake, or nil.
|
|
11
|
+
module OptionCheck
|
|
12
|
+
KINDS = {
|
|
13
|
+
patterns: lambda { |value|
|
|
14
|
+
next "must be a list of patterns" unless Array(value).all?(String)
|
|
15
|
+
|
|
16
|
+
Array(value).lazy.filter_map { |source| Patterns.mistake(source) }.first
|
|
17
|
+
},
|
|
18
|
+
count: ->(value) { "must be a whole number, not #{value.inspect}" unless value.is_a?(Integer) && value >= 0 },
|
|
19
|
+
path: ->(value) { "must be a path, not #{value.inspect}" unless value.is_a?(String) && !value.empty? },
|
|
20
|
+
globs: ->(value) { "must be a list of paths, not #{value.inspect}" unless Array(value).all?(String) },
|
|
21
|
+
word: ->(value) { "must be a word, not #{value.inspect}" unless value.is_a?(String) },
|
|
22
|
+
level: lambda { |value|
|
|
23
|
+
"must be one of trace, debug, info, warn, error or fatal, not #{value.inspect}" unless LogRecord.rank(value)
|
|
24
|
+
},
|
|
25
|
+
fields: lambda { |value|
|
|
26
|
+
"must map time, level, logger, message and stack to field names" unless mapping_of_strings?(value)
|
|
27
|
+
},
|
|
28
|
+
levels: ->(value) { "must map each level's number to its name" unless mapping_of_strings?(value) }
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def self.mapping_of_strings?(value) = value.is_a?(Hash) && value.values.all?(String)
|
|
32
|
+
|
|
33
|
+
def self.mistake(name, built_in, options)
|
|
34
|
+
unknown = options.keys.find { |key| !built_in::OPTIONS.key?(key) }
|
|
35
|
+
return "#{name} doesn't take '#{unknown}'" if unknown
|
|
36
|
+
|
|
37
|
+
missing = built_in::REQUIRED.find { |key| !options.key?(key) }
|
|
38
|
+
return "#{name} needs '#{missing}'" if missing
|
|
39
|
+
|
|
40
|
+
kinds(name, built_in, options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.kinds(name, built_in, options)
|
|
44
|
+
options.each do |key, value|
|
|
45
|
+
problem = KINDS.fetch(built_in::OPTIONS.fetch(key)).call(value)
|
|
46
|
+
return "#{name}: '#{key}' #{problem}" if problem
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
private_class_method :kinds
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# How a failed stage ended, known once it has: its state in the words
|
|
6
|
+
# `status --json` uses, its exit status or signal, and the other stages
|
|
7
|
+
# that shared its slot while it ran, and for an overrun, what was found
|
|
8
|
+
# before the kill (an Extraction::Result).
|
|
9
|
+
Outcome = Data.define(:state, :exit_status, :signal, :alongside, :overrun)
|
|
10
|
+
|
|
11
|
+
class Outcome
|
|
12
|
+
def initialize(**given) = super(exit_status: nil, signal: nil, alongside: [], overrun: nil, **given)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Evidence
|
|
5
|
+
# Patterns a project or a preset supplies, each compiled with a timeout of
|
|
6
|
+
# its own rather than the process-wide Regexp.timeout, since stages run in
|
|
7
|
+
# threads side by side. The timeout bounds each match.
|
|
8
|
+
module Patterns
|
|
9
|
+
TIMEOUT = 1.0
|
|
10
|
+
|
|
11
|
+
def self.compile(sources, timeout: TIMEOUT) = Array(sources).map { |source| Regexp.new(source, timeout: timeout) }
|
|
12
|
+
|
|
13
|
+
# Why a pattern doesn't compile, or nil.
|
|
14
|
+
def self.mistake(source)
|
|
15
|
+
Regexp.new(source)
|
|
16
|
+
nil
|
|
17
|
+
rescue RegexpError => e
|
|
18
|
+
"has '#{source}', which doesn't compile: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Presets: config entries fun-ci ships for popular tools, one file each, named
|
|
2
|
+
for the preset, in the shape a project would write an entry (`use:` and the
|
|
3
|
+
built-in's options), with when it applies: `markers`, files any of which the
|
|
4
|
+
worktree must have (a path or a glob, or `{ path:, contains: }`), and `signature`, a
|
|
5
|
+
pattern only the tool prints, matched within one line. Each is pinned by the
|
|
6
|
+
recorded output of a real failing run in `test/fixtures/evidence/<name>/`, and
|
|
7
|
+
none ships without one (`test/policy/test_evidence_fixtures.rb`).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: ESLint's problems, file by file
|
|
3
|
+
start: ['^/\S+$']
|
|
4
|
+
end: ['^\S+ \d+ problems? \(\d+ errors?, \d+ warnings?\)']
|
|
5
|
+
markers: [eslint.config.js, eslint.config.mjs, .eslintrc.json, .eslintrc.js]
|
|
6
|
+
signature: '\d+ problems? \(\d+ errors?, \d+ warnings?\)'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: gcc's errors, file by file
|
|
3
|
+
start: ['^(In file included from )?\S+\.(c|cc|cp|cpp|cxx|c\+\+|h|hh|hpp|hxx):']
|
|
4
|
+
end: ['^g?make(\[\d+\])?: ', '^(cc|c\+\+|gcc|g\+\+|clang|clang\+\+) ']
|
|
5
|
+
markers: [Makefile, makefile, GNUmakefile, CMakeLists.txt]
|
|
6
|
+
signature: '^\S+\.(c|cc|cp|cpp|cxx|c\+\+|h|hh|hpp|hxx):\d+:\d+: (fatal )?error: '
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: Gradle's failed tests, and what went wrong
|
|
3
|
+
start: [' FAILED$', '^\* What went wrong:']
|
|
4
|
+
end: ['^\d+ tests? completed', '^\* Try:']
|
|
5
|
+
markers: [build.gradle, build.gradle.kts, settings.gradle, settings.gradle.kts, gradlew]
|
|
6
|
+
signature: '^BUILD FAILED in '
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: Jest's failed tests
|
|
3
|
+
start: ['^\s+● ']
|
|
4
|
+
end: ['^Test Suites: ', '^(FAIL|PASS) ', '^\s+● ']
|
|
5
|
+
markers: [{ path: package.json, contains: '"jest"' }, jest.config.js, jest.config.ts, jest.config.mjs, jest.config.cjs]
|
|
6
|
+
signature: '^Test Suites: \d+ '
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: mypy's type errors
|
|
3
|
+
start: ['^\S+\.pyi?:\d+(:\d+)?: (error|note): ']
|
|
4
|
+
end: ['^Found \d+ errors? in \d+ files? \(checked ']
|
|
5
|
+
markers: [{ path: pyproject.toml, contains: mypy }, mypy.ini, .mypy.ini, { path: setup.cfg, contains: mypy }]
|
|
6
|
+
signature: '^Found \d+ errors? in \d+ files? \(checked \d+ source files?\)$'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: PHPUnit's errors and failures
|
|
3
|
+
start: ['^There (was|were) \d+ (errors?|failures?):$']
|
|
4
|
+
end: ['^(ERRORS|FAILURES)!$', '^Tests: \d+, Assertions: \d+']
|
|
5
|
+
markers: [phpunit.xml, phpunit.xml.dist, { path: composer.json, contains: phpunit }]
|
|
6
|
+
signature: '^Tests: \d+, Assertions: \d+, '
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
use: json-log
|
|
2
|
+
fields: { time: time, level: level, logger: name, message: msg, stack: err.stack }
|
|
3
|
+
levels: { '10': trace, '20': debug, '30': info, '40': warn, '50': error, '60': fatal }
|
|
4
|
+
markers: [{ path: package.json, contains: '"pino"' }]
|
|
5
|
+
signature: '"level":\d+,"time":\d+,"pid":\d+,"hostname":'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: pytest's failures and errors
|
|
3
|
+
start: ['^=+ (FAILURES|ERRORS) =+$']
|
|
4
|
+
end: ['^=+ (short test summary info|warnings summary) =+$', '^=+ .* in [\d.]+s( \([\d:]+\))? =+$']
|
|
5
|
+
markers: [pyproject.toml, pytest.ini, setup.cfg, tox.ini, conftest.py]
|
|
6
|
+
signature: '^=+ (FAILURES|ERRORS|short test summary info) =+$'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: ruff's lint errors
|
|
3
|
+
start: ['^[A-Z]+[0-9]+ ', '^\S+:\d+:\d+: [A-Z]+[0-9]+ ']
|
|
4
|
+
end: ['^Found \d+ errors?']
|
|
5
|
+
markers: [{ path: pyproject.toml, contains: ruff }, ruff.toml, .ruff.toml, { path: .pre-commit-config.yaml, contains: ruff }]
|
|
6
|
+
signature: '^Found \d+ errors?( \(\d+ fixed, \d+ remaining\))?\.$'
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
use: json-log
|
|
2
|
+
fields: { time: timestamp, level: level, logger: logger, message: event, stack: exception }
|
|
3
|
+
markers: [{ path: pyproject.toml, contains: structlog }, { path: requirements.txt, contains: structlog }, { path: setup.cfg, contains: structlog }, { path: setup.py, contains: structlog }]
|
|
4
|
+
signature: '"event": ?".*"level": ?"(debug|info|warning|error|critical)"'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
use: section
|
|
2
|
+
title: Vitest's failed tests
|
|
3
|
+
start: ['^ FAIL ']
|
|
4
|
+
end: ['⎯\[\d+/\d+\]⎯', '^\s+Test Files ']
|
|
5
|
+
markers: [{ path: package.json, contains: '"vitest"' }, vitest.config.ts, vitest.config.js, vitest.config.mts, vitest.config.mjs]
|
|
6
|
+
signature: '^\s+Test Files \d+ '
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Evidence
|
|
7
|
+
# The presets in presets/, one YAML file each (see its README): each a
|
|
8
|
+
# built-in (`use`) and its options.
|
|
9
|
+
module Presets
|
|
10
|
+
DIR = File.join(__dir__, "presets")
|
|
11
|
+
|
|
12
|
+
# markers: files any of which the worktree must have, each a path or
|
|
13
|
+
# { path, contains }; signature: a pattern only the tool prints. Either
|
|
14
|
+
# may be missing (architecture.md, "Evidence of a failed stage").
|
|
15
|
+
Preset = Data.define(:name, :use, :options, :markers, :signature)
|
|
16
|
+
OWN = %w[use markers signature].freeze
|
|
17
|
+
|
|
18
|
+
def self.all
|
|
19
|
+
@all ||= Dir.glob("*.yml", base: DIR).sort.map do |file|
|
|
20
|
+
raw = YAML.safe_load_file(File.join(DIR, file))
|
|
21
|
+
Preset.new(name: File.basename(file, ".yml"), use: raw.fetch("use"), options: raw.except(*OWN),
|
|
22
|
+
markers: raw.fetch("markers", []), signature: raw["signature"])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.fetch(name) = all.find { |preset| preset.name == name } || raise(KeyError, "no preset '#{name}'")
|
|
27
|
+
def self.find(use, name) = all.find { |preset| preset.use == use && preset.name == name }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|