Skip to content

[Bug]: workflow update rejects an archive catalog release when its version has leading zeros (for example CalVer 2024.02.01) #5075

Open

Description

@hnipps

Bug Description

specify workflow update normalises the installed and catalog versions with packaging before passing them on (src/specify_cli/workflows/command_update.py:88-92), so 2024.02.01 becomes 2024.2.1. Such versions are valid workflow versions, because the format is \d+\.\d+\.\d+ (src/specify_cli/workflows/engine.py:132). When the catalog URL is a YAML file, the downloaded version is compared with versions_match, which compares Version objects, and the update succeeds. When the catalog URL is an archive (a workflow package), _install_workflow_package compares plain strings (src/specify_cli/workflows/_commands.py:761 and :828-832). "2024.02.01" != "2024.2.1", so the update fails even though the package matches the catalog.

Steps to Reproduce

Save the script below as repro.py in a fresh clone (after uv sync) and run uv run python -I repro.py. It serves a catalog and a zip package from a local 127.0.0.1 HTTP server.

import functools, http.server, io, json, os, subprocess, tempfile, threading, zipfile
from pathlib import Path

def wf(ver):
    return (f'schema_version: "1.0"\nworkflow:\n  id: "calwf"\n  name: "Cal WF"\n'
            f'  version: "{ver}"\nsteps:\n  - id: s1\n    type: shell\n    run: "true"\n')

proj = Path(tempfile.mkdtemp())
site = proj / "site"
site.mkdir()
class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *args):
        pass
handler = functools.partial(Quiet, directory=str(site))
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_address[1]}"

buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
    z.writestr("workflow.yml", wf("2024.02.01"))
    z.writestr("README.md", "companion file")
(site / "calwf.zip").write_bytes(buf.getvalue())
(site / "catalog.json").write_text(json.dumps({"workflows": {"calwf": {
    "id": "calwf", "name": "Cal WF", "version": "2024.02.01", "url": base + "/calwf.zip"}}}))

wdir = proj / ".specify" / "workflows" / "calwf"
wdir.mkdir(parents=True)
(wdir / "workflow.yml").write_text(wf("2024.01.15"))
(proj / ".specify/workflows/workflow-registry.json").write_text(json.dumps({
    "schema_version": "1.0", "workflows": {"calwf": {
        "name": "Cal WF", "version": "2024.01.15", "description": "",
        "source": "catalog", "catalog_name": "env-override", "url": base + "/calwf.zip"}}}))

env = {**os.environ, "SPECKIT_WORKFLOW_CATALOG_URL": base + "/catalog.json"}
r = subprocess.run(["specify", "workflow", "update", "calwf"], cwd=proj, env=env,
                   input="y\n", capture_output=True, text=True)
print(r.stdout, r.stderr, f"exit={r.returncode}", sep="")
reg = json.loads((proj / ".specify/workflows/workflow-registry.json").read_text())
print("registry version:", reg["workflows"]["calwf"]["version"])

Expected Behavior

The update succeeds, exits 0 and records version 2024.02.01, as it does when the same catalog entry points at a .yml URL. The docstring at src/specify_cli/workflows/_commands.py:1116 says expected_version "rejects a downloaded workflow whose version does not match the catalog version"; here the downloaded version does match.

Actual Behavior

The update fails and the registry keeps the old version:

🔄 Checking for updates...


Updates available:

  • calwf: 2024.1.15 → 2024.2.1

Update these workflows? [y/N]: 
Error: Downloaded workflow version (2024.02.01) does not match the catalog 
version (2024.2.1).

Failed to update: calwf
exit=1
registry version: 2024.01.15

Specify CLI Version

1.2.4.dev0 (main at 27d14e2)

AI Agent

Not applicable

Operating System

Linux 6.18 (Ubuntu container)

Python Version

Python 3.12.3

Error Logs

Error: Downloaded workflow version (2024.02.01) does not match the catalog
version (2024.2.1).

Additional Context

The expected_installed_version check in the same function (_commands.py:828-832) uses the same exact string comparison, so it would reject the update for the same reason. Comparing both with versions_match, as the YAML path does, would fix it.

Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.

AI Disclosure

Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.

Activity

  1. added
    triage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after review
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after review

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions