HN user

stevepike

656 karma

steve (at) infield.ai - We automate Ruby on Rails upgrades.

Posts12
Comments133
View on HN

The real issue with Rails apps is keeping up with the framework and language versions. There are really two categories of dependencies.

One-off libraries that don't have a runtime dependency on Rails are typically very low-maintenance. You can mostly leave them alone (even a security vulnerability is unlikely to be exploitable for how you're using one of these, as often user input isn't even getting through to them). For instance a gem you install to communicate with the stripe API is not typically going to break when you upgrade Rails. Or adding httparty to make some API requests to other services.

Then there are libraries that are really framework extensions, like devise for authentication or rspec for testing. These are tightly coupled to Rails, sometimes to its private internals, and you get all sorts of nasty compatibility issues when the framework changes. You have to upgrade Rails itself because you really do need to care about security support at that level, even for a relatively small company, so you can end up in a situation where leaving these other dependencies to fester makes upgrading Rails very hard.

(I run a startup that's a software-enabled service to upgrade old Rails apps).

It looks like this is a community continuation of the axlsx gem which was maintained back in the day by Randy Morgan (randym) over at https://github.com/randym/axlsx. One of my earliest open source contributions was adding support so that you could output spreadsheets with "conditional" formatting (color something red if it is below some value, for instance). I remember Randy being extremely supportive of new contributors and it made me want to be a part of the ruby community.

Thanks for continuing the work!

Claude Sonnet 4.6 5 months ago

Yeah, earlier in the GPT days I felt like this was a good example of LLMs being "a blurry jpeg of the web", since you could give them something that was very close to an existing puzzle that exists commonly on the web, and they'd regurgitate an answer from that training set. It was neat to me to see the question get solved consistently by the reasoning models (though often by churning a bunch of tokens trying and verifying to count 888 + 88 + 8 + 8 + 8 as nine digits).

I wonder if it's a temperature thing or if things are being throttled up/down on time of day. I was signed in to a paid claude account when I ran the test.

Thanks! We support Python, JS, and Ruby right now (started with dynamic languages).

I'm not sure what you mean by prioritization on the issues, but generally we are trying to help you figure out what to upgrade next, and to actually do it too.

This is hard for lots of companies. Some ignore the problem entirely until there's a fire drill (which can be a huge risk if you end up on an old major version that won't get patched). Some keep everything up to date, and then taking a new security patch is trivial. It's always risk/reward tradeoff between the risk of breaking production with an upgrade and the value an org sees from staying up to date. We work on this problem at Infield (https://www.infield.ai/post/introducing-infield) where we tackle both sides of the project management: "Which dependencies should I prioritize upgrading" and "How difficult and likely to break production is this upgrade".

To your specific points

1. How do you decide what's actually urgent? CVSS? EPSS? Manual assessment?

The risk factors we track are open CVEs, abandonment (is this package supported by the maintainer?), and staleness (how deep in the hole am I?).

We also look at the libyear metric as an overall indication of dependency health.

2. Do you treat "outdated but not vulnerable" dependencies differently from "has CVEs"?

We group upgrades into three general swimlanes:

  - "trivial" upgrades (minor/patch versions of packages that respect semantic versioning, dev/test only packages). We batch these together for our customers regardless of priority.

  - "could break". These deserve standalone PRs and an engineer triaging when these become worth tackling, if ever.

  - "major frameworks". Think something like Rails. These are critical to keep on supported versions of because the rest of the ecosystem moves with them, and vulnerabilities in them tend to have a large blast radius. Upgrading these can be hard. You'll definitely need to upgrade someday to stay supported, and getting there has follow-on benefits on all your other dependencies, so these are high priority.
> 3. For those using Dependabot/Renovate/Snyk - what's your workflow? Do you review every alert or have you found a good filtering system?

We offer a Github app that integrates with alerts from Dependabot. While security teams are happy with just a scanner, the engineering teams that actually do this upgrade work need to mash that up with all the other data we're talking about here.

This is cool, it looks to me like you're integrating static analysis on the user's codebase and the underlying dependency. Very curious to see where it goes.

We've found dependency upgrades to be deceptively complex to evaluate safety for. Often you need context that's difficult or impossible to determine statically in a dynamically typed language. An example I use for Ruby is the kwarg migration from ruby 2.7->3 (https://www.ruby-lang.org/en/news/2019/12/12/separation-of-p...). It's trivial to profile for impacted sites at runtime but basically impossible to do it statically without adopting something like sorbet. Do you have any benchmarks on how reliable your evaluations are on plain JS vs. typescript codebases?

We ended up embracing runtime profiling for deprecation warnings / breaking changes as part of upgrading dependencies for our customers and have found that context to unlock more reliable code transformations. But you're stuck building an SDK for every language you want to support, and it's more friction than installing a github app.

This is very true. It can be a real fire drill if it turns out you need to go up a major version in some other dependency in order to get a security fix. It can get even worse in JS if you're on some abandoned package that's pinned to an old version of some transient dependency which turns out to be vulnerable. Then you're scrambling to migrate to some alternate package with no clear upgrade path.

On the flipside sometimes you get lucky and being on an old version of a package means you don't have the vulnerability in the first place.

libyear is a helpful metric for tracking how much of this debt you might have.

This seems to show the power of the reasoning models over interacting with a prompted chat-tuned LLM directly. If I navigate backwards on your link Sonnet 4 gets it right.

I've used a similar prompt - "How can you make 1000 with exactly nine 8s using only addition?"

Here's GPT 4.5 getting it wrong: https://chatgpt.com/share/683f3aca-8fbc-8000-91e4-717f5d81bc...

It tricks it because it's a slight variation of an existing puzzle (making 1000 with 8 8s and addition only).

The reasoning models seem to reliably figure it out, though. Some of them even come up with a proof of why it's impossible to do with 9 8s. Here's o4 getting it right: https://chatgpt.com/share/683f3bc2-70b8-8000-9675-4d96e72b58...

I think the kind of application here matters a lot, specifically whether you're trying to make a change to a web app or if you're hacking on library code.

In ruby, for example, I can pretty trivially clone any open source gem and run the specs in < 5 minutes. Patching something and opening a PR in under an hour is definitely standard.

On the other hand, getting a development environment running for a company's proprietary web app is often a hassle. Mostly though this isn't because of the language or dependencies, it's because of:

  - Getting all the dependent services up and running (postgres version X, redis Y, whatever else) with appropriate seed data. 
  - Getting access to development secrets
My company (infield.ai) upgrades legacy apps, so we deal with setting up a lot of these. We run them in individual siloed remote developer environments using devcontainers. It works OK once we've configured the service containers.
Libyear 2 years ago

They're tracking different related things. I run a startup in this space and we track: aggregate libyear of your direct dependencies; total # of direct dependencies with libyear > 2; # of direct dependencies at least one major version behind; dependencies that have been abandoned by the maintainer.

I think the top-line aggregate libyear number is helpful to monitor over time to get a general sense of the slope of your technical debt. If the number is trending upwards then your situation is getting worse and you're increasing the chance you find yourself in an emergency (i.e., a CVE comes out and you're on an unsupported release line and need to go up major versions to take the patch).

Tracking total # of major versions behind gets at the same thing but it's less informative. If you're on v1 of some package that has a v2 but is actively releasing patches for the v1 line that should be a lower priority upgrade than some other package where your v1 line hasn't had a release in 5 years.

Oh man, this brings me back! Almost 10 years ago I was working on a rails app trying to detect the file type of uploaded spreadsheets (xlsx files were being detected as application/zip, which is technically true but useless).

I found "magic" that could detect these and submitted a patch at https://bugs.freedesktop.org/show_bug.cgi?id=78797. My patch got rejected for needing to look at the first 3KB bytes of the file to figure out the type. They had a hard limit that they wouldn't see past the first 256 bytes. Now in 2024 we're doing this with deep learning! It'd be cool if google released some speed performance benchmarks here against the old-fashioned implementations. Obviously it'd be slower, but is it 1000x or 10^6x?

How does this work on the backend? Does it only trace method calls when an exception is thrown, or does it profile the call stack of every request?

Something I've been interested in is the performance impact of using https://docs.ruby-lang.org/en/3.2/Coverage.html to find unused code by profiling production. Particularly using that to figure out any gems that are never called in production. Seems like it could be made fast.

(Good project management is the key to this work. You need to spend the time up front (scripts can help here, but the tooling isn't awesome) to figure out everything that's going to block your language upgrade. For all those blockers you want to work as hard as you can to find backwards-compatible fixes (like upgrading a blocking dependency to a version that's dual-compatible with your current and next language version). Then you work through small incremental PRs so your eventual large upgrade is easier. This also works for code changes around breaking changes - often you can make these changes ahead of time, standalone. I've done individual rails minor version upgrades that took 100 PRs of pre-work.

In my experience often developers try to take a shortcut where they try to bang out the large upgrade in one giant PR. Sometimes this works but it's very risky - more often I see very long running branches that get abandoned. It's much better to make guaranteed incremental progress, plus your PRs are much easier for your team to review.

My startup Infield solves this problem for python / ruby / JS backends through software for the project planning (we run a solver over your dependency graph and read every changelog to identify breaking changes) and people for the breaking changes (our team will open PRs where we fix breaking changes so your code is compatible). We're starting to think about moving into the statically typed backend world with java / .net / maybe scala. I'd love to hear more about exactly what you've run into and whether my experience here matches with these ecosystems.

Rails 7.1 Released 3 years ago

I wouldn't necessarily recommend moving from an existing react frontend to hotwire at the same time as upgrading Rails itself. I'd get to Rails 7.1 first/independently.

FWIW the Rails 7+ way of doing single page app JS is also more reasonable than it used to be. You pretty much just use whatever build tool you want in isolation from your rails app. You have to run something like `yarn build --watch` alongside `rails s` in development but IMO that's not so bad and makes things a lot simpler.

My company builds software to automate Rails upgrades, and offers a full service where we'll upgrade Rails for you. steve (at) infield.ai if that's interesting or you can generate a free rails upgrade plan at https://app.infield.ai/users/sign_up (no cc required). https://docs.infield.ai/docs/creating-an-upgrade-path for some more details.

This is really cool. Do you have any results you can share in using this on your own projects (e.g., contributions you made you wouldn't have otherwise come up with)? I'm similarly looking to do some hacktoberfest contributions and want to find ways to make them more impactful.

One thing I'd love to do with a graph analysis like this is to find the most commonly used _old unsupported versions_ of open source packages across projects that use them on github. My startup tries to make upgrading open source easier and we'd spend some time contributing upgrade guides / changelogs / writing upgrade scripts based on that data.

(disclaimer - I'm the founder of infield.ai; we're software for upgrading dependencies safely, even when there are breaking changes). I spent most of last year upgrading large Rails/React apps as a consultant so I have lots of thoughts on this. In general companies are in one of two states - deep in a hole of technical debt, or in a good place and trying to stay relatively up-to-date.

Most established JS/Ruby codebases I've seen have dependency technical debt and they _know_ they're going to run into breaking changes trying to upgrade. These companies need to do the project management work to break their debt down into as many small incremental changes as they can. Any upgrade that has a breaking change gets done standalone (you need to read the changelog to figure out if there's a breaking change, you don't trust semver). Upgrades that are necessary but safe can be batched together. When you have to make code changes for breaking changes these should also get done incrementally ahead of time. Usually these changes are backwards compatible (e.g., if a deprecated method is going away in v2.0, you can switch to the new method in v1.X so you're not upgrading the package and changing your code in the same PR). Even if they're not trivially backwards compatible you can often make them so, and you should. I've done individual upgrades consisting of dozens of small pull requests. When you do it this way you don't get stuck with a long-running branch that gets abandoned, you roll back fewer deploys, and changes are easy to review.

Once you're on the latest major version of all your packages then you want to get into an ongoing cadence of dependency maintenance. Good small-medium teams tend to do a maintenance rotation where you spend ~1 day/sprint of one developer's time on this upgrade work. Here project management is also important - ideally someone is looking at CVEs, stale packages, and other indicators of package risk, reading changelogs to identify effort, and breaking out maintenance tickets. Some of these tickets will be individual package upgrades, some will be batches of safe upgrades, and some (when a new major version of a framework comes out) will get treated like the previous paragraph. Larger teams might consolidate this work in a platform engineering team, but that has its own organizational challenges.

So I'm sitting here on a linux (KDE) desktop machine with the following apps open:

1. Slack 2. Discord 3. Signal 4. Mullvad VPN 5. Kate 6. 1Password 7. Emacs 8. Chrome 9. Spotify 10. Konsole

How many of these wouldn't exist on Linux if they had to be written with native SDKs? Konsole and Kate use QT and are clearly the best UX, but it's not like emacs (gtk) integrates natively in KDE any better than Slack does. Zoom is clearly the worst experience of all of them, and I think that's actually a QT app?

For me it is. Partly though that's because I boot windows so rarely that when I do I have to sit through a whole bunch of updates.

I prefer linux to windows generally so the ability to game without re-booting is a bonus on top of that. If I preferred being in windows I wouldn't run linux just for gaming.

Mostly agreed to all this.

I've had a better experience since I switched from arch to ubuntu. For example steam remote play works w/ my Apple TV upstairs. Under arch I couldn't ever get it to work.

I've had mixed experience with AMD vs Nvidia. I bought a 5700xt which was way less reliable than my old nvidia 980ti, which never once crashed under linux. I upgraded to a 6700xt last year and that's been smooth. I'd originally bought the AMD card hoping to run Wayland but am still on X11.

I'm mostly playing single player games.

This is just an excellent comment, thank you.

One note - we're not as concerned with predicting exactly the user's build as I think your $DAYJOB might be. We need to scan the space of possible valid resolutions that'd do a big framework upgrade, then pick an optimal (least effort) option from that space and chart your path towards it. Concretely that'll mean opening incremental PRs against your codebase where we provide lockfiles that are individually valid and accumulate in getting your upgrade done.