engineering-process 2.2.0__tar.gz → 2.3.0__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.
- {engineering_process-2.2.0/engineering_process.egg-info → engineering_process-2.3.0}/PKG-INFO +39 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/README.md +38 -1
- engineering_process-2.3.0/RELEASE_NOTES.md +23 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/__init__.py +1 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_posix.py +10 -4
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_windows.py +2 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/artifact_standards.py +8 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/cli.py +17 -4
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/commands.py +6 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/contracts.py +1 -0
- engineering_process-2.3.0/engineering_process/issue.py +112 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/lifecycle.py +36 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/project.py +5 -0
- engineering_process-2.3.0/engineering_process/publication_compat.py +58 -0
- engineering_process-2.2.0/engineering_process/publication_compat.py → engineering_process-2.3.0/engineering_process/source_publication.py +55 -41
- {engineering_process-2.2.0 → engineering_process-2.3.0/engineering_process.egg-info}/PKG-INFO +39 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/SOURCES.txt +4 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-complete/SKILL.md +32 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-review/SKILL.md +9 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-start/SKILL.md +7 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-verify/SKILL.md +10 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/deliver-change/SKILL.md +26 -0
- engineering_process-2.3.0/process_assets/standards/issue.v1.json +78 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/pyproject.toml +3 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/artifact-standard.schema.json +86 -1
- engineering_process-2.3.0/schemas/issue-data.schema.json +44 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/project.schema.json +8 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/receipt.schema.json +20 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_adoption.py +11 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_architecture.py +2 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_artifact_standards.py +155 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_commands.py +100 -2
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_contracts.py +70 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_lifecycle.py +153 -1
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_publication_compat.py +101 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_supervisor_posix.py +35 -0
- engineering_process-2.2.0/RELEASE_NOTES.md +0 -18
- {engineering_process-2.2.0 → engineering_process-2.3.0}/LICENSE +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/MANIFEST.in +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/__main__.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_contract.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_windows_job.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/adoption.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/automation_name.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/distribution.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/helper_launch.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/pr_description.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/production_engineering.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/release.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/release_notes.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/repository.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/skills.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/supervision.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process-graph.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-implement/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-plan/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/process-improve/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/production-engineering/invariants.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/automation-name.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/pull-request.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/release-notes.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/artifact-selection.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/automation-name-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/change.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/plan.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/pr-description-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/process-graph.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/process-lock.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/production-engineering.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release-change.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release-notes-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/renovate-preset.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/review.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/run.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/setup.cfg +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/AGENTS.process.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/adopt-process.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/renovate.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_automation.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_cli.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_distribution.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_production_engineering.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_publication_workflow.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_pypi_cache_horizon.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_release.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_repository.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_skills.py +0 -0
{engineering_process-2.2.0/engineering_process.egg-info → engineering_process-2.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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
|
|
@@ -73,10 +73,31 @@ The coordinator hands review to an actual reviewer. A new agent review starts in
|
|
|
73
73
|
fresh context with the accepted source artifacts and no suggested verdict; the same
|
|
74
74
|
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
75
75
|
own report. The runner's existing task/session interaction and returned result make
|
|
76
|
-
that work inspectable. Sharing a
|
|
76
|
+
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
77
77
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
78
78
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
79
79
|
|
|
80
|
+
Every delegated agent and reviewer follows the active user-selected model and
|
|
81
|
+
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
82
|
+
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
83
|
+
and permits no autonomous upgrade or downgrade. The portable guidance does not make
|
|
84
|
+
processctl a provider runtime or a model-quality evaluator.
|
|
85
|
+
|
|
86
|
+
Consumers using the packaged publication compatibility policy can enable
|
|
87
|
+
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
88
|
+
version that supports it. Start then rejects an invalid branch before creating a
|
|
89
|
+
run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
|
|
90
|
+
range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
|
|
91
|
+
consumers without the opt-in remain supported. Missing pinned bases and mutations
|
|
92
|
+
during the check fail completion.
|
|
93
|
+
|
|
94
|
+
PR readiness additionally requires the selected ready-state body/title and exact
|
|
95
|
+
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
96
|
+
and repeat those checks in required CI. This repository's existing
|
|
97
|
+
`verification/verify_publication.py --pull-request` uses the actual PR metadata and
|
|
98
|
+
selected consumer standard; local lifecycle completion alone does not establish
|
|
99
|
+
external PR readiness. Integration-branch pushes remain consumer-owned operations.
|
|
100
|
+
|
|
80
101
|
Runtime architecture is enforced by semantic fitness functions, not module or source-
|
|
81
102
|
line quotas. Every module has an explicit dependency layer, imports point toward lower
|
|
82
103
|
layers, the internal graph remains acyclic, and lifecycle.py alone owns state
|
|
@@ -122,6 +143,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
122
143
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
123
144
|
stdout or stderr that could contain secrets.
|
|
124
145
|
|
|
146
|
+
For a fresh checkout of this repository, create a virtual environment with a supported
|
|
147
|
+
Python and install `engineering_process/requirements-runtime.txt`,
|
|
148
|
+
`engineering_process/requirements-dev.txt`, and
|
|
149
|
+
`engineering_process/requirements-build.txt` through that interpreter. Use
|
|
150
|
+
`.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
|
|
151
|
+
source CLI and verification scripts. Other consumers own their runtime paths and
|
|
152
|
+
setup commands; installed consumers can use their environment's `processctl` entry
|
|
153
|
+
point without a source checkout.
|
|
154
|
+
|
|
155
|
+
The bounded runner prepends the invoking Python executable's directory to child
|
|
156
|
+
`PATH`. Bare commands therefore search that environment before the inherited path,
|
|
157
|
+
while declared argument arrays, explicit executable paths, secret filtering and
|
|
158
|
+
remaining path entries are preserved. This alignment does not install dependencies.
|
|
159
|
+
|
|
125
160
|
A failed profile also reports a safe `diagnostic` descriptor containing only its
|
|
126
161
|
validated profile identifier, check identifier, canonical one-based profile position,
|
|
127
162
|
and a fixed `processctl` argument array. The position remains unambiguous even when a
|
|
@@ -273,6 +308,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
|
|
|
273
308
|
Consumer-owned `.process/standards.json` selections and override definitions are also
|
|
274
309
|
preserved; the managed PR template follows the effective supported standard. See
|
|
275
310
|
[consumer document standards](ARTIFACT_STANDARDS.md).
|
|
311
|
+
[Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
|
|
312
|
+
mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
|
|
276
313
|
[Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
|
|
277
314
|
selection and override mechanism; consumers apply it in their bootstrap and provider checks.
|
|
278
315
|
The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
|
|
@@ -49,10 +49,31 @@ The coordinator hands review to an actual reviewer. A new agent review starts in
|
|
|
49
49
|
fresh context with the accepted source artifacts and no suggested verdict; the same
|
|
50
50
|
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
51
51
|
own report. The runner's existing task/session interaction and returned result make
|
|
52
|
-
that work inspectable. Sharing a
|
|
52
|
+
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
53
53
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
54
54
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
55
55
|
|
|
56
|
+
Every delegated agent and reviewer follows the active user-selected model and
|
|
57
|
+
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
58
|
+
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
59
|
+
and permits no autonomous upgrade or downgrade. The portable guidance does not make
|
|
60
|
+
processctl a provider runtime or a model-quality evaluator.
|
|
61
|
+
|
|
62
|
+
Consumers using the packaged publication compatibility policy can enable
|
|
63
|
+
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
64
|
+
version that supports it. Start then rejects an invalid branch before creating a
|
|
65
|
+
run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
|
|
66
|
+
range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
|
|
67
|
+
consumers without the opt-in remain supported. Missing pinned bases and mutations
|
|
68
|
+
during the check fail completion.
|
|
69
|
+
|
|
70
|
+
PR readiness additionally requires the selected ready-state body/title and exact
|
|
71
|
+
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
72
|
+
and repeat those checks in required CI. This repository's existing
|
|
73
|
+
`verification/verify_publication.py --pull-request` uses the actual PR metadata and
|
|
74
|
+
selected consumer standard; local lifecycle completion alone does not establish
|
|
75
|
+
external PR readiness. Integration-branch pushes remain consumer-owned operations.
|
|
76
|
+
|
|
56
77
|
Runtime architecture is enforced by semantic fitness functions, not module or source-
|
|
57
78
|
line quotas. Every module has an explicit dependency layer, imports point toward lower
|
|
58
79
|
layers, the internal graph remains acyclic, and lifecycle.py alone owns state
|
|
@@ -98,6 +119,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
98
119
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
99
120
|
stdout or stderr that could contain secrets.
|
|
100
121
|
|
|
122
|
+
For a fresh checkout of this repository, create a virtual environment with a supported
|
|
123
|
+
Python and install `engineering_process/requirements-runtime.txt`,
|
|
124
|
+
`engineering_process/requirements-dev.txt`, and
|
|
125
|
+
`engineering_process/requirements-build.txt` through that interpreter. Use
|
|
126
|
+
`.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
|
|
127
|
+
source CLI and verification scripts. Other consumers own their runtime paths and
|
|
128
|
+
setup commands; installed consumers can use their environment's `processctl` entry
|
|
129
|
+
point without a source checkout.
|
|
130
|
+
|
|
131
|
+
The bounded runner prepends the invoking Python executable's directory to child
|
|
132
|
+
`PATH`. Bare commands therefore search that environment before the inherited path,
|
|
133
|
+
while declared argument arrays, explicit executable paths, secret filtering and
|
|
134
|
+
remaining path entries are preserved. This alignment does not install dependencies.
|
|
135
|
+
|
|
101
136
|
A failed profile also reports a safe `diagnostic` descriptor containing only its
|
|
102
137
|
validated profile identifier, check identifier, canonical one-based profile position,
|
|
103
138
|
and a fixed `processctl` argument array. The position remains unambiguous even when a
|
|
@@ -249,6 +284,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
|
|
|
249
284
|
Consumer-owned `.process/standards.json` selections and override definitions are also
|
|
250
285
|
preserved; the managed PR template follows the effective supported standard. See
|
|
251
286
|
[consumer document standards](ARTIFACT_STANDARDS.md).
|
|
287
|
+
[Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
|
|
288
|
+
mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
|
|
252
289
|
[Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
|
|
253
290
|
selection and override mechanism; consumers apply it in their bootstrap and provider checks.
|
|
254
291
|
The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Engineering Process v2.3.0
|
|
2
|
+
|
|
3
|
+
Changes since v2.2.0.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Generate and verify open and closed issue records from a versioned default or consumer\-owned override while keeping tracker actions and product workflow consumer\-owned\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
|
|
8
|
+
- Opt into publication checks at lifecycle start and completion\, with pinned source metadata in version 2 receipts and exact PR head\/body checks before readiness\; existing consumers and version 1 receipts remain supported\. ([#180](https://github.com/phuongnse/engineering-process/issues/180))
|
|
9
|
+
|
|
10
|
+
## Fixes
|
|
11
|
+
|
|
12
|
+
- Require delegated agents and reviewers to preserve the active user\-selected model and reasoning effort\, with native runtime confirmation on spawn and resume and no autonomous model changes\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
|
|
13
|
+
- Resolve bare child commands through the invoking Python environment in fresh sessions\, preserving installed dependencies\, declared argument arrays\, explicit executables and environment filtering\. ([#181](https://github.com/phuongnse/engineering-process/issues/181))
|
|
14
|
+
|
|
15
|
+
## Upgrade and compatibility
|
|
16
|
+
|
|
17
|
+
Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
|
|
18
|
+
|
|
19
|
+
Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
|
|
20
|
+
|
|
21
|
+
See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.3.0/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.3.0/SELF_HOSTING.md).
|
|
22
|
+
|
|
23
|
+
[Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.2.0...v2.3.0)
|
{engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_posix.py
RENAMED
|
@@ -7,6 +7,7 @@ import ctypes
|
|
|
7
7
|
import os
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import secrets
|
|
10
|
+
import shutil
|
|
10
11
|
import signal
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
@@ -35,9 +36,13 @@ def _enable_subreaper() -> None:
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def _ps_process_table() -> tuple[dict[int, int], str | None]:
|
|
39
|
+
# Supervision must not depend on the consumer's PATH.
|
|
40
|
+
application = shutil.which("ps", path=os.defpath)
|
|
41
|
+
if application is None:
|
|
42
|
+
return {}, "process table snapshot could not start"
|
|
38
43
|
try:
|
|
39
44
|
result = subprocess.run(
|
|
40
|
-
[
|
|
45
|
+
[application, "-axo", "pid=,ppid="],
|
|
41
46
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
42
47
|
timeout=PROCESS_TABLE_TIMEOUT_SECONDS, check=False, text=True,
|
|
43
48
|
)
|
|
@@ -190,7 +195,7 @@ class PosixProcessSupervisor:
|
|
|
190
195
|
if not command or "\x00" in command:
|
|
191
196
|
raise OSError("command executable is invalid")
|
|
192
197
|
supplied = Path(command)
|
|
193
|
-
if supplied.is_absolute() or
|
|
198
|
+
if supplied.is_absolute() or os.path.dirname(command):
|
|
194
199
|
candidate = supplied if supplied.is_absolute() else working_directory / supplied
|
|
195
200
|
try:
|
|
196
201
|
resolved = candidate.resolve(strict=True)
|
|
@@ -198,7 +203,8 @@ class PosixProcessSupervisor:
|
|
|
198
203
|
raise OSError(f"executable is unavailable: {command}") from error
|
|
199
204
|
if not resolved.is_file() or not os.access(resolved, os.X_OK):
|
|
200
205
|
raise OSError(f"executable is unavailable: {command}")
|
|
201
|
-
|
|
206
|
+
# The symlink path may select a virtual environment.
|
|
207
|
+
return candidate.absolute()
|
|
202
208
|
for raw_directory in environment.get("PATH", "").split(os.pathsep):
|
|
203
209
|
directory = Path(raw_directory or ".")
|
|
204
210
|
if not directory.is_absolute():
|
|
@@ -209,7 +215,7 @@ class PosixProcessSupervisor:
|
|
|
209
215
|
except OSError:
|
|
210
216
|
continue
|
|
211
217
|
if resolved.is_file() and os.access(resolved, os.X_OK):
|
|
212
|
-
return
|
|
218
|
+
return candidate.absolute()
|
|
213
219
|
raise OSError(f"executable is unavailable: {command}")
|
|
214
220
|
|
|
215
221
|
def spawn(
|
{engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_windows.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import ntpath
|
|
6
7
|
import os
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import subprocess
|
|
@@ -99,7 +100,7 @@ def resolve_windows_application(
|
|
|
99
100
|
raise OSError("command executable is invalid")
|
|
100
101
|
separator = os.pathsep if path_separator is None else path_separator
|
|
101
102
|
supplied = Path(command)
|
|
102
|
-
explicit_path = supplied.is_absolute() or
|
|
103
|
+
explicit_path = supplied.is_absolute() or bool(ntpath.dirname(command))
|
|
103
104
|
suffix = supplied.suffix.casefold()
|
|
104
105
|
if suffix in _UNSUPPORTED_SHELL_SUFFIXES:
|
|
105
106
|
raise OSError(
|
{engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/artifact_standards.py
RENAMED
|
@@ -84,6 +84,14 @@ def _validate_relations(document: dict[str, Any]) -> None:
|
|
|
84
84
|
_unique([group["type"] for group in rules["groups"]], "change types")
|
|
85
85
|
_unique([section["id"] for section in rules["sections"]], "release section ids")
|
|
86
86
|
_unique([item["heading"] for item in rules["groups"] + rules["sections"]], "release headings")
|
|
87
|
+
elif document["adapter"] == "issue":
|
|
88
|
+
for state, definition in rules["states"].items():
|
|
89
|
+
sections = definition["sections"]
|
|
90
|
+
_unique([section["heading"] for section in sections], f"{state} issue section headings")
|
|
91
|
+
for kind in ("fields", "checks"):
|
|
92
|
+
entries = [entry for section in sections for entry in section[kind]]
|
|
93
|
+
_unique([entry["id"] for entry in entries], f"{state} issue {kind} ids")
|
|
94
|
+
_unique([entry["label"] for entry in entries], f"{state} issue {kind} labels")
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def read_document(path: Path) -> bytes:
|
|
@@ -40,6 +40,7 @@ from .lifecycle import (
|
|
|
40
40
|
submit_review,
|
|
41
41
|
verify_change,
|
|
42
42
|
)
|
|
43
|
+
from .issue import render_issue
|
|
43
44
|
from .project import load_project, readiness_summary
|
|
44
45
|
from .production_engineering import (
|
|
45
46
|
validate_plan_assessments,
|
|
@@ -420,17 +421,29 @@ def command_artifact(args: argparse.Namespace) -> Result:
|
|
|
420
421
|
elif operation == "renovate-preset":
|
|
421
422
|
payload = formatted_json_bytes(render_renovate_preset(standard, process_root=process_root))
|
|
422
423
|
else:
|
|
424
|
+
state = args.state or ("open" if standard.document["adapter"] == "issue" else "ready")
|
|
425
|
+
allowed_states = {
|
|
426
|
+
"pr-description": {"draft", "ready"},
|
|
427
|
+
"release-notes": {"draft", "ready"},
|
|
428
|
+
"automation-name": {"draft", "ready"},
|
|
429
|
+
"issue": {"open", "closed"},
|
|
430
|
+
}[standard.document["adapter"]]
|
|
431
|
+
if state not in allowed_states:
|
|
432
|
+
raise ProcessError(
|
|
433
|
+
f"{standard.document['adapter']} state must be "
|
|
434
|
+
+ " or ".join(sorted(allowed_states))
|
|
435
|
+
)
|
|
423
436
|
data = load_and_validate(args.data_file, standard.document["adapter"] + "-data", schema_root=schemas_root(process_root)) if args.data_file is not None else None
|
|
424
437
|
if data is not None:
|
|
425
|
-
renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name}[standard.document["adapter"]]
|
|
426
|
-
payload = renderer(standard, data, state=
|
|
438
|
+
renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name, "issue": render_issue}[standard.document["adapter"]]
|
|
439
|
+
payload = renderer(standard, data, state=state, process_root=process_root).encode("utf-8")
|
|
427
440
|
details["dataDigest"] = digest_json(data)
|
|
428
441
|
elif operation == "render" or standard.document["adapter"] != "pr-description":
|
|
429
442
|
raise ProcessError("this artifact operation requires --data-file")
|
|
430
443
|
if operation == "validate":
|
|
431
444
|
actual = read_document(args.body_file)
|
|
432
445
|
if data is None:
|
|
433
|
-
issues = body_issues(actual.decode("utf-8"),
|
|
446
|
+
issues = body_issues(actual.decode("utf-8"), state, standard)
|
|
434
447
|
elif actual != payload:
|
|
435
448
|
issues.append("artifact bytes differ from the selected standard and input data")
|
|
436
449
|
payload = actual
|
|
@@ -551,7 +564,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
551
564
|
leaf = _leaf(artifact_commands, name, command_artifact)
|
|
552
565
|
leaf.add_argument("--artifact", required=True)
|
|
553
566
|
if name in {"render", "validate"}:
|
|
554
|
-
leaf.add_argument("--state", choices=("draft", "ready"
|
|
567
|
+
leaf.add_argument("--state", choices=("draft", "ready", "open", "closed"))
|
|
555
568
|
leaf.add_argument("--data-file", type=Path, required=name == "render")
|
|
556
569
|
if name == "validate":
|
|
557
570
|
leaf.add_argument("--body-file", type=Path, required=True)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
import sys
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
from typing import Any, BinaryIO
|
|
@@ -28,6 +29,11 @@ def _child_environment() -> dict[str, str]:
|
|
|
28
29
|
if any(marker in upper for marker in SECRET_MARKERS):
|
|
29
30
|
continue
|
|
30
31
|
environment[name] = value
|
|
32
|
+
runtime_directory = str(Path(sys.executable).absolute().parent)
|
|
33
|
+
inherited_path = environment.get("PATH", "")
|
|
34
|
+
environment["PATH"] = os.pathsep.join(
|
|
35
|
+
entry for entry in (runtime_directory, inherited_path) if entry
|
|
36
|
+
)
|
|
31
37
|
environment["PYTHONUNBUFFERED"] = "1"
|
|
32
38
|
return environment
|
|
33
39
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Issue record generation from one consumer-selected standard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
from .artifact_standards import ArtifactStandard, MAX_DOCUMENT_BYTES
|
|
11
|
+
from .contracts import ProcessError, validate_document
|
|
12
|
+
from .distribution import distribution_root, schemas_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_HTTPS_AUTHORITY = re.compile(
|
|
16
|
+
r"(?:\[[^\[\]]+\]|(?:[A-Za-z0-9._~!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+)"
|
|
17
|
+
r"(?::[0-9]*)?"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _record_url(value: str) -> bool:
|
|
22
|
+
if any(character.isspace() or ord(character) < 32 or 127 <= ord(character) <= 159
|
|
23
|
+
for character in value):
|
|
24
|
+
return False
|
|
25
|
+
try:
|
|
26
|
+
parsed = urlsplit(value)
|
|
27
|
+
# urlsplit defers port validation until this attribute is read.
|
|
28
|
+
parsed.port
|
|
29
|
+
return (
|
|
30
|
+
parsed.scheme == "https"
|
|
31
|
+
and parsed.hostname is not None
|
|
32
|
+
and parsed.username is None
|
|
33
|
+
and parsed.password is None
|
|
34
|
+
and _HTTPS_AUTHORITY.fullmatch(parsed.netloc) is not None
|
|
35
|
+
)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_field(field: dict[str, Any], value: str) -> None:
|
|
41
|
+
format_name = field.get("format", "text")
|
|
42
|
+
if format_name == "record-url" and not _record_url(value):
|
|
43
|
+
raise ProcessError(f"issue field {field['label']} requires one durable HTTPS URL")
|
|
44
|
+
if format_name == "record-references":
|
|
45
|
+
references = value.split(", ")
|
|
46
|
+
if value.casefold() != "none" and (
|
|
47
|
+
not references or any(not _record_url(reference) for reference in references)
|
|
48
|
+
):
|
|
49
|
+
raise ProcessError(
|
|
50
|
+
f"issue field {field['label']} requires none or comma-separated durable HTTPS URLs"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_issue(
|
|
55
|
+
standard: ArtifactStandard,
|
|
56
|
+
data: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
state: str = "open",
|
|
59
|
+
process_root: Path | None = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
if standard.document["adapter"] != "issue":
|
|
62
|
+
raise ProcessError("selected standard requires a different issue adapter")
|
|
63
|
+
if state not in {"open", "closed"}:
|
|
64
|
+
raise ProcessError("issue state must be open or closed")
|
|
65
|
+
validate_document(
|
|
66
|
+
data,
|
|
67
|
+
"issue-data",
|
|
68
|
+
schema_root=schemas_root(distribution_root(process_root)),
|
|
69
|
+
)
|
|
70
|
+
title_rules = standard.rules["title"]
|
|
71
|
+
title = data["title"]
|
|
72
|
+
if len(title) > title_rules["maxLength"]:
|
|
73
|
+
raise ProcessError("issue title exceeds the selected length limit")
|
|
74
|
+
if prefix := title_rules.get("prefix"):
|
|
75
|
+
if not title.startswith(prefix):
|
|
76
|
+
raise ProcessError(f"issue title must start with {prefix}")
|
|
77
|
+
title_value = title[len(prefix):] if prefix else title
|
|
78
|
+
if not title_value.strip() or standard.unresolved(title_value):
|
|
79
|
+
raise ProcessError(f"{state} issue has unresolved title")
|
|
80
|
+
|
|
81
|
+
sections = standard.rules["states"][state]["sections"]
|
|
82
|
+
fields = [field for section in sections for field in section["fields"]]
|
|
83
|
+
checks = [check for section in sections for check in section["checks"]]
|
|
84
|
+
expected_fields = {field["id"] for field in fields}
|
|
85
|
+
expected_checks = {check["id"] for check in checks}
|
|
86
|
+
unknown_fields = sorted(data["fields"].keys() - expected_fields)
|
|
87
|
+
unknown_checks = sorted(data["checks"].keys() - expected_checks)
|
|
88
|
+
if unknown_fields:
|
|
89
|
+
raise ProcessError("issue data contains unknown fields: " + ", ".join(unknown_fields))
|
|
90
|
+
if unknown_checks:
|
|
91
|
+
raise ProcessError("issue data contains unknown checks: " + ", ".join(unknown_checks))
|
|
92
|
+
|
|
93
|
+
lines = [f"# {title}", ""]
|
|
94
|
+
for section in sections:
|
|
95
|
+
lines.extend([section["heading"], ""])
|
|
96
|
+
for field in section["fields"]:
|
|
97
|
+
value = data["fields"].get(field["id"], standard.document["pendingValues"][0])
|
|
98
|
+
if standard.unresolved(value):
|
|
99
|
+
raise ProcessError(f"{state} issue has unresolved value for {field['label']}")
|
|
100
|
+
_validate_field(field, value)
|
|
101
|
+
lines.extend([f"### {field['label']}", "", value, ""])
|
|
102
|
+
for check in section["checks"]:
|
|
103
|
+
completed = data["checks"].get(check["id"], False)
|
|
104
|
+
if state == "closed" and not completed:
|
|
105
|
+
raise ProcessError(f"closed issue has unchecked item: {check['label']}")
|
|
106
|
+
lines.append(f"- [{'x' if completed else ' '}] {check['label']}")
|
|
107
|
+
if section["checks"]:
|
|
108
|
+
lines.append("")
|
|
109
|
+
body = "\n".join(lines)
|
|
110
|
+
if len(body.encode("utf-8")) > MAX_DOCUMENT_BYTES:
|
|
111
|
+
raise ProcessError("issue record exceeds its size limit")
|
|
112
|
+
return body
|
|
@@ -24,6 +24,8 @@ from .contracts import (
|
|
|
24
24
|
from .distribution import schemas_root
|
|
25
25
|
from .project import (
|
|
26
26
|
accepted_issue_url_prefix,
|
|
27
|
+
load_project,
|
|
28
|
+
publication_required,
|
|
27
29
|
require_consumer_evidence,
|
|
28
30
|
required_profiles,
|
|
29
31
|
)
|
|
@@ -33,6 +35,7 @@ from .production_engineering import (
|
|
|
33
35
|
validate_plan_assessments,
|
|
34
36
|
validate_review_assessments,
|
|
35
37
|
)
|
|
38
|
+
from .source_publication import branch_issues, current_branch, validate_current_source
|
|
36
39
|
from .repository import repository_snapshot, resolve_commit, same_checkpoint
|
|
37
40
|
|
|
38
41
|
|
|
@@ -180,6 +183,12 @@ def start_change(
|
|
|
180
183
|
)
|
|
181
184
|
if project["project"] not in contract["affectedProjects"]:
|
|
182
185
|
raise ProcessError("change affectedProjects must include the current project")
|
|
186
|
+
if publication_required(project):
|
|
187
|
+
issues = branch_issues(current_branch(project_root))
|
|
188
|
+
if issues:
|
|
189
|
+
raise ProcessError(
|
|
190
|
+
"publication branch validation failed: " + "; ".join(issues)
|
|
191
|
+
)
|
|
183
192
|
|
|
184
193
|
actor = _actor(actor_id, context_id, kind)
|
|
185
194
|
comparison_base = resolve_commit(project_root, contract["comparisonBase"])
|
|
@@ -538,7 +547,27 @@ def finish_change(
|
|
|
538
547
|
state = _load_state(project_root, process_root, change_id)
|
|
539
548
|
_require_phase(state, "approved")
|
|
540
549
|
actor = _actor(actor_id, context_id, kind)
|
|
550
|
+
project = load_project(project_root, process_root)
|
|
541
551
|
checkpoint = repository_snapshot(project_root)
|
|
552
|
+
publication = None
|
|
553
|
+
if publication_required(project):
|
|
554
|
+
if not (comparison_base := state.get("comparisonBaseCommit")):
|
|
555
|
+
raise ProcessError("publication completion requires a pinned comparison base")
|
|
556
|
+
publication = validate_current_source(
|
|
557
|
+
project_root, comparison_base
|
|
558
|
+
)
|
|
559
|
+
if publication["range"] != f"{comparison_base}..{checkpoint['head']}":
|
|
560
|
+
raise ProcessError("publication validation does not match the reviewed HEAD")
|
|
561
|
+
after_publication = repository_snapshot(project_root)
|
|
562
|
+
if (not same_checkpoint(checkpoint, after_publication)
|
|
563
|
+
or current_branch(project_root) != publication["branch"]):
|
|
564
|
+
raise ProcessError("repository changed while publication validation was running")
|
|
565
|
+
if publication["issues"]:
|
|
566
|
+
raise ProcessError(
|
|
567
|
+
"publication compatibility checks failed: "
|
|
568
|
+
+ "; ".join(publication["issues"])
|
|
569
|
+
)
|
|
570
|
+
checkpoint = after_publication
|
|
542
571
|
if not same_checkpoint(checkpoint, state["reviewAssignment"]["checkpoint"]):
|
|
543
572
|
raise ProcessError("repository changed after approval")
|
|
544
573
|
|
|
@@ -573,6 +602,13 @@ def finish_change(
|
|
|
573
602
|
"verdict": state["review"]["document"]["verdict"],
|
|
574
603
|
},
|
|
575
604
|
}
|
|
605
|
+
if publication is not None:
|
|
606
|
+
receipt["schemaVersion"] = 2
|
|
607
|
+
receipt["publication"] = {
|
|
608
|
+
"branch": publication["branch"],
|
|
609
|
+
"subject": publication["subject"],
|
|
610
|
+
"range": publication["range"],
|
|
611
|
+
}
|
|
576
612
|
validate_document(
|
|
577
613
|
receipt,
|
|
578
614
|
"receipt",
|
|
@@ -172,6 +172,11 @@ def require_consumer_evidence(project: dict[str, Any]) -> bool:
|
|
|
172
172
|
return bool(policy.get("requireConsumerEvidence", False))
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def publication_required(project: dict[str, Any]) -> bool:
|
|
176
|
+
policy = project["lifecycle"].get("publication", {})
|
|
177
|
+
return bool(policy.get("required", False))
|
|
178
|
+
|
|
179
|
+
|
|
175
180
|
def accepted_issue_url_prefix(project: dict[str, Any]) -> str | None:
|
|
176
181
|
return project["lifecycle"].get("processChanges", {}).get(
|
|
177
182
|
"acceptedIssueUrlPrefix"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Small 1.x compatibility checks for pre-1.0 consumer workflows.
|
|
2
|
+
|
|
3
|
+
Publication policy is consumer-owned in the new process. These four read-only checks
|
|
4
|
+
remain temporarily so an existing consumer can adopt 1.0 before moving the equivalent
|
|
5
|
+
rules into its own repository.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .contracts import ProcessError
|
|
14
|
+
from .artifact_standards import resolve_standard
|
|
15
|
+
from .distribution import distribution_root
|
|
16
|
+
from .pr_description import _without_html_comments, body_issues
|
|
17
|
+
from .source_publication import (
|
|
18
|
+
BRANCH, SUBJECT, MAX_COMMITS, branch_issues, commit_issues,
|
|
19
|
+
current_branch, current_commit_subject, validate_current_source, validate_range,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MAX_BODY_BYTES = 1_000_000
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pull_request_body_issues(body: str, state: str) -> list[str]:
|
|
27
|
+
standard = resolve_standard(Path.cwd(), distribution_root(), "pull-request")
|
|
28
|
+
return body_issues(body, state, standard)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_pull_request(
|
|
32
|
+
*,
|
|
33
|
+
title: str,
|
|
34
|
+
branch: str,
|
|
35
|
+
state: str,
|
|
36
|
+
body_path: Path | None,
|
|
37
|
+
project_root: Path | None = None,
|
|
38
|
+
process_root: Path | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
issues = branch_issues(branch) + commit_issues(title)
|
|
41
|
+
if state not in {"draft", "ready"}:
|
|
42
|
+
issues.append("pull request state must be draft or ready")
|
|
43
|
+
body = ""
|
|
44
|
+
if body_path is not None:
|
|
45
|
+
try:
|
|
46
|
+
data = body_path.read_bytes()
|
|
47
|
+
except OSError as error:
|
|
48
|
+
raise ProcessError(f"cannot read pull request body: {error}") from error
|
|
49
|
+
if len(data) > MAX_BODY_BYTES:
|
|
50
|
+
issues.append("pull request body exceeds its size limit")
|
|
51
|
+
else:
|
|
52
|
+
try:
|
|
53
|
+
body = data.decode("utf-8")
|
|
54
|
+
except UnicodeError:
|
|
55
|
+
issues.append("pull request body must be UTF-8")
|
|
56
|
+
standard = resolve_standard(project_root or Path.cwd(), distribution_root(process_root), "pull-request")
|
|
57
|
+
issues.extend(body_issues(body, state, standard))
|
|
58
|
+
return {"issues": issues, "standard": standard.metadata}
|