Skip to content

${workspace} version range placeholder rejected by NpmSpec in CodeQLPackConfig.from_dict #18

@data-douser

Description

@data-douser

Summary

codeql-bundle crashes with ValueError: Invalid NPM block in '${workspace}': '${workspace}' when any qlpack.yml in the workspace uses the documented ${workspace} / ~${workspace} / ^${workspace} version-range placeholders.

These placeholders are an officially supported feature of CodeQL workspaces and are commonly used for sibling-pack dependencies (e.g. a test/qlpack.yml depending on the matching src/qlpack.yml). They appear in advanced-security/codeql-development-template's per-language qlpack.yml files, so any custom bundle built from a workspace derived from that template fails immediately.

Repro

# qlpack.yml
name: my-org/my-test
version: 0.0.2
dependencies:
  my-org/my-src: ${workspace}
$ codeql-bundle --bundle <bundle-path> --workspace . ...

Traceback

File ".../codeql_bundle/helpers/codeql.py", line 110, in load
    qlpack_config = CodeQLPackConfig.from_dict(qlpack_yml_as_dict)
File ".../codeql_bundle/helpers/codeql.py", line 37, in from_dict
    filtered_dict = {k: _convert_value(k, v) for k, v in dict_.items() if k in fieldset}
File ".../codeql_bundle/helpers/codeql.py", line 33, in _convert_value
    return {k: NpmSpec(v) for k, v in v.items()}
File ".../semantic_version/base.py", line 1294, in parse
    raise ValueError("Invalid NPM block in %r: %r" % (expression, block))
ValueError: Invalid NPM block in '${workspace}': '${workspace}'

Full failing run for reference: https://github.com/ql-mcp-playground/codeql-copilot-control/actions/runs/25194864966/job/73875255677

Root cause

codeql_bundle/helpers/codeql.py calls NpmSpec(v) unconditionally on every dependency value:

def _convert_value(k: str, v: Any) -> Any:
    if k == "version":
        return Version(v)
    elif k == "dependencies":
        return {k: NpmSpec(v) for k, v in v.items()}
    else:
        return v

NpmSpec does not understand the ${workspace} family.

Suggested fix

Detect and either resolve or pass through workspace placeholders. Since codeql-bundle already discovers the local pack set via codeql pack ls, the resolved version of the workspace pack is known and could substitute for the placeholder, mirroring what codeql pack publish does. Minimal sketch:

WORKSPACE_PLACEHOLDERS = ("${workspace}", "~${workspace}", "^${workspace}")

def _convert_dep(name: str, spec: str, workspace_versions: Dict[str, Version]) -> NpmSpec:
    if spec in WORKSPACE_PLACEHOLDERS:
        v = workspace_versions.get(name)
        if v is None:
            return NpmSpec("*")  # source-resolved; any version
        prefix = spec[: spec.index("$")]  # "", "~", "^"
        return NpmSpec(f"{prefix}{v}")
    return NpmSpec(spec)

* is also acceptable for source-resolved deps per the GitHub docs ("Using \"*\" for source dependencies makes it explicit that the version is inherited from the workspace").

Impact

Blocks codeql-bundle-action and any direct codeql-bundle invocation against workspaces that use the documented ${workspace} placeholder. Cross-ref: filing a companion issue against advanced-security/codeql-bundle-action.

Environment

  • codeql-bundle 0.4.1 (as bundled in advanced-security/codeql-bundle-action@v2.2.0)
  • Python 3.11.15
  • semantic-version 2.10.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions