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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bcc25c75b990e312e5fb3a458f5e3eace8b5dbbe02fcb8c50f0ca0df01d8f54
4
- data.tar.gz: a6b29676404b6e35d14321d05f428d902d230d169f084abd65afd7104081fed1
3
+ metadata.gz: 4e5ae40fccca2ac222b0b74531bf35a79df8e05555f58210c966c505f13800a6
4
+ data.tar.gz: 166a8ea74a1000fdb64f859475bab318b9f9c31aa60e0e78837f49369dc184dd
5
5
  SHA512:
6
- metadata.gz: 85ed8cb50f379354f0ba9a5769688fbcc1608bb4ee8dfff8e7586e2f736ed4db193bb99d82c7d76a37b6177175cb010364e60e0e83e6a6aec4fc11539ec30767
7
- data.tar.gz: a514a4ddd962ac4b5c5711ec829ccb14a95d9e46cd1be6fc4a289d0bae2518995e49216f57c15e0e6d321832951f611c49e5db4563a5af580475f573f1fae682
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.0...HEAD
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 each pull request with Duel's report, updating one comment on every push. See [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). |
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
- for a pull request, also the main branch's, so a pull request that
330
- upgrades Ruby works there too.
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Duel
4
4
  module Rails
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  end
@@ -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): for a pull request that upgrades Ruby, the main branch's
46
- # too, linked where Duel finds it (as chruby would). It's set up
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
- if: github.event_name == 'pull_request'
51
- run: echo "ruby=$(git show "origin/$GITHUB_BASE_REF:./.ruby-version" 2>/dev/null)" >> "$GITHUB_OUTPUT"
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 "$prefix" ~/.rubies/ruby-"$("$prefix/bin/ruby" -e 'print RUBY_VERSION')"
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, and writes it to duel.md for
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, as a comment on the pull request: one comment, updated
92
- # on every push. (A pull request from a fork gets no comment, as its
93
- # token can't write one: the job's summary has the report.)
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: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
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
- [ -f "$RUNNER_TEMP/duel.md" ] || exit 0
101
- marker="<!-- duel<%= " #{working_directory}" if working_directory %> -->"
102
- { echo "$marker"; cat "$RUNNER_TEMP/duel.md"; } > "$RUNNER_TEMP/comment.md"
103
- ids=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
104
- --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id")
105
- if [ -n "$ids" ]; then
106
- gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/${ids%%$'\n'*}" -F body=@"$RUNNER_TEMP/comment.md" > /dev/null
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duel-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin