Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to Bootprint are documented here. The project follows Semant

## Unreleased

## 0.5.0 - 2026-08-14

### Added

- Capture timezone, default encodings, and locale metadata on the operating-system fingerprint.
- Built-in rules `timezone-drift`, `encoding-drift`, and `locale-drift` for those fields.

## 0.4.0 - 2026-08-11

### Added
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
bootprint (0.4.0)
bootprint (0.5.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -57,7 +57,7 @@ DEPENDENCIES

CHECKSUMS
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
bootprint (0.4.0)
bootprint (0.5.0)
bundler (4.0.17) sha256=214e21431b5665dd2f99df8a5511c6b151d7a72e8015c8b38f8b775b61cbb6c1
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
json (2.21.2-java) sha256=f01521cd90639cc26b63040fb00d2b949f18f41ecaab5c82b02fb31d0b8b736c
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Bootprint 0.2 combines a dependency compatibility analyzer, Rails boot inspector

`Gemfile.lock` captures dependency resolution, but not the complete runtime contract. Ruby engine and patch level, native clients, libc, CPU architecture, Rails adapters, required configuration, filesystem behavior, and initializer ordering can all change application behavior.

Bootprint records those facts as deterministic schema-v2 JSON and evaluates them with 39 independently testable built-in rules. A finding answers four questions that a plain diff cannot:
Bootprint records those facts as deterministic schema-v2 JSON and evaluates them with 42 independently testable built-in rules. A finding answers four questions that a plain diff cannot:

1. What changed?
2. How dangerous is it?
Expand All @@ -79,7 +79,7 @@ ERROR Required environment variable is missing

| Capability | Bootprint 0.2 |
|---|---|
| Diagnostic knowledge | 39 built-in rules across runtime, dependencies, native libraries, configuration, filesystem, and Rails boot |
| Diagnostic knowledge | 42 built-in rules across runtime, dependencies, native libraries, configuration, filesystem, locale, and Rails boot |
| Severity model | `info`, `warning`, `error`, `critical` |
| Report formats | Human terminal output, JSON, SARIF 2.1, Markdown |
| Snapshot contract | Deterministic schema v2 with in-memory v1 migration |
Expand All @@ -97,6 +97,7 @@ ERROR Required environment variable is missing
| Native libraries | OpenSSL, libyaml, SQLite, PostgreSQL, MySQL, libc, compiler and header availability when detectable |
| Configuration | Environment-variable names and presence, Rails environment, adapters, framework settings |
| Filesystem | Temporary/log path availability, writability, path separators, case sensitivity, symlink behavior |
| Locale | Timezone name and UTC offset, default internal/external encodings, LANG / LC_ALL / charmap |
| Rails boot | Framework configuration, autoload/eager-load paths, initializers, and opt-in initializer timings |
| Docker | Image runtime, platforms, installed package metadata when available, workdir, entrypoint, command, permissions |

Expand Down
29 changes: 28 additions & 1 deletion lib/bootprint/collectors/operating_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ def capture
"processors" => Etc.nprocessors,
"capabilities" => TOOLS.to_h { |tool| [tool, executable?(tool)] },
"ruby_headers" => header_state,
"ci" => ci_provider
"ci" => ci_provider,
"timezone" => timezone_state,
"encoding" => encoding_state,
"locale" => locale_state
}
end

Expand All @@ -45,6 +48,30 @@ def ci_provider

nil
end

def timezone_state
now = Time.now
{
"name" => now.zone,
"utc_offset" => now.utc_offset,
"tz" => ENV.fetch("TZ", nil)
}
end

def encoding_state
{
"external" => Encoding.default_external.name,
"internal" => Encoding.default_internal&.name
}
end

def locale_state
{
"lang" => ENV.fetch("LANG", nil),
"lc_all" => ENV.fetch("LC_ALL", nil),
"charmap" => Encoding.locale_charmap
}
end
end
end
end
18 changes: 18 additions & 0 deletions lib/bootprint/rules/builtin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ def runtime_rules
fix: "Use a standard release build for production.") do |_source, target|
dig(target, "runtime.debug_build") == true && { "description" => dig(target, "runtime.description") }
end
rule("timezone-drift", "Timezone mismatch", :runtime, :warning,
cause: "The environments select different timezones.",
impact: "Time.now, cron, and timestamp formatting can disagree across machines.",
fix: "Set TZ consistently in local, container, and CI environments.") do |source, target|
difference(source, target, "operating_system.timezone")
end
rule("encoding-drift", "Default encoding mismatch", :runtime, :warning,
cause: "Ruby default encodings differ between environments.",
impact: "String transcoding, file IO, and JSON payloads can fail only in one environment.",
fix: "Align Encoding.default_external (and LANG/LC_ALL) with the deployment runtime.") do |source, target|
difference(source, target, "operating_system.encoding")
end
rule("locale-drift", "Locale mismatch", :runtime, :info,
cause: "LANG / LC_ALL / locale charmap differ between environments.",
impact: "Sort order, number formatting, and encoding defaults can change.",
fix: "Export the same LANG and LC_ALL in every environment.") do |source, target|
difference(source, target, "operating_system.locale")
end
end

def dependency_rules
Expand Down
2 changes: 1 addition & 1 deletion lib/bootprint/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Bootprint
VERSION = "0.4.0"
VERSION = "0.5.0"
end
50 changes: 50 additions & 0 deletions test/locale_capture_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require "test_helper"

class LocaleCaptureTest < Minitest::Test
def test_operating_system_records_timezone_encoding_and_locale
captured = Bootprint::Collectors::OperatingSystem.capture

refute_nil captured["timezone"]["utc_offset"]
refute_empty captured["encoding"]["external"]
assert captured["locale"].key?("charmap")
end

def test_timezone_encoding_and_locale_drift_rules
source = snapshot("environment" => { "operating_system" => os_payload("UTC", "UTF-8", "C") })
target = snapshot("environment" => { "operating_system" => os_payload("EST", "Windows-1252", "en_US.UTF-8") })
report = Bootprint::Diagnosis.new(source, target).run

ids = report.findings.map(&:rule_id)
assert_includes ids, "timezone-drift"
assert_includes ids, "encoding-drift"
assert_includes ids, "locale-drift"
end

def test_matching_locale_metadata_does_not_fire
payload = os_payload("UTC", "UTF-8", "C")
source = snapshot("environment" => { "operating_system" => payload })
target = snapshot("environment" => { "operating_system" => payload.dup })
report = Bootprint::Diagnosis.new(source, target).run

ids = report.findings.map(&:rule_id)
refute_includes ids, "timezone-drift"
refute_includes ids, "encoding-drift"
refute_includes ids, "locale-drift"
end

private

def os_payload(zone, encoding, lang)
{
"name" => "linux",
"cpu" => "x86_64",
"capabilities" => { "make" => true },
"ruby_headers" => { "present" => true },
"timezone" => { "name" => zone, "utc_offset" => zone == "UTC" ? 0 : -18_000, "tz" => zone },
"encoding" => { "external" => encoding, "internal" => nil },
"locale" => { "lang" => lang, "lc_all" => nil, "charmap" => encoding }
}
end
end
Loading