quality_gate 0.2.2 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +292 -21
- data/docs/codex.md +37 -9
- data/docs/releasing.md +16 -4
- data/lib/generators/quality_gate/install/install_generator.rb +18 -3
- data/lib/generators/quality_gate/install/templates/agents_section.md.tt +13 -2
- data/lib/generators/quality_gate/install/templates/codex_fast.rb.tt +36 -0
- data/lib/generators/quality_gate/install/templates/codex_hooks.json.tt +1 -0
- data/lib/generators/quality_gate/install/templates/codex_verify_stop.rb.tt +28 -0
- data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +6 -0
- data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +16 -3
- data/lib/generators/quality_gate/install/templates/quality_gate_workflow.yml.tt +28 -0
- data/lib/generators/quality_gate/install/templates/rails_quality_gate.yml.tt +45 -0
- data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +13 -2
- data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +5 -0
- data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +1 -0
- data/lib/quality_gate/adapters/bundler_audit.rb +28 -3
- data/lib/quality_gate/adapters/database_consistency.rb +51 -0
- data/lib/quality_gate/adapters/database_consistency_report.rb +61 -0
- data/lib/quality_gate/adapters/database_consistency_report_item.rb +122 -0
- data/lib/quality_gate/adapters/debride.rb +67 -0
- data/lib/quality_gate/adapters/debride_report.rb +80 -0
- data/lib/quality_gate/adapters/herb.rb +303 -0
- data/lib/quality_gate/adapters/reek.rb +21 -2
- data/lib/quality_gate/adapters/rubocop.rb +23 -9
- data/lib/quality_gate/adapters/rubycritic.rb +74 -0
- data/lib/quality_gate/adapters/rubycritic_report.rb +112 -0
- data/lib/quality_gate/adapters/test_suite.rb +12 -1
- data/lib/quality_gate/baseline.rb +269 -0
- data/lib/quality_gate/baseline_run.rb +80 -0
- data/lib/quality_gate/cli.rb +129 -6
- data/lib/quality_gate/codex_fast_hook.rb +163 -0
- data/lib/quality_gate/codex_patch_files.rb +95 -0
- data/lib/quality_gate/codex_stop_hook.rb +102 -0
- data/lib/quality_gate/config.rb +20 -6
- data/lib/quality_gate/database_consistency_runner.rb +124 -0
- data/lib/quality_gate/doctor.rb +217 -0
- data/lib/quality_gate/doctor_bounded_file.rb +74 -0
- data/lib/quality_gate/doctor_command.rb +130 -0
- data/lib/quality_gate/doctor_coverage.rb +113 -0
- data/lib/quality_gate/doctor_git.rb +121 -0
- data/lib/quality_gate/doctor_hooks.rb +99 -0
- data/lib/quality_gate/doctor_launchers.rb +300 -0
- data/lib/quality_gate/doctor_path_lookup.rb +52 -0
- data/lib/quality_gate/doctor_report.rb +38 -0
- data/lib/quality_gate/init_command.rb +3 -1
- data/lib/quality_gate/installation.rb +72 -17
- data/lib/quality_gate/installer.rb +3 -0
- data/lib/quality_gate/reporters/doctor.rb +57 -0
- data/lib/quality_gate/reporters/json.rb +18 -1
- data/lib/quality_gate/reporters/markdown.rb +11 -1
- data/lib/quality_gate/reporters/text.rb +14 -6
- data/lib/quality_gate/ruby_profile.rb +121 -2
- data/lib/quality_gate/runner.rb +15 -17
- data/lib/quality_gate/version.rb +1 -1
- data/lib/quality_gate.rb +15 -0
- data/sig/quality_gate.rbs +117 -2
- metadata +30 -1
data/docs/releasing.md
CHANGED
|
@@ -27,7 +27,9 @@ gem archive. `bin/console` opens an interactive session with the gem loaded.
|
|
|
27
27
|
|
|
28
28
|
## Release checklist
|
|
29
29
|
|
|
30
|
-
1. Update `lib/quality_gate/version.rb` and `CHANGELOG.md`
|
|
30
|
+
1. Update `lib/quality_gate/version.rb` and add the matching `CHANGELOG.md` entry.
|
|
31
|
+
Treat these as one release record: keep the changelog heading, gemspec version,
|
|
32
|
+
and built artifact version synchronized.
|
|
31
33
|
2. Run `bin/prepare_release` and review the generated documentation and package.
|
|
32
34
|
Run `bundle exec quality_gate verify` to enforce the repository coverage budgets.
|
|
33
35
|
On the initial commit, Undercover cannot compare against a parent commit, so
|
|
@@ -38,9 +40,19 @@ gem archive. `bin/console` opens an interactive session with the gem loaded.
|
|
|
38
40
|
run normal verification, including diff coverage; no extra baseline commit is required.
|
|
39
41
|
3. Confirm the archive includes runtime code, configuration, licensing, and public
|
|
40
42
|
documentation, with no local paths, credentials, private logs, or internal plans.
|
|
41
|
-
4.
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
4. Before publishing, confirm the version matches in
|
|
44
|
+
`lib/quality_gate/version.rb`, the `CHANGELOG.md` heading, the gemspec, and
|
|
45
|
+
the built gem filename. Review the source commit and confirm it contains the
|
|
46
|
+
intended source and changelog entry. Separately confirm the gem artifact was
|
|
47
|
+
built from that reviewed commit.
|
|
48
|
+
5. Commit the reviewed source, create the matching version tag, then push the
|
|
49
|
+
reviewed branch and tag to the public repository. Confirm the remote branch
|
|
50
|
+
and tag point to the reviewed commit before creating the GitHub release.
|
|
51
|
+
Publish the artifact with `gem push pkg/quality_gate-VERSION.gem`, replacing
|
|
52
|
+
`VERSION` with the actual release version; RubyGems MFA is required. Create
|
|
53
|
+
the GitHub release from the pushed tag, using the changelog entry as its notes.
|
|
54
|
+
6. After publication, confirm the RubyGems release version and the GitHub
|
|
55
|
+
release title, tag, and notes match the version and changelog entry.
|
|
44
56
|
|
|
45
57
|
The Rakefile also loads Bundler's gem tasks. Its `release` task can publish and
|
|
46
58
|
push Git changes; use it only when those actions are intended.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../quality_gate/installation"
|
|
4
|
+
require_relative "../../../quality_gate/ruby_profile"
|
|
4
5
|
|
|
5
6
|
if defined?(Rails::Generators::Base)
|
|
6
7
|
module QualityGate
|
|
@@ -32,15 +33,25 @@ if defined?(Rails::Generators::Base)
|
|
|
32
33
|
class_option :skip_initializers, type: :boolean, default: false
|
|
33
34
|
class_option :skip_coverage, type: :boolean, default: false
|
|
34
35
|
class_option :agents, type: :boolean, default: false
|
|
36
|
+
class_option :codex, type: :boolean, default: false
|
|
37
|
+
class_option :test_framework, type: :string
|
|
38
|
+
class_option :test_helper, type: :string
|
|
39
|
+
class_option :test_command, type: :string
|
|
40
|
+
class_option :ci, type: :boolean, default: false
|
|
35
41
|
|
|
36
42
|
# Rails discovers generator tasks from methods declared on this class.
|
|
37
43
|
# Keep these forwarding methods so the shared module remains framework-neutral.
|
|
38
44
|
# rubocop:disable Lint/UselessMethodDefinition
|
|
39
|
-
def create_settings_file
|
|
45
|
+
def create_settings_file
|
|
46
|
+
profile if behavior == :invoke
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
def create_rules_file = super
|
|
41
51
|
def create_initializers = super
|
|
42
52
|
def inject_coverage = super
|
|
43
53
|
def create_agent_integration = super
|
|
54
|
+
def create_ci_workflow = super
|
|
44
55
|
def print_summary = super
|
|
45
56
|
# rubocop:enable Lint/UselessMethodDefinition
|
|
46
57
|
|
|
@@ -55,11 +66,15 @@ if defined?(Rails::Generators::Base)
|
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
def template_for(name)
|
|
58
|
-
name
|
|
69
|
+
@profile ? @profile.template_for(name) : name
|
|
59
70
|
end
|
|
60
71
|
|
|
61
72
|
def test_helper_path
|
|
62
|
-
|
|
73
|
+
profile.test_helper
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def profile
|
|
77
|
+
@profile ||= RailsProfile.new(destination_root:, options:)
|
|
63
78
|
end
|
|
64
79
|
end
|
|
65
80
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- quality_gate agent contract — start -->
|
|
2
2
|
## Quality Gate agent contract
|
|
3
3
|
|
|
4
|
-
Run `bundle exec quality_gate fast` after Ruby edits.
|
|
4
|
+
Run `bundle exec quality_gate fast` after Ruby edits. Run `bundle exec quality_gate verify` before you finish a change, and `bundle exec quality_gate audit` before you merge security-sensitive work.
|
|
5
5
|
|
|
6
6
|
Fix findings before you continue editing.
|
|
7
7
|
|
|
@@ -11,5 +11,16 @@ Fix findings before you continue editing.
|
|
|
11
11
|
|
|
12
12
|
The default fast path keeps feedback tight with a 10-second RuboCop timeout. Verify and audit keep the broader suite work on the 120-second default timeout.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<% if agent_option?(:agents) %>The Claude Code `PostToolUse` fast hook runs after native `Edit` and `Write` calls; Codex's optional patch fast hook runs after native `apply_patch` calls. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end. If those hooks become unavailable, inspect `log/quality_gate_hooks.jsonl`, run `bundle install`, and reinstall them with `bin/rails generate quality_gate:install --agents`.
|
|
15
|
+
<% end %><% if agent_option?(:codex) %>
|
|
16
|
+
The optional Codex patch hook runs `quality_gate fast` after native `apply_patch` calls. It checks changed Ruby files and ERB files when Herb is configured; it does not run after shell writes or other tools. Findings appear as advisory context and never roll back the completed patch. If the gate or its tools are unavailable, Codex is asked to tell you and to run `bundle exec quality_gate fast` manually. Keep the direct manual gate and CI checks as enforcement evidence.
|
|
17
|
+
|
|
18
|
+
The final Codex Stop hook still runs the full `verify` gate, including changes made by shell commands. Findings request one repair continuation; Codex reruns verification on the resumed Stop, and remaining findings are reported while allowing the turn to finish. Unavailable checks also allow the turn to finish. This full gate can add substantial latency at every Stop. The patch hook's native timeout is 30 seconds; the shipped fast RuboCop timeout is 10 seconds, and a longer project fast budget can exceed Codex's hook limit and require manual verification.
|
|
19
|
+
|
|
20
|
+
Enable these project hooks only in a trusted project. In Codex, review `.codex/hooks/quality_gate_fast.rb`, `.codex/hooks/quality_gate_verify_stop.rb`, and both generated command definitions through `/hooks`, then explicitly trust them. Review both again after hook configuration changes. Quality Gate never changes Codex trust settings. Doctor can recognize installed files, but cannot confirm trust, activation, or live execution; Codex provides no execution history for Quality Gate to inspect.
|
|
21
|
+
|
|
22
|
+
To refresh the generated Codex hooks after a Quality Gate upgrade, run `bin/rails generate quality_gate:install --codex` and review both commands in `/hooks` again. If the project moves, update both absolute commands in `.codex/hooks.json` and review them again. The installer upgrades only the exact previous Stop-only configuration generated for this same project root. Resolve customized configuration or a snapshot from another location manually using the proposal printed by the installer. To remove the integration, remove both Quality Gate hook entries and scripts; remove `.codex/hooks.json` only when it contains solely generated Quality Gate configuration. Remove only the Codex-specific instructions from `AGENTS.md` when it also contains Claude guidance, or remove the Quality Gate section when Codex is its only client.
|
|
23
|
+
<% else %>
|
|
24
|
+
Other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Codex can also use `bundle exec quality_gate fast` after edits for file-scoped feedback.
|
|
25
|
+
<% end %>
|
|
15
26
|
<!-- quality_gate agent contract — end -->
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
root = File.expand_path("../..", __dir__)
|
|
9
|
+
Dir.chdir(root)
|
|
10
|
+
gemfile = File.join(root, "Gemfile")
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] = gemfile
|
|
12
|
+
raise LoadError, "project Gemfile is missing" unless File.file?(gemfile)
|
|
13
|
+
|
|
14
|
+
original_stdout, original_stderr = $stdout, $stderr
|
|
15
|
+
$stdout = $stderr = StringIO.new
|
|
16
|
+
begin
|
|
17
|
+
require "bundler"
|
|
18
|
+
Bundler.setup
|
|
19
|
+
ensure
|
|
20
|
+
$stdout, $stderr = original_stdout, original_stderr
|
|
21
|
+
end
|
|
22
|
+
require "quality_gate"
|
|
23
|
+
require "quality_gate/codex_fast_hook"
|
|
24
|
+
input = JSON.parse(STDIN.read)
|
|
25
|
+
puts JSON.generate(QualityGate::CodexFastHook.new(dir: root).call(input))
|
|
26
|
+
rescue StandardError, LoadError => error
|
|
27
|
+
message = "Quality Gate fast feedback unavailable (#{error.class}); " \
|
|
28
|
+
"run `bundle exec quality_gate fast` manually."
|
|
29
|
+
puts JSON.generate(
|
|
30
|
+
"systemMessage" => message,
|
|
31
|
+
"hookSpecificOutput" => {
|
|
32
|
+
"hookEventName" => "PostToolUse",
|
|
33
|
+
"additionalContext" => "Quality Gate fast feedback is unavailable. Run `bundle exec quality_gate fast` manually."
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= JSON.pretty_generate("hooks" => {"PostToolUse" => [{"matcher" => "^apply_patch$", "hooks" => [{"type" => "command", "command" => Shellwords.join(["ruby", File.expand_path(File.join(destination_root, ".codex/hooks/quality_gate_fast.rb"))]), "timeout" => 30}]}], "Stop" => [{"hooks" => [{"type" => "command", "command" => Shellwords.join(["ruby", File.expand_path(File.join(destination_root, ".codex/hooks/quality_gate_verify_stop.rb"))]), "timeout" => 600}]}]}) %>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
root = File.expand_path("../..", __dir__)
|
|
9
|
+
Dir.chdir(root)
|
|
10
|
+
gemfile = File.join(root, "Gemfile")
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] = gemfile
|
|
12
|
+
raise LoadError, "project Gemfile is missing" unless File.file?(gemfile)
|
|
13
|
+
|
|
14
|
+
original_stdout, original_stderr = $stdout, $stderr
|
|
15
|
+
$stdout = $stderr = StringIO.new
|
|
16
|
+
begin
|
|
17
|
+
require "bundler"
|
|
18
|
+
Bundler.setup
|
|
19
|
+
ensure
|
|
20
|
+
$stdout, $stderr = original_stdout, original_stderr
|
|
21
|
+
end
|
|
22
|
+
require "quality_gate"
|
|
23
|
+
require "quality_gate/codex_stop_hook"
|
|
24
|
+
input = JSON.parse(STDIN.read)
|
|
25
|
+
puts JSON.generate(QualityGate::CodexStopHook.new(dir: root).call(input))
|
|
26
|
+
rescue StandardError, LoadError => error
|
|
27
|
+
puts JSON.generate("systemMessage" => "Quality Gate verification unavailable (#{error.class}); check the project bundle and run `bundle exec quality_gate verify --format json` manually.")
|
|
28
|
+
end
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
# audit:
|
|
16
16
|
# - brakeman
|
|
17
17
|
# - bundler_audit
|
|
18
|
+
# deep:
|
|
19
|
+
# - rubycritic
|
|
18
20
|
#
|
|
19
21
|
# # Command overrides by gate. Empty mappings use adapter defaults.
|
|
20
22
|
# commands:
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
# - bin/rails
|
|
25
27
|
# - test
|
|
26
28
|
# audit: {}
|
|
29
|
+
# deep: {}
|
|
27
30
|
#
|
|
28
31
|
# # Command timeouts in seconds. Adapter-specific values override the default.
|
|
29
32
|
# timeouts:
|
|
@@ -40,3 +43,6 @@
|
|
|
40
43
|
#
|
|
41
44
|
# # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
|
|
42
45
|
# rubocop_config:
|
|
46
|
+
|
|
47
|
+
# # Optional warning/info baseline for the fast and verify gates; leave empty to disable.
|
|
48
|
+
# baseline: {}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "open3"
|
|
6
6
|
require "time"
|
|
7
|
+
require "yaml"
|
|
7
8
|
|
|
8
9
|
begin
|
|
9
10
|
require "fiddle/import"
|
|
@@ -71,9 +72,21 @@ def outcome_for(exit_status, findings)
|
|
|
71
72
|
"findings" if exit_status == 1 && findings.any?
|
|
72
73
|
end
|
|
73
74
|
|
|
74
|
-
def
|
|
75
|
+
def herb_configured?(project_root)
|
|
76
|
+
path = File.join(project_root, ".quality_gate.yml")
|
|
77
|
+
return false unless File.file?(path)
|
|
78
|
+
|
|
79
|
+
document = YAML.safe_load(File.read(path))
|
|
80
|
+
gate_adapters = document.fetch("adapters", nil) if document.is_a?(Hash)
|
|
81
|
+
adapters = gate_adapters.fetch("fast", nil) if gate_adapters.is_a?(Hash)
|
|
82
|
+
adapters.is_a?(Array) && adapters.include?("herb")
|
|
83
|
+
rescue Psych::Exception, SystemCallError
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def initial_outcome(file, project_root)
|
|
75
88
|
return "no_file" unless file
|
|
76
|
-
return "skipped" unless file.end_with?(".rb")
|
|
89
|
+
return "skipped" unless file.end_with?(".rb") || (file.end_with?(".erb") && herb_configured?(project_root))
|
|
77
90
|
return "deleted" unless File.file?(file)
|
|
78
91
|
|
|
79
92
|
"unavailable"
|
|
@@ -208,7 +221,7 @@ project_root = Dir.pwd unless project_root.is_a?(String) && !project_root.empty?
|
|
|
208
221
|
tool_input = input["tool_input"]
|
|
209
222
|
file = tool_input["file_path"] if tool_input.is_a?(Hash)
|
|
210
223
|
file = nil unless file.is_a?(String) && !file.empty?
|
|
211
|
-
outcome = initial_outcome(file)
|
|
224
|
+
outcome = initial_outcome(file, project_root)
|
|
212
225
|
attempt_was_made = outcome == "unavailable"
|
|
213
226
|
exit_code = 0
|
|
214
227
|
feedback = nil
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Quality Gate
|
|
2
|
+
# Customize the Ruby version, matrix, services, and environment for your application.
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
quality-gate:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
ruby-version: "<%= RUBY_VERSION %>"
|
|
22
|
+
bundler-cache: true
|
|
23
|
+
- name: Fast checks
|
|
24
|
+
run: bundle exec quality_gate fast
|
|
25
|
+
- name: Verify checks
|
|
26
|
+
run: bundle exec quality_gate verify
|
|
27
|
+
- name: Security audit
|
|
28
|
+
run: bundle exec quality_gate audit
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# # Output format: "text", "json", or "markdown".
|
|
2
|
+
# format: text
|
|
3
|
+
#
|
|
4
|
+
# # Optional file paths passed to RuboCop and Reek; empty means the project scope.
|
|
5
|
+
# files: []
|
|
6
|
+
#
|
|
7
|
+
# # Rails adapters run by each gate, in order.
|
|
8
|
+
# adapters:
|
|
9
|
+
# fast:
|
|
10
|
+
# - rubocop
|
|
11
|
+
# verify:
|
|
12
|
+
# - reek
|
|
13
|
+
# - test_suite
|
|
14
|
+
<% if @profile.coverage? %># - undercover
|
|
15
|
+
<% end %># audit:
|
|
16
|
+
# - brakeman
|
|
17
|
+
# - bundler_audit
|
|
18
|
+
#
|
|
19
|
+
# The installer-selected test command runs during verify with coverage enabled.
|
|
20
|
+
commands:
|
|
21
|
+
verify:
|
|
22
|
+
test_suite:
|
|
23
|
+
<% @profile.test_command.each do |argument| %> - <%= JSON.generate(argument) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
#
|
|
26
|
+
# # Command timeouts in seconds.
|
|
27
|
+
# timeouts:
|
|
28
|
+
# default: 120
|
|
29
|
+
# rubocop: 10
|
|
30
|
+
# test_suite: 120
|
|
31
|
+
# undercover: 120
|
|
32
|
+
#
|
|
33
|
+
# # Aggregate SimpleCov budgets. Leave the entire mapping commented to disable coverage budgeting.
|
|
34
|
+
# coverage:
|
|
35
|
+
#
|
|
36
|
+
# # Git ref or commit for Undercover; blank selects the comparison point automatically.
|
|
37
|
+
# compare_point:
|
|
38
|
+
#
|
|
39
|
+
# # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
|
|
40
|
+
# rubocop_config:
|
|
41
|
+
|
|
42
|
+
# # Optional warning/info baseline for fast and verify; omitted means disabled.
|
|
43
|
+
# baseline:
|
|
44
|
+
# fast: config/quality_gate-fast-baseline.json
|
|
45
|
+
# verify: config/quality_gate-verify-baseline.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- quality_gate agent contract — start -->
|
|
2
2
|
## Quality Gate agent contract
|
|
3
3
|
|
|
4
|
-
Run `bundle exec quality_gate fast` after Ruby edits.
|
|
4
|
+
Run `bundle exec quality_gate fast` after Ruby edits. Run `bundle exec quality_gate verify` before you finish a change, and `bundle exec quality_gate audit` before you merge security-sensitive work.
|
|
5
5
|
|
|
6
6
|
Fix findings before you continue editing.
|
|
7
7
|
|
|
@@ -11,5 +11,16 @@ Fix findings before you continue editing.
|
|
|
11
11
|
|
|
12
12
|
The default fast path keeps feedback tight with a 10-second RuboCop timeout. Verify and audit keep the broader suite work on the 120-second default timeout.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<% if agent_option?(:agents) %>The Claude Code `PostToolUse` fast hook runs after native `Edit` and `Write` calls; Codex's optional patch fast hook runs after native `apply_patch` calls. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end. If those hooks become unavailable, inspect `log/quality_gate_hooks.jsonl`, run `bundle install`, and reinstall them with `bundle exec quality_gate init --profile ruby --agents`.
|
|
15
|
+
<% end %><% if agent_option?(:codex) %>
|
|
16
|
+
The optional Codex patch hook runs `quality_gate fast` after native `apply_patch` calls. It checks changed Ruby files and ERB files when Herb is configured; it does not run after shell writes or other tools. Findings appear as advisory context and never roll back the completed patch. If the gate or its tools are unavailable, Codex is asked to tell you and to run `bundle exec quality_gate fast` manually. Keep the direct manual gate and CI checks as enforcement evidence.
|
|
17
|
+
|
|
18
|
+
The final Codex Stop hook still runs the full `verify` gate, including changes made by shell commands. Findings request one repair continuation; Codex reruns verification on the resumed Stop, and remaining findings are reported while allowing the turn to finish. Unavailable checks also allow the turn to finish. This full gate can add substantial latency at every Stop. The patch hook's native timeout is 30 seconds; the shipped fast RuboCop timeout is 10 seconds, and a longer project fast budget can exceed Codex's hook limit and require manual verification.
|
|
19
|
+
|
|
20
|
+
Enable these project hooks only in a trusted project. In Codex, review `.codex/hooks/quality_gate_fast.rb`, `.codex/hooks/quality_gate_verify_stop.rb`, and both generated command definitions through `/hooks`, then explicitly trust them. Review both again after hook configuration changes. Quality Gate never changes Codex trust settings. Doctor can recognize installed files, but cannot confirm trust, activation, or live execution; Codex provides no execution history for Quality Gate to inspect.
|
|
21
|
+
|
|
22
|
+
To refresh the generated Codex hooks after a Quality Gate upgrade, run `bundle exec quality_gate init --profile ruby --codex` and review both commands in `/hooks` again. If the project moves, update both absolute commands in `.codex/hooks.json` and review them again. The installer upgrades only the exact previous Stop-only configuration generated for this same project root. Resolve customized configuration or a snapshot from another location manually using the proposal printed by the installer. To remove the integration, remove both Quality Gate hook entries and scripts; remove `.codex/hooks.json` only when it contains solely generated Quality Gate configuration. Remove only the Codex-specific instructions from `AGENTS.md` when it also contains Claude guidance, or remove the Quality Gate section when Codex is its only client.
|
|
23
|
+
<% else %>
|
|
24
|
+
Other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Codex can also use `bundle exec quality_gate fast` after edits for file-scoped feedback.
|
|
25
|
+
<% end %>
|
|
15
26
|
<!-- quality_gate agent contract — end -->
|
|
@@ -38,3 +38,8 @@ timeouts:
|
|
|
38
38
|
|
|
39
39
|
# # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
|
|
40
40
|
# rubocop_config:
|
|
41
|
+
|
|
42
|
+
# # Optional warning/info baseline for fast and verify; omitted means disabled.
|
|
43
|
+
# baseline:
|
|
44
|
+
# fast: config/quality_gate-fast-baseline.json
|
|
45
|
+
# verify: config/quality_gate-verify-baseline.json
|
|
@@ -9,8 +9,9 @@ module QualityGate
|
|
|
9
9
|
# rubocop:disable Metrics/ClassLength
|
|
10
10
|
class BundlerAudit < Adapter
|
|
11
11
|
class InvalidReport < StandardError; end
|
|
12
|
+
class InvalidProcessStatus < StandardError; end
|
|
12
13
|
class UnavailableDatabase < StandardError; end
|
|
13
|
-
private_constant :InvalidReport, :UnavailableDatabase
|
|
14
|
+
private_constant :InvalidReport, :InvalidProcessStatus, :UnavailableDatabase
|
|
14
15
|
|
|
15
16
|
LOCK_FILE = "Gemfile.lock".freeze # rubocop:disable Style/RedundantFreeze
|
|
16
17
|
FALLBACK_WARNING = "bundler_audit: advisory database update failed; using cached database"
|
|
@@ -43,7 +44,9 @@ module QualityGate
|
|
|
43
44
|
def updated_findings(tool, argv, deadline, timeout_seconds)
|
|
44
45
|
stderr = +""
|
|
45
46
|
stdout, stderr, status = capture(argv, remaining_timeout(deadline, timeout_seconds))
|
|
47
|
+
validate_normal_completion!(status)
|
|
46
48
|
findings = parse_and_validate(stdout, tool)
|
|
49
|
+
validate_status_findings!(status, findings)
|
|
47
50
|
return findings if local_database?
|
|
48
51
|
|
|
49
52
|
unavailable_database_failure(tool, stderr)
|
|
@@ -51,6 +54,8 @@ module QualityGate
|
|
|
51
54
|
return [failure_finding(tool, e, stderr)] if status&.success?
|
|
52
55
|
|
|
53
56
|
fallback_findings(tool, e, stderr, deadline, timeout_seconds)
|
|
57
|
+
rescue InvalidProcessStatus => e
|
|
58
|
+
[failure_finding(tool, e, stderr)]
|
|
54
59
|
rescue TimeoutError, SystemCallError, IOError, ThreadError => e
|
|
55
60
|
fallback_findings(tool, e, stderr, deadline, timeout_seconds)
|
|
56
61
|
end
|
|
@@ -69,10 +74,12 @@ module QualityGate
|
|
|
69
74
|
|
|
70
75
|
def cached_findings(tool, deadline, timeout_seconds)
|
|
71
76
|
cached_stderr = +""
|
|
72
|
-
stdout, cached_stderr, = capture(cached_command, remaining_timeout(deadline, timeout_seconds))
|
|
77
|
+
stdout, cached_stderr, status = capture(cached_command, remaining_timeout(deadline, timeout_seconds))
|
|
78
|
+
validate_normal_completion!(status)
|
|
73
79
|
findings = parse_and_validate(stdout, tool)
|
|
80
|
+
validate_status_findings!(status, findings)
|
|
74
81
|
emit_fallback_warning(tool, findings, cached_stderr)
|
|
75
|
-
rescue ParseError, TimeoutError, SystemCallError, IOError, ThreadError => e
|
|
82
|
+
rescue ParseError, InvalidProcessStatus, TimeoutError, SystemCallError, IOError, ThreadError => e
|
|
76
83
|
[failure_finding(tool, e, cached_stderr)]
|
|
77
84
|
end
|
|
78
85
|
|
|
@@ -85,6 +92,24 @@ module QualityGate
|
|
|
85
92
|
raise TimeoutError, "timeout after #{timeout_seconds} seconds"
|
|
86
93
|
end
|
|
87
94
|
|
|
95
|
+
def validate_normal_completion!(status)
|
|
96
|
+
return if status&.exited? && [0, 1].include?(status.exitstatus)
|
|
97
|
+
|
|
98
|
+
invalid_process_status!("process did not exit normally with status 0 or 1")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_status_findings!(status, findings)
|
|
102
|
+
clean_status = status.exitstatus.zero?
|
|
103
|
+
return if clean_status == findings.empty?
|
|
104
|
+
|
|
105
|
+
expected = clean_status ? "no vulnerability findings" : "vulnerability findings"
|
|
106
|
+
invalid_process_status!("exit status #{status.exitstatus} conflicts with #{expected}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def invalid_process_status!(reason)
|
|
110
|
+
raise InvalidProcessStatus, reason
|
|
111
|
+
end
|
|
112
|
+
|
|
88
113
|
def unavailable_database_failure(tool, stderr)
|
|
89
114
|
error = UnavailableDatabase.new("no usable advisory database is available")
|
|
90
115
|
[failure_finding(tool, error, stderr)]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "database_consistency_report"
|
|
5
|
+
|
|
6
|
+
module QualityGate
|
|
7
|
+
module Adapters
|
|
8
|
+
# Runs the optional project-wide database consistency bridge.
|
|
9
|
+
class DatabaseConsistency < Adapter
|
|
10
|
+
TOOL_NAME = "database_consistency"
|
|
11
|
+
BRIDGE_PATH = File.expand_path("../database_consistency_runner.rb", __dir__).freeze
|
|
12
|
+
|
|
13
|
+
def name = TOOL_NAME
|
|
14
|
+
|
|
15
|
+
def command
|
|
16
|
+
prefix = config.fetch(:commands).fetch(:audit).fetch(:database_consistency, nil)
|
|
17
|
+
return [RbConfig.ruby, BRIDGE_PATH] unless prefix
|
|
18
|
+
|
|
19
|
+
prefix + [BRIDGE_PATH]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(stdout)
|
|
23
|
+
DatabaseConsistencyReport.new(stdout).findings
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call = capture_findings
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def capture_findings
|
|
31
|
+
stderr = +""
|
|
32
|
+
execute(stderr).tap { validate_findings!(_1, expected_tool: name) }
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
[failure_finding(name, e, stderr)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute(stderr)
|
|
38
|
+
stdout, captured_stderr, status = capture(validated_command, resolved_timeout(name))
|
|
39
|
+
stderr.replace(captured_stderr)
|
|
40
|
+
validate_process_status!(status)
|
|
41
|
+
parse(stdout)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_process_status!(status)
|
|
45
|
+
return if status&.exited? && status.exitstatus.zero?
|
|
46
|
+
|
|
47
|
+
raise ParseError.new(tool: name, reason: "process did not exit normally with status 0")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rubygems"
|
|
5
|
+
require_relative "database_consistency_report_item"
|
|
6
|
+
|
|
7
|
+
module QualityGate
|
|
8
|
+
module Adapters
|
|
9
|
+
# Validates the bridge envelope and converts reports into findings.
|
|
10
|
+
class DatabaseConsistencyReport
|
|
11
|
+
VERSION = 1
|
|
12
|
+
VERSION_REQUIREMENT = Gem::Requirement.new("~> 3.0.14")
|
|
13
|
+
|
|
14
|
+
def initialize(json)
|
|
15
|
+
@envelope = JSON.parse(json)
|
|
16
|
+
rescue JSON::ParserError => e
|
|
17
|
+
raise ParseError.new(tool: "database_consistency", reason: e.message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def findings
|
|
21
|
+
validate_envelope!
|
|
22
|
+
reports.filter_map { DatabaseConsistencyReportItem.new(_1).finding }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_envelope!
|
|
28
|
+
invalid!("envelope must be an object") unless @envelope.is_a?(Hash)
|
|
29
|
+
validate_version!
|
|
30
|
+
validate_analyzer_version!
|
|
31
|
+
invalid!("reports must be an array") unless reports.is_a?(Array)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_version!
|
|
35
|
+
value = @envelope.fetch("version", nil)
|
|
36
|
+
invalid!("version must be integer 1") unless value.instance_of?(Integer) && value == VERSION
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_analyzer_version!
|
|
40
|
+
value = @envelope.fetch("analyzer_version", nil)
|
|
41
|
+
return if supported_version?(value)
|
|
42
|
+
|
|
43
|
+
invalid!("analyzer_version must be a supported version")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def supported_version?(value)
|
|
47
|
+
value.is_a?(String) && VERSION_REQUIREMENT.satisfied_by?(Gem::Version.new(value))
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reports = @envelope.fetch("reports", nil)
|
|
53
|
+
|
|
54
|
+
def invalid!(reason)
|
|
55
|
+
raise ParseError.new(tool: "database_consistency", reason: reason)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private_constant :DatabaseConsistencyReport
|
|
60
|
+
end
|
|
61
|
+
end
|