Skip to content
Open
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
102 changes: 100 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# danger-packwerk

`danger-packwerk` integrates [`packwerk`](https://github.com/Shopify/packwerk) with [`danger`](https://github.com/danger/danger) to provide inline comments in PRs related to boundaries in a Rails application.
`danger-packwerk` integrates [`packwerk`](https://github.com/Shopify/packwerk) (via [`pks`](https://github.com/rubyatscale/pks)) with [`danger`](https://github.com/danger/danger) to provide inline comments in PRs related to boundaries in a Rails application.

## Installation and Basic Usage
Step 1: Add this line to your `Gemfile` (to whatever group your CI uses, as it is not needed in production) and `bundle install`:
Expand All @@ -18,10 +18,108 @@ package_todo_yml_changes.check

That's it for basic usage!

## pks Integration

`danger-packwerk` uses [pks](https://github.com/rubyatscale/pks), a fast Rust implementation of packwerk's boundary checking. This provides significant performance improvements, especially for large codebases.

### Why pks?

- **Speed**: pks is written in Rust and runs 10-100x faster than the Ruby packwerk implementation
- **Compatibility**: pks produces the same violation types (dependency, privacy) as packwerk
- **JSON Output**: Native JSON output format for reliable parsing

### Installing pks

You have two options for installing pks:

**Option 1: Using dotslash (recommended)**

[dotslash](https://dotslash-cli.com/) manages tool versions declaratively. Create a `pks` file in your project:

```json
#!/usr/bin/env dotslash

{
"name": "pks",
"platforms": {
"macos-aarch64": {
"size": 3456789,
"hash": "sha256:...",
"url": "https://github.com/alexcrichton/pks/releases/download/v0.1.0/pks-aarch64-apple-darwin.tar.gz",
"path": "pks"
},
"linux-x86_64": {
"size": 4567890,
"hash": "sha256:...",
"url": "https://github.com/alexcrichton/pks/releases/download/v0.1.0/pks-x86_64-unknown-linux-gnu.tar.gz",
"path": "pks"
}
}
}
```

Then ensure it's in your PATH or use `./pks` directly.

**Option 2: Using cargo**

If you have Rust installed:

```bash
cargo install pks
```

### Migration from packwerk

No code changes are required. `danger-packwerk` automatically uses pks when calling `packwerk.check`. The integration is transparent—your existing `Dangerfile` configuration continues to work.

If you were previously relying on `bin/packwerk check` being called directly, note that danger-packwerk now uses `pks check --output-format json` internally.

### JSON Output Format

pks outputs violations in a structured JSON format that danger-packwerk parses:

```json
{
"offenses": [
{
"violation_type": "privacy",
"file": "packs/my_pack/app/models/user.rb",
"line": 42,
"column": 10,
"constant_name": "::OtherPack::PrivateClass",
"referencing_pack_name": "packs/my_pack",
"defining_pack_name": "packs/other_pack",
"strict": false,
"message": "Privacy violation: ..."
}
]
}
```

This format provides precise line and column information for accurate inline comments.

### Error Handling

If pks is not installed or not found in PATH, danger-packwerk raises `DangerPackwerk::PksWrapper::PksBinaryNotFoundError` with the message:

```
pks binary not found. Please install pks to use this feature.
```

To check if pks is available before running:

```ruby
if DangerPackwerk::PksWrapper.pks_available?
packwerk.check
else
warn "pks not installed, skipping packwerk check"
end
```

## Advanced Usage

There are currently two danger checks that ship with `danger-packwerk`:
1) One that runs `bin/packwerk check` and leaves inline comments in source code on new violations
1) One that runs `pks check` and leaves inline comments in source code on new violations
2) One that looks at changes to `package_todo.yml` files and leaves inline comments on added violations.

In upcoming iterations, we will include other danger checks, including:
Expand Down
2 changes: 2 additions & 0 deletions lib/danger-packwerk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ module DangerPackwerk
require 'danger-packwerk/check/default_formatter'
require 'danger-packwerk/update/offenses_formatter'
require 'danger-packwerk/update/default_formatter'
require 'danger-packwerk/pks_offense'
require 'danger-packwerk/pks_wrapper'
end
12 changes: 6 additions & 6 deletions lib/danger-packwerk/check/default_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ def initialize(custom_help_message: nil)

sig do
override.params(
offenses: T::Array[Packwerk::ReferenceOffense],
offenses: T::Array[PksOffense],
repo_link: String,
org_name: String,
repo_url_builder: T.nilable(T.proc.params(constant_path: String).returns(String))
).returns(String)
end
def format_offenses(offenses, repo_link, org_name, repo_url_builder: nil)
reference_offense = T.must(offenses.first)
offense = T.must(offenses.first)
violation_types = offenses.map(&:violation_type)
referencing_file = reference_offense.reference.relative_path
referencing_file = offense.file
referencing_file_pack = ParsePackwerk.package_from_path(referencing_file).name
# We remove leading double colons as they feel like an implementation detail of packwerk.
constant_name = reference_offense.reference.constant.name.delete_prefix('::')
constant_name = offense.constant_name.delete_prefix('::')

constant_source_package_name = reference_offense.reference.constant.package.name
constant_source_package_name = offense.defining_pack_name

constant_location = reference_offense.reference.constant.location
constant_location = Private.constant_resolver.resolve(offense.constant_name)&.location || offense.file
constant_source_package = T.must(ParsePackwerk.all.find { |p| p.name == constant_source_package_name })
constant_source_package_ownership_info = Private::OwnershipInformation.for_package(constant_source_package, org_name)

Expand Down
2 changes: 1 addition & 1 deletion lib/danger-packwerk/check/offenses_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module OffensesFormatter

sig do
abstract.params(
offenses: T::Array[Packwerk::ReferenceOffense],
offenses: T::Array[PksOffense],
repo_link: String,
org_name: String,
repo_url_builder: T.nilable(T.proc.params(constant_path: String).returns(String))
Expand Down
38 changes: 19 additions & 19 deletions lib/danger-packwerk/danger_packwerk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
require 'packwerk'
require 'parse_packwerk'
require 'sorbet-runtime'
require 'danger-packwerk/packwerk_wrapper'
require 'danger-packwerk/pks_wrapper'
require 'danger-packwerk/pks_offense'
require 'danger-packwerk/private/git'

module DangerPackwerk
Expand All @@ -18,7 +19,7 @@ class DangerPackwerk < Danger::Plugin
# especially given all violations should fail the build anyways.
# We set a max (rather than unlimited) to avoid GitHub rate limiting and general spam if a PR does some sort of mass rename.
DEFAULT_MAX_COMMENTS = 15
OnFailure = T.type_alias { T.proc.params(offenses: T::Array[Packwerk::ReferenceOffense]).void }
OnFailure = T.type_alias { T.proc.params(offenses: T::Array[PksOffense]).void }
DEFAULT_ON_FAILURE = T.let(->(offenses) {}, OnFailure)
DEFAULT_FAIL = false
DEFAULT_FAILURE_MESSAGE = 'Packwerk violations were detected! Please resolve them to unblock the build.'
Expand Down Expand Up @@ -101,54 +102,53 @@ def check(

current_comment_count = 0

packwerk_reference_offenses = PackwerkWrapper.get_offenses_for_files(targeted_files.to_a).compact
pks_offenses = PksWrapper.get_offenses_for_files(targeted_files.to_a)

renamed_files = git_filesystem.renamed_files.map { |before_after_file| before_after_file[:after] }

packwerk_reference_offenses_to_care_about = packwerk_reference_offenses.reject do |packwerk_reference_offense|
constant_name = packwerk_reference_offense.reference.constant.name
filepath_that_defines_this_constant = Private.constant_resolver.resolve(constant_name)&.location
offenses_to_care_about = pks_offenses.reject do |offense|
filepath_that_defines_this_constant = Private.constant_resolver.resolve(offense.constant_name)&.location
# Ignore references that have been renamed
renamed_files.include?(filepath_that_defines_this_constant) ||
# Ignore violations that are not in the allow-list of violation types to leave comments for
!violation_types.include?(packwerk_reference_offense.violation_type)
!violation_types.include?(offense.violation_type)
end

# We group by the constant name, line number, and reference path. Any offenses with these same values should only differ on what type of violation
# they are (privacy or dependency). We put privacy and dependency violation messages in the same comment since they would occur on the same line.
packwerk_reference_offenses_to_care_about.group_by do |packwerk_reference_offense|
offenses_to_care_about.group_by do |offense|
case grouping_strategy
when CommentGroupingStrategy::PerConstantPerLocation
[
packwerk_reference_offense.reference.constant.name,
packwerk_reference_offense.location&.line,
packwerk_reference_offense.reference.relative_path
offense.constant_name,
offense.line,
offense.file
]
when CommentGroupingStrategy::PerConstantPerPack
[
packwerk_reference_offense.reference.constant.name,
ParsePackwerk.package_from_path(packwerk_reference_offense.reference.relative_path)
offense.constant_name,
ParsePackwerk.package_from_path(offense.file)
]
else
T.absurd(grouping_strategy)
end
end.each_value do |unique_packwerk_reference_offenses|
end.each_value do |unique_offenses|
break if current_comment_count >= max_comments

current_comment_count += 1

reference_offense = T.must(unique_packwerk_reference_offenses.first)
line_number = reference_offense.location&.line
referencing_file = reference_offense.reference.relative_path
offense = T.must(unique_offenses.first)
line_number = offense.line
referencing_file = offense.file

message = offenses_formatter.format_offenses(unique_packwerk_reference_offenses, repo_link, org_name, repo_url_builder: repo_url_builder)
message = offenses_formatter.format_offenses(unique_offenses, repo_link, org_name, repo_url_builder: repo_url_builder)
markdown(message, file: git_filesystem.convert_to_filesystem(referencing_file), line: line_number)
end

if current_comment_count > 0
fail(failure_message) if fail_build

on_failure.call(packwerk_reference_offenses)
on_failure.call(pks_offenses)
end
end
end
Expand Down
73 changes: 0 additions & 73 deletions lib/danger-packwerk/packwerk_wrapper.rb

This file was deleted.

Loading