JSON-Rohbericht maschinenlesbar
{
"data": {
"repo": {
"topics": [],
"is_fork": false,
"size_kb": 1914,
"has_wiki": true,
"homepage": null,
"languages": {
"Ruby": 248958
},
"pushed_at": "2026-07-13T15:28:56Z",
"created_at": "2021-08-18T14:13:01Z",
"owner_type": "Organization",
"updated_at": "2026-07-14T03:45:47Z",
"description": "a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process millions of background jobs per day",
"is_archived": false,
"is_disabled": false,
"license_spdx": "MIT",
"default_branch": "main",
"license_spdx_raw": "MIT",
"primary_language": "Ruby",
"significant_languages": [
"Ruby"
]
},
"owner": {
"blog": "https://betterment.com",
"name": "Betterment",
"type": "Organization",
"login": "Betterment",
"company": null,
"location": "New York, NY",
"followers": 105,
"avatar_url": "https://avatars.githubusercontent.com/u/871554?v=4",
"created_at": "2011-06-23T18:42:51Z",
"is_verified": null,
"public_repos": 91,
"account_age_days": 5507
},
"license": {
"state": "standard",
"spdx_id": "MIT",
"raw_spdx": "MIT",
"file_present": true,
"scorecard_found": true,
"profile_has_license": true
},
"activity": {
"releases": [
{
"tag": "v3.1.0",
"kind": "minor",
"published_at": "2026-07-13T15:29:31Z"
},
{
"tag": "v3.0.1",
"kind": "patch",
"published_at": "2026-06-15T15:23:19Z"
},
{
"tag": "v3.0.0",
"kind": "major",
"published_at": "2026-06-08T17:13:25Z"
},
{
"tag": "v2.2.0",
"kind": "minor",
"published_at": "2026-02-12T21:09:53Z"
},
{
"tag": "v2.1.0",
"kind": "minor",
"published_at": "2026-02-05T16:54:20Z"
},
{
"tag": "v2.0.3",
"kind": "patch",
"published_at": "2026-02-02T15:52:05Z"
},
{
"tag": "v2.0.2",
"kind": "patch",
"published_at": "2026-01-05T22:09:33Z"
},
{
"tag": "v2.0.1",
"kind": "patch",
"published_at": "2026-01-05T19:58:15Z"
},
{
"tag": "v2.0.0",
"kind": "major",
"published_at": "2025-12-19T22:16:50Z"
},
{
"tag": "v1.2.1",
"kind": "patch",
"published_at": "2025-10-06T19:23:42Z"
},
{
"tag": "v1.2.0",
"kind": "minor",
"published_at": "2025-10-03T14:16:17Z"
},
{
"tag": "v1.1.0",
"kind": "minor",
"published_at": "2025-09-12T21:13:58Z"
},
{
"tag": "v1.0.0",
"kind": "major",
"published_at": "2025-05-08T20:17:13Z"
},
{
"tag": "v0.8.0",
"kind": "minor",
"published_at": "2025-04-03T17:26:12Z"
},
{
"tag": "v0.7.2",
"kind": "patch",
"published_at": "2025-04-02T22:22:27Z"
},
{
"tag": "v0.7.1",
"kind": "patch",
"published_at": "2025-01-24T22:11:09Z"
},
{
"tag": "v0.7.0",
"kind": "minor",
"published_at": "2025-01-24T22:11:00Z"
},
{
"tag": "v0.6.0",
"kind": "minor",
"published_at": "2024-12-18T18:14:35Z"
},
{
"tag": "v0.5.5",
"kind": "patch",
"published_at": "2024-08-13T21:59:24Z"
},
{
"tag": "v0.5.4",
"kind": "patch",
"published_at": "2024-05-01T01:03:51Z"
},
{
"tag": "v0.5.0",
"kind": "minor",
"published_at": "2023-01-20T22:07:16Z"
},
{
"tag": "v0.4.0",
"kind": "minor",
"published_at": "2021-11-30T23:57:57Z"
},
{
"tag": "v0.3.0",
"kind": "minor",
"published_at": "2021-10-27T16:09:21Z"
},
{
"tag": "v0.2.0",
"kind": "minor",
"published_at": "2021-09-02T16:48:15Z"
},
{
"tag": "v0.1.1",
"kind": "patch",
"published_at": "2021-08-19T20:35:00Z"
},
{
"tag": "v0.1.0",
"kind": "minor",
"published_at": "2021-08-19T20:34:26Z"
}
],
"recent_commits": [
{
"oid": "c726753839bef4bec1b4d5dbe9945cb43d487edc",
"body": "## What\n\nReplaces `method_missing` dispatch for named priority lookups with real\nmethod definitions:\n\n- **Class methods** — `Delayed::Priority.eventual`, `.interactive`, etc.\n- **Instance predicates** — `priority.eventual?`,\n`priority.interactive?`, etc.\n\nMethods are defined when the class loads (fo\n[…]\nn safety.\n- Full suite run locally on sqlite across multiple seeds; only failures\nare pre-existing on `main` in my local environment (Rails 8.0.2\nsnapshot/error-class drift, unrelated to this change).",
"is_bot": false,
"headline": "Define named priority methods as real methods (#115)",
"author_name": "Corey Farwell",
"author_login": "frewsxcv",
"committed_at": "2026-07-13T15:28:26Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "a5375ed749a0fb318cfd3abf0d6b1b65231f5698",
"body": "This pr https://github.com/Betterment/delayed/pull/96 made a change to\nhow max_age is calculated, where it started including the run_at's of\njobs that were currently locked. This made our max_age monitor go off\neven when we had a single long-running job in the queue, instead of\nshowing us when the queue was backed up.\n\nThis restores the previous behavior where max_age only includes jobs\nthat are pending, and not actively being worked off.",
"is_bot": false,
"headline": "fix: max_age metric excludes locked jobs (#114)",
"author_name": "Harris Effron",
"author_login": "effron",
"committed_at": "2026-06-15T14:48:06Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "dbaf6a22f574f038ffa78b7220933af634890f3d",
"body": "…ds without going through the adapter (#113)\n\nIntroduces new assertion that enforces that an ActiveJob cannot be\npassed into DJ enqueue methods, indicating to consumers that\n`perform_later` should be used instead in order to pipe through the\nadapter.",
"is_bot": false,
"headline": "fix: Assert ActiveJobs can not be passed directly to DJ enqueue metho…",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-06-02T19:48:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "564af1120a4b2b9a155377ebdbce9c468ffed1c1",
"body": "- Fixes README to properly reflect the `enqueue` lifecycle event\n- Refactors unnecessary method nesting in the AJ adapter.",
"is_bot": false,
"headline": "fix: README to reflect accurate enqueue lifecycle shape (#112)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-06-02T15:41:06Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "42e5eb9a54caea9ad1f4eb2ca9dd3f4071e90576",
"body": "Adjusts the ActiveJob adapter to pipe all enqueues through enqueue_all.\nResult of convo here:\nhttps://github.com/Betterment/delayed/pull/103#discussion_r3335987931\n\nNote for release: This is a mildly breaking change. Previous to delayed\n3.0.0, a `provider_job_id` was _always_ attached when enqueuing jobs. In\ndelayed 3.0.0, `provider_job_id` will only be attached depending on your\ndatabase configuration(specifically\n`ActiveRecord::Base.connection.supports_insert_returning?` must be true)",
"is_bot": false,
"headline": "fix: switch aj adapter to pipe through enqueue_all (#111)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-06-01T20:39:09Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "5023edb74eb12e50bb4bbef1280b850e06651f5a",
"body": "Implements `enqueue_all` in order to support Rails 7.1 perform_all_later\n\nKey decisions:\n- Maintain safety against `enqueue_after_transaction_commit`\n- Adjusts shape of `enqueue` lifecycle hook\n- Keep enqueueing logic isolated to `Delayed::Job` so that `Delayed::ActiveJobAdapter` can stay pure to job setup and post-enqueue object manipulation.\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "feat: Implement enqueue_all (#103)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-06-01T17:56:40Z",
"body_truncated": false,
"is_coding_agent": true
},
{
"oid": "1f22eb506244bde11f4f2a7e8d69964133a1b675",
"body": "Replacement for https://github.com/Betterment/delayed/pull/107 since I\nforgot to fork the repo before developing.",
"is_bot": false,
"headline": "refactor: Drop support for Delayed::Worker.delay_jobs being proc (#108)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-05-21T17:10:52Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0d920e736e5f0281f8736908cbedfb5fe93ff065",
"body": "Remove the job.hook(:enqueue) invocation from enqueue_job so payloads\ncan no longer mutate or observe the job at enqueue time. The hook method\nnow short-circuits on :enqueue with a deprecation warning for any direct\ncallers, and enqueue_job emits its own deprecation warning (naming the\npayload class\n[…]\ning the regression guard that ActiveJob JobWrapper payloads do not\ntrigger the deprecation warning.\n\ncc @effron @smudge\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>",
"is_bot": false,
"headline": "refactor: deprecate :enqueue job hook (#106)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-05-21T15:39:48Z",
"body_truncated": true,
"is_coding_agent": true
},
{
"oid": "4347124361f140ae2200b85ee55c14381141c78d",
"body": "The before_save :set_default_run_at, :set_name callback is magical and doesn't actually prevent anyone from adding a new before_save that skips callbacks. Move the defaulting into Delayed::Backend::JobPreparer so both the single save path and any future bulk-insert path pick up defaults from the sam\n[…]\nun_at and name is now a DB-level NOT NULL violation -- callers should route through Delayed::Job.enqueue (which runs JobPreparer).\n\nBreaking change for anyone reaching past the documented enqueue API.",
"is_bot": false,
"headline": "refactor: Push run_at / name defaulting into JobPreparer (#104)",
"author_name": "Caleb Donoho",
"author_login": "CelticMajora",
"committed_at": "2026-05-04T20:02:49Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "00d575c23b918cd79a174be7617c0847a40ab9cb",
"body": "@medlefsen noticed that [ActiveJob states this in their\ndocs](https://apidock.com/rails/ActiveJob/Enqueuing/ClassMethods/perform_later)\nw.r.t. `perform_later`:\n\n> Returns an instance of the job class queued with arguments available\nin Job#arguments or false if the enqueue did not succeed.\n\nSince sil\n[…]\nlicit the current behavior, which\npresumably is specific to the AJ backend.\n\nThe `delayed` backend will not return `false` from `perform_later`, and\nwill instead bubble out an exception.\n\n/no-platform",
"is_bot": false,
"headline": "style: Add (passing) tests for regression coverage (#102)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-20T20:46:55Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "02864fbf8a1b8ecd2a46975add3330138ce72ce3",
"body": "We previously [deprecated\nCHANGELOG.md](https://github.com/Betterment/delayed/commit/a8b6c9cd3aedc6f06e6e6421baa827aad7058c14#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed)\nin favor of GitHub release notes.\n\nThis updates the gemspec to properly reflect that change on sites that\nrender a link to the changelog (e.g. rubygems.org).",
"is_bot": false,
"headline": "fix: Update changelog_uri to GH releases (#101)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-13T16:53:49Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1c6c122d00c9e69b9058dcbca338494e0c9b04ae",
"body": "This releases the latest series of monitor changes as v2.2.0\n\n/no-platform",
"is_bot": false,
"headline": "release: v2.2.0 (#98)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-12T21:01:15Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "af898a0f604f6d6ee8ee31fdb5ed7541907e099f",
"body": "Given that `live_count_grouped` is already necessary for computing all\nthe other live queue metrics, we can reduce the work on MySQL by running\n`failed_count_grouped` (and filtering the scan down to fewer rows)\ninstead of an entire count against the table.",
"is_bot": false,
"headline": "refactor: De-complexify 'count' metric (#97)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-12T20:49:20Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "d84ad27cbb0357c59e20f3bc169fc7d3f28b948e",
"body": "This folds `max_lock_age` and `max_age` (and by extension,\n`age_alert_percent`) into the combined monitor queries introduced in the\nprior commit.\n\nAs before, I split these into two aggregate queries, hinging on whether\n`locked_at` (and therefore, a heap lookup) is required to return a\nresult.\n\n/no-platform",
"is_bot": false,
"headline": "perf(monitor): Fold max_* into combo queries (#96)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-12T20:46:22Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "1e3f706c67d38f903e6dbb6737c1fc6766df6236",
"body": "This combines queries for `count` (live), `future_count`, and\n`erroring_count` into a combined query for \"live\" jobs.\n\nIt also combines `locked_count`/`working_count` and `workable_count`\ninto a combined query for \"pending\" jobs.\n\nThe reason this is still two queries and not one is because on\nPostgr\n[…]\ncan and can\ntheoretically benefit from not having to hit the heap. (Though\nunfortunately it sometimes does require heap visits, depending on how\nrecently a given page has been VACUUMed.)\n\n/no-platform",
"is_bot": false,
"headline": "perf(monitor): 4 fewer queries w/ equivalent scans (#95)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-12T20:32:06Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "2b6c0f63de9cf57e546b56d08ff51ca77921d349",
"body": "After the change in #89 to have `delayed` surface DB-accurate max_age\nmetrics, it appears that PostgreSQL will occasionally report a `run_at`\nthat is milliseconds _after_ the current NOW() in UTC time.\n\nThis is more than just clock drift between the DB and the server that\ninserted the row -- in READ\n[…]\n\n- Switches PostgreSQL to `STATEMENT_TIMESTAMP()` to get even closer to\n the time at which the `SELECT` actually began.\n- Clamps at 0 anyways because clock drift is still a possibility.\n\n/no-platform",
"is_bot": false,
"headline": "fix: clamp max_age metrics at 0 (#94)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-11T15:44:40Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d3bfb4cf6b99e7a886e2d2ac25c1eb095e229559",
"body": "This is pretty self-explanatory -- the performance impact is probably\nde minimis, but the readability is certainly improved, IMO!\n\n/no-platform",
"is_bot": false,
"headline": "refactor: Simpler priority case statement (#93)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-11T14:50:42Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "c74f8e037fdcad8df1f68fb885c2006699d75c88",
"body": "This only affects the rspec snapshot output, but I was finding that the\ndiffs were getting pretty noisy and wanted to split these out more\ncleanly before making structural changes to the query.\n\n/no-platform",
"is_bot": false,
"headline": "style: break aliased columns into multiple lines (#92)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-10T20:30:57Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "79425210b3504bc9340469c938f024273d019e65",
"body": "…past (#99)\n\nThis catches potential code bugs that accidentally enqueue jobs with\n`run_at` set in the past. Often time we set `run_at` explicitly to\nforward date jobs, but bugs can lead us to accidentally enqueuing jobs\nin the past, which can lead to unnecessary max_age alarms and other\nconfusing metrics about queue max_age.\n\nBy default `deny_stale_enqueues` is false as to not introduce a breaking\nchange.\n\n/no-platform",
"is_bot": false,
"headline": "feat: Add config to raise exceptions when run_at sufficiently in the …",
"author_name": "Harris Effron",
"author_login": "effron",
"committed_at": "2026-02-10T20:03:05Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0966d03417610c035f47101cc48e751ceff1b8f4",
"body": "This is a test-only change that improves the way that query snapshotting\nis done for the monitor process.\n\nPreviously, each metric was given its own example context, testing its\nqueries in isolation. However, in practice, the monitor does not run\nthis way, and it intentionally memoizes query results\n[…]\nnt test coverage!\n\nWith this PR, monitor queries are dumped out into a single snapshot,\ndelineated by a few text dividers for readability by humans (& better\nhinting for the git differ).\n\n/no-platform",
"is_bot": false,
"headline": "test: Rework monitor coverage to avoid dups (#91)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-10T18:41:51Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "685c4fb37af235e43ce3b1f40f9cf0a3ec0b72fa",
"body": "By inserting more rows up front, MySQL will behave more\ndeterministically.\n\n(In practice, I was finding that 10 was slightly too few, and doing it\non every test query is unnecessary if a single example covers multiple\nqueries.)",
"is_bot": false,
"headline": "fix: avoid test flakes on mysql (#90)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-10T18:06:46Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "107962162c0b831da5091610894d88bd16a1a0f2",
"body": "By default, Rails wants to disable eager loading inside of `rake`\ncommands, even if `eager_load` is set to true. This is done to speed up\nthe boot time of rake tasks that don't need the entire application\nloaded.\n\nThe problem is that long-lived processes like `delayed` **do** want to\neager load the \n[…]\nn,\nwhich tells us whether the app has already eager loaded in a `rake`\ncontext.\n- If `rake_eager_loading` is not defined or `false`, we will then check\n`cache_classes` & explicitly eager load if true.",
"is_bot": false,
"headline": "fix: Eager load when 'rake_eager_load = false' (#88)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-05T16:50:29Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "c06c1978ef07a30534df4de1b911efc69f443000",
"body": "This ensures that `max_lock_age` and `max_age` are not artificially\ninflated by slower query times. Previously, if the query took N seconds\nto perform, N seconds would be artificially added to `max_age` and\n`max_lock_age`. (Typically this is not a problem for sub-second queries\non small-medium table\n[…]\nction (as should the state of the rows it returns), and in other\nDB engines it will correspond to a clock time during execution, so there\nis still room for variance but it is much smaller than before.",
"is_bot": false,
"headline": "fix(monitor): DB-accurate max_age metrics (#89)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-05T16:49:09Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "ac916129facf873ce77f7d38e9cd28f10495ef47",
"body": "Say you have a jobs table with 1M rows, 4 named priority ranges, and 2\nqueues.\n\nRather than:\n- `GROUP BY (CASE ... END) AS priority, queue, COUNT(*)` (1M rows)\n\nThis does:\n- [outer] `GROUP BY (CASE ... END) AS priority, queue, SUM(count)` (~8\nrows)\n - [inner] `GROUP BY priority, queue, COUNT(*)` (1\n[…]\nthis shave ~2-3 seconds off of each\n`delayed:monitor` query in a production table with 10M+ rows. For tables\nwith far fewer rows, the difference is negligible. (And, as always, your\nmileage may vary!)",
"is_bot": false,
"headline": "perf(monitor): reduce per-tuple work in hot path (#87)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-02-02T15:50:42Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "838c5a590ab48908d713d3b38f99c432576a5c52",
"body": "…ation wait timeout) (#86)\n\nPreviously, `with_retry_loop` had wrapped the \"drop\" & \"add\" operations\nseparately. This is fine in the case of lock timeouts, as it will retry\nuntil it acquires a lock as expected.\n\nHowever, if `add_index` experiences a statement timeout (and we have not\nyet exceeded the\n[…]\neSQL, w/ the `:concurrently` opt) an invalid index was left\nbehind. The operation should first remove the invalid index before\nattempting another `add_index` call, and that's what this commit changes.",
"is_bot": false,
"headline": "fix: Allow statement timeouts on index creation to retry (within migr…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-01-05T22:07:34Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d8a44e12fd89266b8832351936ae805e9098c9a9",
"body": "`@migration_start` was never getting set, and there wasn't any code exercising the non-happy path for these migrations.\n\nThis also bumps the version to 2.0.1",
"is_bot": false,
"headline": "fix: Ensure that migration lock timeout loop works as expected. (#85)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2026-01-05T19:57:04Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "62df90556503b0cd9a5d621d783037ae472eaed0",
"body": "This releases `v2.0.0`, packed with improvements to `delayed`'s overall query performance and index coverage.\n\nNew indexes can be installed with `bundle exec rake delayed:install:migrations` (please review & test any migrations in your own application before shipping them!)",
"is_bot": false,
"headline": "build: Ship 2.0.0! (#71)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-19T21:49:20Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f7c87754abaf9cb7789d05d7401c1f23a853a1fa",
"body": "…#82)\n\nThis does two things:\n1. Avoids querying both `locked_count` and `working_count` separately (since they are synonymous)\n2. Splits the (total) `count` metric into two: `failed_count` and `live_count`\n\nWorth noting that when a scan covers the entire table, **a sequential scan might actually be \n[…]\nurthermore, in postgres at least, these don't just become index scans -- they become _INDEX ONLY_ scans, which should be cheaper than table scans since the index should be significantly more compact!)",
"is_bot": false,
"headline": "perf(monitor): avoid querying the same thing twice, avoid seq scans (…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-11T18:18:21Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "8413694b5a2e7d8fc0fe76a1341471c96872b5c9",
"body": "… index (#84)\n\nThis PR aims to provide reasonable unit-level assurance that new code can be rolled out _before_ new indexes are applied.\n\nIt applies the same snapshotting strategy to all notable query plans, but generates the plans against a schema with the legacy index (and none of the new indexes)\n[…]\nt structurally the same (if not better). **This does not account for `cost` changes,** but should give us a good signal to ensure that we don't regress an `Index Scan` into a `Seq Scan` (for example).",
"is_bot": false,
"headline": "test(migration path): ensure that new queries are compatible with old…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-11T17:06:13Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "07adf00c9e85bab79101fcda31f28f0981f8b17f",
"body": "…ing & valid index (#83)\n\nThis allows for proactively adding indexes out of band, so that the migrator will no-op and complete quickly. For very large tables this may be necessary to stay within constraints of synchronous deployment steps (e.g. a timeout on migration processes).\n\nI also adjusted test logger setup so that it relies entirely on log levels, and will log out migration warnings even if debug logging is turned off for the rest of the test suite.",
"is_bot": false,
"headline": "fix: Improve `upsert_index` to only drop+rebuild if there is no match…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-10T20:52:35Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "cc726776815c94954c8b0208fa4388989f28fec4",
"body": "When using `ActiveRecord.default_timezone = :local` and a local timezone that has a daylight savings \"fall back\" hour, it was possible for future-scheduled jobs to be immediately claimable, producing a \"spinloop\" of retries until the job achieves exit velocity.\n\nThis results from the fact that the D\n[…]\n PR is that **future-scheduled jobs will now be deferred to immediately _after_ the \"fall back\" hour.**\n\n(Jobs that do not specify a `run_at` are not affected and will still be claimable immediately.)",
"is_bot": false,
"headline": "fix: prevent DST + `:local` from breaking job backoff (#81)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-09T20:50:55Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "b0a48ace98b85d998c473f4df67f7ad09a1048ae",
"body": "(...which also includes `run_at` even though it's not in the name.)\n\nI'm relying on tests to prove that nothing this framework does relies _strictly_ on this index anymore. As such, none of the snapshotted query plans have changed! 🎉 \n\n**(Naturally, consumers of this gem can choose to keep any indexes they find useful.)**",
"is_bot": false,
"headline": "perf: remove legacy/unused \"delayed_jobs_priority\" index (#78)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-09T15:31:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0a3df1e744cce7c1564ad770df239c5de2819cb5",
"body": "This adjusts pickup queries so that `run_at <= ?` can be included as an `Index Cond` (rather than as a `Filter`).\n\nIt also improves a few monitor queries in minor ways that signifcantly improve the `cost` (not visible in our snapshots, but observed by me in a live testing context).\n\nThe core conceit\n[…]\nd `workable_by` to `claimable_by`\n\nI did **not** adjust any `Delayed::Monitor` metric names. Several of these are already outdated and will merit a migration path, but for now I'm not addressing them.",
"is_bot": false,
"headline": "perf: Improve selectivity of pickup query / \"claimed\" states (#80)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-09T15:31:10Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "8a6dcb156f6ff3832d10b84d117de28f7a166719",
"body": "As I've been testing this series of query/index changes, I noticed that things would hang everytime a new deploy went out. This is because when workers shut down, they perform a cleanup step to clear the locked state for any jobs that they had previously locked (and failed to complete, for whatever \n[…]\n (resulting in a full sequential scan) as there is no index that targets this column. By scoping the update to the `claimed_by(worker)` scope, we can turn it into an index scan against only live rows.",
"is_bot": false,
"headline": "perf(worker): avoid sequential scan during worker shutdown (#79)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-08T16:19:07Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "443edb0417810f07551d5113dd60bfa161911a65",
"body": "…. (#77)\n\nOn PostgreSQL, we already do not index `locked_at` or `locked_by` to optimize for [HOT updates](https://www.postgresql.org/docs/current/storage-hot.html) during job pickup. However, this does not benefit other DBs that don't follow the same MVCC/page architecture.\n\nThis PR:\n- Indexes `lock\n[…]\nker can run it (one to insert the row, one to lock the row). But 50% is the upper bound, so we went with 33% instead to further reduce the odds of page thrashing when picking up multiple jobs at once.",
"is_bot": false,
"headline": "perf: HOT updates for PostgreSQL, index `locked_at` for everyone else…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-05T19:28:38Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "575f92a8272b60edcfcce9f6783988e2163f7231",
"body": "Postgres and Mysql both indicate that this enables them to use an index-only/covering scan for the `erroring` scope/metric 👍 \n\nIt would be perfectly possible to index on `last_error`, but indexing an `integer` column is a lot more straightforward than the strategies we'd need to employ for a `text` \n[…]\ng. we could index on `WHERE last_error IS NOT NULL` for DBs that support indexes on expressions, but we could also do that with `WHERE attempts > 0` just as effectively, so I'm going with `attempts`.)",
"is_bot": false,
"headline": "perf(monitor): use 'attempts > 0' for better indexability (#76)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-05T13:53:53Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "b01d8fc9e896686e0b14fa0f489b6edd3cea11f1",
"body": "Per [this](https://github.com/Betterment/delayed/pull/74#discussion_r2586219398) comment, I've included `WHERE locked_at >= ?` as part of the `claimed_by` scope, making it \"timeout aware\" rather than returning records that have long since had their claim expired.\n\nIn practice, this will have minimal impact on both correctness (slightly better but in the more unlikely cases) & performance (same or a hair worse), so it's more about avoiding unnecessary discrepancies than anything else.",
"is_bot": false,
"headline": "fix: Include 'claimed' as part of 'claimed_by' scope (#75)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-04T19:20:16Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "12148a75ea16a517df4609d7dc35716fd5ca8638",
"body": "… (#74)\n\nSince we already don't index `locked_at` (optimizing for [HOT updates](https://www.postgresql.org/docs/current/storage-hot.html) in PG), this only impacts existing `Filter:` plans.\n\nI believe date comparisons are more expensive than `NOT NULL` checks, but the minor performance difference sh\n[…]\n for some reason we observe this change causing issues, an alternative would be to forego HOT updates and index `locked_at`. (I am already prepping a PR that indexes `locked_at` for non-PG databases.)",
"is_bot": false,
"headline": "fix(monitor): Obey lock timeout when reporting claimed/unclaimed rows…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-03T22:26:50Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "e31fe6ac8533824c528f46fd5f6da183de75e7b6",
"body": "…hem (#73)\n\nThis fixes a longstanding bug in the monitor queries that caused `future_count`, `locked_count`, and `erroring_count` to include failed jobs.\n\n(The latter sort of comes down to semantics, but \"erroring\" to me implies that it has not yet terminally failed. I also checked and it was out of\n[…]\nt stuck at a nonzero baseline.\n\n💪 Conveniently, this also improves the query plans for all three fields. (`future_count` even becomes an `Index Only` (postgres) / `Covering index` (mysql) scan/lookup!",
"is_bot": false,
"headline": "fix(monitor): exclude failed jobs from metrics that shouldn't count t…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-03T18:26:11Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "bbd6815dfeb17ef0a65eb0bea6b8f4e313f8dac7",
"body": "/no-platform\n\nThis also makes `AddIndexToDelayedJobsName` make use of it, and fixes a\nminor spec bug I found with Rails 6.0 appraisals where `drop_table` was\nnot getting run (because `:if_exists` wasn't added until 6.1)",
"is_bot": false,
"headline": "fix: Ensure that upsert_index is reversible (#72)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-03T17:27:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "17be23f401e0447611f56346de4a065a251ec7d1",
"body": "This PR is the first of several query optimizations, and can largely be read in two parts:\n\n1. A pair of heavily-annotated migrations that add new index(es) as safely as I could muster generically (w/ timeouts and a lock retry loop). I also updated the `README` w/ some expanded instructions and ⚠️ s\n[…]\n `Table scan` to `Covering index range scan`\n - `max_lock_age`, `max_age`, `working_count`, `workable_count`, and `alert_age_percent` go from `Table scan` to `Index lookup` w/ index conditions",
"is_bot": false,
"headline": "feat(perf): New indexes for job pickup & monitoring (#70)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-02T18:53:25Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "dafa66ec0ca4ed42ab46a42bae135ac8e6d06842",
"body": "This adds adapter-specific EXPLAIN outputs to the snapshot files introduced in #67.\n\nThe goal is to test that available indexes are usable, and enable us to flag changes/regressions in query plans going forward. **The goal is NOT to test or predict performance.** These are essentially \"unit\" tests f\n[…]\nstable to me, so I wasn't finding a need to attempt to trick it into doing anything different.\n\nThis depends on #68 being merged first (mostly because I want to use mysql's new \"TREE\" plan formatter).",
"is_bot": false,
"headline": "test: Snapshot test all EXPLAIN query plans (#69)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-01T21:35:24Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "710c9f9f1d8c1f4c461d20012c45f47f1e853c42",
"body": "I'd like to get this change to our workflow merged so that subsequent\nPRs can run their tests w/ known-passing versions of their DB\ndependencies. As we are starting to more directly cover the raw adapter\noutputs, this will help us prevent CI from randomly breaking on its own.\n\n/no-platform",
"is_bot": false,
"headline": "test: Normalize DB versions used in CI (#68)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-01T20:15:06Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0c45793f4ff330264b3f3b44d741bf888cbbfac2",
"body": "I'm introducing @rzane's `snapshot_testing` as a means to:\n- Make iterating on the queries a lot easier. (Removes all the manual work required in updating spec files, but still flags notable SQL differences in PRs so they can be reviewed before they ship.)\n- Removes the need to strip/normalize delim\n[…]\n instead of two now.\n\nThis might feel like a big diff, but it's really just capturing what's already there, and I very much intend to make use of this pattern to make several changes more confidently.",
"is_bot": false,
"headline": "test: Use snapshots for easier SQL testing/iteration. (#67)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-01T19:42:26Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "820ca77b7f3b15f1b8929617e7037512dfa8e1d3",
"body": "…Error (#66)\n\nThis fixes a regression flagged by #64. It was originally addressed in\nddd7a1c288b6425cac320e3de410d2dea51fbb86 but got reverted by [a linter\nauto-correct](https://github.com/Betterment/delayed/commit/78bd49e75638bc40c6c437d2848bd5dc31767ad0)\n🤦.\n\nThis reverts the revert, and adds a regression test that proves that\n`rescue StandardError` does not inadvertently catch\n`Delayed::WorkerTimeout`, allowing it to bubble out and be handled by\nthe worker.",
"is_bot": false,
"headline": "fix: max_run_time timeout error should not be rescuable as a Standard…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-01T19:07:24Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "4813b1a7501639d2b353691c38e7d7e8e5bf0a39",
"body": "There are other ways to slice this, but because of the threaded nature\nof the job runners, `Timecop` wasn't cutting it. This gives us ~1.5\nseconds of tolerance rather than ~1 and seems to have fixed my local\nreproduction of the flake (running tests in a loop for a long time).",
"is_bot": false,
"headline": "fix(tests): timing issue on test flake (#65)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-12-01T18:42:36Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ed8b6cebbdf4ee167aeb9bd0b8499895399f030e",
"body": "The goal is to create some unifying structure around the way that scopes\nare defined and organized in the ActiveRecord `Job` class, without\nchanging the actual SQL in any meaningful way. (This sets me up to make\nmeaningful SQL changes in future PRs!)\n\nIn addition to using `Arel` for `gt`/`lt`/`gteq`\n[…]\n `pending` (where run_at <= now)\n\nAgain, this should **not** meaningfully change any SQL! Only minor\nformatting changes without changing the meaning of each query. (I'm\nusing the tests to prove this.)",
"is_bot": false,
"headline": "refactor: Remove 'ready_scope' and improve remaining scopes. (#63)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-11-24T22:46:32Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "f8dad798e68e6fd30d23f750434799416a02381f",
"body": "…ss-wide config (#62)\n\nIdeally `max_run_time` is the same across an entire worker pool, because\notherwise workers might pick up jobs that are still held by other\nworkers in the pool.\n\nSo it has never made sense (based on our current pickup/monitor logic)\nthat this would be configurable on a per-worker-instance or per-query\nbasis.",
"is_bot": false,
"headline": "refactor: Add 'lock_timeout' & clarify that 'max_run_time' is a proce…",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-11-21T21:07:05Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "16c000c047df34018deb0a70cfc5a20c24d3f24d",
"body": "This is the first in a series of PRs where I will aim to rework &\noptimize this gem's query logic. I ultimately aim to add new index(es),\nand generally improve the indexability & runtime performance of these\nqueries, and will produce a new major release at that point.\n\n👉 But step one is to **test th\n[…]\nrbose, but I argue that if the\nlibrary is capable of producing N distinct queries, we should have N\ntests, and look for ways to reduce the number of queries (rather than\nmaking the tests more clever).",
"is_bot": false,
"headline": "test: Add \"golden\" tests for worker/monitor SQL (#61)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-11-20T22:29:14Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "33a485b2063645f91d8951970d6f2d39b5cc115d",
"body": "This updates the method & tests to properly ensure that the column value\ngets assigned before_save.\n\nReason: The `||=` operator results in no assignment, because `name` is\noverridden for backwards-compatibility if the column does not exist.",
"is_bot": false,
"headline": "fix: Fix set_name callback, and improve spec coverage (#60)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-10-06T19:18:32Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "8a3590393e3b668dec1627a2bed633a67a7f8cef",
"body": "This adds a `delayed_jobs.name` column that will be populated by default\nwhen each job is enqueued. While this column starts as nullable (to\nensure zero downtime is required to make this change), it will be\npromoted in a future release to an expected column. **Update: this\nfuture release will be TBD\n[…]\nin the table, etc)\n\nThis is the first real schema change we've made, and as such I had to\nrewire a little bit of plumbing to make it easier to drop a new\nmigration into an existing app (see `README`).",
"is_bot": false,
"headline": "feat: add `delayed_jobs.name` column (#59)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-10-03T14:10:20Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d46a0fb3a4032af549d5226fa57c8ebe51665827",
"body": "**Summary of changes:**\nThis pull request drops support for the oldest Rails and Ruby versions,\nin order to streamline the supportability bracket. By doing so, It also\ncuts a minor version release.",
"is_bot": false,
"headline": "Drop support for Rails 5.2 & Ruby 2.7 (#58)",
"author_name": "Irving C.",
"author_login": "Irving-Betterment",
"committed_at": "2025-09-12T21:12:12Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "397dbe3176ddfd280be7b945e57f7725173f6cb8",
"body": "This PR includes automated changes from running\n[frizbee](https://github.com/stacklok/frizbee) to pin Github Actions.\nFor more information on what this actually means, take a look at [our\ndocumentation on\nUnpinnedActions](https://github.com/betterment/claws?tab=readme-ov-file#unpinnedactions).\nBecau\n[…]\ncd6fda/.github/workflows/claws_fork_friendly.yml#L50-L54))\n\nFor other findings and how to remediate them, check the [Claws\ndocs!](https://github.com/betterment/claws?tab=readme-ov-file#built-in-rules)",
"is_bot": false,
"headline": "chore: Pin Github Actions to Commit Hashes (#56)",
"author_name": "Omar A.",
"author_login": "6f6d6172",
"committed_at": "2025-07-01T22:24:59Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "7e5a1123414ec3265ff13fcc3e2fd164648813e7",
"body": "This PR bumps us to `1.0.0` because in cleaning up our lifecycle hooks,\nI made a breaking change to remove the `job` argument from the `:thread`\nlifecycle callback. Coupling threads & jobs 1:1 was an implementation\ndecision, and we are restricting ourselves by baking that decision into\nthe callback \n[…]\nattempting to work\noff the same jobs over & over. (Even with `locked_at`, these workers can\nslowly degrade if enough jobs are consistently stuck in a locked state\npast the lock timeout.)\n\n/no-platform",
"is_bot": false,
"headline": "Delayed 1.0: Lifecycle & job cleanup improvements (#55)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-05-08T20:15:20Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "427bc304715961e5c56c3b08d90bfc6a43b0d87b",
"body": "I'm adding regression coverage for the changes made in #53, and making\none additional change in line with the way that the `:perform` callback\n[works in the `delayed_job`\ngem](https://github.com/collectiveidea/delayed_job/blob/ea4879dd3c2f3f5aa7f616a6f0fd58f2bbc2a481/lib/delayed/worker.rb#L313)\n- th\n[…]\nast\npriority to a float, which was raising a method undefined error. So I\nadded `Delayed::Priority#to_f` and added a test to cover all the\nexplicit conversion methods (`to_s`, `to_i`, `to_f`, `to_d`).",
"is_bot": false,
"headline": "0.8.0 - `Priority#to_f`, plus some regression coverage (#54)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-04-03T17:24:08Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "2ea5f07954a89242d74af3b0990d3b1f0281b0df",
"body": "/no-platform\n/domain @argvniyx-enroute @mavenraven \n\nPR https://github.com/Betterment/delayed/pull/42 inadvertently flipped the order of `:thread` and `:perform`, and also pushed `:thread` far enough in that cleanup steps would happen after the `with_connection`'s`end` block in the connection plugin.\n\nThis introduces the possibility of thread safety issues, or connections being held longer than intended (exhausting the connection pool / connection limits).",
"is_bot": false,
"headline": "[fix] Thread callback ordering (#53)",
"author_name": "Nathan G",
"author_login": "smudge",
"committed_at": "2025-04-02T22:20:01Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "b5abf39a0b8fcc23c31534f85c65f14f4b1437e6",
"body": "### Description\r\n\r\nUpgrade betterlint 1.18.0 dependencies and regenerate RuboCop todo.",
"is_bot": false,
"headline": "Upgrade betterlint to 1.18.0 (#52)",
"author_name": "Dan Kubb",
"author_login": "dkubb",
"committed_at": "2025-02-03T18:35:47Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f0fcc71fac3ed3adecccdd917a41f769cb6f8460",
"body": "I configured deprecations to raise exceptions.\r\n\r\nOnce I did that, I had to suppress deprecations from `enqueue_after_transaction_commit = :always` and `enqueue_after_transaction_commit = :never`.\r\n\r\nThose values are still supported, but deprecated. Rails 8.0 now expects `true` or `false`. I've updated delayed to ensure that `enqueue_after_transaction_commit` is not set to `true`.",
"is_bot": false,
"headline": "Fix Rails 8.0 deprecation (#51)",
"author_name": "Ray Zane",
"author_login": "rzane",
"committed_at": "2025-01-24T21:10:27Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "7e184ba7f8207e78b6ac9be3bc3893b3bf5a2072",
"body": "Adding 3.4 is a bit more complicated, because no versions of Nokogiri\r\nsupport 3.0 and 3.4. We'd need separate gemfiles, ro we'd need to only\r\ntest Ruby 3.4 against specific gemfiles.",
"is_bot": false,
"headline": "Add Ruby 3.3 to the CI matrix (#50)",
"author_name": "Ray Zane",
"author_login": "rzane",
"committed_at": "2025-01-22T16:17:01Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a8b6c9cd3aedc6f06e6e6421baa827aad7058c14",
"body": "This PR runs our test suite against Rails 8 on CI.\r\n\r\nThere was only one major change. `ActiveSupport::ProxyObject` was\r\ndeprecated in 7.2 and removed in 8.0.",
"is_bot": false,
"headline": "Rails 8 support (#49)",
"author_name": "Ray Zane",
"author_login": "rzane",
"committed_at": "2025-01-15T16:01:30Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "c8ac357f005081cad6076c605c7c81c6db03c055",
"body": "… for now) (#48)\n\nThis is related to #41 and #42 insofar as a kind of DB-bound \"spinloop\"\r\nis still possible if a worker picks up jobs that take so little time\r\nthat the worker immediately turns around and asks for more. As of now,\r\nthere has been no way to tune the amount of time a worker should wa\n[…]\nracted from `min_reserve_interval` when\r\nthe worker sleeps, and if jobs take more than `min_reserve_interval` to\r\ncomplete than the worker will not sleep before the next reserve query.\r\n\r\n/no-platform",
"is_bot": false,
"headline": "Add a config for enforcing a minimum job reserve interval (default: 0…",
"author_name": "smudge",
"author_login": "smudge",
"committed_at": "2024-12-18T18:12:39Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "c0c36c0bed9fc62ab0cb0790e786ba9e4e6ae9ed",
"body": "/no-platform\r\n\r\nmore updates @smudge",
"is_bot": false,
"headline": "update rails deps to fix security alerts (#45)",
"author_name": "Sam Moore",
"author_login": "samandmoore",
"committed_at": "2024-08-20T14:25:40Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a352b1dec688fa9025ca1cb4532a9c2f73a53609",
"body": "just what it says. we've got some dependabot alerts for these deps, so\r\nupgrading to fix that.",
"is_bot": false,
"headline": "update rexml + nokogiri to fix security alerts (#44)",
"author_name": "Sam Moore",
"author_login": "samandmoore",
"committed_at": "2024-08-20T14:16:15Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "37dff237d53040abd6984990942f5c27f05db693",
"body": "…or Rails 7.2 support (#43)\n\nHi Betterment friends!\r\n\r\nAs addressed in @smudge's PR\r\nhttps://github.com/Betterment/delayed/pull/40, Rails 7.2 introduces the\r\nconcept of `enqueue_after_transaction_commit`, which should not be used\r\nin conjunction with Delayed. The implementation, and the associated\r\n\n[…]\n13a4899c9f996ae51f54cdb8fa5f79f50efab872f9aR20)\r\nwhere `!!:never == true` and causes the condition to be true (and\r\nraising the exception) when `job.class.enqueue_after_transaction_commit\r\n== :never`.",
"is_bot": false,
"headline": "Handle proper values for ActiveJob.enqueue_after_transaction_commit f…",
"author_name": "Chris Zega",
"author_login": "warmerzega",
"committed_at": "2024-08-13T21:55:22Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "fd97a38875cf3eaa2081972f2bbf4405bffc09af",
"body": "This ensures that exceptions raised in thread callback hooks are rescued\r\nand properly mark jobs as failed.\r\n\r\nThis is also a good opportunity to change the `num` argument (of\r\n`work_off(num)`) to mean number of jobs (give or take a few due to\r\n`max_claims`), not number of iterations. Previously (be\n[…]\n we only\r\nincrement `success` and treat `total - success` as the \"failure\" number\r\nwhen we return from the method.\r\n\r\nFixes #23 and #41\r\n\r\nThis is also a prereq for a resolution I'm cooking up for #36",
"is_bot": false,
"headline": "Fix spin-loop/cleanup failure mode within run loop (#42)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2024-06-27T16:35:43Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "24b75f56c43e470ecdec2d3afdcc11bb21a6e2e3",
"body": "### Summary\r\nThis PR Adds a configuration value (`.assign_at_midpoint`) to `Delayed::Priority` \r\n\r\nThe intent is to allow users to change the default priority of a job (i.e. when using a named priority) to be the middle of the configured priority ranges -- derived from the default or custom priority\n[…]\n\n\r\n**Note**: since the last priority uses a range that is infinite, it's not possible to derive the midpoint. So, for now, I've defaulted to adding 5, since that matches our defaulted priority values.",
"is_bot": false,
"headline": "Allow configuring default priority value at midpoint of range (#39)",
"author_name": "Terrance Koar",
"author_login": "tkoar",
"committed_at": "2024-04-29T17:16:29Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "9a328c6e0518bbd141980653140379a12d6f8c98",
"body": "…commit) (#40)\n\nThis anticipates the release of a new feature in Rails 7.2 introduced here: https://github.com/rails/rails/pull/51426\r\n\r\nIn Rails 7.2, ActiveJob will default `enqueue_after_transaction_commit` to `true`, and we would like to explicitly set this to `false` (retaining transactional enq\n[…]\n I also:\r\n- Added a new Appraisal for the latest Rails main build on GitHub.\r\n- Re-generated the rubocop todo file. There are changes we can pull out & autocorrect, but we can do them in separate PRs.",
"is_bot": false,
"headline": "Prepare for Rails 7.2 support (and disable enqueue_after_transaction_…",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2024-04-29T17:15:40Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "77b97e9f540f0cce849de4c9665f9963d3a15c47",
"body": "We commit the lockfiles now, so version changes need a re-bundle. (I\r\nmissed this in the last PR.) Once this is in I can cut a new release.",
"is_bot": false,
"headline": "Fix the lockfile / linter build step (#38)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2024-01-31T03:45:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "f2e5edc5d3f9481a9636d967d5cbe7ac191995eb",
"body": "This PR introduces a change to avoid calling `ActiveJob::Base#deserialize` during queueing the job whilst maintaining existing functionality.\r\n\r\nThe change is in `JobWrapper`, which previously only took in a hash of `job_data` and knew how to deserialize it into a job object so we can interrogate it\n[…]\nialize` during the enqueuing, even those adapters that support invoking methods on the Job class instance like delayed does.\r\n\r\nFrom one angle it's an optimization, from another angle it's a bugfix (:",
"is_bot": false,
"headline": "Optimize JobWrapper queueing ActiveJobs (#35)",
"author_name": "Caius Durling",
"author_login": "caius",
"committed_at": "2024-01-31T03:38:12Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "869c08a893b97d8fad51b2cc5c08e8e151c74b1b",
"body": "Committing the lockfile triggered a bunch of dependabot errors, and I\r\nrealized that the versions committed were very out of date, so this\r\nupdates the lockfile with newer versions.\r\n\r\n/no-platform",
"is_bot": false,
"headline": "Address dependabot alerts (#34)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2024-01-08T18:23:41Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "737f2fd4eaa149758bce30da299e07a09489c680",
"body": "**Summary of changes:**\r\nCommit lockfile previously ignored (see\r\nhttps://bundler.io/guides/faq.html#using-gemfiles-inside-gems).\r\n\r\n/task https://app.asana.com/0/0/1206244989881229/f\r\n\r\n---------\r\n\r\nCo-authored-by: Irving Caro <112433591+betterment-coding-agent@users.noreply.github.com>",
"is_bot": false,
"headline": "Add lockfile (#32)",
"author_name": "Irving Caro",
"author_login": "Irving-Betterment",
"committed_at": "2024-01-08T17:58:06Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "342acb982a575ecfbd5693bddbb1feab96b56589",
"body": "This fixes #29, which can be reproduced on new Rails 7.1.1 apps in\r\ndevelopment.\r\n\r\nThe issue stems from the fact that Rails no longer loads ActiveJob at\r\nall until the first `ActiveJob::Base` class is referenced, but we\r\ndefined our `Delayed::JobWrapper` class in a file that only loads\r\n_after_ Act\n[…]\napper` out into its own file in `lib/`\r\nand load it when `lib/delayed.rb` loads. The class itself has no\r\ndependencies outside of `activesupport` (`delegate_missing_to`) so this\r\nis safe enough to do.",
"is_bot": false,
"headline": "Fix Rails 7.1 regression: Delayed::JobWrapper not defined (#30)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2023-10-19T18:26:00Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "429e20f0baddb1205d5ee7be7082e2a4afcd1f0e",
"body": "This adds Rails 7.1, Ruby 3.1, and Ruby 3.2 to the build matrix.\r\n\r\nThe gem already works with these versions, but now they are formally\r\ntested. (As such, I bumped the gem by a patch version, to signify that\r\nthis is the first verified-compatible release.)",
"is_bot": false,
"headline": "Rails 7.1, Ruby 3.1, and Ruby 3.2 (#28)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2023-10-11T16:10:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "66125d123cff8742e729503dfdd1cc2dac7bb34a",
"body": "…inconsistency (#24)\n\nThe issue here is that some ActiveJob-enqueued jobs would include the `@job` ivar in their serialized handler, which is redundant with `@job_data` and only existed for memoization purposes. We can use Ruby's `encode_with` API to ensure that we only encode `@job_data` into the h\n[…]\nsubsequent retries) occur. `DeserializationError` is really supposed to be only for cases where we fundamentally can't even attempt the job and allow the normal pattern of exception-handling to occur.",
"is_bot": false,
"headline": "Reduce handler size in some ActiveJob cases, and fix deserialization …",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2023-01-20T22:02:09Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "f7a27c8da949ec73f0fc0fe06182788544f497fc",
"body": "There are a couple linter rules complaining. This will ensure that anyone who branches off of main won't have to fix them independently.\r\n\r\nThis also fixes the Ruby 3.0 build so that it doesn't inadvertently run with Ruby 3.1 in CI.",
"is_bot": false,
"headline": "Fix main builds: appease the linter (#14)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2022-02-11T20:27:44Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "abad99fb2562a22e7d924c597b235dc4c74f1ae9",
"body": null,
"is_bot": false,
"headline": "Move appraisal onto official Rails 7.0.0 release (#9)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-12-16T03:43:39Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6d9d831a731f20595f99655c3ae5436003caaeca",
"body": "Resolves #6. While we already fully support Ruby 3.0 kwargs via ActiveJob, the legacy `delay` and `handle_asynchronously` APIs did not support the new [separation of positional and keyword arguments](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-\n[…]\npt keyword arguments. It should also support jobs that were enqueued by the prior `delayed` gem version (where the new `kwargs` accessor would be nil, and `args` contains the kwargs as its last item).",
"is_bot": false,
"headline": "Support Ruby 3.0 kwarg-handling within `.delay` API (#7)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-11-30T21:30:50Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "2d296b9f49c24c99b91d8bf78690a20436811bae",
"body": "This:\r\n- Adds an `Appraisal` entry for `7.0.0.alpha2`. (I had to specify multiple gems to get the pre-release dependencies to resolve properly.)\r\n- Bumps `.ruby-version` to `2.7`, but keeps linting at `2.6`. (CI will still run against all of 2.6-3.0)\r\n- Fixes a deprecation warning about `ActiveRecor\n[…]\nassic` autoloader is no longer used/supported in Rails 7)\r\n- Autocorrects some new linter rules pulled from the latest version of the linter gem\r\n- Bumps the version to 0.3.0 and updates the CHANGELOG",
"is_bot": false,
"headline": "Add support for Rails 7 (alpha2) (#3)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-10-26T19:52:48Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "d82f789360a8c82404646189441b3739b545859d",
"body": "I just realized that we hadn't cross-referenced this in the other direction!\r\n\r\nThe linter issues are fixed in #3",
"is_bot": false,
"headline": "Plug the blog post in the README (#4)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-10-26T19:25:48Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "484d68a7a1eea573d52fe12a963002925b9171e9",
"body": "This fixes an issue I was seeing on a very slim Rails 6.1 app -- essentially, `ActiveSupport.on_load(:active_record)` was not triggering at all during `rake delayed:work`, resulting in a missing constant error. Previous versions of rails seem to load `ActiveRecord::Base` during boot, but 6.1 seems t\n[…]\n to `syck` which dates back to Ruby 1.9, and we should consider removing support for it entirely. (It can still be installed via the `syck` gem, but `psych` has been the default for many, many years.)",
"is_bot": false,
"headline": "Minor Rails 6.1 fix, plus autoload Delayed::Job constant (#2)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-09-02T16:45:07Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "941f02da326ffe13280d5a1cf4c1683a22f886d2",
"body": "This fixes the gemspec description, which had previously been written in rdoc format (causing it to appear garbled on rubygems.org). It also adds a CHANGELOG and bumps the version to `0.1.1`.",
"is_bot": false,
"headline": "Fix rubygems description and add CHANGELOG (#1)",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-08-19T20:29:25Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "844bbca1e397b500f77eb9d2ecdfef94376cd766",
"body": null,
"is_bot": false,
"headline": "Don't allow alert_age_percent to exceed 100",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-07-22T20:39:57Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "4c61327ec887d229c96e74fb227c2bb5040ca22e",
"body": "Hello! Here's another stab at the `README` that attempts to incorporate\nall of the feedback I've received, plus describe the zillions of big and\nlittle things that have changed in the 30+ PRs since that initial\ndraft!!!\n\nThis also changes the default queue name to `'default'` (to align with\nActiveJob), and renames the install command from\n`rails g delayed_job:active_record` to `rails g delayed:migration`",
"is_bot": false,
"headline": "A README: Our vision for `delayed`",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-25T18:00:05Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "a65965a1a1b158772028fd9ffc2b3e0007ddee24",
"body": "We were investigating a bug where jobs were not being enqueued with the\ncorrect priority. To rule out `ActiveJob`'s `queue_with_priority`\nconfig, I added this spec. It turns out that it works (the actual bug\nwas with `ActiveJob`'s `retry_on` behavior), but is helpful test\ncoverage to have!",
"is_bot": false,
"headline": "Add test for queue_with_priority",
"author_name": "Harris Effron",
"author_login": "effron",
"committed_at": "2021-06-23T17:47:34Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "d8526e62fd31bdb71ff19c6bc704b9f507320d3d",
"body": "This really only applies in legacy cases where we set DELAY_JOBS=0 -- if\njobs are run inline, they are never technically locked, so there is no\nway of tracking `run_time` via anything db-backed.",
"is_bot": false,
"headline": "Fix run_time_alert? when job has no run time",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-22T18:43:07Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "714ba2259806ffeef7808991a667f6ac573ff9f5",
"body": "🤦 🤦 🤦\n\nI've been passively trying to figure out why a few of the metrics just\ndidn't seem to line up. Suddenly it struck me while I was making tea\nbefore bed, and so I felt compelled to get a fix up.\n\n...it's because we cached `@as_of = Job.db_time_now` when initializing\nthe monitor instance. I had \n[…]\noves the cleverness and goes back to the old strategy of\nusing an ever-changing `Time.zone.now` (provided by `Job.db_time_now` -\nto be consistent with the rest of the codebase) throughout the queries.",
"is_bot": false,
"headline": "Fix metrics that depend on db_time_now",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-22T13:02:52Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "4f92a30a54a105518fc278aa7002b413ce4f5974",
"body": "I confirmed that this `:eager_load!` initializer is available across all\nof our supported Rails versions. It varies slightly depending on Rails\nversion, so this ensures that the gem's eager-loading behavior will use\nthe same code as whatever Rails version is running, because we call\ndirectly into Rails' eager_load initializer.",
"is_bot": false,
"headline": "Run the full eager-loading initializer",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-17T18:02:52Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ca8a55b9efb3555f63c6cb1ff8bf267862c64df9",
"body": "…a single value\n\nI baked this check into the matcher, so if it detects events with the\nsame payload but _different_ values, the tests will fail. I then used\nthis to fix the bug and get the tests green.\n\nThis should fix the weird issue where gauge metrics cycle between real\nvalues and zero with no rhyme or reason -- it's because we were emitting\nduplicate metrics with value zero!",
"is_bot": false,
"headline": "Ensure that events with matching payloads are only emitted ONCE with …",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-16T21:30:46Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "190a32b79d5ff62abc3ae93a2f13bbc16f689aa5",
"body": "Rails always sets `eager_load` to `false` inside of rake tasks, which we\nnever noticed because either:\n\n- we were running the `:classic` autoloader with the single-threaded\n implementation, so autoloading worked, or\n- we were running the `:zeitwerk` autoloader, which is thread-safe\n\nOn Rails 6.1, w\n[…]\nqueued JobClass (Job ID: SOME-UUID) to Delayed(default)\n```\n\nTo demonstrate the underlying reason:\n\n```\nclass B; end\nA = B\nA.name # => 'B'\n\nA = Class.new(B) # A now inherits from B\nA.name # => 'A'\n```",
"is_bot": false,
"headline": "Work around Rails' disabling of eager_load during rake tasks",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-14T17:31:22Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "c7fd37a643b4239113564b8a2c51a4b67d301f39",
"body": "The way these ENV var handlers were set up was overriding the default\nvalue we set in our initializer (which runs as part of `:environment`).\nThis must be why we relied on a monkey patch (`def self.queues`) rather\nthan setting the queues config directly. Glad to finally have gotten to\nthe bottom of this 😌",
"is_bot": false,
"headline": "Fix 'queue(s)' opts when running via rake tasks",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-14T14:16:02Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "6a518c0b1e24467eef4c130caac9104795f3917e",
"body": "During initialization, Rails uses the `cache_classes` and\n`reload_classes_only_on_change` config to set a \"`check`\" lambda on its\nreloader config:\n\n```ruby\n if config.cache_classes\n app.reloader.check = lambda { false }\n elsif config.reload_classes_only_on_change\n app\n[…]\n\n\nSo I'm replacing this gem's implementation of `reload_app?` with Rails'\nbuilt-in `.check!` method (which calls the lambda and caches `true`\nvalues until you call `reload!`, which we do immediately).",
"is_bot": false,
"headline": "Optimize 'reload!', prevent reloading app every time",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-14T14:15:53Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "6068ad9e7b09b1bc2a17233cdd7f117999391608",
"body": "I assumed that since tests were passing, I had covered all of my bases.\nBut it turns out we pass opts into `Worker.new` inside of the rake\ntasks, and these were not tested. 🤦 🤕",
"is_bot": false,
"headline": "Fix rake tasks, and actually test them",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-08T21:14:15Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0e02ab44b3c90369ee55f3f77573bcb79180327a",
"body": "We used the ability to override `attempt_alert_threshold` downstream in\na couple spots. I considered just ditching that functionality with\n`alert_attempts`, etc, but decided that it was easy enough to enable for\nnow.",
"is_bot": false,
"headline": "Allow overriding per-job alert thresholds",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-08T18:10:19Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "ebd8c14766057003a02b60da4839e3b055dbd56f",
"body": null,
"is_bot": false,
"headline": "Fix test flake: SimpleJob is not threadsafe",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-08T16:54:08Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "cc63b47e373d5f11e6dba06219f04511085bf41c",
"body": "I caught some issues in runtime relating to ActiveJob default job\npriority behavior.\n\nThis makes two changes:\n\n1. If a priority option is _not_ passed to `set`, this ensures that it\n doesn't get overridden to `0`. (Essentially, it disobeys class-level\n priorities that can be set via ActiveJob. -\n[…]\nPreparer` to watch for `payload_object.priority` if it\n responds to it. (This seems to have been a pre-existing bug in DJ\n that would prevent the use of ActiveJob class-level priority\n configs.)",
"is_bot": false,
"headline": "Fix default priority behavior with ActiveJobs",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-08T16:52:47Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "1bebd6877bf3cb4f319564b98ad1f7753e97e002",
"body": "This is my attempt to incorporate the scaling metric\n(`delayed.job.alert_age_percent`) into the monitor process, as well as\nmore general priority-based alert thresholds that can be used to\ndetermine whether or not to fire off alerts when subscribing to the\nActiveSupport notifications.\n\nAlert thresho\n[…]\nic to each priority name. (I\nconsidered adding global defaults too! But I wanted to see how this\nplayed out before making it more complicated.)\n\nCo-authored-by: Harris Effron <harris.effron@gmail.com>",
"is_bot": false,
"headline": "Priority based alerts and autoscaling",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-07T20:24:07Z",
"body_truncated": true,
"is_coding_agent": false
},
{
"oid": "b278de29fec7b0915dfc26c9668357951ac75246",
"body": "…tate\n\nAfter chatting offline, we decided to ditch the idea that worker\ninstances could hold their own config state, and instead rely on global\nstate for all of these configs. Things behave mostly the way they did\nbefore now, except that worker instances _don't have setters_ for any of\nthese configs. The only way to change a config is to set it globally on\nthe class.",
"is_bot": false,
"headline": "Change default priority to 10, max_claims to 5, and simplify worker s…",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-07T18:02:39Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "0f1638c6482303751d0c033090c21d2ddf4f9cb7",
"body": "We had talked about these! Figured I'd take a stab at them.",
"is_bot": false,
"headline": "Add additional instrumentation around job pickup and enqueue",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-07T15:52:18Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "b0db8a46caa39c21f13fb641b3347c65f6d2bcc9",
"body": "The other new defaults that I had already changed are `max_claims` at 2\n(up from 1) and `destroy_failed_jobs` (set to `false` now).",
"is_bot": false,
"headline": "Set max_run_time to 20 minutes by default",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-04T14:57:54Z",
"body_truncated": false,
"is_coding_agent": false
},
{
"oid": "993b757b345a40d112f3c00533054cfc6b5cf02f",
"body": "…pters\n\nI noticed this when running tests that use the `:test` queue adapter,\nbut it would be true of any other ActiveJob adapters.\n\nActiveJob tries to call `.to_i` on priority (which doesn't work on\nsymbols), which is why I had originally passed it through\n`provider_attributes` instead, but I realized that it's better to just\ntype-cast to a `Delayed::Priority` explicitly so that `to_i` works as\nexpected.",
"is_bot": false,
"headline": "Prevent adapter patches from hiding job priority from other queue ada…",
"author_name": "Nathan Griffith",
"author_login": "smudge",
"committed_at": "2021-06-03T19:18:01Z",
"body_truncated": false,
"is_coding_agent": false
}
],
"releases_count": 26,
"commits_last_year": 52,
"latest_release_at": "2026-07-13T15:29:31Z",
"latest_release_tag": "v3.1.0",
"releases_from_tags": false,
"days_since_last_push": 8,
"active_weeks_last_year": 18,
"days_since_latest_release": 8,
"mean_days_between_releases": 31.1
},
"community": {
"has_readme": true,
"has_license": true,
"has_description": true,
"has_contributing": false,
"health_percentage": 37,
"has_issue_template": false,
"has_code_of_conduct": false,
"has_pull_request_template": false
},
"ecosystem": {
"packages": [
{
"name": "delayed",
"exists": true,
"license": "MIT",
"keywords": [],
"ecosystem": "rubygems",
"matches_repo": true,
"registry_url": "https://rubygems.org/gems/delayed",
"is_deprecated": false,
"latest_version": "3.1.0",
"repository_url": "https://github.com/betterment/delayed",
"versions_count": 29,
"total_downloads": 1132655,
"dependents_count": null,
"deprecation_note": null,
"maintainers_count": null,
"monthly_downloads": null,
"first_published_at": "2021-08-17T14:19:16.612000Z",
"latest_published_at": "2026-07-13T15:29:09.213000Z",
"latest_version_yanked": null,
"days_since_latest_publish": 8
}
]
},
"popularity": {
"forks": 12,
"stars": 175,
"watchers": 44,
"fork_history": {
"days": [
{
"date": "2021-08-19",
"count": 1
},
{
"date": "2022-08-17",
"count": 1
},
{
"date": "2022-10-02",
"count": 1
},
{
"date": "2023-03-02",
"count": 1
},
{
"date": "2023-10-15",
"count": 1
},
{
"date": "2023-12-20",
"count": 1
},
{
"date": "2024-04-02",
"count": 1
},
{
"date": "2024-08-13",
"count": 1
},
{
"date": "2024-10-21",
"count": 1
},
{
"date": "2026-02-12",
"count": 1
},
{
"date": "2026-05-21",
"count": 1
},
{
"date": "2026-05-28",
"count": 1
}
],
"complete": true,
"collected": 12,
"total_forks": 12
},
"star_history": {
"days": [
{
"date": "2021-08-18",
"count": 1
},
{
"date": "2021-08-19",
"count": 5
},
{
"date": "2021-08-26",
"count": 1
},
{
"date": "2021-08-29",
"count": 1
},
{
"date": "2021-09-09",
"count": 2
},
{
"date": "2021-09-11",
"count": 1
},
{
"date": "2021-09-13",
"count": 1
},
{
"date": "2021-10-05",
"count": 1
},
{
"date": "2021-10-07",
"count": 1
},
{
"date": "2021-10-27",
"count": 1
},
{
"date": "2021-11-08",
"count": 1
},
{
"date": "2021-11-09",
"count": 3
},
{
"date": "2021-11-10",
"count": 1
},
{
"date": "2021-11-23",
"count": 2
},
{
"date": "2021-11-24",
"count": 3
},
{
"date": "2021-11-29",
"count": 1
},
{
"date": "2021-12-10",
"count": 1
},
{
"date": "2021-12-16",
"count": 1
},
{
"date": "2021-12-17",
"count": 2
},
{
"date": "2021-12-20",
"count": 1
},
{
"date": "2021-12-24",
"count": 2
},
{
"date": "2021-12-25",
"count": 1
},
{
"date": "2021-12-26",
"count": 1
},
{
"date": "2021-12-30",
"count": 1
},
{
"date": "2022-01-02",
"count": 1
},
{
"date": "2022-01-06",
"count": 1
},
{
"date": "2022-01-07",
"count": 1
},
{
"date": "2022-01-15",
"count": 1
},
{
"date": "2022-01-22",
"count": 1
},
{
"date": "2022-01-29",
"count": 1
},
{
"date": "2022-02-13",
"count": 1
},
{
"date": "2022-02-14",
"count": 1
},
{
"date": "2022-02-16",
"count": 13
},
{
"date": "2022-02-17",
"count": 1
},
{
"date": "2022-02-18",
"count": 1
},
{
"date": "2022-02-19",
"count": 1
},
{
"date": "2022-02-20",
"count": 1
},
{
"date": "2022-02-21",
"count": 1
},
{
"date": "2022-02-23",
"count": 1
},
{
"date": "2022-02-24",
"count": 1
},
{
"date": "2022-02-28",
"count": 1
},
{
"date": "2022-04-11",
"count": 2
},
{
"date": "2022-05-03",
"count": 1
},
{
"date": "2022-05-09",
"count": 1
},
{
"date": "2022-05-17",
"count": 1
},
{
"date": "2022-05-18",
"count": 1
},
{
"date": "2022-05-24",
"count": 1
},
{
"date": "2022-05-27",
"count": 1
},
{
"date": "2022-05-29",
"count": 1
},
{
"date": "2022-05-30",
"count": 3
},
{
"date": "2022-06-03",
"count": 1
},
{
"date": "2022-06-07",
"count": 1
},
{
"date": "2022-06-20",
"count": 3
},
{
"date": "2022-06-27",
"count": 1
},
{
"date": "2022-07-07",
"count": 1
},
{
"date": "2022-07-28",
"count": 1
},
{
"date": "2022-08-15",
"count": 1
},
{
"date": "2022-08-21",
"count": 1
},
{
"date": "2022-08-24",
"count": 1
},
{
"date": "2022-08-26",
"count": 1
},
{
"date": "2022-09-02",
"count": 1
},
{
"date": "2022-09-04",
"count": 1
},
{
"date": "2022-09-15",
"count": 1
},
{
"date": "2022-09-16",
"count": 1
},
{
"date": "2022-10-15",
"count": 3
},
{
"date": "2022-10-21",
"count": 1
},
{
"date": "2022-11-04",
"count": 1
},
{
"date": "2022-11-07",
"count": 1
},
{
"date": "2022-11-09",
"count": 1
},
{
"date": "2022-11-28",
"count": 1
},
{
"date": "2022-12-07",
"count": 1
},
{
"date": "2023-01-26",
"count": 1
},
{
"date": "2023-02-03",
"count": 1
},
{
"date": "2023-02-06",
"count": 1
},
{
"date": "2023-02-07",
"count": 1
},
{
"date": "2023-03-08",
"count": 1
},
{
"date": "2023-03-09",
"count": 1
},
{
"date": "2023-04-05",
"count": 1
},
{
"date": "2023-04-10",
"count": 1
},
{
"date": "2023-05-02",
"count": 1
},
{
"date": "2023-05-03",
"count": 1
},
{
"date": "2023-05-05",
"count": 1
},
{
"date": "2023-05-09",
"count": 1
},
{
"date": "2023-06-09",
"count": 1
},
{
"date": "2023-06-11",
"count": 1
},
{
"date": "2023-06-12",
"count": 1
},
{
"date": "2023-06-20",
"count": 1
},
{
"date": "2023-07-22",
"count": 1
},
{
"date": "2023-08-04",
"count": 1
},
{
"date": "2023-08-08",
"count": 1
},
{
"date": "2023-08-19",
"count": 1
},
{
"date": "2023-09-17",
"count": 2
},
{
"date": "2023-10-02",
"count": 1
},
{
"date": "2023-10-12",
"count": 1
},
{
"date": "2023-10-13",
"count": 1
},
{
"date": "2023-10-14",
"count": 1
},
{
"date": "2023-10-18",
"count": 1
},
{
"date": "2023-11-18",
"count": 1
},
{
"date": "2023-11-26",
"count": 1
},
{
"date": "2023-12-03",
"count": 1
},
{
"date": "2023-12-18",
"count": 1
},
{
"date": "2023-12-22",
"count": 1
},
{
"date": "2024-01-10",
"count": 1
},
{
"date": "2024-01-13",
"count": 1
},
{
"date": "2024-01-18",
"count": 1
},
{
"date": "2024-02-09",
"count": 1
},
{
"date": "2024-02-18",
"count": 1
},
{
"date": "2024-03-25",
"count": 1
},
{
"date": "2024-04-15",
"count": 1
},
{
"date": "2024-04-18",
"count": 1
},
{
"date": "2024-05-08",
"count": 1
},
{
"date": "2024-05-31",
"count": 1
},
{
"date": "2024-06-13",
"count": 1
},
{
"date": "2024-07-17",
"count": 1
},
{
"date": "2024-07-18",
"count": 1
},
{
"date": "2024-08-21",
"count": 1
},
{
"date": "2024-10-18",
"count": 1
},
{
"date": "2024-10-27",
"count": 1
},
{
"date": "2024-11-20",
"count": 1
},
{
"date": "2025-05-15",
"count": 3
},
{
"date": "2025-05-20",
"count": 1
},
{
"date": "2025-05-25",
"count": 1
},
{
"date": "2025-09-16",
"count": 1
},
{
"date": "2025-09-18",
"count": 1
},
{
"date": "2025-09-19",
"count": 2
},
{
"date": "2025-09-25",
"count": 1
},
{
"date": "2025-09-27",
"count": 1
},
{
"date": "2025-10-07",
"count": 1
},
{
"date": "2025-10-28",
"count": 1
},
{
"date": "2025-12-09",
"count": 1
},
{
"date": "2026-01-08",
"count": 1
},
{
"date": "2026-01-09",
"count": 1
},
{
"date": "2026-02-19",
"count": 1
},
{
"date": "2026-03-04",
"count": 1
},
{
"date": "2026-03-11",
"count": 1
},
{
"date": "2026-03-18",
"count": 1
},
{
"date": "2026-05-26",
"count": 1
},
{
"date": "2026-07-01",
"count": 1
},
{
"date": "2026-07-09",
"count": 1
},
{
"date": "2026-07-14",
"count": 1
}
],
"complete": true,
"collected": 175,
"total_stars": 175
},
"open_issues_and_prs": 9
},
"ai_readiness": {
"has_nix": false,
"example_dirs": [],
"has_llms_txt": false,
"has_dockerfile": false,
"has_mcp_signal": false,
"bootstrap_files": [],
"api_schema_files": [],
"has_devcontainer": false,
"typecheck_configs": [],
"toolchain_manifests": [],
"largest_source_bytes": 48940,
"source_files_sampled": 58,
"oversized_source_files": 0,
"agent_instruction_files": [],
"agent_instruction_max_bytes": null
},
"dependencies": {
"manifests": [
"Gemfile"
],
"advisories": {
"error": null,
"scope": "repository_graph",
"source": "osv",
"findings": [
{
"name": "rack-session",
"direct": false,
"version": "2.1.0",
"severity": "critical",
"ecosystem": "rubygems",
"cvss_score": 9.1,
"advisory_ids": [
"GHSA-33qg-7wpp-89cq",
"GHSA-9j94-67jr-4cqj"
],
"fixed_version": "2.1.2",
"advisory_count": 2,
"oldest_advisory_days": 439
},
{
"name": "activesupport",
"direct": false,
"version": "8.0.2",
"severity": "high",
"ecosystem": "rubygems",
"cvss_score": 7.5,
"advisory_ids": [
"GHSA-2j26-frm8-cmj9",
"GHSA-89vf-4333-qx8v",
"GHSA-cg4j-q9v8-6v38"
],
"fixed_version": "8.1.2.1",
"advisory_count": 3,
"oldest_advisory_days": 120
},
{
"name": "concurrent-ruby",
"direct": false,
"version": "1.3.5",
"severity": "high",
"ecosystem": "rubygems",
"cvss_score": 7.5,
"advisory_ids": [
"GHSA-6wx8-w4f5-wwcr",
"GHSA-h8w8-99g7-qmvj",
"GHSA-wv3x-4vxv-whpp"
],
"fixed_version": "1.3.7",
"advisory_count": 3,
"oldest_advisory_days": 32
},
{
"name": "nokogiri",
"direct": false,
"version": "1.18.6",
"severity": "high",
"ecosystem": "rubygems",
"cvss_score": 7.5,
"advisory_ids": [
"GHSA-353f-x4gh-cqq8",
"GHSA-5prr-v3j2-97mh",
"GHSA-5v8h-3h3q-446p",
"GHSA-5w6v-399v-w3cc",
"GHSA-8678-w3jw-xfc2",
"GHSA-9cv2-cfxc-v4v2",
"GHSA-c4rq-3m3g-8wgx",
"GHSA-p67v-3w7g-wjg7",
"GHSA-phwj-rprq-35pp",
"GHSA-v2fc-qm4h-8hqv"
],
"fixed_version": "1.19.4",
"advisory_count": 13,
"oldest_advisory_days": 455
},
{
"name": "rack",
"direct": false,
"version": "3.1.12",
"severity": "high",
"ecosystem": "rubygems",
"cvss_score": 7.5,
"advisory_ids": [
"GHSA-47m2-26rw-j2jw",
"GHSA-6xw4-3v39-52mm",
"GHSA-7mqq-6cf9-v2qp",
"GHSA-8vqr-qjwx-82mw",
"GHSA-g2pf-xv49-m2h5",
"GHSA-gjh7-p2fx-99vx",
"GHSA-h2jq-g4cq-5ppq",
"GHSA-mxw3-3hh2-x2mh",
"GHSA-p543-xpfm-54cp",
"GHSA-q2ww-5357-x388"
],
"fixed_version": "3.2.6",
"advisory_count": 21,
"oldest_advisory_days": 439
},
{
"name": "activerecord",
"direct": true,
"version": "8.0.2",
"severity": "moderate",
"ecosystem": "rubygems",
"cvss_score": null,
"advisory_ids": [
"GHSA-76r7-hhxj-r776"
],
"fixed_version": "8.0.2.1",
"advisory_count": 1,
"oldest_advisory_days": 341
},
{
"name": "net-imap",
"direct": false,
"version": "0.5.6",
"severity": "moderate",
"ecosystem": "rubygems",
"cvss_score": 5.3,
"advisory_ids": [
"GHSA-46q3-7gv7-qmgg",
"GHSA-75xq-5h9v-w6px",
"GHSA-87pf-fpwv-p7m7",
"GHSA-8p34-64r3-mwg8",
"GHSA-c4fp-cxrr-mj66",
"GHSA-hm49-wcqc-g2xg",
"GHSA-j3g3-5qv5-52mj",
"GHSA-q2mw-fvj9-vvcw",
"GHSA-vcgp-9326-pqcp"
],
"fixed_version": "0.6.4.1",
"advisory_count": 9,
"oldest_advisory_days": 449
},
{
"name": "actionview",
"direct": false,
"version": "8.0.2",
"severity": "low",
"ecosystem": "rubygems",
"cvss_score": null,
"advisory_ids": [
"GHSA-v55j-83pf-r9cq"
],
"fixed_version": "8.1.2.1",
"advisory_count": 1,
"oldest_advisory_days": 120
}
],
"collected": true,
"truncated": false,
"by_severity": {
"low": 1,
"high": 4,
"critical": 1,
"moderate": 2
},
"advisory_count": 53,
"affected_count": 8,
"assessed_count": 78,
"assessed_package": null,
"unassessed_count": 2,
"direct_affected_count": 1
},
"ecosystems": [
"rubygems"
],
"dependencies": [
{
"name": "actionmailer",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "activejob",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "activerecord",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "appraisal",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "betterlint",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "mysql2",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "pg",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "rake",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "rspec",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "timecop",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "zeitwerk",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
},
{
"name": "sqlite3",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": ">= 1.4"
},
{
"name": "snapshot_testing",
"manifest": "Gemfile",
"ecosystem": "rubygems",
"version_constraint": null
}
],
"all_dependencies": {
"error": null,
"source": "github-sbom",
"packages": [
{
"name": "actionmailer",
"direct": true,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "activejob",
"direct": true,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "activerecord",
"direct": true,
"version": null,
"ecosystem": "rubygems"
},
{
"name": "activerecord",
"direct": true,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "appraisal",
"direct": true,
"version": "2.5.0",
"ecosystem": "rubygems"
},
{
"name": "betterlint",
"direct": true,
"version": "1.19.0",
"ecosystem": "rubygems"
},
{
"name": "mysql2",
"direct": true,
"version": "0.5.6",
"ecosystem": "rubygems"
},
{
"name": "pg",
"direct": true,
"version": "1.5.9",
"ecosystem": "rubygems"
},
{
"name": "rake",
"direct": true,
"version": "13.2.1",
"ecosystem": "rubygems"
},
{
"name": "rspec",
"direct": true,
"version": "3.13.0",
"ecosystem": "rubygems"
},
{
"name": "snapshot_testing",
"direct": true,
"version": "0.3.4",
"ecosystem": "rubygems"
},
{
"name": "sqlite3",
"direct": true,
"version": "2.6.0",
"ecosystem": "rubygems"
},
{
"name": "timecop",
"direct": true,
"version": "0.9.10",
"ecosystem": "rubygems"
},
{
"name": "zeitwerk",
"direct": true,
"version": "2.7.2",
"ecosystem": "rubygems"
},
{
"name": "actionpack",
"direct": false,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "actionview",
"direct": false,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "activemodel",
"direct": false,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "activesupport",
"direct": false,
"version": "8.0.2",
"ecosystem": "rubygems"
},
{
"name": "ast",
"direct": false,
"version": "2.4.3",
"ecosystem": "rubygems"
},
{
"name": "base64",
"direct": false,
"version": "0.2.0",
"ecosystem": "rubygems"
},
{
"name": "benchmark",
"direct": false,
"version": "0.4.0",
"ecosystem": "rubygems"
},
{
"name": "bigdecimal",
"direct": false,
"version": "3.1.9",
"ecosystem": "rubygems"
},
{
"name": "builder",
"direct": false,
"version": "3.3.0",
"ecosystem": "rubygems"
},
{
"name": "concurrent-ruby",
"direct": false,
"version": null,
"ecosystem": "rubygems"
},
{
"name": "concurrent-ruby",
"direct": false,
"version": "1.3.5",
"ecosystem": "rubygems"
},
{
"name": "connection_pool",
"direct": false,
"version": "2.5.0",
"ecosystem": "rubygems"
},
{
"name": "crass",
"direct": false,
"version": "1.0.6",
"ecosystem": "rubygems"
},
{
"name": "date",
"direct": false,
"version": "3.4.1",
"ecosystem": "rubygems"
},
{
"name": "diff-lcs",
"direct": false,
"version": "1.6.1",
"ecosystem": "rubygems"
},
{
"name": "drb",
"direct": false,
"version": "2.2.1",
"ecosystem": "rubygems"
},
{
"name": "erubi",
"direct": false,
"version": "1.13.1",
"ecosystem": "rubygems"
},
{
"name": "globalid",
"direct": false,
"version": "1.2.1",
"ecosystem": "rubygems"
},
{
"name": "i18n",
"direct": false,
"version": "1.14.7",
"ecosystem": "rubygems"
},
{
"name": "json",
"direct": false,
"version": "2.10.2",
"ecosystem": "rubygems"
},
{
"name": "language_server-protocol",
"direct": false,
"version": "3.17.0.4",
"ecosystem": "rubygems"
},
{
"name": "lint_roller",
"direct": false,
"version": "1.1.0",
"ecosystem": "rubygems"
},
{
"name": "logger",
"direct": false,
"version": "1.7.0",
"ecosystem": "rubygems"
},
{
"name": "loofah",
"direct": false,
"version": "2.24.0",
"ecosystem": "rubygems"
},
{
"name": "mail",
"direct": false,
"version": "2.8.1",
"ecosystem": "rubygems"
},
{
"name": "mini_mime",
"direct": false,
"version": "1.1.5",
"ecosystem": "rubygems"
},
{
"name": "minitest",
"direct": false,
"version": "5.25.5",
"ecosystem": "rubygems"
},
{
"name": "net-imap",
"direct": false,
"version": "0.5.6",
"ecosystem": "rubygems"
},
{
"name": "net-pop",
"direct": false,
"version": "0.1.2",
"ecosystem": "rubygems"
},
{
"name": "net-protocol",
"direct": false,
"version": "0.2.2",
"ecosystem": "rubygems"
},
{
"name": "net-smtp",
"direct": false,
"version": "0.5.1",
"ecosystem": "rubygems"
},
{
"name": "nokogiri",
"direct": false,
"version": "1.18.6",
"ecosystem": "rubygems"
},
{
"name": "parallel",
"direct": false,
"version": "1.26.3",
"ecosystem": "rubygems"
},
{
"name": "parser",
"direct": false,
"version": "3.3.7.3",
"ecosystem": "rubygems"
},
{
"name": "prism",
"direct": false,
"version": "1.4.0",
"ecosystem": "rubygems"
},
{
"name": "racc",
"direct": false,
"version": "1.8.1",
"ecosystem": "rubygems"
},
{
"name": "rack",
"direct": false,
"version": "3.1.12",
"ecosystem": "rubygems"
},
{
"name": "rack-session",
"direct": false,
"version": "2.1.0",
"ecosystem": "rubygems"
},
{
"name": "rack-test",
"direct": false,
"version": "2.2.0",
"ecosystem": "rubygems"
},
{
"name": "rails-dom-testing",
"direct": false,
"version": "2.2.0",
"ecosystem": "rubygems"
},
{
"name": "rails-html-sanitizer",
"direct": false,
"version": "1.6.2",
"ecosystem": "rubygems"
},
{
"name": "rainbow",
"direct": false,
"version": "3.1.1",
"ecosystem": "rubygems"
},
{
"name": "regexp_parser",
"direct": false,
"version": "2.10.0",
"ecosystem": "rubygems"
},
{
"name": "rspec-core",
"direct": false,
"version": "3.13.3",
"ecosystem": "rubygems"
},
{
"name": "rspec-expectations",
"direct": false,
"version": "3.13.3",
"ecosystem": "rubygems"
},
{
"name": "rspec-mocks",
"direct": false,
"version": "3.13.2",
"ecosystem": "rubygems"
},
{
"name": "rspec-support",
"direct": false,
"version": "3.13.2",
"ecosystem": "rubygems"
},
{
"name": "rubocop",
"direct": false,
"version": "1.75.1",
"ecosystem": "rubygems"
},
{
"name": "rubocop-ast",
"direct": false,
"version": "1.43.0",
"ecosystem": "rubygems"
},
{
"name": "rubocop-capybara",
"direct": false,
"version": "2.22.1",
"ecosystem": "rubygems"
},
{
"name": "rubocop-factory_bot",
"direct": false,
"version": "2.27.1",
"ecosystem": "rubygems"
},
{
"name": "rubocop-graphql",
"direct": false,
"version": "1.5.5",
"ecosystem": "rubygems"
},
{
"name": "rubocop-performance",
"direct": false,
"version": "1.24.0",
"ecosystem": "rubygems"
},
{
"name": "rubocop-rails",
"direct": false,
"version": "2.30.3",
"ecosystem": "rubygems"
},
{
"name": "rubocop-rake",
"direct": false,
"version": "0.7.1",
"ecosystem": "rubygems"
},
{
"name": "rubocop-rspec",
"direct": false,
"version": "2.31.0",
"ecosystem": "rubygems"
},
{
"name": "rubocop-rspec_rails",
"direct": false,
"version": "2.29.1",
"ecosystem": "rubygems"
},
{
"name": "ruby-progressbar",
"direct": false,
"version": "1.13.0",
"ecosystem": "rubygems"
},
{
"name": "securerandom",
"direct": false,
"version": "0.4.1",
"ecosystem": "rubygems"
},
{
"name": "thor",
"direct": false,
"version": "1.3.2",
"ecosystem": "rubygems"
},
{
"name": "timeout",
"direct": false,
"version": "0.4.3",
"ecosystem": "rubygems"
},
{
"name": "tzinfo",
"direct": false,
"version": "2.0.6",
"ecosystem": "rubygems"
},
{
"name": "unicode-display_width",
"direct": false,
"version": "3.1.4",
"ecosystem": "rubygems"
},
{
"name": "unicode-emoji",
"direct": false,
"version": "4.0.4",
"ecosystem": "rubygems"
},
{
"name": "uri",
"direct": false,
"version": "1.1.1",
"ecosystem": "rubygems"
},
{
"name": "useragent",
"direct": false,
"version": "0.16.11",
"ecosystem": "rubygems"
}
],
"collected": true,
"truncated": false,
"total_count": 80,
"direct_count": 14,
"indirect_count": 66
}
},
"maintainership": {
"issues": {
"open_prs": 3,
"merged_prs": 81,
"open_issues": 6,
"closed_ratio": 0.76,
"closed_issues": 19,
"closed_unmerged_prs": 7
},
"bus_factor": 3,
"bot_contributors": 0,
"top_contributors": [
{
"type": "User",
"login": "albus522",
"commits": 335,
"avatar_url": "https://avatars.githubusercontent.com/u/7812?v=4"
},
{
"type": "User",
"login": "bkeepers",
"commits": 228,
"avatar_url": "https://avatars.githubusercontent.com/u/173?v=4"
},
{
"type": "User",
"login": "sferik",
"commits": 131,
"avatar_url": "https://avatars.githubusercontent.com/u/10308?v=4"
},
{
"type": "User",
"login": "bryckbost",
"commits": 96,
"avatar_url": "https://avatars.githubusercontent.com/u/692?v=4"
},
{
"type": "User",
"login": "smudge",
"commits": 96,
"avatar_url": "https://avatars.githubusercontent.com/u/83998?v=4"
},
{
"type": "User",
"login": "betamatt",
"commits": 65,
"avatar_url": "https://avatars.githubusercontent.com/u/23032?v=4"
},
{
"type": "User",
"login": "laserlemon",
"commits": 34,
"avatar_url": "https://avatars.githubusercontent.com/u/34264?v=4"
},
{
"type": "User",
"login": "lpetre",
"commits": 12,
"avatar_url": "https://avatars.githubusercontent.com/u/193260?v=4"
},
{
"type": "User",
"login": "gaffneyc",
"commits": 10,
"avatar_url": "https://avatars.githubusercontent.com/u/7095?v=4"
},
{
"type": "User",
"login": "bethesque",
"commits": 8,
"avatar_url": "https://avatars.githubusercontent.com/u/446228?v=4"
}
],
"contributors_sampled": 100,
"top_contributor_share": 0.275
},
"quality_signals": {
"has_ci": true,
"has_tests": true,
"ci_workflows": [
"ci.yml",
"linter.yml"
],
"has_docs_dir": false,
"linter_configs": [
".rubocop.yml"
],
"has_editorconfig": false,
"has_linter_config": true,
"has_precommit_config": false
},
"security_signals": {
"lockfiles": [
"Gemfile.lock"
],
"scorecard": {
"checks": [
{
"name": "Binary-Artifacts",
"score": 10,
"reason": "no binaries found in the repo",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
},
{
"name": "Branch-Protection",
"score": 8,
"reason": "branch protection is not maximal on development and all release branches",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection"
},
{
"name": "CI-Tests",
"score": 10,
"reason": "30 out of 30 merged PRs checked by a CI test -- score normalized to 10",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
},
{
"name": "CII-Best-Practices",
"score": 0,
"reason": "no effort to earn an OpenSSF best practices badge detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
},
{
"name": "Code-Review",
"score": 10,
"reason": "all changesets reviewed",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
},
{
"name": "Contributors",
"score": 10,
"reason": "project has 34 contributing companies or organizations",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
},
{
"name": "Dangerous-Workflow",
"score": 10,
"reason": "no dangerous workflow patterns detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dangerous-workflow"
},
{
"name": "Dependency-Update-Tool",
"score": 0,
"reason": "no update tool detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
},
{
"name": "Fuzzing",
"score": 0,
"reason": "project is not fuzzed",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#fuzzing"
},
{
"name": "License",
"score": 10,
"reason": "license file detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license"
},
{
"name": "Maintained",
"score": 7,
"reason": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
},
{
"name": "Packaging",
"score": null,
"reason": "packaging workflow not detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#packaging"
},
{
"name": "Pinned-Dependencies",
"score": 10,
"reason": "all dependencies are pinned",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#pinned-dependencies"
},
{
"name": "SAST",
"score": 0,
"reason": "SAST tool is not run on all commits -- score normalized to 0",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
},
{
"name": "Security-Policy",
"score": 0,
"reason": "security policy file not detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
},
{
"name": "Signed-Releases",
"score": null,
"reason": "no releases found",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
},
{
"name": "Token-Permissions",
"score": 0,
"reason": "detected GitHub workflow tokens with excessive permissions",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
},
{
"name": "Vulnerabilities",
"score": 0,
"reason": "53 existing vulnerabilities detected",
"documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
}
],
"commit": "c726753839bef4bec1b4d5dbe9945cb43d487edc",
"ran_at": "2026-07-21T21:02:01Z",
"aggregate_score": 5.3,
"scorecard_version": "v5.5.0"
},
"has_codeql_workflow": false,
"has_security_policy": false,
"has_dependabot_config": false
}
},
"config": {
"disabled_metrics": [],
"disabled_categories": [],
"disabled_components": {}
},
"source": {
"url": "https://github.com/Betterment/delayed",
"host": "github.com",
"name": "delayed",
"owner": "Betterment"
},
"metrics": {
"overall": {
"key": "overall",
"band": "good",
"name": "Overall health",
"note": null,
"notes": [],
"value": 71,
"inputs": {
"security": 59,
"vitality": 78,
"community": 59,
"governance": 82,
"engineering": 70
},
"components": []
},
"categories": [
{
"key": "vitality",
"band": "good",
"name": "Vitality",
"value": 78,
"weight": 0.22,
"metrics": [
{
"key": "development_activity",
"band": "moderate",
"name": "Development activity",
"note": null,
"notes": [],
"value": 64,
"inputs": {
"commits_last_year": 52,
"human_commit_share": 1,
"days_since_last_push": 8,
"active_weeks_last_year": 18
},
"components": [
{
"key": "push_recency",
"name": "Push recency",
"detail": "last push 8 days ago",
"points": 28.8,
"status": "partial",
"details": [
{
"code": "push_recency",
"params": {
"days": 8
}
}
],
"max_points": 36
},
{
"key": "commit_cadence",
"name": "Commit cadence",
"detail": "18/52 weeks with commits",
"points": 12.5,
"status": "partial",
"details": [
{
"code": "commit_cadence_weeks",
"params": {
"weeks": 18
}
}
],
"max_points": 36
},
{
"key": "commit_volume",
"name": "Commit volume",
"detail": "52 commits in the last year",
"points": 15.5,
"status": "partial",
"details": [
{
"code": "commits_last_year",
"params": {
"count": 52
}
}
],
"max_points": 18
},
{
"key": "openssf_scorecard_maintained",
"name": "OpenSSF Scorecard: Maintained",
"detail": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
"points": 7,
"status": "partial",
"details": [],
"max_points": 10
}
]
},
{
"key": "release_discipline",
"band": "excellent",
"name": "Release discipline",
"note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: Signed-Releases. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"openssf_scorecard_signed_releases"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 100,
"inputs": {
"releases_count": 26,
"latest_release_tag": "v3.1.0",
"releases_from_tags": false,
"days_since_latest_release": 8,
"mean_days_between_releases": 31.1
},
"components": [
{
"key": "ships_releases",
"name": "Ships releases",
"detail": "26 releases published",
"points": 27,
"status": "met",
"details": [
{
"code": "releases_published",
"params": {
"count": 26
}
}
],
"max_points": 27
},
{
"key": "release_recency",
"name": "Release recency",
"detail": "latest release 8 days ago",
"points": 36,
"status": "met",
"details": [
{
"code": "release_recency",
"params": {
"days": 8
}
}
],
"max_points": 36
},
{
"key": "release_cadence",
"name": "Release cadence",
"detail": "a release every ~31.1 days",
"points": 27,
"status": "met",
"details": [
{
"code": "release_cadence",
"params": {
"gap": 31.1
}
}
],
"max_points": 27
},
{
"key": "openssf_scorecard_signed_releases",
"name": "OpenSSF Scorecard: Signed-Releases",
"detail": "no releases found",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 10
}
]
},
{
"key": "abandonment",
"band": "excellent",
"name": "Abandonment",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"cap": null,
"state": "maintained",
"guards": [],
"signals": [],
"red_flag": false,
"multiplier_pct": 100,
"declared_reason": null,
"unverified_reason": null,
"unanswered_open_prs": null,
"unanswered_open_issues": null,
"days_since_last_merged_pr": null,
"days_since_last_human_commit": 8,
"days_since_last_human_commit_is_floor": false
},
"components": [
{
"key": "project_is_still_maintained",
"name": "Project is still maintained",
"detail": "last human commit 8 days ago",
"points": 100,
"status": "met",
"details": [
{
"code": "abandonment_maintained",
"params": {
"days": 8
}
}
],
"max_points": 100
}
]
}
],
"description": "Is the project alive — is code being written and are releases shipping?"
},
{
"key": "community",
"band": "moderate",
"name": "Community & Adoption",
"value": 59,
"weight": 0.18,
"metrics": [
{
"key": "popularity",
"band": "moderate",
"name": "Popularity & adoption",
"note": null,
"notes": [],
"value": 54,
"inputs": {
"forks": 12,
"stars": 175,
"watchers": 44,
"growth_state": "organic",
"growth_factor_pct": 100
},
"components": [
{
"key": "stars",
"name": "Stars",
"detail": "175 stars",
"points": 36.3,
"status": "partial",
"details": [
{
"code": "stars",
"params": {
"count": 175
}
}
],
"max_points": 60
},
{
"key": "forks",
"name": "Forks",
"detail": "12 forks",
"points": 8.7,
"status": "partial",
"details": [
{
"code": "forks",
"params": {
"count": 12
}
}
],
"max_points": 25
},
{
"key": "watchers",
"name": "Watchers",
"detail": "44 watchers",
"points": 9.1,
"status": "partial",
"details": [
{
"code": "watchers",
"params": {
"count": 44
}
}
],
"max_points": 15
}
]
},
{
"key": "community_health",
"band": "moderate",
"name": "Community health",
"note": null,
"notes": [],
"value": 50,
"inputs": {
"has_readme": true,
"has_license": true,
"has_contributing": false,
"has_issue_template": false,
"has_code_of_conduct": false,
"has_pull_request_template": false
},
"components": [
{
"key": "readme",
"name": "README",
"detail": null,
"points": 22.5,
"status": "met",
"details": [],
"max_points": 22.5
},
{
"key": "license",
"name": "License",
"detail": "recognized license (MIT)",
"points": 22.5,
"status": "met",
"details": [
{
"code": "license_standard",
"params": {}
},
{
"code": "license_spdx",
"params": {
"spdx": "MIT"
}
}
],
"max_points": 22.5
},
{
"key": "contributing_guide",
"name": "CONTRIBUTING guide",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 18
},
{
"key": "code_of_conduct",
"name": "Code of conduct",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 13.5
},
{
"key": "issue_template",
"name": "Issue template",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.2
},
{
"key": "pr_template",
"name": "PR template",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 6.3
}
]
},
{
"key": "ecosystem_adoption",
"band": "good",
"name": "Ecosystem adoption (downloads)",
"note": "Excluded from scoring (no data or not applicable): Registry dependents. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"registry_dependents"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 79,
"inputs": {
"packages": [
"delayed"
],
"dependents": null,
"ecosystems": "rubygems",
"total_downloads": 1132655,
"monthly_downloads": null
},
"components": [
{
"key": "total_downloads",
"name": "Total downloads",
"detail": "1,132,655 downloads all-time across rubygems",
"points": 62.9,
"status": "partial",
"details": [
{
"code": "downloads_total",
"params": {
"count": 1132655,
"ecosystems": "rubygems"
}
}
],
"max_points": 80
},
{
"key": "registry_dependents",
"name": "Registry dependents",
"detail": "not reported by this ecosystem",
"points": 0,
"status": "excluded",
"details": [
{
"code": "not_reported_by_this_ecosystem",
"params": {}
}
],
"max_points": 20
}
]
}
],
"description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
},
{
"key": "governance",
"band": "good",
"name": "Sustainability & Governance",
"value": 82,
"weight": 0.24,
"metrics": [
{
"key": "maintainer_resilience",
"band": "good",
"name": "Maintainer resilience (bus factor)",
"note": null,
"notes": [],
"value": 76,
"inputs": {
"bus_factor": 3,
"contributors_sampled": 100,
"top_contributor_share": 0.275
},
"components": [
{
"key": "bus_factor",
"name": "Bus factor",
"detail": "3 contributor(s) cover half of all commits",
"points": 36,
"status": "partial",
"details": [
{
"code": "bus_factor",
"params": {
"count": 3
}
}
],
"max_points": 54
},
{
"key": "commit_distribution",
"name": "Commit distribution",
"detail": "top contributor authored 28% of commits",
"points": 16.3,
"status": "partial",
"details": [
{
"code": "top_contributor_share",
"params": {
"share": 28
}
}
],
"max_points": 22.5
},
{
"key": "contributor_breadth",
"name": "Contributor breadth",
"detail": "100 contributors",
"points": 13.5,
"status": "met",
"details": [
{
"code": "contributors_sampled",
"params": {
"count": 100
}
}
],
"max_points": 13.5
},
{
"key": "openssf_scorecard_contributors",
"name": "OpenSSF Scorecard: Contributors",
"detail": "project has 34 contributing companies or organizations",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
},
{
"key": "responsiveness",
"band": "excellent",
"name": "Issue & PR responsiveness",
"note": null,
"notes": [],
"value": 86,
"inputs": {
"merged_prs": 81,
"open_issues": 6,
"closed_issues": 19,
"issue_closed_ratio": 0.76,
"closed_unmerged_prs": 7
},
"components": [
{
"key": "issue_resolution",
"name": "Issue resolution",
"detail": "76% of issues closed",
"points": 35.5,
"status": "partial",
"details": [
{
"code": "issues_closed_share",
"params": {
"share": 76
}
}
],
"max_points": 46.75
},
{
"key": "pr_acceptance",
"name": "PR acceptance",
"detail": "81/88 decided PRs merged",
"points": 35.2,
"status": "partial",
"details": [
{
"code": "decided_prs_merged",
"params": {
"merged": 81,
"decided": 88
}
}
],
"max_points": 38.25
},
{
"key": "openssf_scorecard_code_review",
"name": "OpenSSF Scorecard: Code-Review",
"detail": "all changesets reviewed",
"points": 15,
"status": "met",
"details": [],
"max_points": 15
}
]
},
{
"key": "stewardship",
"band": "good",
"name": "Ownership & stewardship",
"note": null,
"notes": [],
"value": 70,
"inputs": {
"followers": 105,
"owner_type": "Organization",
"is_verified": null,
"owner_login": "Betterment",
"public_repos": 91,
"account_age_days": 5507
},
"components": [
{
"key": "ownership_backing",
"name": "Ownership backing",
"detail": "organization-owned",
"points": 30,
"status": "met",
"details": [
{
"code": "owner_organization",
"params": {}
}
],
"max_points": 30
},
{
"key": "verified_domain",
"name": "Verified domain",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 20
},
{
"key": "owner_reach",
"name": "Owner reach",
"detail": "105 followers of Betterment",
"points": 14.6,
"status": "partial",
"details": [
{
"code": "owner_followers",
"params": {
"count": 105,
"login": "Betterment"
}
}
],
"max_points": 25
},
{
"key": "track_record",
"name": "Track record",
"detail": "91 public repos, account ~15 yr old",
"points": 25,
"status": "met",
"details": [
{
"code": "public_repos",
"params": {
"count": 91
}
},
{
"code": "account_age_years",
"params": {
"years": 15
}
}
],
"max_points": 25
}
]
},
{
"key": "package_maintenance",
"band": "excellent",
"name": "Package maintenance",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"packages": [
"delayed"
],
"ecosystems": "rubygems",
"any_deprecated": false,
"min_days_since_publish": 8
},
"components": [
{
"key": "published_resolvable",
"name": "Published & resolvable",
"detail": "1 package(s) on rubygems",
"points": 25,
"status": "met",
"details": [
{
"code": "packages_published",
"params": {
"count": 1,
"ecosystems": "rubygems"
}
}
],
"max_points": 25
},
{
"key": "publish_recency",
"name": "Publish recency",
"detail": "latest publish 8 days ago",
"points": 35,
"status": "met",
"details": [
{
"code": "publish_recency",
"params": {
"days": 8
}
}
],
"max_points": 35
},
{
"key": "version_history",
"name": "Version history",
"detail": "29 published versions",
"points": 20,
"status": "met",
"details": [
{
"code": "published_versions",
"params": {
"count": 29
}
}
],
"max_points": 20
},
{
"key": "not_deprecated",
"name": "Not deprecated",
"detail": "active, not deprecated or yanked",
"points": 20,
"status": "met",
"details": [
{
"code": "package_not_deprecated",
"params": {}
}
],
"max_points": 20
}
]
}
],
"description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
},
{
"key": "engineering",
"band": "good",
"name": "Engineering Quality",
"value": 70,
"weight": 0.2,
"metrics": [
{
"key": "engineering_practices",
"band": "good",
"name": "Engineering practices",
"note": null,
"notes": [],
"value": 84,
"inputs": {
"has_ci": true,
"has_tests": true,
"has_editorconfig": false,
"has_linter_config": true,
"has_precommit_config": false
},
"components": [
{
"key": "ci_workflows",
"name": "CI workflows",
"detail": "2 workflow(s)",
"points": 24,
"status": "met",
"details": [
{
"code": "ci_workflows",
"params": {
"count": 2
}
}
],
"max_points": 24
},
{
"key": "tests_present",
"name": "Tests present",
"detail": null,
"points": 24,
"status": "met",
"details": [],
"max_points": 24
},
{
"key": "linter_config",
"name": "Linter config",
"detail": ".rubocop.yml",
"points": 16,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": ".rubocop.yml"
}
}
],
"max_points": 16
},
{
"key": "pre_commit_hooks",
"name": "Pre-commit hooks",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 9.6
},
{
"key": "editorconfig",
"name": ".editorconfig",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 6.4
},
{
"key": "openssf_scorecard_ci_tests",
"name": "OpenSSF Scorecard: CI-Tests",
"detail": "30 out of 30 merged PRs checked by a CI test -- score normalized to 10",
"points": 20,
"status": "met",
"details": [],
"max_points": 20
}
]
},
{
"key": "documentation",
"band": "moderate",
"name": "Documentation",
"note": null,
"notes": [],
"value": 50,
"inputs": {
"topics": [],
"has_wiki": true,
"homepage": null,
"has_readme": true,
"has_docs_dir": false,
"has_description": true
},
"components": [
{
"key": "readme",
"name": "README",
"detail": null,
"points": 30,
"status": "met",
"details": [],
"max_points": 30
},
{
"key": "documentation_directory",
"name": "Documentation directory",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 25
},
{
"key": "documentation_homepage_site",
"name": "Documentation / homepage site",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 15
},
{
"key": "repository_description",
"name": "Repository description",
"detail": null,
"points": 10,
"status": "met",
"details": [],
"max_points": 10
},
{
"key": "topics",
"name": "Topics",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 10
},
{
"key": "wiki",
"name": "Wiki",
"detail": null,
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
}
],
"description": "Are baseline engineering and documentation practices in place?"
},
{
"key": "security",
"band": "moderate",
"name": "Security",
"value": 59,
"weight": 0.16,
"metrics": [
{
"key": "security_posture",
"band": "moderate",
"name": "Security posture",
"note": "Excluded from scoring (no data or not applicable): Packaging, Signed-Releases. Remaining weights renormalized.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"packaging",
"signed_releases"
]
}
},
{
"code": "weights_renormalized",
"params": {}
}
],
"value": 53,
"inputs": {
"source": "openssf_scorecard",
"checks_evaluated": 16,
"scorecard_version": "v5.5.0",
"checks_inconclusive": 2,
"scorecard_aggregate": 5.3
},
"components": [
{
"key": "binary_artifacts",
"name": "Binary-Artifacts",
"detail": "no binaries found in the repo",
"points": 7.5,
"status": "met",
"details": [],
"max_points": 7.5
},
{
"key": "branch_protection",
"name": "Branch-Protection",
"detail": "branch protection is not maximal on development and all release branches",
"points": 6,
"status": "partial",
"details": [],
"max_points": 7.5
},
{
"key": "ci_tests",
"name": "CI-Tests",
"detail": "30 out of 30 merged PRs checked by a CI test -- score normalized to 10",
"points": 2.5,
"status": "met",
"details": [],
"max_points": 2.5
},
{
"key": "cii_best_practices",
"name": "CII-Best-Practices",
"detail": "no effort to earn an OpenSSF best practices badge detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 2.5
},
{
"key": "code_review",
"name": "Code-Review",
"detail": "all changesets reviewed",
"points": 7.5,
"status": "met",
"details": [],
"max_points": 7.5
},
{
"key": "contributors",
"name": "Contributors",
"detail": "project has 34 contributing companies or organizations",
"points": 2.5,
"status": "met",
"details": [],
"max_points": 2.5
},
{
"key": "dangerous_workflow",
"name": "Dangerous-Workflow",
"detail": "no dangerous workflow patterns detected",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
},
{
"key": "dependency_update_tool",
"name": "Dependency-Update-Tool",
"detail": "no update tool detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "fuzzing",
"name": "Fuzzing",
"detail": "project is not fuzzed",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "license",
"name": "License",
"detail": "license file detected",
"points": 2.5,
"status": "met",
"details": [],
"max_points": 2.5
},
{
"key": "maintained",
"name": "Maintained",
"detail": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
"points": 5.2,
"status": "partial",
"details": [],
"max_points": 7.5
},
{
"key": "packaging",
"name": "Packaging",
"detail": "packaging workflow not detected",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 5
},
{
"key": "pinned_dependencies",
"name": "Pinned-Dependencies",
"detail": "all dependencies are pinned",
"points": 5,
"status": "met",
"details": [],
"max_points": 5
},
{
"key": "sast",
"name": "SAST",
"detail": "SAST tool is not run on all commits -- score normalized to 0",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "security_policy",
"name": "Security-Policy",
"detail": "security policy file not detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 5
},
{
"key": "signed_releases",
"name": "Signed-Releases",
"detail": "no releases found",
"points": 0,
"status": "excluded",
"details": [
{
"code": "no_data",
"params": {}
}
],
"max_points": 7.5
},
{
"key": "token_permissions",
"name": "Token-Permissions",
"detail": "detected GitHub workflow tokens with excessive permissions",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
},
{
"key": "vulnerabilities",
"name": "Vulnerabilities",
"detail": "53 existing vulnerabilities detected",
"points": 0,
"status": "missed",
"details": [],
"max_points": 7.5
}
]
},
{
"key": "dependency_advisories",
"band": "excellent",
"name": "Dependency advisories",
"note": "Excluded from scoring (no data or not applicable): Indirect dependencies free of known advisories. Remaining weights renormalized. Matched 78 resolved dependencies against OSV; 2 could not be assessed (no resolved version, an unsupported ecosystem, or beyond the reported package list). This repository publishes no package the index resolves, so the repository dependency graph was assessed instead. That graph mixes development and test pins with shipped dependencies, so only the declared runtime dependencies are scored; transitive findings are reported as context and excluded from the score. Reachability is not analyzed.",
"notes": [
{
"code": "excluded_no_data",
"params": {
"components": [
"indirect_dependencies_free_of_known_advisories"
]
}
},
{
"code": "weights_renormalized",
"params": {}
},
{
"code": "advisories_scope_repository",
"params": {
"assessed": 78
}
},
{
"code": "advisories_unassessed",
"params": {
"count": 2
}
},
{
"code": "advisories_repo_graph_caveat",
"params": {}
},
{
"code": "advisories_reachability",
"params": {}
}
],
"value": 85,
"inputs": {
"source": "osv",
"advisories": 53,
"affected_packages": 8,
"assessed_packages": 78,
"unassessed_packages": 2,
"affected_by_severity": "critical 1, high 4, moderate 2, low 1",
"direct_affected_packages": 1
},
"components": [
{
"key": "direct_dependencies_free_of_known_advisories",
"name": "Direct dependencies free of known advisories",
"detail": "1 affected: activerecord 8.0.2 (moderate)",
"points": 26.6,
"status": "partial",
"details": [
{
"code": "advisories_affected",
"params": {
"count": 1,
"packages": "activerecord 8.0.2 (moderate)"
}
}
],
"max_points": 35
},
{
"key": "indirect_dependencies_free_of_known_advisories",
"name": "Indirect dependencies free of known advisories",
"detail": "transitive set not separable from development and test dependencies in this scope",
"points": 0,
"status": "excluded",
"details": [
{
"code": "advisories_scope_not_separable",
"params": {}
}
],
"max_points": 25
},
{
"key": "no_advisories_left_outstanding",
"name": "No advisories left outstanding",
"detail": "1 advisory-carrying package(s) unaddressed past 90 days; oldest published 341 days ago",
"points": 37.2,
"status": "partial",
"details": [
{
"code": "advisories_stale",
"params": {
"days": 90,
"count": 1,
"oldest": 341
}
}
],
"max_points": 40
}
]
},
{
"key": "malicious_dependencies",
"band": "excellent",
"name": "Malicious dependencies",
"note": null,
"notes": [],
"value": 100,
"inputs": {
"source": "osv",
"meaning": "reported as a malicious package by the OpenSSF corpus; the remedy is removal or moving off the compromised name, never an upgrade of the same artifact. Versions the registry has since pulled are listed but not scored",
"packages": [],
"red_flag": false,
"assessed_packages": 78,
"malicious_packages": 0,
"direct_malicious_packages": 0,
"withdrawn_malicious_packages": 0,
"installable_malicious_packages": 0
},
"components": [
{
"key": "no_dependency_reported_as_a_malicious_package",
"name": "No dependency reported as a malicious package",
"detail": "no dependency is reported as a malicious package",
"points": 100,
"status": "met",
"details": [
{
"code": "no_malicious_dependencies",
"params": {}
}
],
"max_points": 100
}
]
},
{
"key": "high_risk_jurisdiction_exposure",
"band": "excellent",
"name": "High-Risk Jurisdiction Exposure",
"note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
"notes": [
{
"code": "jurisdiction_evidence_limits",
"params": {}
}
],
"value": 100,
"inputs": {
"meaning": "self-published location evidence; not nationality or citizenship",
"red_flag": false,
"exposures": [],
"policy_countries": [
"Russia",
"Iran",
"North Korea"
],
"review_only_matches": 0,
"assessed_self_published_locations": 19
},
"components": [
{
"key": "policy_exposure_multiplier",
"name": "Policy exposure multiplier",
"detail": "no confirmed policy-scope location match",
"points": 100,
"status": "met",
"details": [
{
"code": "jurisdiction_no_match",
"params": {}
}
],
"max_points": 100
}
]
}
],
"description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
},
{
"key": "ai_readiness",
"band": "moderate",
"name": "AI Readiness",
"value": 51,
"weight": 0,
"metrics": [
{
"key": "ai_agent_context",
"band": "at_risk",
"name": "Agent context & guidance",
"note": null,
"notes": [],
"value": 40,
"inputs": {
"has_llms_txt": false,
"legible_history_share": 0.97,
"agent_instruction_files": [],
"agent_instruction_max_bytes": null
},
"components": [
{
"key": "agent_instructions",
"name": "Agent instructions",
"detail": "no CLAUDE.md / AGENTS.md / editor rules",
"points": 0,
"status": "missed",
"details": [
{
"code": "no_agent_instructions",
"params": {}
}
],
"max_points": 45
},
{
"key": "machine_readable_docs_llms_txt",
"name": "Machine-readable docs (llms.txt)",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 15
},
{
"key": "legible_commit_history",
"name": "Legible commit history",
"detail": "97 of 100 human commits state their intent (structured subject or explanatory body)",
"points": 40,
"status": "met",
"details": [
{
"code": "legible_history",
"params": {
"legible": 97,
"sampled": 100
}
}
],
"max_points": 40
}
]
},
{
"key": "ai_verify_loop",
"band": "moderate",
"name": "Verify loop (build / test / typecheck)",
"note": null,
"notes": [],
"value": 57,
"inputs": {
"has_nix": false,
"has_tests": true,
"lockfiles": [
"Gemfile.lock"
],
"has_dockerfile": false,
"typed_language": false,
"bootstrap_files": [],
"has_devcontainer": false,
"has_linter_config": true,
"typecheck_configs": [],
"agent_commit_share": 0.02,
"toolchain_manifests": [],
"dependency_bot_commit_share": 0
},
"components": [
{
"key": "one_command_bootstrap",
"name": "One-command bootstrap",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 18
},
{
"key": "automated_tests",
"name": "Automated tests",
"detail": null,
"points": 22,
"status": "met",
"details": [],
"max_points": 22
},
{
"key": "lint_format_config",
"name": "Lint / format config",
"detail": ".rubocop.yml",
"points": 11,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": ".rubocop.yml"
}
}
],
"max_points": 11
},
{
"key": "static_type_checking",
"name": "Static type checking",
"detail": null,
"points": 0,
"status": "missed",
"details": [],
"max_points": 11
},
{
"key": "reproducible_environment",
"name": "Reproducible environment",
"detail": "lockfile",
"points": 10,
"status": "met",
"details": [
{
"code": "file_list",
"params": {
"files": "lockfile"
}
}
],
"max_points": 10
},
{
"key": "demonstrated_agent_practice",
"name": "Demonstrated agent practice",
"detail": "2 of the last 100 commits agent-authored or agent-credited",
"points": 4,
"status": "partial",
"details": [
{
"code": "agent_authored_commits",
"params": {
"count": 2,
"sampled": 100
}
}
],
"max_points": 10
},
{
"key": "automated_maintenance",
"name": "Automated maintenance",
"detail": "no automated dependency updates observed",
"points": 0,
"status": "missed",
"details": [
{
"code": "no_dependency_automation",
"params": {}
}
],
"max_points": 8
},
{
"key": "openssf_scorecard_pinned_dependencies",
"name": "OpenSSF Scorecard: Pinned-Dependencies",
"detail": "all dependencies are pinned",
"points": 10,
"status": "met",
"details": [],
"max_points": 10
}
]
},
{
"key": "ai_code_legibility",
"band": "moderate",
"name": "Code legibility for models",
"note": null,
"notes": [],
"value": 55,
"inputs": {
"primary_language": "Ruby",
"largest_source_bytes": 48940,
"source_files_sampled": 58,
"oversized_source_files": 0
},
"components": [
{
"key": "type_checkable_code",
"name": "Type-checkable code",
"detail": "Ruby without a type-check config",
"points": 0,
"status": "missed",
"details": [
{
"code": "no_typecheck_config_language",
"params": {
"language": "Ruby"
}
}
],
"max_points": 45
},
{
"key": "manageable_file_sizes",
"name": "Manageable file sizes",
"detail": "0/58 source files over 60KB",
"points": 55,
"status": "met",
"details": [
{
"code": "oversized_source_files",
"params": {
"kb": 60,
"sampled": 58,
"oversized": 0
}
}
],
"max_points": 55
}
]
}
],
"description": "How well is the repo equipped to be developed and maintained with AI coding agents? An independent, experimental badge — weight 0.0, so it is surfaced on its own and does not affect the overall health score."
}
],
"metrics_version": "1.13.0"
},
"warnings": [],
"report_type": "repository",
"generated_at": "2026-07-21T21:02:32.815288Z",
"schema_version": "0.23.0",
"badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/b/Betterment/delayed.svg",
"full_name": "Betterment/delayed",
"license_state": "standard",
"license_spdx": "MIT"
}