Skip to content

fix(workflows): refuse overlay add that would overwrite a different overlay - #4141

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/overlay-add-no-clobber
Open

fix(workflows): refuse overlay add that would overwrite a different overlay#4141
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/overlay-add-no-clobber

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

Overlay identity in this module is the manifest id, not the filename. _find_overlay_file says so in its own docstring:

Locate a project-local overlay file by its manifest ID, not filename. […] This aligns with ProjectOverlaySource.collect() which also derives identity from the manifest, not the filename.

…and TestOverlayFilenameVsManifestId exists to assert it. So .specify/workflows/overlays/wf/lint.yml legitimately containing id: format is a supported state.

But when _find_overlay_file finds no file carrying the incoming overlay's id, workflow_overlay_add falls back to a target derived purely from the filename:

target_path = _ensure_contained_path(
    target_dir / f"{overlay.id}.yml", _overlay_root(project_root)
)

…and commits onto it unconditionally, never checking who already lives there.

Reproduction on current main (bf88c9f)

lint.yml holds the format overlay; then specify workflow overlay add an unrelated overlay whose id happens to be lint:

BEFORE: lint.yml holds id='format'
add exit: 0
AFTER : lint.yml holds id='lint'  -> the format overlay is GONE
backups left: none

The loss is permanent: _commit_workflow_file renames the victim to a .bak, and _discard_committed_backup_file(backup) on the success path deletes that backup. Exit code 0, no warning.

This is a silent loss of a project-local customization — the exact thing overlays exist to protect (docs/reference/workflows.md: "project overlays in .specify/workflows/overlays/<id>/ are preserved").

Fix

Before committing to the filename-derived fallback, read the occupant and refuse if it is a different overlay. Returning None is this function's existing failure contract; the CLI wrapper turns it into typer.Exit(1).

_read_overlay, err_console and _escape_markup are all already available in this module — no new imports.

Narrowly scoped. The guard fires only when the occupant's id differs. Verified all three paths:

different overlay at lint.yml   exit=1  lint.yml id now='format'   (preserved)
SAME overlay id (an update)     exit=0  lint.yml id now='lint'
no existing file                exit=0  lint.yml id now='lint'

Verification

  • Fail-before / pass-after: 1 new-vs-baseline failure with the source reverted → 26 passed with the fix.
  • Three tests: the clobber is refused and the victim (including its priority: 3) is byte-intact with no stray .bak; an in-place update still works; a fresh add still works.
  • Scoped regression over tests/workflows: no new failures vs a clean-main baseline captured on bf88c9f9 (10 pre-existing, Windows symlink-privilege).
  • uvx ruff@0.15.0 check src tests → clean

Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

… overlay

Overlay identity in this module is the manifest `id`, not the filename —
`_find_overlay_file`'s own docstring says so, and a whole test class asserts
it. So `.specify/workflows/overlays/wf/lint.yml` can legitimately contain
`id: format`.

When `_find_overlay_file` finds no file carrying the incoming overlay's id,
the fallback target is derived purely from the filename and committed onto
unconditionally, with no check for who already lives there:

  BEFORE: lint.yml holds id='format'
  add exit: 0
  AFTER : lint.yml holds id='lint'  -> the format overlay is GONE
  backups left: none

The loss is permanent: `_commit_workflow_file` renames the victim to a
`.bak` and the success path then discards that backup. Exit code 0, no
warning — a silent loss of exactly the project-local customization overlays
exist to protect.

Refuse instead. The guard fires only for a *different* id, so updating an
overlay in place and creating a new file both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner August 15, 2026 14:45
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.

1 participant