Skip to content

[1/6] chore: CI consolidation, RuboCop autocorrect, locale files, project docs - #533

Open
aaronlippold wants to merge 2 commits into
devise-security:mainfrom
mitre:upstream-pr/1-tooling
Open

aaronlippold wants to merge 2 commits into
devise-security:mainfrom
mitre:upstream-pr/1-tooling

Conversation

@aaronlippold

@aaronlippold aaronlippold commented Mar 1, 2026 •

Copy link
Copy Markdown

Summary

Split from #530 (1 of 6). Adds CI/project infrastructure and autocorrects all existing RuboCop offenses. Zero functional code changes.

Addresses #335, #336, #330, #346, #347.

Changes

Test results

  • 156 runs, 0 failures, 0 errors
  • 0 RuboCop offenses
  • 0 Brakeman warnings

Stacked PR series

This is PR 1 of 6. The remaining 5 PRs are staged on our fork with clean per-PR diffs:

  1. [1/6] chore: CI consolidation, RuboCop autocorrect, locale files, project docs #533 ← this PR (infrastructure)
  2. mitre#9 — Test modernization (Use FactoryBot in test suite  #445)
  3. mitre#5 — Bug fixes (:secure_validatable doesn't set @minimum_password_length #290, Rails server won't start using secure_validatable because login_attribute is nil #351, Unable to get some validators of Password column after upgrade version v0.16 to v0.17, 0.18 #441, Conflict with Uniqueness Validation for Email Scoped to project_id Model when we upgraded to 0.18.0 from 0.17.0 #448, expired_for only catches manually expired users, not those who were expired due to inactivity #449, Allowing Procs on password_archiving_count and deny_old_passwords  #453)
  4. mitre#6 — Mongoid compatibility
  5. mitre#7 — Module enhancements (Make configs in other modules dynamic #357, Throttling update_last_activity! to reduce excessive DB writes #494, Add configuration to require any changes to email to require knowing the current password #359, PasswordArchivable with time period #141, Create a generator for devise-security migrations #295, Figure out how email validation works and document it #339)
  6. mitre#8 — session_traceable (feat: Support Traceable Sessions #442, Support logging of sessions with flexible session limits #71)

Each will be submitted here after the previous one merges. See #530 for full context.

Copilot AI review requested due to automatic review settings March 1, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (1 of 6 in a stacked series) is a CI consolidation, RuboCop autocorrect, locale, and project docs PR with no functional code changes. It consolidates GitHub Actions workflows into a single ci.yml, migrates RuboCop config to the plugins: format, autocorrects 234 existing RuboCop offenses (primarily assert_predicate, %i[], assert_operator, whitespace/formatting), adds the missing incorrect i18n key to all 20 locale files, and introduces several project docs/infrastructure files.

Changes:

  • CI/Workflows: New ci.yml (consolidated lint + test + coverage), nightly.yml, release.yml, and updated codeql-analysis.yml (v2→v4); existing test_suite.yml retained alongside new ci.yml
  • RuboCop + code cleanup: Migrated .rubocop.yml to plugins: format; autocorrected test assertions, symbol arrays, delegate patterns, scope syntax, and inline rubocop disable comments
  • Locale/Docs/Infrastructure: Added incorrect i18n key to all 20 locale files; added CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, .editorconfig, lefthook.yml; updated README.md, devise-security.gemspec, Gemfile

Reviewed changes

Copilot reviewed 80 out of 82 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
.github/workflows/ci.yml New consolidated CI workflow with fail-fast lint gate; duplicate of test_suite.yml still present
.github/workflows/release.yml New automated release workflow triggered on tag push; uses potentially unreliable head_commit.timestamp
.github/workflows/nightly.yml New nightly CI workflow for ruby-head and rails-main testing
.github/workflows/codeql-analysis.yml CodeQL upgraded v2→v4 (CHANGELOG incorrectly says v3)
lefthook.yml Pre-commit hooks; RuboCop and yaml-lint globs use *.rb/*.{yml,yaml} not **/*.rb/**/*.{yml,yaml}
lib/devise-security/hooks/session_limitable.rb Bug fix: swapped inverted if/else branches for skip_session_limitable; refactored fetch hook conditions
lib/devise-security/hooks/expirable.rb Removed redundant record && nil guard (safe; respond_to? on nil returns false)
lib/devise-security/models/secure_validatable.rb Cleaned up case_sensitive logic and removed dead email_changed? condition
lib/devise-security/models/password_expirable.rb Replaced SQL string with Ruby range syntax for Mongoid compatibility
lib/devise-security/models/expirable.rb Range syntax for expired_for scope; inactive_message logic simplified
lib/devise-security/models/password_archivable.rb Delegated class methods; improved exists? query
config/locales/*.yml Added incorrect key to all 20 locale files; non-English locales use untranslated English text
CHANGELOG.md New CHANGELOG pre-populated with all 6 PRs' planned changes, not just this PR's changes
README.md Documents session_traceable module that doesn't exist in this PR
devise-security.gemspec Added factory_bot, brakeman; updated changelog_uri; added rubygems_mfa_required
gemfiles/rails_main.gemfile New gemfile for nightly Rails main testing; missing minitest-rails
.ruby-version Updated from 3.2.6 to 3.4.8 (version may need verification)
test/**/*.rb RuboCop autocorrect: assert_predicate, assert_operator, %i[], blank line normalization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +31
# Rails main branch (head) for nightly CI testing

source "https://rubygems.org"

gem "railties", github: "rails/rails", branch: "main"
gem "activemodel", github: "rails/rails", branch: "main"
gem "activerecord", github: "rails/rails", branch: "main"
gem "activesupport", github: "rails/rails", branch: "main"
gem "actionmailer", github: "rails/rails", branch: "main"
gem "actionpack", github: "rails/rails", branch: "main"
gem "bigdecimal", require: false
gem "drb", require: false

group :active_record do
gem "pg"
gem "sqlite3", "~> 2.1"
end

group :mongoid do
gem "mongoid", "~> 9.0"

group :test do
gem "database_cleaner-mongoid", "~> 2.0"
end
end

group :test do
gem "rails-controller-testing"
end

gemspec path: "../"

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rails_main.gemfile is missing minitest-rails, which is present in all other gemfiles (rails_7.0.gemfile, rails_7.1.gemfile, rails_7.2.gemfile, rails_8.0.gemfile). The nightly CI runs with rails: main will fail to load the test framework correctly unless minitest-rails is available as a transitive dependency. A gem "minitest-rails" line (without a pinned version constraint, to pick up the latest) should be added.

Copilot uses AI. Check for mistakes.
Comment thread config/locales/zh_TW.yml
taken_in_past: '曾被使用過。'
equal_to_current_password: '必須與目前密碼不同。'
equal_to_email: '必須與電子郵件地址不同。'
incorrect: 'is incorrect.'

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incorrect i18n key added to all 20 non-English locale files uses the English string 'is incorrect.' without a translation. Since other locale keys in the same files are properly translated into the native language, these should also be translated. Using untranslated English text in locale-specific files defeats the purpose of i18n, and will display English text to users of those locales.

Copilot uses AI. Check for mistakes.
Comment thread lefthook.yml
Comment on lines +9 to +12
rubocop:
glob: "*.rb"
run: bundle exec rubocop --autocorrect
stage_fixed: true

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RuboCop command's glob pattern is "*.rb", which only matches Ruby files in the root directory and not in subdirectories like lib/, test/, app/, etc. Lefthook uses the glob to filter staged files, so changes to files in subdirectories won't trigger RuboCop. The glob should be "**/*.rb" to match all Ruby files in the repository.

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines 11 to +25
@@ -19,8 +19,10 @@ It is composed of 7 additional Devise modules:
password validation). Don't use with Devise `:validatable` module!
- `:password_archivable` - save used passwords in an `old_passwords` table for
history checks (prevent reusing passwords)
- `:session_limitable` - ensures, that there is only one session usable per
account at once
- `:session_limitable` - ensures that a user can only have x active sessions
(where x is a configurable number)
- `:session_traceable` - ensures that every session is logged (timestamps, user-agents and
IP addresses) and sessions can only be used on specified IP address (minimize session hijacking)

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README update adds documentation for :session_traceable (including module listing, migration example, and configuration options like max_active_sessions and session_ip_verification) and counts "8 additional Devise modules". However, the session_traceable module implementation does not exist in this PR — it's described as coming in a later PR. This means the README describes features that are not yet present in the codebase, which could confuse users who try to use them based on the README.

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md
Comment on lines +8 to +55
## [Unreleased]

### Added

- `:session_traceable` module — track active sessions per user via `session_histories` table with configurable `max_active_sessions` (based on PR #442 by @itsmechlark)
- Per-module migration generators: `rails g devise_security:install_<module> <Model>` (#295)
- Time-based password archive denial via `deny_old_passwords_period` config (#141)
- Throttle `update_last_activity!` to reduce DB writes via `last_activity_update_interval` (#494)
- Require current password for email change via `require_password_on_email_change` (#359)
- Proc support for `password_archiving_count` and `deny_old_passwords` (#453)
- Dynamic per-record config overrides via Proc or instance methods for all modules (#357)
- Env-based skip for session_limitable hooks via `devise.skip_session_limitable` (#421, #375)
- Full Mongoid compatibility for password_archivable, session_traceable, session_limitable, and expirable
- Missing `incorrect` i18n key added to all 20 locale files
- CHANGELOG.md following Keep a Changelog format
- CI: changelog enforcement on PRs, automated release notes from changelog

### Fixed

- `verify_code` always returned truthy, masking failed paranoid verifications
- Paranoid verification controller now adds error on failed code and re-renders form
- `:secure_validatable` exposes `@minimum_password_length` to controllers (#290)
- `login_attribute` nil crash (#351)
- `updated_at` now updates when setting `unique_session_id` (#350)
- `validators_on(:password)` discoverability restored via EachValidator wrappers (#441)
- Scoped uniqueness validation conflict with `:secure_validatable` (#448)
- `expired_for` scope fix (#449)

### Changed

- CI: consolidated rubocop.yml + brakeman.yml + test_suite.yml into single `ci.yml` with fail-fast lint gate
- CI: consolidated 3 nightly jobs into single `nightly.yml` with explicit matrix
- CI: updated CodeQL actions v2 → v3, Coveralls `@master` → `@v2`
- Refactored paranoid_verification with ActiveSupport::Concern and YARD docs (#206)
- Refactored password_archivable with YARD docs (#204)
- Refactored secure_validatable with YARD docs and DRY validators (#203)
- Refactored password_expirable with YARD docs (#193)
- Migrated test suite from fixtures to FactoryBot (#445)
- Standardized hook guards on `devise_modules.include?(:module_name)`
- Email validation: clear error message when EmailValidator gem missing (#339, #77)

### Removed

- Dead compatibility code for Rails < 5.1 (active_record_patch.rb methods)
- Dead compatibility code for Rails < 7.1 (migration API branch, password_expirable branch)
- Dead Rails < 5 engine branch from rails.rb
- Unused test helpers: `valid_new_attributes`, `clear_cached_variables`

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [Unreleased] section of CHANGELOG.md contains entries for features and fixes from all 6 PRs in the series (e.g., session_traceable module, verify_code bug fix, paranoid verification improvements, login_attribute nil crash, etc.) but this PR is only PR 1 of 6. After this PR merges, the CHANGELOG will advertise changes that don't yet exist in the codebase. The CHANGELOG should only contain what this PR actually delivers; entries for future PRs should be added by those PRs.

Copilot uses AI. Check for mistakes.
Comment thread config/locales/de.yml
taken_in_past: 'wurde bereits in der Vergangenheit verwendet.'
equal_to_current_password: 'darf nicht dem aktuellen Passwort entsprechen.'
equal_to_email: 'darf nicht dem E-mail entsprechen.'
incorrect: 'is incorrect.'

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in the other locale files: the incorrect key is untranslated and uses the English string 'is incorrect.'. All non-English locale files have this same problem (zh_TW, zh_CN, uk, tr, ru, pt, pl, nl, ko, ja, it, hi, fr, fa, es, de, cs, by, bg).

Suggested change
incorrect: 'is incorrect.'
incorrect: 'ist falsch.'

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +1 to +130
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
workflow_dispatch:
schedule:
# 00:00 on the 1st of every month
- cron: '0 0 1 * *'

jobs:
changelog:
name: Changelog
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dangoslen/changelog-enforcer@v3
with:
changeLogPath: CHANGELOG.md
skipLabels: ":arrow_up: dependencies,skip-changelog"
missingUpdateErrorMessage: >
Please update CHANGELOG.md with your changes under the [Unreleased] section.

rubocop:
name: RuboCop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libmagickwand-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop --format github

brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libmagickwand-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Run Brakeman
run: bundle exec brakeman -p test/dummy -z -q --format sarif --output brakeman.sarif
- name: Upload Brakeman results
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: brakeman.sarif

test:
name: "Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / ${{ matrix.orm }}"
needs: [rubocop, brakeman]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
orm: [active_record, mongoid]
rails: ["7.0", "7.1", "7.2", "8.0"]
ruby: ["3.1", "3.2", "3.3", "3.4"]
exclude:
- rails: "8.0"
ruby: "3.1"
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- uses: actions/cache@v4
with:
path: |
**/bundle
key:
bundle-use-ruby-${{ matrix.ruby }}-rails-${{ matrix.rails }}-orm-${{
matrix.orm }}-gemfile-${{
hashFiles(format('**/gemfiles/rails_{0}.gemfile', matrix.rails))
}}-gemspec-${{ hashFiles('**/devise-security.gemspec') }}
- name: bundle install
env:
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile
run: |
sudo apt-get update
sudo apt-get install -y libmagickwand-dev
bundle config path vendor/bundle
bundle install
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
if: ${{ matrix.orm == 'mongoid' }}
- name: Run tests
env:
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile
CI: true
DEVISE_ORM: ${{ matrix.orm }}
run: |
bundle exec rake
- name: Coveralls Parallel
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name:
run-orm-${{ matrix.orm }}-rails-${{ matrix.rails }}-ruby-${{
matrix.ruby }}
parallel: true
path-to-lcov: ./coverage/lcov/devise-security.lcov

coverage:
name: Coverage
needs: test
runs-on: ubuntu-latest
steps:
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
file: ./coverage/lcov/devise-security.lcov

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states that test_suite.yml was consolidated into ci.yml, but the old test_suite.yml file was not deleted. Since both workflows trigger on push to main and pull_request targeting main, every PR and push will run the full test matrix twice, doubling CI cost. The old test_suite.yml should be removed.

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md

- CI: consolidated rubocop.yml + brakeman.yml + test_suite.yml into single `ci.yml` with fail-fast lint gate
- CI: consolidated 3 nightly jobs into single `nightly.yml` with explicit matrix
- CI: updated CodeQL actions v2 → v3, Coveralls `@master` → `@v2`

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CHANGELOG.md states "CI: updated CodeQL actions v2 → v3" but the actual change in codeql-analysis.yml updates CodeQL from v2 to v4 (github/codeql-action/init@v4, autobuild@v4, analyze@v4). The CHANGELOG entry is inaccurate and should say "v2 → v4".

Copilot uses AI. Check for mistakes.
Comment thread lefthook.yml
Comment on lines +13 to +25
yaml-lint:
glob: "*.{yml,yaml}"
run: >
ruby -ryaml -e '
ARGV.each do |f|
begin
YAML.load_file(f, aliases: true)
rescue => e
warn "#{f}: #{e.message}"
exit 1
end
end
' {staged_files}

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The yaml-lint command's glob pattern is "*.{yml,yaml}", which only matches YAML files in the root directory. Files in subdirectories like .github/workflows/*.yml or config/locales/*.yml will not be linted. The glob should be "**/*.{yml,yaml}" to cover all YAML files.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
- name: Update CHANGELOG.md
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ github.ref_name }}
release-date: ${{ github.event.head_commit.timestamp }}

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the update-changelog job, ${{ github.event.head_commit.timestamp }} may be empty when a tag is pushed pointing to an existing commit (GitHub only populates head_commit for branch pushes, not necessarily for tag pushes). If the value is empty, the changelog-updater-action may use a default or fail. Consider using github.event.repository.updated_at or a date shell command ($(date -u +%Y-%m-%d)) as a more reliable release date source.

Suggested change
- name: Update CHANGELOG.md
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ github.ref_name }}
release-date: ${{ github.event.head_commit.timestamp }}
- name: Set release date
id: metadata
run: echo "release-date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- name: Update CHANGELOG.md
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ github.ref_name }}
release-date: ${{ steps.metadata.outputs.release-date }}

Copilot uses AI. Check for mistakes.
- CI: Consolidated workflows into ci.yml with fail-fast lint gate
- CI: Nightly, release, CodeQL v4, Coveralls v2
- RuboCop: migrated to plugins format, autocorrected all existing offenses
- Locale: Added missing 'incorrect' i18n key (20 files)
- Docs: CHANGELOG, CONTRIBUTING, SECURITY
- Project: .editorconfig, Lefthook, gemspec, .ruby-version 3.4.8
- Test: Fixed Devise 5 email capitalization in test expectations

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold
aaronlippold force-pushed the upstream-pr/1-tooling branch from 6f175b8 to c4f4bae Compare March 1, 2026 18:22
Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold

Copy link
Copy Markdown
Author

I broke things into 6 PRs ... this is the first thet other 5 are on my fork for your review ... we will have to merge them in one by one. Let me know if this looks better.

@dillonwelch dillonwelch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the review bot left some feedback as well to address.

Comment thread config/locales/bg.yml
taken_in_past: 'е използвана и преди.'
equal_to_current_password: 'трябва да е различна от настоящата парола.'
equal_to_email: 'трябва да е различна от e-mail адреса.'
incorrect: 'is incorrect.'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Where is the need for this locale string? I don't see an incorrect reference in the code.
  2. When adding locale strings, please use Google Translate to put the translated result instead of using the English string for all locales.

end

assert_equal 'Invalid Email or password.', flash[:alert]
assert_match(/invalid.*email.*password/i, flash[:alert])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this change made?

Comment thread .rubocop.yml
# edit default behavior
- test/dummy/tmp/**/*
NewCops: enable
SuggestExtensions: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good to have actually

Comment thread .rubocop.yml
- compact
Enabled: false

# Mongoid models use @@class_vars for test tracking

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on that? I'm not sure what you mean.

Comment thread Brewfile
@@ -1,2 +1,4 @@
# frozen_string_literal: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a Ruby file.

Comment thread Gemfile
group :active_record do
gem 'pg'
gem 'sqlite3', '~> 2.8'
gem 'sqlite3', '~> 1.4'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change here? I don't think we should be going backwards.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely a typo

Comment thread README.md
[Devise Security Extension](https://github.com/phatworx/devise_security_extension)

It is composed of 7 additional Devise modules:
It is composed of 8 additional Devise modules:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like some changes in here reference the code you extracted into other PRs. Can you add the changes with those PRs instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants