Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/changed-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ on:
required: false
type: string
default: '.contributes.configuration.properties'
schema_overrides_path:
description: A repo-relative path to a JSON file with optional "add", "remove", "transform" keys used to transform and override upstream schema. "add" is a dictionary of schemas to add, "remove" a list of keys to remove and "transform" is a list of JQ queries to apply the input schema.
required: false
type: string
default: 'sublime-package.overrides.json'
version_file:
description: Relative path to the file that contains version information
description: Repo-relative path to the file that contains version information
required: true
type: string
version_regexp:
Expand Down Expand Up @@ -94,6 +99,7 @@ jobs:
REPOSITORY_URL: ${{ inputs.repository_url }}
CONFIGURATION_FILE_PATH: ${{ inputs.configuration_file_path }}
CONFIGURATION_JQ_QUERY: ${{ inputs.configuration_jq_query }}
SCHEMA_OVERRIDES_PATH: ${{ inputs.schema_overrides_path }}
run: |
VERSION_FROM="${{ steps.tag-from.outputs.version }}"
VERSION_TO="${{ steps.tag-to.outputs.version }}"
Expand All @@ -104,7 +110,7 @@ jobs:

{
echo 'CHANGES<<EOF'
./workflow-repo/scripts/changed_settings.py "$REPOSITORY_URL" "$CONFIGURATION_FILE_PATH" "$CONFIGURATION_JQ_QUERY" "$TRANSFORMED_FROM" "$TRANSFORMED_TO"
./workflow-repo/scripts/changed_settings.py "$REPOSITORY_URL" "$CONFIGURATION_FILE_PATH" "$CONFIGURATION_JQ_QUERY" "$TRANSFORMED_FROM" "$TRANSFORMED_TO" --schema-overrides-path="$SCHEMA_OVERRIDES_PATH"
echo EOF
} >> "$GITHUB_OUTPUT"

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
configuration_file_path: 'editors/code/package.json'
# Optional
configuration_jq_query: '.contributes.configuration.properties'
# Optional
schema_overrides_path: 'sublime-package.overrides.json'
version_file: 'plugin.py'
version_regexp: 'TAG = "([^"]+)"'
# Optional string used to transform the tag captured by version_regexp. This can for example add a 'v' in front of the tag. The {} is replaced with the captured tag.
Expand Down
124 changes: 121 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,127 @@
[tool.ruff]
target-version = "py312"

[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
reportAny = "none"
reportExplicitAny = "none"
reportUnusedCallResult = "none"

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.format]
quote-style = "preserve"
indent-style = "space"
# Respect magic trailing commas.
skip-magic-trailing-comma = false
# Automatically detect the appropriate line ending.
line-ending = "auto"

[tool.ruff.lint.isort]
case-sensitive = false
force-single-line = true
from-first = true
no-sections = true
order-by-type = false
required-imports = ["from __future__ import annotations"]

[tool.ruff.lint]
# Enable preview rules.
preview = true
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
select = ["ALL"]
# Refer to https://docs.astral.sh/ruff/rules/
ignore = [
"ANN401", # https://docs.astral.sh/ruff/rules/any-type/
"ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/
"ARG003", # https://docs.astral.sh/ruff/rules/unused-class-method-argument/
"ARG005", # https://docs.astral.sh/ruff/rules/unused-lambda-argument/
"B010", # https://docs.astral.sh/ruff/rules/set-attr-with-constant/
"BLE", # https://docs.astral.sh/ruff/rules/blind-except/
"C401", # https://docs.astral.sh/ruff/rules/unnecessary-generator-set/
"C901", # https://docs.astral.sh/ruff/rules/complex-structure/
"COM812", # https://docs.astral.sh/ruff/rules/missing-trailing-comma/
"CPY001", # https://docs.astral.sh/ruff/rules/missing-copyright-notice/
"D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/#undocumented-public-module-d100
"D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/
"D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/
"D103", # https://docs.astral.sh/ruff/rules/undocumented-public-function/
"D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/
"D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/
"D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/
"D203", # https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class/
"D205", # https://docs.astral.sh/ruff/rules/missing-blank-line-after-summary/
"D212", # https://docs.astral.sh/ruff/rules/multi-line-summary-first-line/
"D401", # https://docs.astral.sh/ruff/rules/non-imperative-mood/
"D413", # https://docs.astral.sh/ruff/rules/missing-blank-line-after-last-section/
"DOC201", # https://docs.astral.sh/ruff/rules/docstring-missing-returns/
"DOC402", # https://docs.astral.sh/ruff/rules/docstring-missing-yields/
"DOC501", # https://docs.astral.sh/ruff/rules/docstring-missing-exception/
"DTZ005", # https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/
"EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em
"ERA001", # https://docs.astral.sh/ruff/rules/commented-out-code/
"FBT001", # https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument/
"FBT002", # https://docs.astral.sh/ruff/rules/boolean-default-value-positional-argument/
"FBT003", # https://docs.astral.sh/ruff/rules/boolean-positional-value-in-call/
"FIX", # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix
"N802", # https://docs.astral.sh/ruff/rules/invalid-function-name/#invalid-function-name-n802
"PERF203", # https://docs.astral.sh/ruff/rules/try-except-in-loop/
"PGH003", # https://docs.astral.sh/ruff/rules/blanket-type-ignore/
"PGH004", # https://docs.astral.sh/ruff/rules/blanket-noqa/
"PIE790", # https://docs.astral.sh/ruff/rules/unnecessary-placeholder/
"PLC0415", # https://docs.astral.sh/ruff/rules/import-outside-top-level/
"PLC2701", # https://docs.astral.sh/ruff/rules/import-private-name/
"PLR0904", # https://docs.astral.sh/ruff/rules/too-many-public-methods/
"PLR0911", # https://docs.astral.sh/ruff/rules/too-many-return-statements/
"PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/
"PLR0913", # https://docs.astral.sh/ruff/rules/too-many-arguments/
"PLR0914", # https://docs.astral.sh/ruff/rules/too-many-locals/
"PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/
"PLR0917", # https://docs.astral.sh/ruff/rules/too-many-positional-arguments/
"PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/
"PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/
"PLR6301", # https://docs.astral.sh/ruff/rules/no-self-use/
"PLW0603", # https://docs.astral.sh/ruff/rules/global-statement/
"PLW2901", # https://docs.astral.sh/ruff/rules/redefined-loop-name/
"PT009", # https://docs.astral.sh/ruff/rules/pytest-unittest-assertion/
"PT027", # https://docs.astral.sh/ruff/rules/pytest-unittest-raises-assertion/
"PTH100", # https://docs.astral.sh/ruff/rules/os-path-abspath/
"PTH109", # https://docs.astral.sh/ruff/rules/os-getcwd/
"PTH110", # https://docs.astral.sh/ruff/rules/os-path-exists/
"PTH111", # https://docs.astral.sh/ruff/rules/os-path-expanduser/
"PTH112", # https://docs.astral.sh/ruff/rules/os-path-isdir/
"PTH113", # https://docs.astral.sh/ruff/rules/os-path-isfile/
"PTH118", # https://docs.astral.sh/ruff/rules/os-path-join/
"PTH119", # https://docs.astral.sh/ruff/rules/os-path-basename/
"PTH120", # https://docs.astral.sh/ruff/rules/os-path-dirname/
"PTH121", # https://docs.astral.sh/ruff/rules/os-path-samefile/
"PTH122", # https://docs.astral.sh/ruff/rules/os-path-splitext/
"PTH208", # https://docs.astral.sh/ruff/rules/os-listdir/
"Q", # flake8-quotes - https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
"RUF001", # https://docs.astral.sh/ruff/rules/ambiguous-unicode-character-string/
"RUF012", # https://docs.astral.sh/ruff/rules/mutable-class-default/
"RUF023", # https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/
"RUF031", # https://docs.astral.sh/ruff/rules/incorrectly-parenthesized-tuple-in-subscript/
"RUF067", # https://docs.astral.sh/ruff/rules/non-empty-init-module/
"S101", # https://docs.astral.sh/ruff/rules/assert/
"S110", # https://docs.astral.sh/ruff/rules/try-except-pass/
"S307", # https://docs.astral.sh/ruff/rules/suspicious-eval-usage/
"S310", # https://docs.astral.sh/ruff/rules/suspicious-url-open-usage/
"S404", # https://docs.astral.sh/ruff/rules/suspicious-subprocess-import/
"S603", # https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true/
"S606", # https://docs.astral.sh/ruff/rules/start-process-with-no-shell/
"SIM102", # https://docs.astral.sh/ruff/rules/collapsible-if/
"SIM103", # https://docs.astral.sh/ruff/rules/needless-bool/
"SIM105", # https://docs.astral.sh/ruff/rules/suppressible-exception/
"SIM110", # https://docs.astral.sh/ruff/rules/reimplemented-builtin/
"SLF001", # https://docs.astral.sh/ruff/rules/private-member-access/
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
"TD001", # https://docs.astral.sh/ruff/rules/invalid-todo-tag/
"TD002", # https://docs.astral.sh/ruff/rules/missing-todo-author/
"TD003", # https://docs.astral.sh/ruff/rules/missing-todo-link/
"TID252", # https://docs.astral.sh/ruff/rules/relative-imports/
"TRY002", # https://docs.astral.sh/ruff/rules/raise-vanilla-class/
"TRY003", # https://docs.astral.sh/ruff/rules/raise-vanilla-args/
"TRY301", # https://docs.astral.sh/ruff/rules/raise-within-try/
]
65 changes: 50 additions & 15 deletions scripts/changed_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import PurePosixPath
from typing import Any
from typing import cast
from typing import NotRequired
from typing import TypedDict
from urllib.error import HTTPError
from urllib.request import urlopen
Expand All @@ -32,15 +33,21 @@ class Configuration(TypedDict):
type ConfigurationsDict = dict[str, Configuration]


class SchemaOverrides(TypedDict):
add: NotRequired[dict[str, Configuration]]
remove: NotRequired[list[str]]
transform: NotRequired[list[str]]


def download_github_artifact_by_tag(repository_url: str, tag: str, target_dir: str) -> Path:
archive_url = f'{repository_url}/archive/refs/tags/{tag}.zip'
zip_path = Path(target_dir, f'archive-{re.sub(r'[<>:"/\\|?*]', '_', tag)}.zip')
try:
with urlopen(archive_url) as response, zip_path.open('wb') as out_file: # noqa: S310
with urlopen(archive_url) as response, zip_path.open('wb') as out_file:
shutil.copyfileobj(response, out_file)
except HTTPError as ex:
except HTTPError:
print(f'Error downloading {archive_url}', file=sys.stderr)
raise ex
raise
return zip_path


Expand Down Expand Up @@ -74,7 +81,6 @@ def get_parent_directory(zip_file: zipfile.ZipFile) -> str | None:
Check if all files in the ZIP are contained within a parent directory.
Returns str | None: Common parent name if present.
"""

# Filter out directory entries and get top-level paths.
top_levels: set[str] = set()
for name in zip_file.namelist():
Expand All @@ -98,26 +104,39 @@ def generate_sublime_settings_markdown(settings: dict[str, Configuration]) -> st
return f'```\n{sublime_settings_str}\n```'


def compare_json(
jq_query: str, contents_1: str, contents_2: str
def override_settings(settings: ConfigurationsDict, overrides: SchemaOverrides) -> ConfigurationsDict:
# Remove
remove = overrides.get('remove', [])
for key in list(settings.keys()):
if key in remove:
del settings[key]
# Transform
transform = overrides.get('transform', [])
for query in transform:
settings = jq(query, json.dumps(settings))
# Add (always at the beginning)
add = overrides.get('add', {})
return {**add, **settings}


def compare_settings(
settings_1: ConfigurationsDict, settings_2: ConfigurationsDict
) -> tuple[dict[str, Configuration], dict[str, Configuration], list[str]]:
flatten_settings_1 = jq(jq_query, contents_1)
flatten_settings_2 = jq(jq_query, contents_2)
# Find added, removed and changed keys.
added: dict[str, Configuration] = {}
changed: dict[str, Configuration] = {}
removed: list[str] = [key for key in flatten_settings_1 if key not in flatten_settings_2]
for key, value in flatten_settings_2.items():
if key not in flatten_settings_1:
removed: list[str] = [key for key in settings_1 if key not in settings_2]
for key, value in settings_2.items():
if key not in settings_1:
added[key] = value
continue
if value != flatten_settings_1[key]:
if value != settings_1[key]:
changed[key] = value
return (added, changed, removed)


def jq(query: str, contents: str) -> ConfigurationsDict:
return cast(ConfigurationsDict,
return cast('ConfigurationsDict',
json.loads(subprocess.check_output(['jq', query], input=contents, text=True, encoding='utf-8'))) # noqa: S607


Expand All @@ -143,13 +162,17 @@ def main() -> None:
help='A path to the configuration file relative to the repository_url.')
parser.add_argument('configuration_jq_query',
help='The JQ query to use to retrieve configuration settings.')
parser.add_argument('--schema-overrides-path',
default='sublime-package.overrides.json',
help='A file with augmentation used to transform the full schema.')
parser.add_argument('tag_from', help='First tag to compare.')
parser.add_argument('tag_to', help='Second tag to compare.')
args = parser.parse_args()

repository_url: str = args.repository_url
configuration_file_path: str = args.configuration_file_path
configuration_jq_query: str = args.configuration_jq_query
schema_overrides_path = Path(args.schema_overrides_path)
tag_from: str = args.tag_from
tag_to: str = args.tag_to

Expand All @@ -173,7 +196,13 @@ def main() -> None:
]

if diff:
added, changed, removed = compare_json(configuration_jq_query, configuration_1, configuration_2)
settings_1 = jq(configuration_jq_query, configuration_1)
settings_2 = jq(configuration_jq_query, configuration_2)
if schema_overrides_path.is_file():
overrides = json.loads(schema_overrides_path.read_text(encoding='utf-8'))
settings_1 = override_settings(settings_1, overrides)
settings_2 = override_settings(settings_2, overrides)
added, changed, removed = compare_settings(settings_1, settings_2)

if added:
output.append(markdown_collapsible_section(
Expand All @@ -189,7 +218,13 @@ def main() -> None:
key_list = '\n'.join([f' - `{k}`' for k in removed])
output.append(f'Removed keys (${len(key_list)}):\n{key_list}')

output.append(markdown_collapsible_section('All changes in schema', f'```diff\n{diff}\n```'))
output.extend((
markdown_collapsible_section('All changes in the schema', f'```diff\n{diff}\n```'),
markdown_collapsible_section('Whole sublime-settings configuration',
generate_sublime_settings_markdown(settings_2)),
markdown_collapsible_section('Whole sublime-package schema',
f'```jsonc\n{json_serialize(settings_2)}\n```')
))
else:
output.append('No changes')

Expand Down