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
25 changes: 25 additions & 0 deletions .duel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

# Duel's configuration: https://github.com/sferik/duel-ruby/blob/main/docs/configuration.md
#
# Duel follows conventions, so set only what differs: benchmarks are in
# duel/**/*_duel.rb, each version's lib/ is first on the load path, a
# change is compared when it touches lib/ or ext/ (or the benchmarks, this
# file, or the version files), and releases are tagged v*.
#
# kettle-jem creates this file when it is missing, and leaves it alone
# after that, so it is this project's to change.
Duel.configure do |c|
# Each version runs on the Ruby that its own version file names, so a
# branch that upgrades Ruby is compared on each version's own Ruby.
c.ruby = :version_file
# Compare a change to the Perf workflow too, which decides how the code
# is measured. (Duel already watches lib/, ext/, the benchmarks, this
# file, and the version files.)
c.watch += %w[.github/workflows/perf.yml]

# When the workflow comments on a pull request with the report: :always,
# :results (passed or failed), :failures (failed, or couldn't compare),
# or :never.
# c.comment = :failures
end
186 changes: 186 additions & 0 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: Perf

# Performance regression guards: Duel runs the benchmarks in duel/ on the
# baseline and on this code side by side, and fails if this code is slower.
# See .duel.rb and https://github.com/sferik/duel-ruby

# The Duel job runs this branch's code, so it only reads. Commenting is a
# job of its own, which runs none of it.
permissions:
contents: read

on:
pull_request:
push:
branches:
- 'main'
schedule:
- cron: "0 4 * * *"
# Allow manually triggering the workflow.
workflow_dispatch:

# Cancels a pull request's previous runs that have not yet completed. Every
# other run is a group of its own, so none is cancelled: each push to main
# is compared with the commit before it, and a cancelled run would skip
# that comparison.
concurrency:
group: "${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}"
cancel-in-progress: true

jobs:
duel:
if: "github.event_name != 'push' || (!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]'))"
name: Duel
runs-on: ubuntu-latest
# Whether the Comment job comments (post, change or none), and the
# run's status (passed, failed or neutral), from Duel.
outputs:
comment: ${{ steps.compare.outputs.comment }}
status: ${{ steps.compare.outputs.status }}
env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps
# The perf appraisal: the gemspec's dependencies and Duel (Ruby 3.4 or later)
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/perf.gemfile
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # the baseline comes from history, tags included
persist-credentials: false

# .duel.rb sets c.ruby = :version_file, so each version runs on the Ruby
# that its own version file names (.ruby-version, .tool-versions, or
# mise.toml, the first that names one, as Duel reads them). When the
# baseline's names another Ruby than this one's, install that Ruby too,
# and link it where Duel finds it. The baseline is the one the Compare
# step uses: the main branch for a pull request, the commit before a
# push, the last release on a schedule (the newest v* tag before HEAD,
# as Duel finds it), and where HEAD left the default branch when run by
# hand.
- name: Find the baseline's Ruby
id: baseline
env:
BEFORE: ${{ github.event.before }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
case "$GITHUB_EVENT_NAME" in
pull_request) base="origin/$GITHUB_BASE_REF" ;;
push) base="$BEFORE" ;;
schedule) base=$(git describe --tags --abbrev=0 --match 'v*' HEAD^ 2>/dev/null) || base= ;;
*) base=$(git merge-base HEAD "origin/$DEFAULT_BRANCH" 2>/dev/null) || base= ;;
esac
ruby_of() {
for file in .ruby-version .tool-versions mise.toml .mise.toml; do
text=$(git show "$1:$file" 2>/dev/null) || continue
case "$file" in
.ruby-version) version=$(printf '%s\n' "$text" | awk 'NF { print $1; exit }') ;;
.tool-versions) version=$(printf '%s\n' "$text" | awk '$1 == "ruby" { print $2; exit }') ;;
*) version=$(printf '%s\n' "$text" | awk '/^\[/ { tools = ($0 ~ /^\[tools\]/) } tools && match($0, /^[ \t]*"?ruby"?[ \t]*=[ \t]*[[{]?[ \t]*(version[ \t]*=[ \t]*)?"[^"]+"/) { s = substr($0, RSTART, RLENGTH); sub(/"$/, "", s); sub(/.*"/, "", s); print s; exit }') ;;
esac
if [ -n "$version" ]; then
echo "$version"
return
fi
done
}
ruby=
if [ -n "$base" ] && git cat-file -e "$base^{commit}" 2>/dev/null; then
ruby=$(ruby_of "$base")
[ "$ruby" = "$(ruby_of HEAD)" ] && ruby=
fi
echo "ruby=$ruby" >> "$GITHUB_OUTPUT"

- name: Setup the baseline's Ruby
id: baseline-ruby
if: steps.baseline.outputs.ruby != ''
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
with:
ruby-version: ${{ steps.baseline.outputs.ruby }}
# The bundle for this Ruby too, which the baseline's workers load.
# (Gems installed for one Ruby version don't load on another.)
bundler-cache: true

- name: Link the baseline's Ruby where Duel finds it
if: steps.baseline.outputs.ruby != ''
env:
PREFIX: ${{ steps.baseline-ruby.outputs.ruby-prefix }}
run: |
mkdir -p ~/.rubies
ln -sfn "$PREFIX" ~/.rubies/ruby-"$("$PREFIX/bin/ruby" -e 'print RUBY_VERSION')"

# The candidate's Ruby, from its version file (the same one Duel reads)
- name: Setup Ruby & RubyGems
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
with:
bundler-cache: true

# Against the main branch for a pull request, what was there before
# for a push, and the last release on a schedule (slow creep). Duel
# adds its report to the job's summary, writes it to duel.md for the
# comment, and sets the step's outputs to say whether to comment, as
# c.comment in .duel.rb says, whatever the outcome.
- name: Compare
id: compare
env:
DUEL_BASE: ${{ github.event_name == 'schedule' && 'last-release' || github.event_name == 'push' && github.event.before || '' }}
# (Run by hand, it compares anyway: with nothing changed, the code
# with itself, which checks the setup.)
DUEL_FORCE: ${{ github.event_name == 'workflow_dispatch' && '1' || '' }}
run: |
# (A branch's first push, or one that rewrote history, has no
# commit before it here to compare against.)
if [ "$GITHUB_EVENT_NAME" = push ] && ! git cat-file -e "$DUEL_BASE^{commit}" 2>/dev/null; then
echo "::notice::No commit before this push to compare against"
exit 0
fi
bundle exec duel --markdown "$RUNNER_TEMP/duel.md"

# The report, for the Comment job
- name: Keep the report
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: duel-report
path: ${{ runner.temp }}/duel.md
if-no-files-found: ignore # none when the comparison didn't run
retention-days: 1

# The report, as a new comment on the pull request, when Duel says to:
# always (post), or only when its last comment's status differs from
# this run's (change), so the latest comment is never stale. It never
# edits or deletes a comment. This job checks nothing out and runs none
# of the branch's code, so it alone can write. (A pull request from a
# fork gets no comment, as its token can't write one: the Duel job's
# summary has the report.)
comment:
name: Comment
needs: duel
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') }}
runs-on: ubuntu-latest
permissions:
pull-requests: write # to comment on the pull request with the report
Comment thread
sferik marked this conversation as resolved.
steps:
- name: Get the report
id: report
continue-on-error: true # the Duel job keeps no report when it fails before comparing
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: duel-report
path: ${{ runner.temp }}/report

- name: Comment on the pull request
if: steps.report.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
COMMENT: ${{ needs.duel.outputs.comment }}
STATUS: ${{ needs.duel.outputs.status }}
run: |
marker="<!-- duel"
# The first line of the last comment Duel posted here: its marker,
# with its status (none in Duel 0.1.0's, which counts as another).
last=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
--jq ".[] | select(.user.login == \"github-actions[bot]\") | .body | split(\"\n\")[0] | rtrimstr(\"\r\") | select(startswith(\"$marker status=\") or . == \"$marker -->\")" | tail -n 1)
if [ "$COMMENT" = post ] || { [ -n "$last" ] && [ "$last" != "$marker status=$STATUS -->" ]; }; then
{ echo "$marker status=$STATUS -->"; cat "$RUNNER_TEMP/report/duel.md"; } > "$RUNNER_TEMP/comment.md"
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@"$RUNNER_TEMP/comment.md" > /dev/null
fi
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ RBS:
inherit_mode:
merge:
- Exclude
# Merge Include too, so .duel.rb (a dotfile) is added to the defaults
- Include

AllCops:
# Duel's configuration is a dotfile, which RuboCop skips unless included
Include:
- .duel.rb
DisplayCopNames: true # Display the name of the failing cops
Exclude:
- 'bin/*'
Expand Down
11 changes: 11 additions & 0 deletions .structuredmerge/kettle-jem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,17 @@ workflows:
# Include VCR/WebMock recording modular gemfiles in generated Appraisals.
# Enable only for projects that actually require recording support.
recording: false
# Duel performance regression guards (https://github.com/sferik/duel-ruby).
# Off by default. Set `duel: true` to generate the Perf workflow
# (.github/workflows/perf.yml), gemfiles/modular/duel.gemfile (loaded by
# the root Gemfile and a `perf` appraisal), the `rake duel` task, the
# RuboCop include for .duel.rb, and the Duel README badge. .duel.rb is
# created only when it is missing, and the benchmarks in duel/ are the
# project's own to write. Duel requires Ruby 3.4 or later: on older
# Rubies it is not installed, and `rake duel` only warns. While Duel is
# off, kettle-jem neither writes nor removes .duel.rb, perf.yml, or
# duel.gemfile, so a project's own copies stay as they are.
duel: true
# preset: framework
# framework_matrix:
# dimension: activerecord
Expand Down
7 changes: 7 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,10 @@ appraise "templating" do
eval_gemfile "modular/templating.gemfile"
eval_gemfile "modular/x_std_libs.gemfile"
end

appraise "perf" do
# Performance regression guards, for .github/workflows/perf.yml
# (Duel requires Ruby 3.4 or later)
eval_gemfile "modular/duel.gemfile"
eval_gemfile "modular/x_std_libs.gemfile"
end
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Please file a bug if you notice a violation of semantic versioning.

### Added

- kettle-jem-template-20261025-001 - Duel performance regression guards: the
Perf workflow compares each pull request, each push to main, and, on a
schedule, the last release, and `rake duel` compares local changes, with
the benchmarks in `duel/`.

### Changed

### Deprecated
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ eval_gemfile "gemfiles/modular/changelog.gemfile"
# Optional
eval_gemfile "gemfiles/modular/optional.gemfile"

# Performance regression guards (Duel requires Ruby 3.4 or later)
eval_gemfile "gemfiles/modular/duel.gemfile"

### Std Lib Extracted Gems
eval_gemfile "gemfiles/modular/x_std_libs.gemfile"

Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ GEM
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
duel (0.1.1)
fiddle (~> 1.1)
emoji_regex (15.0.0)
erb (6.0.7)
faraday (2.15.0)
Expand All @@ -146,6 +148,7 @@ GEM
logger
faraday-net_http (3.5.0)
net-http (~> 0.5)
fiddle (1.1.8)
gem_bench (2.0.5)
bundler (>= 1.14)
version_gem (~> 1.1, >= 1.1.4)
Expand Down Expand Up @@ -506,6 +509,7 @@ GEM
hashie (>= 0.1.0, < 6)
version_gem (~> 1.1, >= 1.1.16)
sorbet-runtime (0.6.13594)
stackprof (0.2.28)
standard (1.57.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
Expand Down Expand Up @@ -633,6 +637,7 @@ DEPENDENCIES
bundler-audit (~> 0.9.3)
cgi (~> 0.5)
debug (>= 1.11.1)
duel (~> 0.1, >= 0.1.1)
erb (~> 6.0, >= 6.0.7)
gem_bench (~> 2.0, >= 2.0.5)
gitmoji-regex (~> 2.0, >= 2.0.15)
Expand Down Expand Up @@ -662,6 +667,7 @@ DEPENDENCIES
rubocop-packaging!
rubocop-ruby2_2 (~> 3.0, >= 3.0.7)
ruby-progressbar (~> 1.13)
stackprof (~> 0.2, >= 0.2.28)
standard (~> 1.57, >= 1.57.0)
stone_checksums (~> 1.0, >= 1.0.9)
stringio (>= 3.2.0)
Expand Down Expand Up @@ -720,10 +726,12 @@ CHECKSUMS
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
dry-schema (1.16.0) sha256=cd3aaeabc0f1af66ec82a29096d4c4fb92a0a58b9dae29a22b1bbceb78985727
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
duel (0.1.1) sha256=5f8f42f9a9dd1d3f2925dbb8d8f11cb9f5a8ad05d926097c17ac13a4d907d52e
emoji_regex (15.0.0) sha256=a508b2c0bee561169564e0aec96085020c900520d335e5533fd8e78e59493829
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
faraday (2.15.0) sha256=3be0eff0187ed913a1b4f679e9fcfe845b542cdd4f75b76cd6181a4ae1335fff
faraday-net_http (3.5.0) sha256=2e7094c34e38754ee048cc3df47376f8657005d9375f8473bce1c3b526a22e36
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
gem_bench (2.0.5) sha256=0dc0fb44a5a5eb7b2f5c1c68a5b0164d72007132822c012bac3abe976b199ead
geom2d (0.4.1) sha256=ea0998ea90c4f2752e24fe13d85a4f89bee689d151316140ebcc6369bf634ed9
gitmoji-regex (2.0.15) sha256=c6571bffd757f95c06d84a7d2d0b3f180eca7fe576882c371cee1f1a081cd968
Expand Down Expand Up @@ -835,6 +843,7 @@ CHECKSUMS
simplecov_lcov_formatter (0.9.0) sha256=4c1839bb31dcf7a8f7a6474c94ed228aa03b994e9dd5e1965e58b4121b049bed
snaky_hash (2.0.9) sha256=e93e6ec80632f8c8c4a3fe6e25d51eaef253e0e58dc7100f0f8370320feb3128
sorbet-runtime (0.6.13594) sha256=cf26f625a84c73d0d4d151fc19cafa1a6c44ad1814f2454e545893773eddd5e3
stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04
standard (1.57.0) sha256=4a65ea92421fbaf77fb54da91e16093cf79b335b8f62c35aaa3dd714c8592858
standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Choose the option that best fits your use case:

## Copyright Notice

- Copyright (c) 2010-2014, 2016-2017 Erik Michaels-Ober
- Copyright (c) 2010-2014, 2016-2017 Erik Berlin
- Copyright (c) 2010 Jeremy Kemper
- Copyright (c) 2010-2011 Michael Bleigh
- Copyright (c) 2010-2011 Paul Walker
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# 🔐 OAuth2

[![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license] [![Total downloads][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]
[![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license] [![Total downloads][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf] [![Duel][⚔️duel-img]][⚔️duel]

`if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know on Discord][✉️discord-invite] or [RubyForum][✉️ruby-forum], as I may have missed the notification.

Expand Down Expand Up @@ -773,7 +773,7 @@ See [LICENSE.md][📄license] for the official copyright notice.
<details markdown="1">
<summary>Copyright holders</summary>

- Copyright (c) 2010-2014, 2016-2017 Erik Michaels-Ober
- Copyright (c) 2010-2014, 2016-2017 Erik Berlin
- Copyright (c) 2010 Jeremy Kemper
- Copyright (c) 2010-2011, 2014 Michael Bleigh
- Copyright (c) 2010-2011 Paul Walker
Expand Down Expand Up @@ -1094,6 +1094,8 @@ Thanks for RTFM. ☺️
[📄license-compat]: https://www.apache.org/legal/resolved.html#category-a
[📄license-compat-img]: https://img.shields.io/badge/Apache_Compatible:_Category_A-%E2%9C%93-259D6C.svg?style=flat&logo=Apache

[⚔️duel]: https://github.com/sferik/duel-ruby
[⚔️duel-img]: https://raw.githubusercontent.com/sferik/duel-ruby/main/assets/badge.svg
[📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
[📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
[🚎yard-current]: http://rubydoc.info/gems/oauth2
Expand Down
14 changes: 14 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ if kettle_jem_selftest_unavailable_note
end
end

### PERFORMANCE TASKS
# Duel's performance regression guards: rake duel compares this tree with
# the main branch (see .duel.rb and duel/). Duel requires Ruby 3.4 or
# later, so older Rubies get a stub task.
begin
require "duel/rake_task"
Duel::RakeTask.new
rescue LoadError
desc("(stub) duel is unavailable")
task("duel") do
warn("NOTE: duel isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
end
end

### RELEASE TASKS
# Setup stone_checksums
begin
Expand Down
Loading
Loading