Skip to content

Formula, Cask: add livecheck_defined hash value#21717

Closed
samford wants to merge 1 commit into
mainfrom
formula-cask-add-livecheck_defined_value
Closed

Formula, Cask: add livecheck_defined hash value#21717
samford wants to merge 1 commit into
mainfrom
formula-cask-add-livecheck_defined_value

Conversation

@samford
Copy link
Copy Markdown
Member

@samford samford commented Mar 11, 2026

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) with your changes locally?

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

The Tap.autobump logic was recently updated to not skip deprecated packages but this has lead to autobump checking packages that livecheck will automatically skip as deprecated. We want autobump to only check deprecated packages that are checkable and that effectively means only checking those with a livecheck block. To be able to do this, we need to include the livecheck_defined? value in the JSON API data.

This adds a livecheck_defined value to Cask#to_h and Formula#to_hash as a preliminary step. We need to add this value and for it to be present in the latest JSON API data before we can use it in Tap.autobump.

An alternative approach is to modify skip_livecheck to also factor in automatic skips from Livecheck::SkipConditions (or add a different boolean field for that). I didn't go that route because it's much more involved due to needing to identify and check formulae/casks that are referenced in a livecheck block, so it ends up being a fair amount of code. That may be a more robust approach in the long-term (as there are skip conditions other than deprecation) but this livecheck_defined approach should be a simple improvement in the interim time.


This is a follow-up to #21651, as I didn't manage to review it before it was merged. I didn't think anything of it until I was reviewing autobump output and saw a dramatically higher number of "unable to get versions" output, due to deprecated packages without a livecheck block being checked but automatically skipped.

The `Tap.autobump` logic was recently updated to not skip deprecated
packages but this has lead to autobump checking packages that
livecheck will automatically skip as deprecated. We want autobump to
only check deprecated packages that are checkable and that effectively
means only checking those with a `livecheck` block. To be able to do
this, we need to include the `livecheck_defined?` value in the JSON
API data for formulae and casks.

This adds a `livecheck_defined` value to `Cask#to_h` and
`Formula#to_hash` as a preliminary step. We need to add this value and
for it to be present in the latest JSON API data before we can use it
in `Tap.autobump`.
Copilot AI review requested due to automatic review settings March 11, 2026 21:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a livecheck_defined boolean to formula/cask hash outputs intended for inclusion in Homebrew’s JSON API so Tap.autobump can distinguish deprecated packages that are still livecheck-checkable.

Changes:

  • Add "livecheck_defined" to Formula#to_hash.
  • Add "livecheck_defined" to Cask#to_h.
  • Update the everything.json cask fixture to include the new key.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Library/Homebrew/formula.rb Exposes livecheck_defined? in formula hash output.
Library/Homebrew/cask/cask.rb Exposes livecheck_defined? in cask hash output.
Library/Homebrew/test/support/fixtures/cask/everything.json Updates expected cask JSON fixture to include livecheck_defined.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 426 to 429
"autobump" => autobump?,
"no_autobump_message" => no_autobump_message,
"livecheck_defined" => livecheck_defined?,
"skip_livecheck" => livecheck.skip?,
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Adding "livecheck_defined" => livecheck_defined? to Cask#to_h won’t make this value available via the JSON API: Homebrew::API::Cask::CaskStructGenerator ends by calling CaskStruct.from_hash, which slices to CaskStruct props and will drop the new key unless Homebrew::API::CaskStruct is updated. If the goal is to have this in API JSON for Tap.autobump, add a livecheck_defined prop to CaskStruct and plumb it through struct generation/serialization (and then set it on the API-loaded cask’s DSL so cask.livecheck_defined? matches API data).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure that's correct. With this branch checked out, I ran brew generate-formula-api and brew generate-cask-api in the formulae.brew.sh repository and confirmed that the generated JSON contained the expected "livecheck_defined" value.

Comment on lines +2867 to 2868
"livecheck_defined" => livecheck_defined?,
"skip_livecheck" => livecheck.skip?,
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Adding "livecheck_defined" => livecheck_defined? here won’t actually make the field appear in the generated JSON API data: API generation runs FormulaStructGenerator.generate_formula_struct_hash and then FormulaStruct.from_hash, which slices to FormulaStruct props (dropping unknown keys). To expose this in the API output, you’ll need to add a corresponding prop to Homebrew::API::FormulaStruct and plumb it through generate_formula_struct_hash/serialization.

Suggested change
"livecheck_defined" => livecheck_defined?,
"skip_livecheck" => livecheck.skip?,

Copilot uses AI. Check for mistakes.
@bevanjkay
Copy link
Copy Markdown
Member

bevanjkay commented Mar 12, 2026

Just to confirm without looking too deeply, if a deprecated formula doesn't have a defined block, does it still check the default livechekc (for example, using the git strategy)?

My understanding with the previous change was we only wanted to skip bumping formula with a defined livecheck skip block? But I could be wrong.

@MikeMcQuaid
Copy link
Copy Markdown
Member

This adds a livecheck_defined value to Cask#to_h and Formula#to_hash as a preliminary step. We need to add this value and for it to be present in the latest JSON API data before we can use it in Tap.autobump.

@samford Can you elaborate on this? Why do we rely on fields in our public API for livecheck functionality?

@samford
Copy link
Copy Markdown
Member Author

samford commented Mar 13, 2026

Pardon the novella but this requires a fair amount of context/detail.

Just to confirm without looking too deeply, if a deprecated formula doesn't have a defined block, does it still check the default livecheck (for example, using the git strategy)?

My understanding with the previous change was we only wanted to skip bumping formula with a defined livecheck skip block? But I could be wrong.

Deprecated packages without a livecheck block are skipped by default (see Livecheck::SkipConditions) and this is something I originally introduced almost six years ago (I would link you to the original PR but the [archived] homebrew-livecheck repository was unfortunately deleted, so here's the commit in my fork: samford/homebrew-livecheck@9b85087). The only practical difference that removing the previous "deprecated" guard in Tap.autobump made was to allow deprecated packages with a livecheck block to be checked, as the rest are automatically skipped (granted, that appears as an "unable to get versions" error in brew bump because it apparently doesn't handle the skip message correctly).

This PR maintains that behavior but skips deprecated packages that aren't checkable as a way of avoiding wasted effort at an earlier stage. If the intention is really to check all deprecated packages, that takes a lot more work than changing a guard and it's something we should discuss before doing (I wasn't part of the homebrew-core discussion and wasn't able to review the previous brew PR before it was merged). My read of the homebrew-core discussion was that they needed a way to add a deprecated package to autobump but there wasn't one. This PR provides a more refined solution, where we only check deprecated packages that have opted in via a livecheck block (i.e., adding a livecheck block is necessary to allow a deprecated package to be checked by livecheck, so I think this approach makes more sense).

For context on the current skip approach, my perspective was that most upstream situations leading to a package deprecation tend to also imply that there won't be further versions unless something changes. For example, :discontinued tends to be a firm indicator based on explicit language from upstream but :unmaintained can vary depending on if upstream explicitly indicates a project is unmaintained (typically firm but could change) or if there simply hasn't been any activity for years (a soft indicator with greater potential for change). If memory serves, I think:unmaintained was primarily used for the former (at least by me) but it also started being used for the latter over time and that diluted the strength of the signal. [We should probably only use :unmaintained for explicitly unmaintained projects and create a separate reason like :no_recent_activity for the other scenario, as they're notably different.]

If we need to check a package despite deprecation, adding/keeping a livecheck block will do so (the same is true for other skip conditions, so this is common for versioned formulae). Maintainers should account for this when deprecating a package (i.e., removing an existing livecheck block if it shouldn't be checked or adding a simple one like url :stable if it uses the default check and needs to be checked). However, this is often overlooked, so I periodically review deprecated packages and make these changes after the fact (ideally this wouldn't be necessary but I'm not sure of how to improve the situation).

Based on my previous experience with deprecated packages and the statistics for deprecation reasons (see below), I don't think there's enough value to justify checking deprecated packages several times per day using autobump if it's unlikely there will be a new version. This is partly why I presumed that the original goal wasn't to check all deprecated packages (even if they weren't skipped, you can't flip a switch and expect hundreds of default checks to work properly). However, my issue is primarily with the frequency. We may be able to create a separate workflow to check deprecated packages once a week/month/etc. instead and that could make sense but these packages would need to work with livecheck.

If we go down that route, we would have to go through all of the deprecated packages to check livecheck's behavior and add livecheck blocks where needed. Adding a livecheck block would add the packages to the autobump list, so we would also need to add no_autobump! in those cases (maybe adding a new reason for this scenario). This may also require some new logic in brew livecheck to control whether deprecated checks are included, where we would exclude them by default but allow them with a CLI flag (like --autobump).

In my view, flipping the current approach to check deprecated packages by default only makes sense if most deprecated packages should be checked. I can be convinced if there's solid reasoning and data to support this but, as mentioned, I don't think autobump is the right place to check deprecated packages (i.e., it would be a waste of effort for almost all of these to check them several times per day). This also isn't something we should pursue if we will have to manually add hundreds of skip livecheck blocks to skip anything that shouldn't be checked, as that would be a clear indicator that it's the wrong approach.

If we were to check deprecated packages by default, this is also a scenario where we should first go through all deprecated packages, ensure that livecheck works properly (adding livecheck blocks as needed), and add skip livecheck blocks to any that should continue to be skipped. Changing long-standing behavior without doing that first would be putting the cart before the horse and lead to an unnecessarily rough transition.

To summarize, I think our options are:

  • A) Maintain the current skip behavior
    • Allow deprecated packages with a livecheck block to be checked by autobump (this PR).
    • Review existing deprecated packages to remove livecheck blocks from any that don't need to be checked and add a livecheck block to any that should be checked. I usually do this periodically but haven't recently, so I can take another pass. This allows for incremental expansion of checked deprecated packages by adding livecheck blocks to them (even if it's just something like url :stable to enable a default check).
  • B) Maintain the current skip behavior but add another workflow to check deprecated packages less frequently (once a week, once a month, etc.)
    • Requires us to ensure livecheck works properly for all deprecated packages: confirming any default checks, adding a livecheck block where needed along with no_autobump!, adding skip livecheck blocks to any that should always be skipped.
    • Any deprecated packages that should be autobumped instead can simply omit no_autobump!.
    • We would need to decide on whether we rely on maintainers checking the workflow output or have the workflow open an issue with the results, etc.
  • C) Modify SkipConditions to make the skip more granular
    • We could skip when a deprecation reason is one where we have a higher level of confidence there won't be new versions and then check the others. We would have to split low-confidence signals for :unmaintained into a separate reason (this should be done regardless). That said, I don't think this would buy us anything, as the reasons to skip would likely make up the vast majority of deprecated packages (this is the reasoning behind the current approach). We would also end up checking packages with a unique deprecation reason (i.e., a one-off string).
  • D) Check all [non-skipped] deprecated packages by default using autobump
    • Should only be pursued if a majority of deprecated packages should be checked (we don't want to have to add hundreds of skip livecheck blocks) and at the same frequency that autobump uses.
    • Should also ensure that livecheck works properly for all deprecated packages before enabling this behavior.

If we want to check deprecated packages more broadly, I would suggest maintaining the existing skip behavior and creating a separate workflow that checks deprecated packages less frequently than autobump (option B). Otherwise I naturally support option A.

Statistics on deprecations and reasons are as follows:

  • Deprecated formulae: 319
  • Deprecated formulae with a non-skip livecheck block: 33 (I haven't pruned recently and the number is normally much lower)
  • Formula deprecation reasons
    • :unmaintained: 112
    • :repo_archived: 83
    • :unsupported: 42
    • :repo_removed: 16
    • :does_not_build: 13
    • :deprecated_upstream: 11
    • "is not available via HTTPS": 8
    • :versioned_formula: 7
    • "is unsupported, https://docs.brew.sh/Support-Tiers#future-macos-support": 3
    • "needs EOL pcre": 3
    • "changed its license to BUSL on the next release": 2
    • :checksum_mismatch: 1
    • "changed its license to only BUSL in 5.9.0": 1
    • "does not build with 14.1": 1
    • "does not meet homebrew/core's requirements for Python library formulae": 1
    • "fails to run on Linux after rebuild and last release was 2020-12-08": 1
    • "has moved to the ScummVM project": 1
    • "has outstanding CVEs and requires patches to build": 1
    • "is now natively supported by brew": 1
    • "needs EOL pcre, OCaml < 5 and multiple workarounds to build": 1
    • "needs python 2": 1
    • "switched to a commercial license in v4": 1
    • "upstream does not provide tagged versions anymore": 1
    • "uses node@18, which is deprecated": 1
    • "uses deprecated freeimage": 1
    • "uses deprecated libxdiff": 1
    • "uses deprecated node@20": 1
    • "uses deprecated polyglot": 1
    • "uses proprietary licensed software in v4": 1
    • "uses unmaintained protobuf@21": 1
  • Deprecated casks: 956
  • Deprecated casks with a non-skip livecheck block: 0
  • Cask deprecation reasons
    • :fails_gatekeeper_check: 423
    • :unmaintained: 311
    • :discontinued: 164
    • :no_longer_meets_criteria: 31
    • :moved_to_mas: 19
    • "no longer distributes an install package": 2
    • "the package is not compatible with Homebrew's installation parameters": 2
    • :no_longer_available: 2
    • :unreachable: 1
    • "is now distributed with the font variants as individual packages": 1

It's worth noting that the :fails_gatekeeper_check reason is special-cased in SkipConditions, so those packages aren't skipped as deprecated. These use a disable! call with a future date, so they're implicitly deprecated but treated normally until that date.


Can you elaborate on this? Why do we rely on fields in our public API for livecheck functionality?

Tap.autobump is primarily used in brew bump (autobump and bump are intertwined), so this shouldn't be viewed as "livecheck functionality". The brew livecheck command does check the autobump list to skip autobumped packages by default but livecheck continues to use formula and cask files for its functionality (using the JSON API isn't even an option, as it doesn't contain livecheck block information and that would require serializing strategy blocks, which I don't see happening even if we wanted that). Though livecheck's data is essential for bump, it shouldn't be conflated with it or autobump (livecheck doesn't/shouldn't care what downstream consumers of its data do with it).

Tangent aside, I was wondering why the API is used in Tap.autobump as well but you would have to ask Anton why that's the case. It may have simply been, "Other Tap methods use the API and it works, so let's use it here." Neither you nor I asked about it when reviewing the PR where it was introduced, though (it didn't catch my attention at the time but I was admittedly focusing on other code in that PR). I imagine there's a reason for using the API in the Tap methods but I'm not really familiar with the Tap class and its motivations, so I unfortunately don't have any insight there.

@MikeMcQuaid
Copy link
Copy Markdown
Member

Tangent aside, I was wondering why the API is used in Tap.autobump as well but you would have to ask Anton why that's the case. It may have simply been, "Other Tap methods use the API and it works, so let's use it here." Neither you nor I asked about it when reviewing the PR where it was introduced, though (it didn't catch my attention at the time but I was admittedly focusing on other code in that PR). I imagine there's a reason for using the API in the Tap methods but I'm not really familiar with the Tap class and its motivations, so I unfortunately don't have any insight there.

@botantony can you elaborate a bit here?
@samford I'd like to avoid us adding public API entries until we confirm this. Reminder that as-is this increases the JSON API size for everyone when we do this.

@botantony
Copy link
Copy Markdown
Member

"Other Tap methods use the API and it works, so let's use it here."

That's pretty much it. I also wasn't certain how else this could work faster. Using something like Formulary.factory on all formulae in the core tap just to check if a field is true is slow:

$ time brew ruby -e 'CoreTap.new.autobump'
brew ruby -e 'puts CoreTap.new.autobump.first'  0.93s user 0.27s system 64% cpu 1.858 total
$ time brew ruby -e 'CoreTap.new.formula_names.select { |x| Formulary.factory(x).autobump? }'
brew ruby -e   14.07s user 1.09s system 93% cpu 16.211 total

However, I understand if you prefer to prioritize reducing API file size for 99% of regular users

@samford
Copy link
Copy Markdown
Member Author

samford commented Mar 13, 2026

I'd like to avoid us adding public API entries until we confirm this. Reminder that as-is this increases the JSON API size for everyone when we do this.

From some rough math, adding a livecheck_defined boolean field to the JSON API may increase it by 200-250 KB before compression based on our current number of packages. I don't know whether that's a lot in the grand scheme of things (formula.jws.json is 31.9 MB, cask.jws.json is 15.4 MB) but it increases based on the number of packages we have and smaller is better, so I understand your point.

One alternative I considered is that it's technically possible to modify bump/bump-*-pr and brew livecheck to skip packages that are deprecated but don't have a livecheck block, as they also load formula/cask objects. However, TapAuditor also uses Tap.autobump and can't do the same thing as it doesn't load formulae/casks, so that would be an issue.

Using something like Formulary.factory on all formulae in the core tap just to check if a field is true is slow

Full core/cask tap runs in livecheck run into this as well and the time to parse thousands of packages is substantial enough that I've considered printing temporary output to make it clear that it's doing something and not just hanging (~10s on M4 Pro for homebrew/core, ~6s for homebrew/cask). Almost no one does full tap runs, so that's remained a very niche concern.

However, if we checked formula/cask objects in Tap.autobump instead, simple bump and livecheck runs on a small number of packages would incur that overhead on every execution. We could cache the results in a file but users would still periodically pay the price when the cache is invalidated.

Another notable issue is that brew bump and brew livecheck both use Tap.autobump and also parse packages, so a full run on a tap would double the aforementioned times (i.e., parsing all tap packages once in Tap.autobump and again in the command). We could potentially cache the parsed packages when autobump is called to avoid having to do that work twice, so that overhead may be avoidable.

The related Tap methods (autobump, allow_bump?) are written with the assumption that we're reading a list from a file and the overhead will be minor, so we may be able to modify how things work to reduce the substantial overhead when parsing packages (e.g., modify Tap.allow_bump? to function without computing the full autobump list, cache expensive work to avoid duplicating it, parse packages using multiple threads instead of a single-thread if feasible). Unless we're fine adding this JSON field (to avoid all this technical work), I can explore this some more and come back with findings.

@MikeMcQuaid
Copy link
Copy Markdown
Member

That's pretty much it. I also wasn't certain how else this could work faster. Using something like Formulary.factory on all formulae in the core tap just to check if a field is true is slow:

$ time brew ruby -e 'CoreTap.new.autobump'
brew ruby -e 'puts CoreTap.new.autobump.first'  0.93s user 0.27s system 64% cpu 1.858 total
$ time brew ruby -e 'CoreTap.new.formula_names.select { |x| Formulary.factory(x).autobump? }'
brew ruby -e   14.07s user 1.09s system 93% cpu 16.211 total

However, I understand if you prefer to prioritize reducing API file size for 99% of regular users

Now it's in the API it's hard for us to remove it now so no big priority.

We could also just read the raw files or even shell back to grep or rg instead of building the Formula class fully.

One alternative I considered is that it's technically possible to modify bump/bump-*-pr and brew livecheck to skip packages that are deprecated but don't have a livecheck block, as they also load formula/cask objects. However, TapAuditor also uses Tap.autobump and can't do the same thing as it doesn't load formulae/casks, so that would be an issue.

Yeh, I think it'd be worth migrating both to avoid using the API.

The related Tap methods (autobump, allow_bump?) are written with the assumption that we're reading a list from a file and the overhead will be minor, so we may be able to modify how things work to reduce the substantial overhead when parsing packages (e.g., modify Tap.allow_bump? to function without computing the full autobump list, cache expensive work to avoid duplicating it, parse packages using multiple threads instead of a single-thread if feasible).

I think it's reasonable to write a cache onto disk (and into actions/cache) that we can key on the sha256 of the formula file. Similarly, we can write JSON out to disk and perhaps use that as a cache just not publish into the full public API.

Unless we're fine adding this JSON field (to avoid all this technical work), I can explore this some more and come back with findings.

I would rather avoid it if we can.

@samford
Copy link
Copy Markdown
Member Author

samford commented Mar 16, 2026

Thinking about this some more, the issue that spurred me to create this PR was to prevent brew bump from printing misleading "unable to get versions" output for deprecated packages (as it makes it difficult to identify real failures). I will open a separate PR to modify bump to print skip reasons instead (i.e., no need to modify the JSON API), as this is an issue I've been planning to address anyway (#21746). We'll still have deprecated packages in the autobump list but the overhead of skipping those packages is insignificant and we can address that in other ways if needed. Thanks for the discussion.

@samford samford closed this Mar 16, 2026
@MikeMcQuaid MikeMcQuaid deleted the formula-cask-add-livecheck_defined_value branch May 3, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants