Skip to content

Job order is reversed when using extends in remote configs #1218

@joevin-slq-docto

Description

@joevin-slq-docto

Description

When a remote config file uses extends to load another config file, the job execution order is reversed compared to what would be expected. Jobs from the extending file are executed before jobs from the extended (base) file, which breaks dependency chains.

Steps to Reproduce

Create two config files in a remote repository:

lefthook.yml (base config):

templates:
  remote-dir: .git/info/lefthook-remotes/hooks-main
  use-mise: eval "$(mise -C {remote-dir} env)"

pre-commit:
  jobs:
    - name: setup mise
      run: |
        cd {remote-dir}
        mise trust -q
        mise install

    - name: hooks
      group:
        parallel: true
        jobs:
          - name: ggshield
            run: |
              {use-mise}
              {remote-dir}/scripts/ggshield.sh

lefthook-prettier.yml (extending config):

extends:
  - lefthook.yml

pre-commit:
  jobs:
    - name: hooks
      group:
        jobs:
          - name: prettier
            glob: "*.{js,ts,jsx,tsx}"
            run: |
              {use-mise}
              prettier --check {staged_files}

Project's lefthook.yml:

remotes:
  - git_url: https://github.com/org/hooks
    ref: main
    configs:
      - lefthook-prettier.yml

Expected Behavior

Jobs should execute in this order:

  1. setup mise (from base config)
  2. hooks group with ggshield and prettier in parallel

Actual Behavior

Jobs execute in this order:

  1. hooks group with ggshield and prettier in parallel
  2. setup mise

This breaks the dependency chain because prettier and ggshield require mise to be set up first.

Verification

lefthook dump

Output shows jobs in reversed order:

pre-commit:
  jobs:
    - name: hooks
      group:
        parallel: true
        jobs:
          - name: prettier
            ...
          - name: ggshield
            ...
    - name: setup mise
      ...

Root Cause

The issue is in internal/config/load.go in the mergeJobsSlice function (lines 506-593).

When merging jobs:

  1. Line 510-522: Jobs from dest (already loaded config) are added to result first
  2. Line 524-590: Jobs from src (new config being merged) are added after

In the context of extends within loadRemotes (line 229-232):

  • k (dest) contains lefthook-prettier.yml (loaded first)
  • lefthook.yml (src, via extends) is merged into k
  • Result: lefthook-prettier.yml jobs come before lefthook.yml jobs

The merge order is: destsrc, but for extends we need: src (base) → dest (extending).

Workaround

Don't use extends in remote configs. Instead, load both configs explicitly in the correct order:

remotes:
  - git_url: https://github.com/org/hooks
    ref: main
    configs:
      - lefthook.yml          # Base config (loaded first)
      - lefthook-prettier.yml # Extension (loaded second)

Why this works: When configs are loaded explicitly in sequence without extends, they are merged in the order specified, preserving the correct job execution order.

Impact

Makes extends unusable in remote configs when job ordering matters.

Potential solution

See #1217 but be wary, I have my doubts on relevance of this change.
We might need to find an other solution to order jobs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions