Skip to content

Commit 8a1ea6a

Browse files
committed
Preserve configured sync artifacts
1 parent 020a503 commit 8a1ea6a

8 files changed

Lines changed: 116 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ 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.
118+
117119
Before validation, the generated workflow installs the target project's `requirements-dev.txt` so the validation step runs with the generated dependencies available.
118120

119121
The source and target branches come from the repo-specific sync config, so the generated workflow itself stays thin.

docs/msra-repo-b-sync.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +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__`
1112
- после `push` в `main` автоматически стартует `publish.yml`
1213

1314
## Почему workflow должен лежать в `main`
@@ -113,6 +114,19 @@ jobs:
113114
114115
target = Path("target")
115116
generated = Path("generated")
117+
preserved_root = Path("preserved")
118+
preserve_paths = ["tests/__snapshots__"]
119+
120+
for relative_path in preserve_paths:
121+
source_path = target / relative_path
122+
if not source_path.exists():
123+
raise RuntimeError(f'Preserved target path "{relative_path}" does not exist.')
124+
destination_path = preserved_root / relative_path
125+
destination_path.parent.mkdir(parents=True, exist_ok=True)
126+
if source_path.is_dir():
127+
shutil.copytree(source_path, destination_path)
128+
else:
129+
shutil.copy2(source_path, destination_path)
116130
117131
for child in list(target.iterdir()):
118132
if child.name == ".git":
@@ -128,6 +142,15 @@ jobs:
128142
shutil.copytree(item, destination, dirs_exist_ok=True)
129143
else:
130144
shutil.copy2(item, destination)
145+
146+
for relative_path in preserve_paths:
147+
source_path = preserved_root / relative_path
148+
destination_path = target / relative_path
149+
if source_path.is_dir():
150+
shutil.copytree(source_path, destination_path, dirs_exist_ok=True)
151+
else:
152+
destination_path.parent.mkdir(parents=True, exist_ok=True)
153+
shutil.copy2(source_path, destination_path)
131154
PY
132155
133156
- name: Validate generated project

msra_codegen/github_workflows.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ def build_github_workflows_context(project: dict[str, Any], package_name: str) -
1414
tests_config = github_config["tests"]
1515
publish_config = github_config["publish"]
1616
source_sync_config = github_config["source_sync"]
17+
app_sync_config = project.get("app", {}).get("sync", {})
1718
package_name_slug = package_name.replace("_", "-")
1819
source_msra_path = str(Path(project["source_path"]).name)
1920

2021
tests_python_version = str(tests_config["python_version"])
2122
publish_python_version = str(publish_config["python_version"])
2223
source_sync_python_version = str(source_sync_config["python_version"])
24+
preserved_target_paths = app_sync_config.get("preserved_target_paths", [])
25+
if not isinstance(preserved_target_paths, list):
26+
raise RuntimeError("app.sync.preserved_target_paths must be a list.")
2327

2428
return {
2529
"package_name": package_name,
@@ -82,6 +86,7 @@ def build_github_workflows_context(project: dict[str, Any], package_name: str) -
8286
"commit_user_name": str(source_sync_config["commit_user_name"]),
8387
"commit_user_email": str(source_sync_config["commit_user_email"]),
8488
"source_msra_path": source_msra_path,
89+
"preserved_target_paths": [str(item) for item in preserved_target_paths],
8590
},
8691
"makefile": {
8792
"package_name": package_name,

msra_codegen/project_model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None)
107107
"humanize": get_plain_value(get_assignment(app_table, "humanize", False)),
108108
"block_images": bool(get_plain_value(get_assignment(app_table, "block_images", False))),
109109
"disallow_headless": bool(get_plain_value(get_assignment(app_table, "disallow_headless", False))),
110+
"sync": {},
110111
"abstractions": [],
111112
}
112113
abstractions_value = get_plain_value(get_assignment(app_table, "abstractions", []))
@@ -169,6 +170,22 @@ def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None)
169170
"script": warmup_script,
170171
}
171172

173+
sync_table = get_table(["app", "sync"])
174+
if sync_table:
175+
preserved_target_paths_value = get_plain_value(
176+
get_assignment(sync_table, "preserved_target_paths", [])
177+
)
178+
if not isinstance(preserved_target_paths_value, list):
179+
raise TypeError("app.sync.preserved_target_paths must be a list of strings.")
180+
preserved_target_paths: list[str] = []
181+
for item in preserved_target_paths_value:
182+
if not isinstance(item, str):
183+
raise TypeError("app.sync.preserved_target_paths entries must be strings.")
184+
text = item.strip()
185+
if text:
186+
preserved_target_paths.append(text)
187+
app["sync"] = {"preserved_target_paths": preserved_target_paths}
188+
172189
variable_tables = [
173190
table
174191
for table in tables

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ jobs:
6767

6868
target = Path("target")
6969
generated = Path("generated")
70+
preserved_root = Path("preserved")
71+
preserve_paths = {{ source_sync.preserved_target_paths | tojson }}
72+
73+
for relative_path in preserve_paths:
74+
source_path = target / relative_path
75+
if not source_path.exists():
76+
raise RuntimeError(f'Preserved target path "{relative_path}" does not exist.')
77+
destination_path = preserved_root / relative_path
78+
destination_path.parent.mkdir(parents=True, exist_ok=True)
79+
if source_path.is_dir():
80+
shutil.copytree(source_path, destination_path)
81+
else:
82+
shutil.copy2(source_path, destination_path)
7083

7184
for child in list(target.iterdir()):
7285
if child.name == ".git":
@@ -82,6 +95,17 @@ jobs:
8295
shutil.copytree(item, destination, dirs_exist_ok=True)
8396
else:
8497
shutil.copy2(item, destination)
98+
99+
for relative_path in preserve_paths:
100+
source_path = preserved_root / relative_path
101+
destination_path = target / relative_path
102+
if source_path.is_dir():
103+
shutil.copytree(source_path, destination_path, dirs_exist_ok=True)
104+
else:
105+
destination_path.parent.mkdir(parents=True, exist_ok=True)
106+
shutil.copy2(source_path, destination_path)
107+
108+
shutil.rmtree(preserved_root, ignore_errors=True)
85109
PY
86110

87111
- name: Install target project dependencies

tests/codegen.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,42 @@ test("python codegen generates both bundled msra documents without failing", ()
845845
}
846846
});
847847

848+
test("source sync workflow preserves target artifacts from app.sync", () => {
849+
const repoRoot = path.resolve(__dirname, "..");
850+
const workDir = mkdtempSync(path.join(os.tmpdir(), "msra-source-sync-preserve-"));
851+
const inputPath = path.join(workDir, "sync.msra");
852+
const outputDir = path.join(workDir, "generated");
853+
const pythonReleasesFixturePath = createPythonReleasesApiFixture(workDir);
854+
const source = normalizeNewlines(readFileSync(path.join(repoRoot, "examples", "example", "example.msra"), "utf8"))
855+
.replace('package_name="ozon_api"', 'package_name="sync_api"')
856+
.replace(
857+
/\n\[app\.warmup\]\n/,
858+
'\n[app.sync]\npreserved_target_paths=["tests/__snapshots__"]\n\n[app.warmup]\n',
859+
);
860+
861+
try {
862+
writeFileSync(inputPath, source, "utf8");
863+
writeFileSync(
864+
path.join(workDir, "warmup.py"),
865+
readFileSync(path.join(repoRoot, "examples", "example", "warmup.py"), "utf8"),
866+
"utf8",
867+
);
868+
const result = spawnSync("python", ["-m", "msra_codegen", "generate", inputPath, "-o", outputDir], {
869+
cwd: repoRoot,
870+
env: buildCodegenPythonPath(pythonReleasesFixturePath),
871+
encoding: "utf8",
872+
});
873+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
874+
875+
const sourceSyncWorkflowText = normalizeNewlines(
876+
readFileSync(path.join(outputDir, ".github", "workflows", "source-sync.yml"), "utf8"),
877+
);
878+
assert.match(sourceSyncWorkflowText, /preserve_paths = \["tests\/__snapshots__"\]/);
879+
} finally {
880+
rmSync(workDir, { recursive: true, force: true });
881+
}
882+
});
883+
848884
test("python codegen builds the TODO/2 abstractions project", () => {
849885
const repoRoot = path.resolve(__dirname, "..");
850886
const workDir = mkdtempSync(path.join(os.tmpdir(), "msra-abstractions-build-"));

vscode-extension/lsp/assignment-schema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,9 @@ const TABLE_SCHEMAS = [
677677
on_error_screenshot_path: SCREENSHOT_PATH_SPEC,
678678
timeout_ms: integerAtLeast(0),
679679
}),
680+
makeFixedSchema(exactPath(["app", "sync"]), {
681+
preserved_target_paths: arrayOf(STRING),
682+
}),
680683
makeFixedSchema(exactPath(["app", "variables", "*"]), {
681684
types: arrayOf(VARIABLE_TYPE_ITEM_SPEC),
682685
description: STRINGISH,

vscode-extension/lsp/path-schema.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const TABLE_ROOT_SEGMENTS = new Set([
77

88
const APP_CHILD_SEGMENTS = new Set([
99
"warmup",
10+
"sync",
1011
"defaults",
1112
"variables",
1213
"prefixes",
@@ -275,6 +276,9 @@ function validateAppPath(segments, index) {
275276
if (segment.value === "warmup" && !segment.quoted) {
276277
return validateLeafNamespace(segments, index, `"${renderPath(segments, index + 1)}"`);
277278
}
279+
if (segment.value === "sync" && !segment.quoted) {
280+
return validateLeafNamespace(segments, index, `"${renderPath(segments, index + 1)}"`);
281+
}
278282
if (segment.value === "defaults" && !segment.quoted) {
279283
return validateDefaultsNamespace(segments, index + 1);
280284
}
@@ -296,8 +300,8 @@ function validateAppPath(segments, index) {
296300
return invalidPath(
297301
segments,
298302
index,
299-
`Invalid child table "${renderSegment(segment)}" under "app" in "${renderPath(segments)}". Expected "warmup", "defaults", "variables", "prefixes", "regexes", "groups", or "func".`,
300-
["warmup", "defaults", "variables", "prefixes", "regexes", "groups", "func"],
303+
`Invalid child table "${renderSegment(segment)}" under "app" in "${renderPath(segments)}". Expected "warmup", "sync", "defaults", "variables", "prefixes", "regexes", "groups", or "func".`,
304+
["warmup", "sync", "defaults", "variables", "prefixes", "regexes", "groups", "func"],
301305
);
302306
}
303307

0 commit comments

Comments
 (0)