engineering-process 1.0.0__tar.gz → 1.0.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 (67) hide show
  1. {engineering_process-1.0.0/engineering_process.egg-info → engineering_process-1.0.1}/PKG-INFO +1 -1
  2. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/__init__.py +1 -1
  3. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/_supervisor_posix.py +24 -25
  4. {engineering_process-1.0.0 → engineering_process-1.0.1/engineering_process.egg-info}/PKG-INFO +1 -1
  5. {engineering_process-1.0.0 → engineering_process-1.0.1}/pyproject.toml +1 -1
  6. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_architecture.py +1 -1
  7. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_automation.py +39 -1
  8. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_commands.py +28 -0
  9. {engineering_process-1.0.0 → engineering_process-1.0.1}/LICENSE +0 -0
  10. {engineering_process-1.0.0 → engineering_process-1.0.1}/MANIFEST.in +0 -0
  11. {engineering_process-1.0.0 → engineering_process-1.0.1}/README.md +0 -0
  12. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/__main__.py +0 -0
  13. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/_supervisor_windows.py +0 -0
  14. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/_windows_job.py +0 -0
  15. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/adoption.py +0 -0
  16. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/cli.py +0 -0
  17. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/commands.py +0 -0
  18. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/contracts.py +0 -0
  19. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/distribution.py +0 -0
  20. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/helper_launch.py +0 -0
  21. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/lifecycle.py +0 -0
  22. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/project.py +0 -0
  23. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/publication_compat.py +0 -0
  24. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/release.py +0 -0
  25. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/repository.py +0 -0
  26. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/requirements-dev.txt +0 -0
  27. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/requirements-runtime.txt +0 -0
  28. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/skills.py +0 -0
  29. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process/supervision.py +0 -0
  30. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process.egg-info/SOURCES.txt +0 -0
  31. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process.egg-info/dependency_links.txt +0 -0
  32. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process.egg-info/entry_points.txt +0 -0
  33. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process.egg-info/requires.txt +0 -0
  34. {engineering_process-1.0.0 → engineering_process-1.0.1}/engineering_process.egg-info/top_level.txt +0 -0
  35. {engineering_process-1.0.0 → engineering_process-1.0.1}/process-graph.json +0 -0
  36. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/finish-change/SKILL.md +0 -0
  37. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/implement-change/SKILL.md +0 -0
  38. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/improve-process/SKILL.md +0 -0
  39. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/plan-change/SKILL.md +0 -0
  40. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/review-change/SKILL.md +0 -0
  41. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/run-change/SKILL.md +0 -0
  42. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/start-change/SKILL.md +0 -0
  43. {engineering_process-1.0.0 → engineering_process-1.0.1}/process_assets/skills/verify-change/SKILL.md +0 -0
  44. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/change.schema.json +0 -0
  45. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/plan.schema.json +0 -0
  46. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/process-graph.schema.json +0 -0
  47. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/process-lock.schema.json +0 -0
  48. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/project-legacy.schema.json +0 -0
  49. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/project.schema.json +0 -0
  50. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/receipt.schema.json +0 -0
  51. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/release-change.schema.json +0 -0
  52. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/release.schema.json +0 -0
  53. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/review.schema.json +0 -0
  54. {engineering_process-1.0.0 → engineering_process-1.0.1}/schemas/run.schema.json +0 -0
  55. {engineering_process-1.0.0 → engineering_process-1.0.1}/setup.cfg +0 -0
  56. {engineering_process-1.0.0 → engineering_process-1.0.1}/templates/AGENTS.process.md +0 -0
  57. {engineering_process-1.0.0 → engineering_process-1.0.1}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  58. {engineering_process-1.0.0 → engineering_process-1.0.1}/templates/adopt-process-windows-job.py +0 -0
  59. {engineering_process-1.0.0 → engineering_process-1.0.1}/templates/adopt-process.py +0 -0
  60. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_adoption.py +0 -0
  61. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_cli.py +0 -0
  62. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_contracts.py +0 -0
  63. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_lifecycle.py +0 -0
  64. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_publication_compat.py +0 -0
  65. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_release.py +0 -0
  66. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_repository.py +0 -0
  67. {engineering_process-1.0.0 → engineering_process-1.0.1}/tests/test_skills.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: A small agent-neutral engineering lifecycle with managed adoption
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "1.0.0"
3
+ VERSION = "1.0.1"
@@ -98,13 +98,24 @@ def _pid_alive(pid: int) -> bool:
98
98
  status = Path(f"/proc/{pid}/stat")
99
99
  if status.is_file():
100
100
  try:
101
- return status.read_text(encoding="utf-8").split()[2] != "Z"
101
+ state = status.read_text(encoding="utf-8").rsplit(")", 1)[1].split()[0]
102
+ if state == "Z":
103
+ try:
104
+ os.waitpid(pid, os.WNOHANG)
105
+ except OSError:
106
+ pass
107
+ return False
102
108
  except (OSError, IndexError):
103
109
  pass
104
110
  return True
105
111
 
106
112
 
107
113
  def _process_group_exists(process_group: int) -> bool:
114
+ try:
115
+ while os.waitpid(-process_group, os.WNOHANG)[0]:
116
+ pass
117
+ except OSError:
118
+ pass
108
119
  try:
109
120
  os.killpg(process_group, 0)
110
121
  except ProcessLookupError:
@@ -114,6 +125,14 @@ def _process_group_exists(process_group: int) -> bool:
114
125
  return True
115
126
 
116
127
 
128
+ def _signal_processes(processes: set[int], signal_number: int) -> None:
129
+ for pid in processes:
130
+ try:
131
+ os.kill(pid, signal_number)
132
+ except OSError:
133
+ pass
134
+
135
+
117
136
  def _wait_for_process_group(process_group: int, timeout: float) -> bool:
118
137
  deadline = time.monotonic() + timeout
119
138
  while time.monotonic() < deadline:
@@ -144,11 +163,7 @@ def _terminate_group(process_group: int, grace_seconds: float) -> CleanupOutcome
144
163
  return CleanupOutcome(
145
164
  bounded=bounded,
146
165
  descendants_found=True,
147
- error=(
148
- None
149
- if bounded
150
- else "command process group could not be terminated within the bounded grace period"
151
- ),
166
+ error=None if bounded else "command process group exceeded its termination grace period",
152
167
  )
153
168
 
154
169
 
@@ -250,24 +265,12 @@ class PosixProcessSupervisor:
250
265
  live = {pid for pid in candidates if _pid_alive(pid)}
251
266
  if not live:
252
267
  return CleanupOutcome(bounded=True)
253
- for pid in live:
254
- try:
255
- os.kill(pid, signal.SIGTERM)
256
- except ProcessLookupError:
257
- pass
258
- except OSError:
259
- pass
268
+ _signal_processes(live, signal.SIGTERM)
260
269
  deadline = time.monotonic() + grace_seconds
261
270
  while time.monotonic() < deadline and any(_pid_alive(pid) for pid in live):
262
271
  time.sleep(0.02)
263
272
  survivors = {pid for pid in live if _pid_alive(pid)}
264
- for pid in survivors:
265
- try:
266
- os.kill(pid, signal.SIGKILL)
267
- except ProcessLookupError:
268
- pass
269
- except OSError:
270
- pass
273
+ _signal_processes(survivors, signal.SIGKILL)
271
274
  deadline = time.monotonic() + grace_seconds
272
275
  while time.monotonic() < deadline and any(
273
276
  _pid_alive(pid) for pid in survivors
@@ -282,11 +285,7 @@ class PosixProcessSupervisor:
282
285
  return CleanupOutcome(
283
286
  bounded=bounded,
284
287
  descendants_found=True,
285
- error=(
286
- None
287
- if bounded
288
- else "detached descendants survived bounded termination"
289
- ),
288
+ error=None if bounded else "detached descendants survived bounded termination",
290
289
  )
291
290
 
292
291
  def terminate(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: A small agent-neutral engineering lifecycle with managed adoption
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,7 +28,7 @@ class ArchitectureTests(unittest.TestCase):
28
28
  actual = {path.name for path in (ROOT / "engineering_process").glob("*.py")}
29
29
  self.assertTrue(removed_modules.isdisjoint(actual))
30
30
  workflows = {path.name for path in (ROOT / ".github" / "workflows").glob("*.yml")}
31
- self.assertEqual({"ci.yml", "release-pr.yml", "release.yml"}, workflows)
31
+ self.assertEqual({"ci.yml", "publish.yml", "release-pr.yml"}, workflows)
32
32
 
33
33
  def test_source_skills_have_no_orphaned_legacy_directories(self) -> None:
34
34
  skills = {
@@ -43,7 +43,7 @@ class AutomationTests(unittest.TestCase):
43
43
  )
44
44
 
45
45
  def test_release_dispatches_the_published_version_to_the_control_plane(self) -> None:
46
- workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text(
46
+ workflow = (ROOT / ".github" / "workflows" / "publish.yml").read_text(
47
47
  encoding="utf-8"
48
48
  )
49
49
  self.assertIn("engineering-process-published", workflow)
@@ -54,6 +54,44 @@ class AutomationTests(unittest.TestCase):
54
54
  self.assertIn("SOURCE_DATE_EPOCH", workflow)
55
55
  self.assertIn("verification/normalize_sdist.py", workflow)
56
56
  self.assertIn("points to a different commit", workflow)
57
+ self.assertIn("workflow_dispatch:", workflow)
58
+ self.assertIn("release_sha:", workflow)
59
+ self.assertIn('git merge-base --is-ancestor "$RELEASE_SOURCE_SHA" origin/main', workflow)
60
+ self.assertIn("needs.metadata.outputs.source_sha", workflow)
61
+ self.assertNotIn('--target "$GITHUB_SHA"', workflow)
62
+ self.assertEqual(
63
+ 3,
64
+ workflow.count(
65
+ 'path.suffix == ".whl" or path.name.endswith(".tar.gz")'
66
+ ),
67
+ )
68
+ self.assertIn("for file in dist/*.whl dist/*.tar.gz; do", workflow)
69
+ self.assertNotIn("for file in dist/*; do", workflow)
70
+ publish_job = workflow.split(" publish:\n", maxsplit=1)[1].split(
71
+ "\n dispatch-adoption:\n", maxsplit=1
72
+ )[0]
73
+ self.assertIn("id: release-token", publish_job)
74
+ self.assertIn("repositories: engineering-process", publish_job)
75
+ self.assertIn("permission-contents: write", publish_job)
76
+ self.assertIn(
77
+ "GH_TOKEN: ${{ steps.release-token.outputs.token }}", publish_job
78
+ )
79
+ self.assertNotIn("GH_TOKEN: ${{ github.token }}", publish_job)
80
+ self.assertIn('"repos/$GITHUB_REPOSITORY/git/refs"', publish_job)
81
+ self.assertIn('-f ref="refs/tags/$RELEASE_TAG"', publish_job)
82
+ self.assertIn('-f sha="${{ needs.metadata.outputs.source_sha }}"', publish_job)
83
+ self.assertIn("--verify-tag", publish_job)
84
+ self.assertNotIn("--target", publish_job)
85
+ trusted_checkout = workflow.index(" ref: main")
86
+ preflight = workflow.index("name: Authorize release source from trusted main")
87
+ source_checkout = workflow.index("ref: ${{ steps.release.outputs.source_sha }}")
88
+ editable_install = workflow.index(
89
+ "python -m pip install -r engineering_process/requirements-runtime.txt "
90
+ "--editable ."
91
+ )
92
+ self.assertLess(trusted_checkout, preflight)
93
+ self.assertLess(preflight, source_checkout)
94
+ self.assertLess(source_checkout, editable_install)
57
95
 
58
96
  def test_ci_checks_the_adopted_hash_locked_distribution_separately(self) -> None:
59
97
  workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(
@@ -111,6 +111,34 @@ class CommandTests(unittest.TestCase):
111
111
  self.assertEqual("failed", report["status"])
112
112
  self.assertTrue(report["descendantsTerminated"])
113
113
 
114
+ @unittest.skipUnless(
115
+ sys.platform.startswith("linux"), "Linux subreaper reaping assertion"
116
+ )
117
+ def test_exited_descendant_is_reaped_and_passes(self) -> None:
118
+ with tempfile.TemporaryDirectory() as directory:
119
+ root = Path(directory)
120
+ pid_path = root / "child.pid"
121
+ child = "import time; time.sleep(0.01)"
122
+ script = (
123
+ "import pathlib, subprocess, sys, time; "
124
+ f"p=subprocess.Popen([sys.executable, '-c', {child!r}]); "
125
+ "pathlib.Path(sys.argv[1]).write_text(str(p.pid), encoding='utf-8'); "
126
+ "time.sleep(0.05)"
127
+ )
128
+ report = run_check(
129
+ root,
130
+ {
131
+ "id": "naturally-drained-descendant",
132
+ "run": [sys.executable, "-c", script, str(pid_path)],
133
+ "timeoutSeconds": 10,
134
+ },
135
+ )
136
+ pid = int(pid_path.read_text(encoding="utf-8"))
137
+ self.assertEqual("passed", report["status"])
138
+ self.assertFalse(report["descendantsTerminated"])
139
+ self.assertFalse(report["streamFailed"])
140
+ self.assertFalse(Path(f"/proc/{pid}").exists())
141
+
114
142
  @unittest.skipUnless(
115
143
  sys.platform.startswith("linux"), "Linux subreaper containment assertion"
116
144
  )