duel-rails 0.1.0 → 0.1.1
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 +42 -1
- data/README.md +7 -4
- data/lib/duel/rails/version.rb +1 -1
- data/lib/generators/duel/install/templates/duel.rb.tt +5 -0
- data/lib/generators/duel/install/templates/perf.yml.tt +89 -22
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e5ae40fccca2ac222b0b74531bf35a79df8e05555f58210c966c505f13800a6
|
|
4
|
+
data.tar.gz: 166a8ea74a1000fdb64f859475bab318b9f9c31aa60e0e78837f49369dc184dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5c8ef296bf6e55639b68f90e76ecee308bc4c054e9c3c2f19351e9a5a7568f76ab597399ccc9d1b58bf67762db264ebb47fe6a5f2e80bd256f72880ba1be8dd
|
|
7
|
+
data.tar.gz: 3c9c0b98b9e589de2da84e9f9fbea1b0182a956f273adca6437a44870df91645f24f69e1af0d77f8c4ca917539f230b39ac619b48fc12263daed473565cc0ce8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-10-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The workflow that `rails generate duel:install` writes no longer
|
|
15
|
+
comments on every pull request. By default, it comments only when a
|
|
16
|
+
benchmark fails or Duel couldn't compare, and `c.comment` in
|
|
17
|
+
`.duel.rb` chooses otherwise (`:always` for the old behavior,
|
|
18
|
+
`:results`, or `:never`). The generated `.duel.rb` shows the setting.
|
|
19
|
+
- The workflow never edits a comment. It used to update one comment on
|
|
20
|
+
every push, and now posts a new one each time it comments, so the pull
|
|
21
|
+
request keeps every report. When a run wouldn't get a comment of its
|
|
22
|
+
own, the workflow still posts one if its last comment on the pull
|
|
23
|
+
request had another status, so after a failure the first run that
|
|
24
|
+
passes says so.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- The workflow that `rails generate duel:install` writes no longer gives
|
|
29
|
+
the job that runs a pull request's code the `pull-requests: write`
|
|
30
|
+
permission. The Duel job only reads (`contents: read`, and its
|
|
31
|
+
checkout keeps no credentials), and keeps the report as an artifact
|
|
32
|
+
for a day. A Comment job of its own, which checks nothing out and runs
|
|
33
|
+
none of the app's code, downloads the report and comments with it, for
|
|
34
|
+
a pull request from the same repository.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- With `c.ruby = :version_file`, the workflow installs the bundle for
|
|
39
|
+
the baseline's Ruby too, when it differs from the candidate's. The
|
|
40
|
+
baseline's workers load that bundle, and gems installed for one Ruby
|
|
41
|
+
version don't load on another, so a branch that changed the Ruby's
|
|
42
|
+
minor version failed before it compared anything.
|
|
43
|
+
|
|
44
|
+
- With a `.ruby-version`, the generated workflow installs the baseline's
|
|
45
|
+
Ruby for every event that compares, not only for a pull request: the
|
|
46
|
+
commit before a push, the last release on a schedule, and where HEAD
|
|
47
|
+
left the main branch for a pull request and a manual run, when its
|
|
48
|
+
`.ruby-version` names another Ruby than HEAD's.
|
|
49
|
+
|
|
10
50
|
## [0.1.0] - 2026-10-24
|
|
11
51
|
|
|
12
52
|
The first release. duel-rails is a Duel plugin for Rails apps, open
|
|
@@ -95,5 +135,6 @@ Rails 7.2 or later, and Ruby 3.4 or later.
|
|
|
95
135
|
CI.
|
|
96
136
|
- RBS signatures in sig/, checked with Steep.
|
|
97
137
|
|
|
98
|
-
[Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-rails-v0.1.
|
|
138
|
+
[Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-rails-v0.1.1...HEAD
|
|
139
|
+
[0.1.1]: https://github.com/sferik/duel-ruby/compare/duel-rails-v0.1.0...duel-rails-v0.1.1
|
|
99
140
|
[0.1.0]: https://github.com/sferik/duel-ruby/releases/tag/duel-rails-v0.1.0
|
data/README.md
CHANGED
|
@@ -68,7 +68,7 @@ production does, with everything of its own:
|
|
|
68
68
|
| `config/environments/benchmark.rb` | The environment that each version boots in, based on production. |
|
|
69
69
|
| `duel/requests_duel.rb` | A benchmark of each of your app's first 10 routes that a GET reaches without parameters. |
|
|
70
70
|
| `duel/boot_duel.rb` | A benchmark of your app's boot. See [Benchmark your app's boot](#benchmark-your-apps-boot). |
|
|
71
|
-
| `.github/workflows/perf.yml` | A GitHub Actions workflow that runs Duel on pull requests, on pushes to `main`, and nightly against your last release, and comments on
|
|
71
|
+
| `.github/workflows/perf.yml` | A GitHub Actions workflow that runs Duel on pull requests, on pushes to `main`, and nightly against your last release, and comments on a pull request with Duel's report when a benchmark fails. It never edits a comment, and after a failure, posts one more when a run passes. To comment on every run, or never, set `c.comment` in `.duel.rb`: see [Choose when to comment on a pull request](https://github.com/sferik/duel-ruby/blob/main/docs/configuration.md#choose-when-to-comment-on-a-pull-request) and [Report on a pull request](https://github.com/sferik/duel-ruby#report-on-a-pull-request). For an app in a subdirectory of its repository, the workflow is at the repository's root. See [An app in a monorepo](#an-app-in-a-monorepo). |
|
|
72
72
|
|
|
73
73
|
The generator also adds `gem "stackprof", platforms: :mri` to your
|
|
74
74
|
`Gemfile`'s `development` group, unless it has StackProf already, so
|
|
@@ -326,8 +326,9 @@ Each Ruby must be installed where Duel finds it: with rbenv, chruby,
|
|
|
326
326
|
mise, or asdf, or as the Ruby that runs Duel. For more information, see
|
|
327
327
|
Duel's [Run each version on its own Ruby](https://github.com/sferik/duel-ruby/blob/main/docs/configuration.md#run-each-version-on-its-own-ruby).
|
|
328
328
|
On GitHub Actions, the generated workflow installs your app's Ruby, and,
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
when the baseline's `.ruby-version` names another Ruby, that one too, so
|
|
330
|
+
a pull request, a push, or a nightly run against your last release
|
|
331
|
+
works there when your app upgrades Ruby.
|
|
331
332
|
|
|
332
333
|
To run both versions on the Ruby that runs Duel, remove
|
|
333
334
|
`c.ruby = :version_file`.
|
|
@@ -428,7 +429,9 @@ end
|
|
|
428
429
|
```
|
|
429
430
|
|
|
430
431
|
duel-rails also tells Duel to compare a change to `app/`, `config/`,
|
|
431
|
-
`db/`, `lib/`, `Gemfile`, `Gemfile.lock`, or `.ruby-version
|
|
432
|
+
`db/`, `lib/`, `Gemfile`, `Gemfile.lock`, or `.ruby-version`, as well
|
|
433
|
+
as one to your benchmarks (`duel/`) or `.duel.rb`, which Duel always
|
|
434
|
+
watches.
|
|
432
435
|
|
|
433
436
|
## An app in a monorepo
|
|
434
437
|
|
data/lib/duel/rails/version.rb
CHANGED
|
@@ -20,4 +20,9 @@ Duel.configure do |c|
|
|
|
20
20
|
# fixtures: :all, or seeds: true.
|
|
21
21
|
c.use :rails, database: <%= database ? ":#{database}" : "nil" %>
|
|
22
22
|
<% end -%>
|
|
23
|
+
|
|
24
|
+
# When the workflow comments on a pull request with the report: :always,
|
|
25
|
+
# :results (passed or failed), :failures (failed, or couldn't compare),
|
|
26
|
+
# or :never.
|
|
27
|
+
# c.comment = :failures
|
|
23
28
|
end
|
|
@@ -8,14 +8,20 @@ on:
|
|
|
8
8
|
- cron: "0 4 * * *"
|
|
9
9
|
workflow_dispatch:
|
|
10
10
|
|
|
11
|
+
# The Duel job runs the branch's code (its benchmarks too), so it only
|
|
12
|
+
# reads. Commenting is the Comment job's, which runs none of it.
|
|
11
13
|
permissions:
|
|
12
14
|
contents: read
|
|
13
|
-
pull-requests: write # to comment on a pull request with the report
|
|
14
15
|
|
|
15
16
|
jobs:
|
|
16
17
|
duel:
|
|
17
18
|
name: Duel
|
|
18
19
|
runs-on: ubuntu-latest
|
|
20
|
+
# Whether the Comment job comments (post, change or none), and the
|
|
21
|
+
# run's status (passed, failed or neutral), from Duel.
|
|
22
|
+
outputs:
|
|
23
|
+
comment: ${{ steps.compare.outputs.comment }}
|
|
24
|
+
status: ${{ steps.compare.outputs.status }}
|
|
19
25
|
<% if working_directory -%>
|
|
20
26
|
# The app is in a subdirectory of the repository: Duel runs there, and
|
|
21
27
|
# checks out the whole repository for the baseline.
|
|
@@ -27,6 +33,7 @@ jobs:
|
|
|
27
33
|
- uses: actions/checkout@v7
|
|
28
34
|
with:
|
|
29
35
|
fetch-depth: 0 # the baseline comes from history, tags included
|
|
36
|
+
persist-credentials: false
|
|
30
37
|
<% if database == "postgresql" -%>
|
|
31
38
|
# Each worker starts a PostgreSQL server of its own (duel-postgresql).
|
|
32
39
|
- run: sudo apt-get install -y postgresql libpq-dev
|
|
@@ -42,24 +49,52 @@ jobs:
|
|
|
42
49
|
<% end -%>
|
|
43
50
|
<% if version_file? -%>
|
|
44
51
|
# Each version runs on the Ruby its own .ruby-version names (see
|
|
45
|
-
# .duel.rb)
|
|
46
|
-
# too, linked where Duel finds it (as
|
|
47
|
-
# before the app's own Ruby, which runs Duel.
|
|
52
|
+
# .duel.rb). When the baseline's names another Ruby than this code's,
|
|
53
|
+
# that one is installed too, and linked where Duel finds it (as
|
|
54
|
+
# chruby would), before the app's own Ruby, which runs Duel. The
|
|
55
|
+
# baseline is the Compare step's: where HEAD left the main branch
|
|
56
|
+
# (for a pull request, and run by hand), the commit before a push, or
|
|
57
|
+
# on a schedule the last release, the newest tag before HEAD that
|
|
58
|
+
# RELEASE_TAGS matches. (If .duel.rb sets c.main_branch or
|
|
59
|
+
# c.release_tags, set MAIN_BRANCH or RELEASE_TAGS to match.)
|
|
48
60
|
- name: Find the baseline's Ruby
|
|
49
61
|
id: baseline
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
env:
|
|
63
|
+
BEFORE: ${{ github.event.before }}
|
|
64
|
+
MAIN_BRANCH: origin/${{ github.event.repository.default_branch }}
|
|
65
|
+
RELEASE_TAGS: "v*"
|
|
66
|
+
run: |
|
|
67
|
+
case "$GITHUB_EVENT_NAME" in
|
|
68
|
+
push) base=$BEFORE ;;
|
|
69
|
+
schedule) base=$(git describe --tags --abbrev=0 --match "$RELEASE_TAGS" HEAD^ 2>/dev/null) || base= ;;
|
|
70
|
+
*) base=$(git merge-base HEAD "$MAIN_BRANCH" 2>/dev/null) || base= ;;
|
|
71
|
+
esac
|
|
72
|
+
ruby_of() { git show "$1:./.ruby-version" 2>/dev/null | awk 'NF { print $1; exit }'; }
|
|
73
|
+
ruby=
|
|
74
|
+
# (None without a baseline: a branch's first push, or no release yet.)
|
|
75
|
+
if [ -n "$base" ] && git cat-file -e "$base^{commit}" 2>/dev/null; then
|
|
76
|
+
ruby=$(ruby_of "$base")
|
|
77
|
+
[ "$ruby" = "$(ruby_of HEAD)" ] && ruby=
|
|
78
|
+
fi
|
|
79
|
+
echo "ruby=$ruby" >> "$GITHUB_OUTPUT"
|
|
52
80
|
- uses: ruby/setup-ruby@v1
|
|
53
81
|
id: baseline-ruby
|
|
54
82
|
if: steps.baseline.outputs.ruby != ''
|
|
55
83
|
with:
|
|
84
|
+
<% if working_directory -%>
|
|
85
|
+
working-directory: <%= working_directory %>
|
|
86
|
+
<% end -%>
|
|
56
87
|
ruby-version: ${{ steps.baseline.outputs.ruby }}
|
|
88
|
+
# The bundle for this Ruby too, which the baseline's workers load.
|
|
89
|
+
# (Gems installed for one Ruby version don't load on another.)
|
|
90
|
+
bundler-cache: true
|
|
57
91
|
- name: Link the baseline's Ruby where Duel finds it
|
|
58
92
|
if: steps.baseline.outputs.ruby != ''
|
|
93
|
+
env:
|
|
94
|
+
PREFIX: ${{ steps.baseline-ruby.outputs.ruby-prefix }}
|
|
59
95
|
run: |
|
|
60
|
-
prefix="${{ steps.baseline-ruby.outputs.ruby-prefix }}"
|
|
61
96
|
mkdir -p ~/.rubies
|
|
62
|
-
ln -sfn "$
|
|
97
|
+
ln -sfn "$PREFIX" ~/.rubies/ruby-"$("$PREFIX/bin/ruby" -e 'print RUBY_VERSION')"
|
|
63
98
|
<% end -%>
|
|
64
99
|
- uses: ruby/setup-ruby@v1
|
|
65
100
|
with:
|
|
@@ -72,9 +107,11 @@ jobs:
|
|
|
72
107
|
bundler-cache: true
|
|
73
108
|
# Against the main branch for a pull request, what was there before
|
|
74
109
|
# for a push, and the last release on a schedule (slow creep). Duel
|
|
75
|
-
# adds its report to the job's summary,
|
|
76
|
-
# the comment
|
|
110
|
+
# adds its report to the job's summary, writes it to duel.md for the
|
|
111
|
+
# comment, and sets the step's outputs to say whether to comment, as
|
|
112
|
+
# c.comment in .duel.rb says, whatever the outcome.
|
|
77
113
|
- name: Compare
|
|
114
|
+
id: compare
|
|
78
115
|
env:
|
|
79
116
|
DUEL_BASE: ${{ github.event_name == 'schedule' && 'last-release' || github.event_name == 'push' && github.event.before || '' }}
|
|
80
117
|
# (Run by hand, it compares anyway: with nothing changed, the code
|
|
@@ -88,22 +125,52 @@ jobs:
|
|
|
88
125
|
exit 0
|
|
89
126
|
fi
|
|
90
127
|
bundle exec duel --markdown "$RUNNER_TEMP/duel.md"
|
|
91
|
-
# The report,
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
# The report, for the Comment job.
|
|
129
|
+
- name: Keep the report
|
|
130
|
+
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
|
131
|
+
uses: actions/upload-artifact@v7
|
|
132
|
+
with:
|
|
133
|
+
name: duel-report
|
|
134
|
+
path: ${{ runner.temp }}/duel.md
|
|
135
|
+
if-no-files-found: ignore # none when Duel didn't get to compare
|
|
136
|
+
retention-days: 1
|
|
137
|
+
|
|
138
|
+
# The report, as a new comment on the pull request, when Duel says to:
|
|
139
|
+
# always (post), or only when its last comment's status differs from
|
|
140
|
+
# this run's (change), so the latest comment is never stale. It never
|
|
141
|
+
# edits or deletes a comment. This job checks nothing out and runs none
|
|
142
|
+
# of the branch's code, so it alone can write. (A pull request from a
|
|
143
|
+
# fork gets no comment, as its token can't write one: the Duel job's
|
|
144
|
+
# summary has the report.)
|
|
145
|
+
comment:
|
|
146
|
+
name: Comment
|
|
147
|
+
needs: duel
|
|
148
|
+
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.duel.outputs.comment == 'post' || needs.duel.outputs.comment == 'change') }}
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
permissions:
|
|
151
|
+
pull-requests: write # to comment on the pull request with the report
|
|
152
|
+
steps:
|
|
153
|
+
- name: Get the report
|
|
154
|
+
id: report
|
|
155
|
+
continue-on-error: true # there's none when the Duel job failed before comparing
|
|
156
|
+
uses: actions/download-artifact@v8
|
|
157
|
+
with:
|
|
158
|
+
name: duel-report
|
|
159
|
+
path: ${{ runner.temp }}/report
|
|
94
160
|
- name: Comment on the pull request
|
|
95
|
-
if:
|
|
161
|
+
if: steps.report.outcome == 'success'
|
|
96
162
|
env:
|
|
97
163
|
GH_TOKEN: ${{ github.token }}
|
|
98
164
|
PR: ${{ github.event.pull_request.number }}
|
|
165
|
+
COMMENT: ${{ needs.duel.outputs.comment }}
|
|
166
|
+
STATUS: ${{ needs.duel.outputs.status }}
|
|
99
167
|
run: |
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
--jq ".[] | select(.user.login == \"github-actions[bot]\"
|
|
105
|
-
if [ -n "$
|
|
106
|
-
|
|
107
|
-
else
|
|
168
|
+
marker="<!-- duel<%= " #{working_directory}" if working_directory %>"
|
|
169
|
+
# The first line of the last comment Duel posted here: its marker,
|
|
170
|
+
# with its status (none in Duel 0.1.0's, which counts as another).
|
|
171
|
+
last=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
|
|
172
|
+
--jq ".[] | select(.user.login == \"github-actions[bot]\") | .body | split(\"\n\")[0] | rtrimstr(\"\r\") | select(startswith(\"$marker status=\") or . == \"$marker -->\")" | tail -n 1)
|
|
173
|
+
if [ "$COMMENT" = post ] || { [ -n "$last" ] && [ "$last" != "$marker status=$STATUS -->" ]; }; then
|
|
174
|
+
{ echo "$marker status=$STATUS -->"; cat "$RUNNER_TEMP/report/duel.md"; } > "$RUNNER_TEMP/comment.md"
|
|
108
175
|
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@"$RUNNER_TEMP/comment.md" > /dev/null
|
|
109
176
|
fi
|