Skip to content

fix: unbreak mix igniter.upgrade --git-ci, and positional args after flags - #397

Merged
zachdaniel merged 2 commits into
mainfrom
fix/igniter-upgrade-git-ci
Aug 4, 2026
Merged

fix: unbreak mix igniter.upgrade --git-ci, and positional args after flags#397
zachdaniel merged 2 commits into
mainfrom
fix/igniter-upgrade-git-ci

Conversation

@jimsynz

@jimsynz jimsynz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The shared mix igniter.upgrade job in ash-project's reusable CI workflow has
failed on every dependabot PR across the ecosystem. Investigating it turned up
two independent bugs in igniter.

1. --git-ci rejects its own documented invocation

$ mix igniter.upgrade --git-ci --yes
Must specify at least one package to upgrade or use --all to upgrade all packages.
$ echo $?
1

Under --git-ci, Igniter.Upgrades.upgrade/1 takes the "before" versions from
git show HEAD~1:mix.lock and skips apply_and_fetch_dependencies/2 entirely,
so the upgrade set comes purely from the lockfile diff. The positional package
list is never read on that path — deps_to_update is computed and discarded.
Requiring it only rejected a valid invocation.

The guard now only applies when it can actually mean something.

2. A boolean flag swallows the positional argument after it

extract_positional_args/1 walked argv with OptionParser.next(argv, switches: []).
With no schema, OptionParser can't tell a boolean flag from one that takes a
value, so it consumes the following token as that flag's value:

OptionParser.next(["--yes", "igniter"], switches: [])
#=> {:ok, :yes, "igniter", []}

OptionParser.next(["--yes", "igniter"], switches: [yes: :boolean])
#=> {:ok, :yes, true, ["igniter"]}

So positional arguments written after a boolean flag were silently dropped:

invocation before after
mix igniter.apply_upgrades igniter:0.8.0:0.8.1 -y
mix igniter.apply_upgrades -y igniter:0.8.0:0.8.1 Must provide one or more values for positional argument packages
mix igniter.upgrade --git-ci jason ❌ package silently ignored

The task's schema was available at the call site all along; it just wasn't
passed to next/2. This affects every Igniter task with positional arguments,
and aliases were affected the same way (-y foo ate foo).

While fixing it, the three byte-identical copies of extract_positional_args/1
(Igniter.Mix.Task, Igniter.CopiedTasks, Mix.Tasks.Igniter.Install) are
consolidated into the one in Igniter.Mix.Task, which now takes OptionParser
options.

Verification

  • Three regression tests in test/igniter/mix/task_test.exs. The two covering
    flag-then-positional fail on main; the third asserts a value-taking flag
    still consumes its value, guarding against over-correction.

  • Full suite, mix credo --strict, mix dialyzer and mix format --check-formatted
    pass. Mix.Tasks.Igniter.Phx.InstallTest fails identically before and after
    (it asserts a generated-file count that depends on the local phx_new).

  • End-to-end against a scratch project with igniter as a path dep and a
    simulated dependabot lock bump, reproducing the CI conditions:

    $ mix igniter.upgrade --git-ci --yes     # exactly what CI runs
    * Upgraded packages:
      jason 1.4.4 => 1.4.5
    $ echo $?
    0
    

    mix.lock is left untouched, confirming the --git-ci path still doesn't
    fetch.

Note on a related latent bug, not fixed here

When a dependabot PR adds a dependency, dep_changes_in_order/2 yields
{app, nil, version}, and if that new package ships an upgrade task
Igniter.Upgrades.run/5 builds the requirement "> #{nil} and <= #{to}" and
crashes with Version.InvalidRequirementError: invalid requirement: "> and <= 0.8.3".
I hit this while setting up the reproduction and left it alone as out of scope —
happy to file it separately.

jimsynz added 2 commits August 5, 2026 10:13
`extract_positional_args/1` walked argv with `OptionParser.next(argv,
switches: [])`. Without a schema `OptionParser` cannot know an option is a
boolean, so it treats the next token as that option's value:

    OptionParser.next(["--yes", "igniter"], switches: [])
    #=> {:ok, :yes, "igniter", []}

    OptionParser.next(["--yes", "igniter"], switches: [yes: :boolean])
    #=> {:ok, :yes, true, ["igniter"]}

Any positional argument written after a boolean flag was therefore swallowed
into the flags and silently lost, so `mix igniter.apply_upgrades -y
igniter:0.8.0:0.8.1` failed with "Must provide one or more values for
positional argument `packages`", while the same arguments in the other order
worked.

Pass the task's own switches and aliases, merged with the global options, so
each flag's arity is known. Also consolidates the three byte-identical copies
of this function into the one in `Igniter.Mix.Task`.
Under `--git-ci` the set of packages to upgrade comes entirely from diffing
`HEAD~1:mix.lock` against the loaded deps, and the dependency fetch that
consumes the package list is skipped. The positional arguments are never read,
so demanding them served only to reject a valid invocation:

    $ mix igniter.upgrade --git-ci --yes
    Must specify at least one package to upgrade or use --all to upgrade all packages.

That is how the shared ash-project CI job invokes the task, so every dependabot
PR in the ecosystem failed this check.
@zachdaniel
zachdaniel merged commit 31db3a5 into main Aug 4, 2026
42 of 50 checks passed
@jimsynz
jimsynz deleted the fix/igniter-upgrade-git-ci branch August 5, 2026 00:40
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.

2 participants