engineering-process 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {engineering_process-0.2.0/engineering_process.egg-info → engineering_process-0.2.1}/PKG-INFO +1 -1
  2. {engineering_process-0.2.0 → engineering_process-0.2.1}/VERSIONING.md +10 -1
  3. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/__init__.py +1 -1
  4. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/publication.py +57 -0
  5. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/release.py +0 -4
  6. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/release_candidate.py +67 -2
  7. {engineering_process-0.2.0 → engineering_process-0.2.1/engineering_process.egg-info}/PKG-INFO +1 -1
  8. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process.egg-info/SOURCES.txt +4 -0
  9. {engineering_process-0.2.0 → engineering_process-0.2.1}/pyproject.toml +1 -1
  10. engineering_process-0.2.1/release.json +106 -0
  11. {engineering_process-0.2.0 → engineering_process-0.2.1}/templates/adopt-process.py +27 -2
  12. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_adoption_runner.py +61 -1
  13. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_publication.py +53 -0
  14. engineering_process-0.2.1/tests/test_publish_event.py +57 -0
  15. engineering_process-0.2.1/tests/test_pypi_publication.py +156 -0
  16. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_release.py +43 -0
  17. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_release_candidate.py +157 -1
  18. engineering_process-0.2.1/tests/test_release_preparation.py +68 -0
  19. engineering_process-0.2.1/tests/test_release_qualification.py +99 -0
  20. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_self_hosting.py +182 -12
  21. engineering_process-0.2.0/release.json +0 -114
  22. {engineering_process-0.2.0 → engineering_process-0.2.1}/LICENSE +0 -0
  23. {engineering_process-0.2.0 → engineering_process-0.2.1}/MANIFEST.in +0 -0
  24. {engineering_process-0.2.0 → engineering_process-0.2.1}/PRODUCTION_STANDARD.md +0 -0
  25. {engineering_process-0.2.0 → engineering_process-0.2.1}/README.md +0 -0
  26. {engineering_process-0.2.0 → engineering_process-0.2.1}/bundles.json +0 -0
  27. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/__main__.py +0 -0
  28. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/_download_worker.py +0 -0
  29. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/_supervisor_posix.py +0 -0
  30. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/_supervisor_windows.py +0 -0
  31. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/_windows_job.py +0 -0
  32. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/adoption.py +0 -0
  33. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/artifact_attestation.py +0 -0
  34. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/bootstrap.py +0 -0
  35. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/bundles.py +0 -0
  36. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/cli.py +0 -0
  37. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/contracts.py +0 -0
  38. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/distribution.py +0 -0
  39. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/distribution_verify.py +0 -0
  40. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/environment.py +0 -0
  41. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/evidence.py +0 -0
  42. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/git.py +0 -0
  43. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/git_attributes.py +0 -0
  44. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/helper_launch.py +0 -0
  45. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/impact.py +0 -0
  46. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/lifecycle.py +0 -0
  47. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/managed.py +0 -0
  48. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/markdown.py +0 -0
  49. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/requirements-build.txt +0 -0
  50. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/requirements-dev.txt +0 -0
  51. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/requirements-release.txt +0 -0
  52. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/requirements-runtime.txt +0 -0
  53. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/runner.py +0 -0
  54. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/runtime.py +0 -0
  55. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/skills.py +0 -0
  56. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/supervision.py +0 -0
  57. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/supplemental.py +0 -0
  58. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/syncing.py +0 -0
  59. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process/tooling.py +0 -0
  60. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process.egg-info/dependency_links.txt +0 -0
  61. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process.egg-info/entry_points.txt +0 -0
  62. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process.egg-info/requires.txt +0 -0
  63. {engineering_process-0.2.0 → engineering_process-0.2.1}/engineering_process.egg-info/top_level.txt +0 -0
  64. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/adoption-migration.json +0 -0
  65. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/change.json +0 -0
  66. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/plan.json +0 -0
  67. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/project.json +0 -0
  68. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/release-change.json +0 -0
  69. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/release.json +0 -0
  70. {engineering_process-0.2.0 → engineering_process-0.2.1}/examples/review.json +0 -0
  71. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/assess-design/SKILL.md +0 -0
  72. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/build-frontend/SKILL.md +0 -0
  73. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/build-frontend-foundation/SKILL.md +0 -0
  74. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/change-api/SKILL.md +0 -0
  75. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/cross-repo-change/SKILL.md +0 -0
  76. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/define-change-contract/SKILL.md +0 -0
  77. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/design-module/SKILL.md +0 -0
  78. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/evolve-process/SKILL.md +0 -0
  79. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/finish-change/SKILL.md +0 -0
  80. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/govern-ui/SKILL.md +0 -0
  81. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/implement-change/SKILL.md +0 -0
  82. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/implement-module/SKILL.md +0 -0
  83. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/implement-use-case/SKILL.md +0 -0
  84. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/integrate-mcp/SKILL.md +0 -0
  85. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/maintain-docs/SKILL.md +0 -0
  86. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/plan-change/SKILL.md +0 -0
  87. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/publish-change/SKILL.md +0 -0
  88. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/review-change/SKILL.md +0 -0
  89. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/run-change/SKILL.md +0 -0
  90. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/run-change/references/execution.md +0 -0
  91. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/run-project-command/SKILL.md +0 -0
  92. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/specify-use-case/SKILL.md +0 -0
  93. {engineering_process-0.2.0 → engineering_process-0.2.1}/process_assets/skills/verify-change/SKILL.md +0 -0
  94. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/adoption-migration.schema.json +0 -0
  95. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/change.schema.json +0 -0
  96. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/evidence-receipt.schema.json +0 -0
  97. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/lifecycle.schema.json +0 -0
  98. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/plan.schema.json +0 -0
  99. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/process-lock.schema.json +0 -0
  100. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/project.schema.json +0 -0
  101. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/release-change.schema.json +0 -0
  102. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/release.schema.json +0 -0
  103. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/review.schema.json +0 -0
  104. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/reviewer-attestation.schema.json +0 -0
  105. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/supplemental-verification.schema.json +0 -0
  106. {engineering_process-0.2.0 → engineering_process-0.2.1}/schemas/verification.schema.json +0 -0
  107. {engineering_process-0.2.0 → engineering_process-0.2.1}/setup.cfg +0 -0
  108. {engineering_process-0.2.0 → engineering_process-0.2.1}/templates/AGENTS.process.md +0 -0
  109. {engineering_process-0.2.0 → engineering_process-0.2.1}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  110. {engineering_process-0.2.0 → engineering_process-0.2.1}/templates/adopt-process-windows-job.py +0 -0
  111. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_adoption.py +0 -0
  112. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_artifact_attestation.py +0 -0
  113. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_bootstrap.py +0 -0
  114. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_bundles.py +0 -0
  115. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_cli.py +0 -0
  116. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_contracts.py +0 -0
  117. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_distribution.py +0 -0
  118. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_distribution_verify.py +0 -0
  119. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_environment.py +0 -0
  120. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_evals.py +0 -0
  121. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_impact.py +0 -0
  122. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_lifecycle.py +0 -0
  123. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_runner.py +0 -0
  124. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_runtime.py +0 -0
  125. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_schemas.py +0 -0
  126. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_skills.py +0 -0
  127. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_source_checkout.py +0 -0
  128. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_supervision.py +0 -0
  129. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_supplemental.py +0 -0
  130. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_syncing.py +0 -0
  131. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_tooling.py +0 -0
  132. {engineering_process-0.2.0 → engineering_process-0.2.1}/tests/test_windows_job.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Agent-neutral end-to-end engineering lifecycle and deterministic process CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -71,6 +71,13 @@ verification manifest and project-adoption migration each follow that rule. Addi
71
71
  optional `timeoutSeconds` evidence to verification schema 2 preserves existing
72
72
  schema-2 documents and therefore does not advance that artifact's schema version.
73
73
 
74
+ Every generator must be qualified against the exact released reader that consumes its
75
+ output. For a lifecycle artifact, validation at creation is insufficient: CI runs the
76
+ generated change, plan, review, completion, and exported evidence through public N-1
77
+ up to the final consuming transition. Review schema and quality mappings derive from
78
+ the registered change contract instead of being maintained as an independent version
79
+ constant.
80
+
74
81
  Package `schemaImpact` is `unchanged`, `additive`, or `breaking` for the combined
75
82
  release. Additive schema capability requires at least a capability release; a
76
83
  breaking schema requires an incompatible release.
@@ -97,7 +104,9 @@ requirement pin, omits generated hashes or managed assets, or requires a post-me
97
104
  step must fail closed.
98
105
 
99
106
  `requirements/process.in` owns the direct public pin and `requirements/process.txt`
100
- is its pip-compile hash lock. Renovate's exact allowlisted post-upgrade command runs
107
+ is its pip-compile hash lock. The lock generator is pinned, and the committed lock must
108
+ cover compatible binary artifacts for every supported Python/OS runner rather than
109
+ only the platform that compiled it. Renovate's exact allowlisted post-upgrade command runs
101
110
  `.process/adopt-process.py`; that managed runner creates a bounded temporary
102
111
  environment outside the checkout, rejects symlink, junction, or reparse input in
103
112
  every supplied lock-path component, and makes one stable private snapshot. Component
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "0.2.0"
3
+ VERSION = "0.2.1"
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+ import binascii
5
+ import json
3
6
  import re
4
7
  from pathlib import Path
5
8
 
@@ -243,12 +246,66 @@ def managed_pull_request_visibility_issues(body: str) -> list[str]:
243
246
 
244
247
 
245
248
  PROJECT_EXTENSION_HEADING = "## Project-specific requirements"
249
+ RENOVATE_DEBUG_COMMENT_RE = re.compile(
250
+ r"^<!--renovate-debug:(?P<payload>[A-Za-z0-9+/]{1,4096}={0,2})-->$"
251
+ )
252
+ RENOVATE_DEBUG_KEYS = {
253
+ "createdInVer",
254
+ "updatedInVer",
255
+ "targetBranch",
256
+ "labels",
257
+ }
258
+
259
+
260
+ def _strip_renovate_debug_comment(extension: str) -> tuple[str, list[str]]:
261
+ lines = extension.strip().splitlines()
262
+ matches = [
263
+ (index, match)
264
+ for index, line in enumerate(lines)
265
+ if (match := RENOVATE_DEBUG_COMMENT_RE.fullmatch(line)) is not None
266
+ ]
267
+ if not matches:
268
+ return extension, []
269
+ if len(matches) != 1 or matches[0][0] != len(lines) - 1:
270
+ return extension, ["Renovate metadata comment must appear exactly once at the end"]
271
+ _, match = matches[0]
272
+ try:
273
+ decoded = base64.b64decode(match.group("payload"), validate=True)
274
+ if len(decoded) > 3072:
275
+ raise ValueError("payload exceeds 3072 decoded bytes")
276
+ metadata = json.loads(decoded.decode("utf-8"))
277
+ except (binascii.Error, UnicodeDecodeError, json.JSONDecodeError, ValueError):
278
+ return extension, ["Renovate metadata comment must contain bounded base64 JSON"]
279
+ if not isinstance(metadata, dict) or set(metadata) != RENOVATE_DEBUG_KEYS:
280
+ return extension, ["Renovate metadata comment has an unexpected JSON contract"]
281
+ if any(
282
+ not isinstance(metadata[key], str)
283
+ or not metadata[key]
284
+ or len(metadata[key]) > 64
285
+ for key in ("createdInVer", "updatedInVer")
286
+ ):
287
+ return extension, ["Renovate metadata versions must be bounded strings"]
288
+ if metadata["targetBranch"] != "main":
289
+ return extension, ["Renovate metadata must target protected main"]
290
+ labels = metadata["labels"]
291
+ if (
292
+ not isinstance(labels, list)
293
+ or len(labels) > 50
294
+ or any(not isinstance(label, str) or len(label) > 100 for label in labels)
295
+ ):
296
+ return extension, ["Renovate metadata labels must be a bounded string list"]
297
+ return "\n".join(lines[:-1]).strip(), []
246
298
 
247
299
 
248
300
  def validate_project_extensions(body: str, *, allow_pending: bool) -> list[str]:
249
301
  body = _normalized_markdown(body)
250
302
  _, end = _managed_span(body)
251
303
  extension = body[end:]
304
+ if not extension.strip():
305
+ return []
306
+ extension, metadata_issues = _strip_renovate_debug_comment(extension)
307
+ if metadata_issues:
308
+ return metadata_issues
252
309
  if not extension.strip():
253
310
  return []
254
311
  if contains_raw_html(extension):
@@ -118,10 +118,6 @@ def validate_reviewed_checkpoint(
118
118
  ],
119
119
  )
120
120
  if reviewed_checkpoint != release_checkpoint:
121
- _git(
122
- project_root,
123
- ["merge-base", "--is-ancestor", reviewed_checkpoint, release_checkpoint],
124
- )
125
121
  reviewed_tree = _git(
126
122
  project_root,
127
123
  ["rev-parse", "--verify", f"{reviewed_checkpoint}^{{tree}}"],
@@ -232,7 +232,7 @@ def _release_lifecycle_documents(
232
232
  ) -> tuple[bytes, bytes]:
233
233
  change_id = f"release-{version.replace('.', '-')}"
234
234
  contract = {
235
- "schemaVersion": 2,
235
+ "schemaVersion": 3,
236
236
  "id": change_id,
237
237
  "summary": f"Authorize and publish {project} {version}",
238
238
  "source": "Automatically aggregated reviewed release-change fragments",
@@ -259,6 +259,71 @@ def _release_lifecycle_documents(
259
259
  },
260
260
  ],
261
261
  "requiredProfiles": ["development", "review"],
262
+ "quality": {
263
+ "standard": "production-v1",
264
+ "assessments": [
265
+ {
266
+ "dimension": "compatibility",
267
+ "status": "applicable",
268
+ "rationale": "The release must preserve its declared compatibility and exact identity surfaces.",
269
+ "criteria": ["ac-identity"],
270
+ },
271
+ {
272
+ "dimension": "correctness",
273
+ "status": "applicable",
274
+ "rationale": "The derived identity and published bytes must match the reviewed release contract.",
275
+ "criteria": ["ac-identity", "ac-publication"],
276
+ },
277
+ {
278
+ "dimension": "maintainability",
279
+ "status": "applicable",
280
+ "rationale": "Independent review verifies the generated contract and deterministic release owner.",
281
+ "criteria": ["ac-review"],
282
+ },
283
+ {
284
+ "dimension": "observability",
285
+ "status": "applicable",
286
+ "rationale": "Release evidence and attestations expose the exact publication outcome.",
287
+ "criteria": ["ac-publication"],
288
+ },
289
+ {
290
+ "dimension": "operability",
291
+ "status": "applicable",
292
+ "rationale": "The reviewed release must publish and recover through the declared automated workflow.",
293
+ "criteria": ["ac-publication"],
294
+ },
295
+ {
296
+ "dimension": "performance",
297
+ "status": "not-applicable",
298
+ "rationale": "Release authorization adds no product runtime or scaling behavior.",
299
+ "criteria": [],
300
+ },
301
+ {
302
+ "dimension": "privacy",
303
+ "status": "not-applicable",
304
+ "rationale": "Release metadata contains no personal or sensitive data processing.",
305
+ "criteria": [],
306
+ },
307
+ {
308
+ "dimension": "reliability",
309
+ "status": "applicable",
310
+ "rationale": "Immutable assets and exact external-state checks must fail closed and recover deterministically.",
311
+ "criteria": ["ac-publication"],
312
+ },
313
+ {
314
+ "dimension": "security",
315
+ "status": "applicable",
316
+ "rationale": "Independent review and merge remain the only publication authorization boundary.",
317
+ "criteria": ["ac-review"],
318
+ },
319
+ {
320
+ "dimension": "supply-chain",
321
+ "status": "applicable",
322
+ "rationale": "The release binds source, version, artifacts, attestations, and registry bytes.",
323
+ "criteria": ["ac-identity", "ac-publication"],
324
+ },
325
+ ],
326
+ },
262
327
  "signOff": {
263
328
  "required": False,
264
329
  "status": "not-required",
@@ -268,7 +333,7 @@ def _release_lifecycle_documents(
268
333
  contract_bytes = _canonical_json_bytes(contract)
269
334
  contract_digest = f"sha256:{hashlib.sha256(contract_bytes).hexdigest()}"
270
335
  plan = {
271
- "schemaVersion": 1,
336
+ "schemaVersion": 2,
272
337
  "changeId": change_id,
273
338
  "contractDigest": contract_digest,
274
339
  "approach": "Validate the generated release contract and every declared surface, build from the exact reviewed tree, and permit publication only after independent approval and merge.",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Agent-neutral end-to-end engineering lifecycle and deterministic process CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -110,8 +110,12 @@ tests/test_evals.py
110
110
  tests/test_impact.py
111
111
  tests/test_lifecycle.py
112
112
  tests/test_publication.py
113
+ tests/test_publish_event.py
114
+ tests/test_pypi_publication.py
113
115
  tests/test_release.py
114
116
  tests/test_release_candidate.py
117
+ tests/test_release_preparation.py
118
+ tests/test_release_qualification.py
115
119
  tests/test_runner.py
116
120
  tests/test_runtime.py
117
121
  tests/test_schemas.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Agent-neutral end-to-end engineering lifecycle and deterministic process CLI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,106 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "previousVersion": "0.2.0",
4
+ "version": "0.2.1",
5
+ "classification": "patch",
6
+ "compatibility": "backward-compatible",
7
+ "schemaImpact": "unchanged",
8
+ "migration": null,
9
+ "identity": {
10
+ "package": "engineering-process",
11
+ "distribution": "engineering_process",
12
+ "tag": "v0.2.1",
13
+ "releaseName": "v0.2.1",
14
+ "runtimeVersion": {
15
+ "path": "engineering_process/__init__.py",
16
+ "variable": "VERSION"
17
+ },
18
+ "artifacts": [
19
+ "engineering_process-0.2.1-py3-none-any.whl",
20
+ "engineering_process-0.2.1.tar.gz"
21
+ ],
22
+ "receiptAsset": "engineering-process-v0.2.1-evidence.json",
23
+ "authorizationAsset": null
24
+ },
25
+ "provenance": {
26
+ "mode": "governed",
27
+ "statement": "The public N-1 lifecycle receipt and reviewed Release PR authorize this release.",
28
+ "lifecycleReceipt": {
29
+ "asset": "engineering-process-v0.2.1-evidence.json",
30
+ "project": "engineering-process",
31
+ "changeId": "release-0-2-1",
32
+ "cycle": 1
33
+ }
34
+ },
35
+ "changes": [
36
+ {
37
+ "id": "bootstrap-self-adoption-verifier",
38
+ "type": "fix",
39
+ "surfaces": [
40
+ "adoption",
41
+ "self-hosting",
42
+ "workflow"
43
+ ],
44
+ "rationale": "Verify bootstrap self-adoption with the target-aware producer template so the exact installed public authority performs the check even when the managed N-1 runner predates check-only mode."
45
+ },
46
+ {
47
+ "id": "portable-process-lock-hashes",
48
+ "type": "fix",
49
+ "surfaces": [
50
+ "adoption",
51
+ "portability",
52
+ "supply-chain"
53
+ ],
54
+ "rationale": "Regenerate process dependency locks with the complete binary wheel hash set and pin the lock generator so one immutable authority installs on every supported runtime and platform."
55
+ },
56
+ {
57
+ "id": "post-v0-2-release-adoption-hardening",
58
+ "type": "fix",
59
+ "surfaces": [
60
+ "adoption",
61
+ "publication",
62
+ "workflow"
63
+ ],
64
+ "rationale": "Correct event-driven publication recovery, validate Renovate metadata through a bounded exact contract, require strict consumer outcomes, and verify self-adoption through the public hash-locked authority instead of source under development."
65
+ },
66
+ {
67
+ "id": "release-approval-app-token",
68
+ "type": "fix",
69
+ "surfaces": [
70
+ "publication",
71
+ "workflow"
72
+ ],
73
+ "rationale": "Use the short-lived Release PR owning GitHub App token for the Draft-to-Ready transition after exact lifecycle and receipt authorization, while retaining GITHUB_TOKEN for commit status writes."
74
+ },
75
+ {
76
+ "id": "release-lifecycle-quality-contract",
77
+ "type": "fix",
78
+ "surfaces": [
79
+ "contracts",
80
+ "publication",
81
+ "workflow"
82
+ ],
83
+ "rationale": "Generate new release lifecycle runs with schema-3 production quality assessments and a bounded schema-2 plan so public N-1 authority can validate the Release PR candidate."
84
+ },
85
+ {
86
+ "id": "release-pr-self-adoption-refresh",
87
+ "type": "fix",
88
+ "surfaces": [
89
+ "adoption",
90
+ "publication",
91
+ "workflow"
92
+ ],
93
+ "rationale": "Defer a new Release PR until the latest public authority is self-adopted, refresh that adoption through the exact verified publication event, and resume release derivation automatically after the adoption merge."
94
+ },
95
+ {
96
+ "id": "release-review-qualification-gate",
97
+ "type": "fix",
98
+ "surfaces": [
99
+ "contracts",
100
+ "publication",
101
+ "workflow"
102
+ ],
103
+ "rationale": "Derive release review quality from its registered contract and require a disposable complete public N-1 lifecycle rehearsal before source changes can merge."
104
+ }
105
+ ]
106
+ }
@@ -500,6 +500,11 @@ def main(argv: list[str] | None = None) -> int:
500
500
  type=Path,
501
501
  default=Path("requirements/process.txt"),
502
502
  )
503
+ parser.add_argument(
504
+ "--check",
505
+ action="store_true",
506
+ help="Validate through the installed hash-locked authority without applying",
507
+ )
503
508
  args = parser.parse_args(argv)
504
509
  project_root = Path(os.path.abspath(os.fspath(args.project_root)))
505
510
  requirements_source, requirements_root = _requirements_binding(
@@ -566,7 +571,25 @@ def main(argv: list[str] | None = None) -> int:
566
571
  target_version = _installed_process_version(
567
572
  python, cwd=environment_root
568
573
  )
569
- if target_version == current_version:
574
+ if args.check:
575
+ output = _run(
576
+ [
577
+ str(python),
578
+ "-I",
579
+ "-c",
580
+ "from engineering_process.cli import main; "
581
+ "raise SystemExit(main())",
582
+ "adoption",
583
+ "check",
584
+ "--project-root",
585
+ str(project_root),
586
+ "--requirements-lock",
587
+ str(requirements_source),
588
+ "--json",
589
+ ],
590
+ cwd=environment_root,
591
+ )
592
+ elif target_version == current_version:
570
593
  output = json.dumps(
571
594
  {
572
595
  "requirementsDigest": requirements_digest,
@@ -615,9 +638,11 @@ def main(argv: list[str] | None = None) -> int:
615
638
  if (
616
639
  not isinstance(result, dict)
617
640
  or result.get("requirementsDigest") != requirements_digest
641
+ or result.get("version") != target_version
618
642
  ):
619
643
  raise RuntimeError(
620
- "adoption authority did not attest the private requirements snapshot"
644
+ "adoption authority did not attest the installed version and "
645
+ "private requirements snapshot"
621
646
  )
622
647
  sys.stdout.write(output)
623
648
  return 0
@@ -262,7 +262,12 @@ class AdoptionRunnerTests(unittest.TestCase):
262
262
  self.assertEqual(observed_snapshot, [snapshot])
263
263
  self.assertTrue(source.samefile(checkout))
264
264
  self.assertEqual(digest, expected)
265
- return json.dumps({"requirementsDigest": digest})
265
+ return json.dumps(
266
+ {
267
+ "requirementsDigest": digest,
268
+ "version": "0.1.1",
269
+ }
270
+ )
266
271
  return ""
267
272
 
268
273
  with (
@@ -281,6 +286,61 @@ class AdoptionRunnerTests(unittest.TestCase):
281
286
  ),
282
287
  )
283
288
 
289
+ def test_check_uses_installed_authority_even_when_target_is_adopted(self):
290
+ runner = load_runner()
291
+ content = b"--only-binary :all:\nengineering-process==0.2.0\n"
292
+ digest = "sha256:" + hashlib.sha256(content).hexdigest()
293
+ with tempfile.TemporaryDirectory() as directory:
294
+ root = Path(directory)
295
+ self.write_process_lock(root, "0.2.0")
296
+ source = root / "requirements" / "process.txt"
297
+ source.parent.mkdir()
298
+ source.write_bytes(content)
299
+ adoption_commands: list[list[str]] = []
300
+
301
+ def fake_run(argv, *, cwd):
302
+ del cwd
303
+ if any("engineering_process.VERSION" in part for part in argv):
304
+ return "0.2.0\n"
305
+ if "adoption" in argv:
306
+ adoption_commands.append(argv)
307
+ self.assertIn("check", argv)
308
+ self.assertNotIn("apply", argv)
309
+ supplied = Path(argv[argv.index("--requirements-lock") + 1])
310
+ self.assertTrue(source.samefile(supplied))
311
+ return json.dumps(
312
+ {
313
+ "requirementsDigest": digest,
314
+ "status": "passed",
315
+ "version": "0.2.0",
316
+ }
317
+ )
318
+ return ""
319
+
320
+ with (
321
+ mock.patch.object(runner, "_run", side_effect=fake_run),
322
+ mock.patch.object(runner.sys.stdout, "write"),
323
+ ):
324
+ self.assertEqual(
325
+ 0,
326
+ runner.main(
327
+ [
328
+ "--project-root",
329
+ str(root),
330
+ "--requirements-lock",
331
+ "requirements/process.txt",
332
+ "--check",
333
+ ]
334
+ ),
335
+ )
336
+
337
+ self.assertEqual(1, len(adoption_commands))
338
+ self.assertIn("-I", adoption_commands[0])
339
+ self.assertIn(
340
+ "from engineering_process.cli import main; raise SystemExit(main())",
341
+ adoption_commands[0],
342
+ )
343
+
284
344
  def test_main_noops_when_the_hash_locked_target_is_already_adopted(self):
285
345
  runner = load_runner()
286
346
  content = b"--only-binary :all:\nengineering-process==0.1.1\n"
@@ -1,3 +1,5 @@
1
+ import base64
2
+ import json
1
3
  import subprocess
2
4
  import tempfile
3
5
  import unittest
@@ -96,6 +98,57 @@ class PublicationTests(unittest.TestCase):
96
98
  ),
97
99
  )
98
100
 
101
+ def test_draft_renovate_pr_accepts_only_bounded_debug_metadata(self):
102
+ payload = base64.b64encode(
103
+ json.dumps(
104
+ {
105
+ "createdInVer": "44.37.1",
106
+ "updatedInVer": "44.37.1",
107
+ "targetBranch": "main",
108
+ "labels": [],
109
+ },
110
+ separators=(",", ":"),
111
+ ).encode("utf-8")
112
+ ).decode("ascii")
113
+ body = pr_body("pending") + f"\n<!--renovate-debug:{payload}-->\n"
114
+
115
+ self.assertEqual(
116
+ [],
117
+ validate_pull_request(
118
+ title="chore(deps): update actions/checkout action to v7",
119
+ body=body,
120
+ branch="automation/renovate/actions-checkout-7.x",
121
+ state="draft",
122
+ ),
123
+ )
124
+
125
+ def test_renovate_metadata_does_not_allow_arbitrary_html_or_contracts(self):
126
+ valid = {
127
+ "createdInVer": "44.37.1",
128
+ "updatedInVer": "44.37.1",
129
+ "targetBranch": "main",
130
+ "labels": [],
131
+ }
132
+ unexpected = {**valid, "extra": "not-allowed"}
133
+ encoded = base64.b64encode(
134
+ json.dumps(unexpected, separators=(",", ":")).encode("utf-8")
135
+ ).decode("ascii")
136
+ bodies = (
137
+ pr_body("pending") + "\n<div>hidden policy</div>\n",
138
+ pr_body("pending") + "\n<!--renovate-debug:not-base64-->\n",
139
+ pr_body("pending") + f"\n<!--renovate-debug:{encoded}-->\n",
140
+ )
141
+
142
+ for body in bodies:
143
+ with self.subTest(body=body[-100:]):
144
+ self.assertTrue(
145
+ validate_pull_request(
146
+ title="chore(deps): update actions/checkout action to v7",
147
+ body=body,
148
+ branch="automation/renovate/actions-checkout-7.x",
149
+ state="draft",
150
+ )
151
+ )
99
152
  def test_project_extensions_cannot_redefine_common_policy(self):
100
153
  duplicate_section = pr_body() + (
101
154
  "\n## Independent review\n\nIndependent review is not required here.\n"
@@ -0,0 +1,57 @@
1
+ import unittest
2
+
3
+ from verification.validate_publish_event import (
4
+ PublishEventError,
5
+ validate_publish_event,
6
+ )
7
+
8
+
9
+ def event() -> dict[str, object]:
10
+ return {
11
+ "action": "engineering-process-release-ready",
12
+ "client_payload": {
13
+ "attestationDigest": f"sha256:{'a' * 64}",
14
+ "commit": "b" * 40,
15
+ "repository": "phuongnse/engineering-process",
16
+ "tag": "v0.2.0",
17
+ "version": "0.2.0",
18
+ },
19
+ "repository": {"full_name": "phuongnse/engineering-process"},
20
+ "sender": {"login": "phuongnse-renovate-ops[bot]"},
21
+ }
22
+
23
+
24
+ class PublishEventTests(unittest.TestCase):
25
+ def test_exact_app_authored_event_is_accepted(self):
26
+ self.assertEqual(event()["client_payload"], validate_publish_event(event()))
27
+
28
+ def test_different_sender_is_rejected(self):
29
+ candidate = event()
30
+ candidate["sender"]["login"] = "phuongnse"
31
+
32
+ with self.assertRaisesRegex(PublishEventError, "release GitHub App"):
33
+ validate_publish_event(candidate)
34
+
35
+ def test_extra_payload_authority_is_rejected(self):
36
+ candidate = event()
37
+ candidate["client_payload"]["skipExisting"] = True
38
+
39
+ with self.assertRaisesRegex(PublishEventError, "unexpected fields"):
40
+ validate_publish_event(candidate)
41
+
42
+ def test_tag_commit_and_digest_are_bound(self):
43
+ cases = (
44
+ ("tag", "v0.2.1", "tag does not match"),
45
+ ("commit", "pending", "invalid commit"),
46
+ ("attestationDigest", "sha256:pending", "attestation digest"),
47
+ )
48
+ for field, value, message in cases:
49
+ with self.subTest(field=field):
50
+ candidate = event()
51
+ candidate["client_payload"][field] = value
52
+ with self.assertRaisesRegex(PublishEventError, message):
53
+ validate_publish_event(candidate)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ unittest.main()