Skip to content

Conversation

@soumyaDghosh
Copy link
Contributor

  1. added the new linter
  2. lints based on ranks
  3. added unit tests and spread tests for the new linter
  4. added icon field to SnapMetadata from Project model
  5. added a new LinterResult value named INFO
  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • Have you successfully run make lint?
  • Have you successfully run make test?

closes #5544 #5155

@soumyaDghosh soumyaDghosh requested a review from mr-cal as a code owner July 10, 2025 20:52
@soumyaDghosh soumyaDghosh force-pushed the metadata-linter branch 7 times, most recently from 20dac75 to 13cc377 Compare July 12, 2025 09:01
Copy link
Contributor

@lengau lengau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! I think this is great at a high level - just some implementation nits :-)

@soumyaDghosh soumyaDghosh force-pushed the metadata-linter branch 4 times, most recently from 5272796 to 5b6697c Compare July 17, 2025 21:36
Copy link
Collaborator

@mr-cal mr-cal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, this is looking good. I've left some feedback from testing.

We're missing this part of the task:

New documentation on how to manage warnings from the metadata linter

You can refer to the existing documentation for the classic and library linters.

Can you also add a new row to this table for the common-id? Your new entry in the table should link upstream.

@medubelko medubelko removed the request for review from jahn-junior July 29, 2025 21:16
@soumyaDghosh
Copy link
Contributor Author

class field

MetadataField(
        "common_id",
        LinterResult.WARNING,
        lambda meta: _get_apps_attr(meta, "common_id"),
        f"{_HELP_URL}#apps.<app-name>.common-id",
        lambda meta: bool((meta.type and meta.type != "app") or not meta.apps),
    ),
    

helper to get the attrs

    # Helper to pull the matching attribute values from all the apps
def _get_apps_attr(meta: SnapMetadata, key: str) -> dict[str, list[str]] | None:
    if not meta.apps:
        return None

    values: dict[str, list[str]] = {}
    for name, app in meta.apps.items():
        value: str | list[str] | None = getattr(app, key, None)
        if value and isinstance(value, list):
            values[name] = value
        elif isinstance(value, str):
            values[name] = [value]
        else:
            values[name] = []

    return values if values else None

Condition to get the

        elif isinstance(result, dict):
            for name, is_empty in result.items():
                if is_empty:
                    issues.append(
                        self._create_issue(
                            field,
                            f"Metadata field '{field_name}' for app '{name}' is missing or empty.",
                        )
                    )

Helper for validation

@classmethod
    def _is_dict_empty(
        cls, value: dict[str, list[str]] | None
    ) -> dict[str, bool] | bool:
        """Check if dictionary values are empty or None.

        :param value: Dictionary to check

        :returns: True if all values are empty, Dict mapping keys to empty status otherwise
        """
        if value is None:
            return True
        result = {}
        for key, values in value.items():
            if not values:
                result[key] = True

        return result if result else False

Unit Tests

def test_metadata_linter_common_id(new_dir, snap_yaml_data):
    yaml_data = snap_yaml_data(**{"apps": {"app1": {"command": "bin/mytest"}}})
    project = models.Project.unmarshal(yaml_data)
    snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

    issues = linters.run_linters(new_dir, lint=None)
    assert issues[2] == LinterIssue(
        name="metadata",
        result=LinterResult.WARNING,
        text="Metadata field 'common-id' for app 'app1' is missing or empty.",
        url="https://documentation.ubuntu.com/snapcraft/stable/reference/project-file/snapcraft-yaml/#apps.<app-name>.common-id",
    )

def test_metadata_linter_common_id_with_multiple_app(new_dir, snap_yaml_data):
    yaml_data = snap_yaml_data(
        **{
            "apps": {
                "app1": {"command": "bin/mytest", "common_id": "com.example.mytest"},
                "app2": {"command": "bin/mytest"},
            }
        }
    )
    project = models.Project.unmarshal(yaml_data)
    snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

    issues = linters.run_linters(new_dir, lint=None)
    assert issues[2] == LinterIssue(
        name="metadata",
        result=LinterResult.WARNING,
        text="Metadata field 'common-id' for app 'app2' is missing or empty.",
        url="https://documentation.ubuntu.com/snapcraft/stable/reference/project-file/snapcraft-yaml/#apps.<app-name>.common-id",
    )


def test_metadata_linter_empty_common_id_in_multiple_app(new_dir, snap_yaml_data):
    yaml_data = snap_yaml_data(
        **{
            "apps": {
                "app1": {"command": "bin/mytest"},
                "app2": {"command": "bin/mytest"},
            }
        }
    )
    project = models.Project.unmarshal(yaml_data)
    snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

    issues = linters.run_linters(new_dir, lint=None)
    assert issues[2] == LinterIssue(
        name="metadata",
        result=LinterResult.WARNING,
        text="Metadata field 'common-id' for app 'app1' is missing or empty.",
        url="https://documentation.ubuntu.com/snapcraft/stable/reference/project-file/snapcraft-yaml/#apps.<app-name>.common-id",
    )
    assert issues[3] == LinterIssue(
        name="metadata",
        result=LinterResult.WARNING,
        text="Metadata field 'common-id' for app 'app2' is missing or empty.",
        url="https://documentation.ubuntu.com/snapcraft/stable/reference/project-file/snapcraft-yaml/#apps.<app-name>.common-id",
    )


def test_metadata_linter_ignore_common_id_when_not_app(new_dir, snap_yaml_data):
    yaml_data = snap_yaml_data(
        **{"apps": {"app2": {"command": "bin/mytest"}}, "type": "gadget"}
    )
    project = models.Project.unmarshal(yaml_data)
    snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")

    issues = linters.run_linters(new_dir, lint=None)
    assert len(issues) == 5

@soumyaDghosh soumyaDghosh force-pushed the metadata-linter branch 2 times, most recently from 0f81fc8 to fbb8d57 Compare August 2, 2025 19:24
Copy link
Collaborator

@mr-cal mr-cal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, just feedback about the style at this point.

@soumyaDghosh
Copy link
Contributor Author

All done!

@soumyaDghosh soumyaDghosh force-pushed the metadata-linter branch 2 times, most recently from b43f7ef to bfc22cf Compare August 8, 2025 15:33
@soumyaDghosh soumyaDghosh requested a review from mr-cal August 8, 2025 15:37
@mr-cal mr-cal requested a review from Copilot August 8, 2025 19:40

This comment was marked as outdated.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a new metadata linter for Snapcraft that checks for missing or empty metadata fields and reports them with appropriate severity levels. The linter validates snap metadata completeness to improve store listings and follows a rank-based system where critical fields generate warnings and optional fields generate informational messages.

Key changes:

  • Added a new MetadataLinter class that validates snap metadata fields with rank-based severity
  • Added INFO result type to the linter framework for informational issues
  • Extended test coverage with unit tests and spread tests for various scenarios

Reviewed Changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
snapcraft/linters/metadata_linter.py Core implementation of the metadata linter with field validation logic
snapcraft/linters/base.py Added new INFO linter result type
snapcraft/linters/linters.py Registered metadata linter and added INFO status handling
tests/unit/linters/test_metadata_linter.py Comprehensive unit tests for metadata linter functionality
Multiple spread test files Integration tests covering various metadata linter scenarios
Documentation files Added usage documentation and updated linter reference

@mr-cal
Copy link
Collaborator

mr-cal commented Aug 12, 2025

We're scheduled to discuss this internally on Friday (#5610 (comment)), so we should have another review on Friday.

Copy link
Collaborator

@mr-cal mr-cal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 minor changes, otherwise this looks good to me

Copy link
Collaborator

@mr-cal mr-cal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

We can land this once @medubelko re-reviews the docs.

Copy link
Contributor

@medubelko medubelko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a handful of suggestions for small improvements, but overall this looks good. Thanks @soumyaDghosh!

1. added the new linter
2. lints based on ranks
3. added unit tests and spread tests for the new linter
4. added a new LinterResult value named INFO
5. fix spread tests accordingly
6. add docs regarding the new metadata linter

Signed-off-by: Soumyadeep Ghosh <[email protected]>
@medubelko
Copy link
Contributor

@mr-cal This should be good to go.

@mr-cal mr-cal merged commit 2e9e72a into canonical:main Aug 26, 2025
16 of 17 checks passed
EdmilsonRodrigues pushed a commit to EdmilsonRodrigues/snapcraft that referenced this pull request Aug 27, 2025
Signed-off-by: Soumyadeep Ghosh <[email protected]>
Co-authored-by: Callahan Kovacs <[email protected]>
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.

Metadata linter

4 participants