Following up on @scribu‘s post with further rationale and the 31compat tag.
The many problems. When plugin upgrades were introduced in 2.8, the activation hook fired for them. The deactivation hook did not. This was inconsistency number one.
When bulk plugin upgrades were introduced on the Tools > Updates screen in 2.9, the activation hook did not fire (during the bulk upgrade). This was inconsistency number two.
Bulk plugin upgrades were given greater prominence in 3.0, with the Updates move to under Dashboard, and a new bulk action on the plugins screen. This made the second inconsistency far more prominent.
Additionally, activation hooks could always fire on activation, because that has to be done through the admin, but updates don’t. Updates done manually (including SVN) was just one more instance where we may not have been firing updates. This was inconsistency number three.
We felt that the proper fix was to prevent the activation hook from firing on any upgrades, bulk or not, as this was not intuitive. Sorry if your plugin relied on this undocumented behavior.
The theory behind the right way to do this. The proper way to handle an upgrade path is to only run an upgrade procedure when you need to. Ideally, you would store a “version” in your plugin’s database option, and then a version in the code. If they do not match, you would fire your upgrade procedure, and then set the database option to equal the version in the code. This is how many plugins handle upgrades, and this is how core works as well.
The right way to do this. I would do what many other plugins do and do the upgrade procedure as I described, and as Peter and Andy described in the previous post.
Further reading. You can read more about this issue, on ticket #14915.