Skip to content

Commit 8a33e79

Browse files
committed
Preserve snapshots and ignore pycache in sync
1 parent 8a1ea6a commit 8a33e79

7 files changed

Lines changed: 79 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ The repository ships the sync workflow template used by generated package reposi
114114

115115
The generated `source-sync` workflow stays thin: it checks out this repository as the generator logic, reads the consumer repo's `.msra` source tree, generates the artifact into a staging tree, syncs the generated tree into `main`, validates the result, and pushes the commit so the target repo's `publish.yml` can run on `push` to `main`.
116116

117-
If a project defines `[app.sync].preserved_target_paths` in its MSRA source, the generated sync step preserves those repo-specific runtime artifacts. For the FixPrice layout that means `tests/__snapshots__` stays in `main` and is not rewritten by the generator.
117+
If a project defines `[app.sync]` in its MSRA source, the generated sync step can preserve repo-specific runtime artifacts and ignore generated noise. For the FixPrice layout that means `tests/__snapshots__` stays in `main`, while `**/__pycache__` and `**/*.pyc` are removed before commit.
118118

119119
Before validation, the generated workflow installs the target project's `requirements-dev.txt` so the validation step runs with the generated dependencies available.
120120

docs/msra-repo-b-sync.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- этот workflow запускается вручную через `workflow_dispatch`
99
- внутри job он сам checkout-ит repo с логикой генератора
1010
- затем читает `repo B/source`, генерирует артефакт и пушит его в `repo B/main`
11-
- при синке сохраняет repo-specific runtime artifacts, перечисленные в `[app.sync].preserved_target_paths` source-MSTRA, например `tests/__snapshots__`
11+
- при синке сохраняет repo-specific runtime artifacts, перечисленные в `[app.sync].preserved_target_paths`, и удаляет generated noise из `[app.sync].ignored_generated_patterns`
1212
- после `push` в `main` автоматически стартует `publish.yml`
1313

1414
## Почему workflow должен лежать в `main`
@@ -109,13 +109,31 @@ jobs:
109109
- name: Replace target tree contents
110110
run: |
111111
python - <<'PY'
112+
from fnmatch import fnmatch
112113
from pathlib import Path
113114
import shutil
114115
115116
target = Path("target")
116117
generated = Path("generated")
117118
preserved_root = Path("preserved")
118119
preserve_paths = ["tests/__snapshots__"]
120+
ignored_patterns = ["**/__pycache__", "**/*.pyc"]
121+
122+
def is_ignored(relative_path: Path) -> bool:
123+
relative_text = relative_path.as_posix()
124+
return any(fnmatch(relative_text, pattern) for pattern in ignored_patterns)
125+
126+
def remove_ignored(root: Path) -> None:
127+
ignored_paths = sorted(
128+
[path for path in root.rglob("*") if is_ignored(path.relative_to(root))],
129+
key=lambda path: len(path.parts),
130+
reverse=True,
131+
)
132+
for path in ignored_paths:
133+
if path.is_dir():
134+
shutil.rmtree(path)
135+
else:
136+
path.unlink()
119137
120138
for relative_path in preserve_paths:
121139
source_path = target / relative_path
@@ -151,6 +169,9 @@ jobs:
151169
else:
152170
destination_path.parent.mkdir(parents=True, exist_ok=True)
153171
shutil.copy2(source_path, destination_path)
172+
173+
remove_ignored(target)
174+
shutil.rmtree(preserved_root, ignore_errors=True)
154175
PY
155176
156177
- name: Validate generated project

msra_codegen/github_workflows.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def build_github_workflows_context(project: dict[str, Any], package_name: str) -
2424
preserved_target_paths = app_sync_config.get("preserved_target_paths", [])
2525
if not isinstance(preserved_target_paths, list):
2626
raise RuntimeError("app.sync.preserved_target_paths must be a list.")
27+
ignored_generated_patterns = app_sync_config.get("ignored_generated_patterns", [])
28+
if not isinstance(ignored_generated_patterns, list):
29+
raise RuntimeError("app.sync.ignored_generated_patterns must be a list.")
2730

2831
return {
2932
"package_name": package_name,
@@ -87,6 +90,7 @@ def build_github_workflows_context(project: dict[str, Any], package_name: str) -
8790
"commit_user_email": str(source_sync_config["commit_user_email"]),
8891
"source_msra_path": source_msra_path,
8992
"preserved_target_paths": [str(item) for item in preserved_target_paths],
93+
"ignored_generated_patterns": [str(item) for item in ignored_generated_patterns],
9094
},
9195
"makefile": {
9296
"package_name": package_name,

msra_codegen/project_model.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,22 @@ def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None)
184184
text = item.strip()
185185
if text:
186186
preserved_target_paths.append(text)
187-
app["sync"] = {"preserved_target_paths": preserved_target_paths}
187+
ignored_generated_patterns_value = get_plain_value(
188+
get_assignment(sync_table, "ignored_generated_patterns", [])
189+
)
190+
if not isinstance(ignored_generated_patterns_value, list):
191+
raise TypeError("app.sync.ignored_generated_patterns must be a list of strings.")
192+
ignored_generated_patterns: list[str] = []
193+
for item in ignored_generated_patterns_value:
194+
if not isinstance(item, str):
195+
raise TypeError("app.sync.ignored_generated_patterns entries must be strings.")
196+
text = item.strip()
197+
if text:
198+
ignored_generated_patterns.append(text)
199+
app["sync"] = {
200+
"preserved_target_paths": preserved_target_paths,
201+
"ignored_generated_patterns": ignored_generated_patterns,
202+
}
188203

189204
variable_tables = [
190205
table

msra_codegen/templates/github/workflows/source-sync.yml.tpl

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,44 @@ jobs:
6262
- name: Replace target tree contents
6363
run: |
6464
python - <<'PY'
65+
from fnmatch import fnmatch
6566
from pathlib import Path
6667
import shutil
6768

6869
target = Path("target")
6970
generated = Path("generated")
7071
preserved_root = Path("preserved")
7172
preserve_paths = {{ source_sync.preserved_target_paths | tojson }}
73+
ignored_patterns = {{ source_sync.ignored_generated_patterns | tojson }}
74+
75+
def is_ignored(relative_path: Path) -> bool:
76+
relative_text = relative_path.as_posix()
77+
return any(fnmatch(relative_text, pattern) for pattern in ignored_patterns)
78+
79+
def copy_tree(source_root: Path, destination_root: Path) -> None:
80+
for item in source_root.iterdir():
81+
relative_path = item.relative_to(source_root)
82+
if is_ignored(relative_path):
83+
continue
84+
destination_path = destination_root / relative_path
85+
if item.is_dir():
86+
destination_path.mkdir(parents=True, exist_ok=True)
87+
copy_tree(item, destination_path)
88+
else:
89+
destination_path.parent.mkdir(parents=True, exist_ok=True)
90+
shutil.copy2(item, destination_path)
91+
92+
def remove_ignored(root: Path) -> None:
93+
ignored_paths = sorted(
94+
[path for path in root.rglob("*") if is_ignored(path.relative_to(root))],
95+
key=lambda path: len(path.parts),
96+
reverse=True,
97+
)
98+
for path in ignored_paths:
99+
if path.is_dir():
100+
shutil.rmtree(path)
101+
else:
102+
path.unlink()
72103

73104
for relative_path in preserve_paths:
74105
source_path = target / relative_path
@@ -89,12 +120,7 @@ jobs:
89120
else:
90121
child.unlink()
91122

92-
for item in generated.iterdir():
93-
destination = target / item.name
94-
if item.is_dir():
95-
shutil.copytree(item, destination, dirs_exist_ok=True)
96-
else:
97-
shutil.copy2(item, destination)
123+
copy_tree(generated, target)
98124

99125
for relative_path in preserve_paths:
100126
source_path = preserved_root / relative_path
@@ -105,6 +131,7 @@ jobs:
105131
destination_path.parent.mkdir(parents=True, exist_ok=True)
106132
shutil.copy2(source_path, destination_path)
107133

134+
remove_ignored(target)
108135
shutil.rmtree(preserved_root, ignore_errors=True)
109136
PY
110137

tests/codegen.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ test("source sync workflow preserves target artifacts from app.sync", () => {
855855
.replace('package_name="ozon_api"', 'package_name="sync_api"')
856856
.replace(
857857
/\n\[app\.warmup\]\n/,
858-
'\n[app.sync]\npreserved_target_paths=["tests/__snapshots__"]\n\n[app.warmup]\n',
858+
'\n[app.sync]\npreserved_target_paths=["tests/__snapshots__"]\nignored_generated_patterns=["**/__pycache__", "**/*.pyc"]\n\n[app.warmup]\n',
859859
);
860860

861861
try {
@@ -876,6 +876,7 @@ test("source sync workflow preserves target artifacts from app.sync", () => {
876876
readFileSync(path.join(outputDir, ".github", "workflows", "source-sync.yml"), "utf8"),
877877
);
878878
assert.match(sourceSyncWorkflowText, /preserve_paths = \["tests\/__snapshots__"\]/);
879+
assert.match(sourceSyncWorkflowText, /ignored_patterns = \["\*\*\/__pycache__", "\*\*\/\*\.pyc"\]/);
879880
} finally {
880881
rmSync(workDir, { recursive: true, force: true });
881882
}

vscode-extension/lsp/assignment-schema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ const TABLE_SCHEMAS = [
679679
}),
680680
makeFixedSchema(exactPath(["app", "sync"]), {
681681
preserved_target_paths: arrayOf(STRING),
682+
ignored_generated_patterns: arrayOf(STRING),
682683
}),
683684
makeFixedSchema(exactPath(["app", "variables", "*"]), {
684685
types: arrayOf(VARIABLE_TYPE_ITEM_SPEC),

0 commit comments

Comments
 (0)