git-preflight-gate 0.1.6__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.
- git_preflight_gate-0.1.6/LICENSE +21 -0
- git_preflight_gate-0.1.6/MANIFEST.in +17 -0
- git_preflight_gate-0.1.6/PKG-INFO +218 -0
- git_preflight_gate-0.1.6/README.es.md +258 -0
- git_preflight_gate-0.1.6/README.md +203 -0
- git_preflight_gate-0.1.6/action.yml +109 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/PKG-INFO +218 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/SOURCES.txt +19 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/dependency_links.txt +1 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/entry_points.txt +2 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/requires.txt +4 -0
- git_preflight_gate-0.1.6/git_preflight_gate.egg-info/top_level.txt +1 -0
- git_preflight_gate-0.1.6/preflight-gate.sh +145 -0
- git_preflight_gate-0.1.6/pyproject.toml +53 -0
- git_preflight_gate-0.1.6/setup.cfg +4 -0
- git_preflight_gate-0.1.6/sim/__init__.py +8 -0
- git_preflight_gate-0.1.6/sim/gate_workflows_ci.py +813 -0
- git_preflight_gate-0.1.6/templates/platino-gate.yml +93 -0
- git_preflight_gate-0.1.6/tests/fixtures/sumidero/.github/workflows/publica.yml +20 -0
- git_preflight_gate-0.1.6/tests/must_fail.sh +78 -0
- git_preflight_gate-0.1.6/tests/test_platino_gate_template.py +142 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Platino Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# HUELLA-MANIFEST-SDIST-20261106
|
|
2
|
+
# El sdist es el archivo que viaja; el wheel lleva el paquete `sim`.
|
|
3
|
+
# Medido antes de escribir (2026-11-06, tag v0.1.3): el sdist pesaba 8 920 B y
|
|
4
|
+
# NO contenia preflight-gate.sh, action.yml, sim/ ni templates/ — 0 de 4 piezas
|
|
5
|
+
# del producto. Un sdist que omite el producto no es un paquete pequeno: es un
|
|
6
|
+
# paquete que nombra otra cosa.
|
|
7
|
+
include LICENSE
|
|
8
|
+
include README.md
|
|
9
|
+
include README.es.md
|
|
10
|
+
include action.yml
|
|
11
|
+
include preflight-gate.sh
|
|
12
|
+
include pyproject.toml
|
|
13
|
+
recursive-include sim *.py
|
|
14
|
+
recursive-include templates *.yml
|
|
15
|
+
recursive-include tests *
|
|
16
|
+
# El aparato de CI no viaja: es del repositorio, no del paquete.
|
|
17
|
+
prune .github
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-preflight-gate
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: Pre-write preflight gate (bash) and CI authority gate (Python composite action): fail-closed, standard library only except PyYAML for the CI extra.
|
|
5
|
+
Author: alexisromtz-lab
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexisromtz-lab/git-preflight-gate
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: PyYAML>=6.0
|
|
12
|
+
Provides-Extra: ci-authority-gate
|
|
13
|
+
Requires-Dist: PyYAML==6.0.3; extra == "ci-authority-gate"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
<!-- HUELLA-README-EN-20261105 -->
|
|
17
|
+
**Language:** English · [Español](README.es.md)
|
|
18
|
+
|
|
19
|
+
# CI Authority Gate
|
|
20
|
+
|
|
21
|
+
**Repository:** [`git-preflight-gate`](https://github.com/alexisromtz-lab/git-preflight-gate) · **Marketplace:** [Install the action](https://github.com/marketplace/actions/ci-authority-gate)
|
|
22
|
+
|
|
23
|
+
**An integrity gate for autonomous AI agents: so that a bot does not dirty the tree
|
|
24
|
+
and does not commit concurrency collisions either.**
|
|
25
|
+
|
|
26
|
+
Before an agent writes a line, the gate demands a destination tuple —
|
|
27
|
+
`TARGET` (absolute) · `BLOB_PRE` · `HUELLA` · `LINEAS_PRE` — and returns a code that
|
|
28
|
+
**authorizes or blocks** the write. Without a prior run ending in `RC 0`, nothing is written.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install git-preflight-gate # Python CI gate (PyYAML comes with it)
|
|
34
|
+
ci-authority-gate /path/to/your/repo # RC 0 = pass · RC > 0 = fail-closed, cause printed
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Two doors, measured separately — do not confuse them:
|
|
38
|
+
|
|
39
|
+
| door | command | what it needs |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| **CI authority gate** | `pip install git-preflight-gate` | Python ≥ 3.9 + PyYAML (pip resolves it) |
|
|
42
|
+
| **pre-write gate (bash)** | `curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/preflight-gate.sh -o preflight-gate.sh` | bash + git, **zero dependencies** |
|
|
43
|
+
|
|
44
|
+
The wheel ships the Python gate (`sim/`); the bash gate ships in the source distribution and in the repository, and is deliberately dependency-free so it can run on a machine with nothing installed. Neither is a wrapper around the other.
|
|
45
|
+
|
|
46
|
+
### The pre-write gate, at the shell
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bash preflight-gate.sh /absolute/path/file.py <blob_pre> <exclusive_marker> <lineas_pre>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## The codes (fail-closed)
|
|
53
|
+
|
|
54
|
+
| RC | meaning |
|
|
55
|
+
|---:|---|
|
|
56
|
+
| `0` | PRE-FLIGHT OK — the target accepts an edit on `BLOB_PRE` |
|
|
57
|
+
| `10` | the object does not exist (creation) |
|
|
58
|
+
| `11` | **the marker already lives in the target** ⇒ collision: the row is already written |
|
|
59
|
+
| `12` | measured blob ≠ contract ⇒ the tree moved |
|
|
60
|
+
| `13` | dirty git state ⇒ unadjudicated modifications |
|
|
61
|
+
| `14` | the target is not inside a git tree ⇒ the gate cannot verify identity |
|
|
62
|
+
| `15` | line count differs from the contract |
|
|
63
|
+
| `20` | misuse (incomplete tuple, or `TARGET` not absolute) |
|
|
64
|
+
|
|
65
|
+
Every code comes out **with its cause printed**. A correct `RC` for the wrong reason is not a gate: it is a coincidence.
|
|
66
|
+
|
|
67
|
+
## Why `RC 11` is the case that matters
|
|
68
|
+
|
|
69
|
+
The most expensive defect of an agent is not picking the wrong path: it is **writing again something another hand already wrote**, and reporting success. The marker is an exclusive literal fragment of the text to be inserted; if it is already in the target, the write is rejected before the file is touched. The gate does not guess intent: it denies the collision.
|
|
70
|
+
|
|
71
|
+
## Why `TARGET` must be absolute
|
|
72
|
+
|
|
73
|
+
The gate rebases with `git -C <directory>` to measure the blob and the state. A relative path would be counted from **two different bases** (`cwd` and the object's directory) and could hash *another* file that does exist. Fail-closed: relative path ⇒ `RC 20`.
|
|
74
|
+
|
|
75
|
+
## Why `RC 14` exists
|
|
76
|
+
|
|
77
|
+
Without a git tree, `git status` returns 128 with empty output. A naive gate reads that empty output as "clean" and goes green. Here the impossibility is declared: without a tree there is no blob identity worth anything.
|
|
78
|
+
|
|
79
|
+
## Case bank
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bash tests/must_fail.sh
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
9 cases, **9/9**, each verifying the code **and** the cause: `20` (incomplete tuple), `20` (relative path), `10`, `11`, `12`, `15`, `13`, `14`, and the positive control `0`. A bank without a positive control only proves the instrument knows how to say no.
|
|
86
|
+
|
|
87
|
+
## Out-of-band telemetry
|
|
88
|
+
|
|
89
|
+
Each run records 7 fields (`hora_utc target blob_pre blob_medido rc hash_arbol_ini hash_arbol_fin`) in `${GATE_TELEMETRIA_LOG:-$HOME/preflight-telemetria.log}`. Telemetry **cannot move the RC**, **cannot abort** and **cannot refresh the index** (`--no-optional-locks`). The path is never relative: a log inside the tree becomes an untracked file, and then the gate ends up measuring its own telemetry — the instrument counting itself.
|
|
90
|
+
|
|
91
|
+
## Declared limits
|
|
92
|
+
|
|
93
|
+
- On a nonexistent `TARGET` the gate stops at `RC 10` **before** comparing blob and lines: in the creation branch, the tuple is a **contract**, not a measurement. It is declared, not repaired (fail-closed is correct).
|
|
94
|
+
- It does not judge content: it certifies identity and collision, not correctness.
|
|
95
|
+
|
|
96
|
+
## The root Action: CI Authority Gate
|
|
97
|
+
|
|
98
|
+
The `action.yml` at this repository's root is a **composite action** running a second instrument, `sim/gate_workflows_ci.py`: a structural verifier of the authority topology in `.github/workflows/`. Five counting invariants, seven typed causes, `RC = min(causes)`, `UNIVERSO_VACIO` = cause `31` (zero files is failure, not approval).
|
|
99
|
+
|
|
100
|
+
Measured identity of the instrument copied into this tree:
|
|
101
|
+
|
|
102
|
+
| property | value |
|
|
103
|
+
|---|---|
|
|
104
|
+
| lines / bytes | 615 / 21470 |
|
|
105
|
+
| git blob | `ff9c69a944e7965b3fda0901f7745d80a9e85c7c` |
|
|
106
|
+
| sha256-16 | `073954a84f3c84fd` |
|
|
107
|
+
|
|
108
|
+
Using it as a consumer:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
- uses: alexisromtz-lab/git-preflight-gate@v0.1.4
|
|
112
|
+
with:
|
|
113
|
+
workflows-dir: .github/workflows
|
|
114
|
+
fail-on-red: "true"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
<!-- HUELLA-RELEASE-V013-20261031 -->
|
|
118
|
+
`v0.1.3`, `v0.1.2` and `v0.1.1` run the same `action.yml` (`9ef420ec`) and the same `sim/gate_workflows_ci.py` (`ff9c69a9`): the gate's code did not change across the three. What changes is the adoption wrapper — `v0.1.2` added `templates/platino-gate.yml` and `v0.1.3` added `action-selftest.yml`, the manifest alignment and this documentation. If you pin by SHA, use the commit — not the tag.
|
|
119
|
+
|
|
120
|
+
The four inputs (`workflows-dir`, `root`, `python-bin`, `fail-on-red`) and the two
|
|
121
|
+
outputs (`rc`, `veredicto`) preserve the contract of the `action.yml` that this
|
|
122
|
+
repository **replaces**: any CI that pointed at the root action of a working repository
|
|
123
|
+
migrates by changing only the repository name in `uses:`; the rest of the `with:` block
|
|
124
|
+
does not change.
|
|
125
|
+
|
|
126
|
+
The action does not guess the domain: the instrument resolves a single point, `<root>/.github/workflows`. That is why `workflows-dir` routes to `--file` when the path exists and to `--root` when it does not. A `--root` that accepted any directory with YAML files would be editing the ruler so the demo fits.
|
|
127
|
+
|
|
128
|
+
Single dependency: `PyYAML==6.0.3`, declared as an optional extra (`pip install ".[ci-authority-gate]"`). The rest is stdlib. The instrument does not read the network, does not write files, does not use randomness and does not consult the clock.
|
|
129
|
+
|
|
130
|
+
Installable without cloning the repository, from the release wheel. The
|
|
131
|
+
`ci-authority-gate` extra is mandatory for the CLI: without PyYAML the instrument does
|
|
132
|
+
not measure and exits `RC=30` (fail-closed), not green.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install "git-preflight-gate[ci-authority-gate] @ https://github.com/alexisromtz-lab/git-preflight-gate/releases/download/v0.1.4/git_preflight_gate-0.1.4-py3-none-any.whl"
|
|
136
|
+
ci-authority-gate --self-test # built-in must-fail bank; touches no disk
|
|
137
|
+
ci-authority-gate --root . # measures <root>/.github/workflows and returns its RC
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
<!-- HUELLA-README-V014-20261106 -->
|
|
141
|
+
`v0.1.4` does not change the gate: `action.yml` (`9ef420ec`) and
|
|
142
|
+
`sim/gate_workflows_ci.py` (`ff9c69a9`) are the same blobs as in `v0.1.3`. What changes
|
|
143
|
+
is the package: `MANIFEST.in` makes the sdist carry the product, and the manifest
|
|
144
|
+
declares the `sim` package and the `ci-authority-gate` script. Before this change the
|
|
145
|
+
wheel was 6 768 B of `dist-info` only and no CLI existed. It is not on PyPI yet: the
|
|
146
|
+
only `pip` path today is the release URL.
|
|
147
|
+
|
|
148
|
+
Measured positive control: workflow with `permissions: contents: read` and `uses` pinned by full SHA ⇒ `CAUSAS PRESENTES: ninguna`, `VEREDICTO: PASA`, `RC=0`. Negative control: `pull_request_target` + `permissions: write-all` + `curl | bash` + `actions/checkout@v4` ⇒ `CAUSAS PRESENTES: [1, 2, 5]`, `RC=1`. Empty universe ⇒ `CAUSAS PRESENTES: [31]`, `RC=31`.
|
|
149
|
+
|
|
150
|
+
## The two instruments are not to be confused
|
|
151
|
+
|
|
152
|
+
| instrument | question | form |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `preflight-gate.sh` | may this agent write *this file* now? | bash CLI, target tuple |
|
|
155
|
+
| `sim/gate_workflows_ci.py` | does this CI topology concentrate authority without control? | composite action + Python CLI |
|
|
156
|
+
|
|
157
|
+
The first protects a write; the second audits a workflow tree. They share the doctrine (fail-closed, printed cause, zero files = declared impossibility), not the code.
|
|
158
|
+
|
|
159
|
+
## Install it in your repository in 1 minute
|
|
160
|
+
|
|
161
|
+
<!-- HUELLA-BADGE-PLATINO-README-20261031 -->
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
mkdir -p .github/workflows
|
|
165
|
+
curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/templates/platino-gate.yml \
|
|
166
|
+
-o .github/workflows/platino-gate.yml
|
|
167
|
+
git add .github/workflows/platino-gate.yml
|
|
168
|
+
git commit -m "ci: platino preflight gate"
|
|
169
|
+
git push
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
That is all: the workflow fires on the next `push` or PR.
|
|
173
|
+
|
|
174
|
+
### The file must be named `platino-gate.yml` (this is a contract, not advice)
|
|
175
|
+
|
|
176
|
+
If you rename it — `ci.yml`, `gate.yml`, `platino-gate.yaml` — the first job fails closed with `RC 20` and tells you why. The reason is measured, not aesthetic: the status badge resolves **by file name** (`.../workflow/status/YOUR-REPO/YOUR-REPO/platino-gate.yml`). With another name, the badge would read *your* CI workflow, not this gate, and would publish a green that measures nothing. The contract lives inside the workflow, not on this page.
|
|
177
|
+
|
|
178
|
+
Falsified by `tests/test_platino_gate_template.py`: 5 name cases and 5 structural cases, `RC 0` only if all 10 agree; a template mutated to accept any `.yml` returns `RC 1` naming the offending row.
|
|
179
|
+
|
|
180
|
+
## The badge
|
|
181
|
+
|
|
182
|
+
When the run finishes, the **Summary** tab publishes the badge already filled in with your `owner/repo`. There are two, and they are not the same:
|
|
183
|
+
|
|
184
|
+
| badge | what it claims | permissions it asks for |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| **status** (dynamic) | the real result of the most recent run of `platino-gate.yml` | `contents: read` |
|
|
187
|
+
| **seal** (static) | "this repository passed the gate" | `contents: read` |
|
|
188
|
+
|
|
189
|
+
```markdown
|
|
190
|
+
[](https://github.com/alexisromtz-lab/la-auditora)
|
|
191
|
+
|
|
192
|
+
[](https://github.com/alexisromtz-lab/la-auditora)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The status one cannot lie: GitHub draws it from the run. **The seal can**: it is a literal anyone can paste in green without having run anything. Use it only when the run is green — the gate cannot verify that for you.
|
|
196
|
+
|
|
197
|
+
**Zero write permissions, in both cases.** The workflow does not touch your README, does not ask for `contents: write` and needs no service of ours: the badge is a `shields.io` URL that reads the public status of your own workflow. There is no endpoint to host, no token to expire.
|
|
198
|
+
|
|
199
|
+
### What it measures and what it does not
|
|
200
|
+
|
|
201
|
+
| measures | does not measure |
|
|
202
|
+
|---|---|
|
|
203
|
+
| authority topology in `.github/workflows/`: `permissions`, triggers, `uses` pinned by SHA, execution sinks | whether your code does what it says |
|
|
204
|
+
| seven typed causes, `RC = min(causes)`, `UNIVERSO_VACIO` = cause `31` | default `GITHUB_TOKEN` permissions at organization level |
|
|
205
|
+
| the tree you have, at the commit that runs | OIDC / Trusted Publishing trust in the destination registry |
|
|
206
|
+
|
|
207
|
+
The second block is printed on every run, under `RESIDUO DECLARADO`. A gate that does not declare its limits is not a gate: it is a promise.
|
|
208
|
+
|
|
209
|
+
### How the wrapper tests itself
|
|
210
|
+
|
|
211
|
+
<!-- HUELLA-BADGE-SELFTEST-DOC-20261031 -->
|
|
212
|
+
`action.yml` is not tested by reading its YAML: it is executed. On every push to `main`, the workflow [`action-selftest.yml`](.github/workflows/action-selftest.yml) runs the action twice with `uses: ./` — once on this repository itself, where it demands `rc=0` / `PASA`, and once on a poisoned fixture in `tests/fixtures/sumidero`, where it demands `rc=2` / `SUMIDERO_IMPUDO`. If the extra's installation (`pip install "<action_dir>[ci-authority-gate]"`) or the input routing breaks, it goes red here before going red on your runner.
|
|
213
|
+
|
|
214
|
+
A self-test that only knows how to be green discriminates nothing: that is why the second case demands the action **fail** with the correct cause.
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!-- HUELLA-README-ES-20261105 -->
|
|
2
|
+
**Idioma:** Español · [English](README.md)
|
|
3
|
+
|
|
4
|
+
# CI Authority Gate
|
|
5
|
+
|
|
6
|
+
**Repository:** [`git-preflight-gate`](https://github.com/alexisromtz-lab/git-preflight-gate) · **Marketplace:** [Install the action](https://github.com/marketplace/actions/ci-authority-gate)
|
|
7
|
+
|
|
8
|
+
**Un gate de integridad para agentes de IA autónomos: que un bot no ensucie el árbol
|
|
9
|
+
ni cometa colisiones de concurrencia.**
|
|
10
|
+
|
|
11
|
+
Antes de que un agente escriba una línea, el gate exige una tupla de destino —
|
|
12
|
+
`TARGET` (absoluto) · `BLOB_PRE` · `HUELLA` · `LINEAS_PRE` — y devuelve un código que
|
|
13
|
+
**autoriza o bloquea** la escritura. Sin corrida previa con `RC 0`, no se escribe.
|
|
14
|
+
|
|
15
|
+
## Instalación
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git-preflight-gate # gate de CI en Python (PyYAML viaja con él)
|
|
19
|
+
ci-authority-gate /ruta/a/tu/repo # RC 0 = pasa · RC > 0 = fallo cerrado con causa
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Dos puertas, medidas por separado — no se confunden:
|
|
23
|
+
|
|
24
|
+
| puerta | comando | qué necesita |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **gate de CI** | `pip install git-preflight-gate` | Python ≥ 3.9 + PyYAML (lo resuelve pip) |
|
|
27
|
+
| **pre-write gate (bash)** | `curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/preflight-gate.sh -o preflight-gate.sh` | bash + git, **cero dependencias** |
|
|
28
|
+
|
|
29
|
+
El wheel trae el gate de Python (`sim/`); el gate de bash viaja en el sdist y en el repositorio, y es deliberadamente sin dependencias para correr en una máquina sin nada instalado. Ninguno envuelve al otro.
|
|
30
|
+
|
|
31
|
+
### El pre-write gate, en la terminal
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bash preflight-gate.sh /ruta/absoluta/archivo.py <blob_pre> <huella_exclusiva> <lineas_pre>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Los códigos (fail-closed)
|
|
38
|
+
|
|
39
|
+
| RC | significado |
|
|
40
|
+
|---:|---|
|
|
41
|
+
| `0` | PRE-VUELO OK — el destino acepta la edición sobre `BLOB_PRE` |
|
|
42
|
+
| `10` | el objeto no existe (creación) |
|
|
43
|
+
| `11` | **la huella ya habita en el destino** ⇒ colisión: la fila ya está escrita |
|
|
44
|
+
| `12` | blob medido ≠ contrato ⇒ el árbol se movió |
|
|
45
|
+
| `13` | estado sucio en git ⇒ modificaciones no adjudicadas |
|
|
46
|
+
| `14` | el destino no vive en un árbol git ⇒ el gate no puede validar identidad |
|
|
47
|
+
| `15` | líneas distintas al contrato |
|
|
48
|
+
| `20` | uso incorrecto (tupla incompleta, o `TARGET` no absoluto) |
|
|
49
|
+
|
|
50
|
+
Cada código sale **con su causa impresa**. Un `RC` correcto por el motivo equivocado
|
|
51
|
+
no es un gate: es una coincidencia.
|
|
52
|
+
|
|
53
|
+
## Por qué `RC 11` es el caso que importa
|
|
54
|
+
|
|
55
|
+
El defecto más caro de un agente no es equivocarse de ruta: es **volver a escribir algo
|
|
56
|
+
que otra mano ya escribió**, y reportar éxito. La huella es un fragmento literal
|
|
57
|
+
exclusivo del texto a insertar; si ya está en el destino, la escritura se rechaza antes
|
|
58
|
+
de tocar el archivo. El gate no adivina intenciones: niega la colisión.
|
|
59
|
+
|
|
60
|
+
## Por qué `TARGET` debe ser absoluto
|
|
61
|
+
|
|
62
|
+
El gate re-basa con `git -C <directorio>` para medir el blob y el estado. Una ruta
|
|
63
|
+
relativa se contaría desde **dos bases distintas** (`cwd` y el directorio del objeto) y
|
|
64
|
+
podría hashear *otro* archivo que sí existe. Fail-closed: ruta relativa ⇒ `RC 20`.
|
|
65
|
+
|
|
66
|
+
## Por qué `RC 14` existe
|
|
67
|
+
|
|
68
|
+
Sin árbol git, `git status` devuelve 128 con salida vacía. Un gate ingenuo lee esa
|
|
69
|
+
salida vacía como «limpio» y da verde. Aquí se declara la imposibilidad: sin árbol no
|
|
70
|
+
hay identidad de blob que valga.
|
|
71
|
+
|
|
72
|
+
## Banco de casos
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bash tests/must_fail.sh
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
9 casos, **9/9**, cada uno verificando código **y** causa: `20` (tupla incompleta),
|
|
79
|
+
`20` (ruta relativa), `10`, `11`, `12`, `15`, `13`, `14` y el control positivo `0`.
|
|
80
|
+
Un banco sin control positivo sólo demuestra que el instrumento sabe decir que no.
|
|
81
|
+
|
|
82
|
+
## Telemetría fuera de banda
|
|
83
|
+
|
|
84
|
+
Cada corrida anota 7 campos (`hora_utc target blob_pre blob_medido rc hash_arbol_ini
|
|
85
|
+
hash_arbol_fin`) en `${GATE_TELEMETRIA_LOG:-$HOME/preflight-telemetria.log}`. La
|
|
86
|
+
telemetría **no puede mover el RC**, **no puede abortar** y **no puede refrescar el
|
|
87
|
+
índice** (`--no-optional-locks`). La ruta nunca es relativa: un log dentro del árbol se
|
|
88
|
+
vuelve un archivo no rastreado, y entonces el gate termina midiendo su propia
|
|
89
|
+
telemetría — el instrumento que se cuenta a sí mismo.
|
|
90
|
+
|
|
91
|
+
## Límites declarados
|
|
92
|
+
|
|
93
|
+
- Sobre un `TARGET` inexistente el gate corta en `RC 10` **antes** de comparar blob y
|
|
94
|
+
líneas: en la rama de creación, la tupla es **contrato**, no medición. Se declara; no
|
|
95
|
+
se repara (fail-closed es correcto).
|
|
96
|
+
- No juzga el contenido: certifica identidad y colisión, no corrección.
|
|
97
|
+
|
|
98
|
+
## La Action de la raíz: CI Authority Gate
|
|
99
|
+
|
|
100
|
+
El `action.yml` de este repositorio es una **action composite** que ejecuta un segundo
|
|
101
|
+
instrumento, `sim/gate_workflows_ci.py`: un verificador estructural de la topología de
|
|
102
|
+
autoridad en `.github/workflows/`. Cinco invariantes contables, siete causas tipadas,
|
|
103
|
+
`RC = min(causas)`, `UNIVERSO_VACIO` = causa `31` (cero archivos es fracaso, no
|
|
104
|
+
aprobación).
|
|
105
|
+
|
|
106
|
+
Identidad medida del instrumento copiado a este árbol:
|
|
107
|
+
|
|
108
|
+
| propiedad | valor |
|
|
109
|
+
|---|---|
|
|
110
|
+
| líneas / bytes | 615 / 21470 |
|
|
111
|
+
| git blob | `ff9c69a944e7965b3fda0901f7745d80a9e85c7c` |
|
|
112
|
+
| sha256-16 | `073954a84f3c84fd` |
|
|
113
|
+
|
|
114
|
+
Uso como consumidor:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- uses: alexisromtz-lab/git-preflight-gate@v0.1.4
|
|
118
|
+
with:
|
|
119
|
+
workflows-dir: .github/workflows
|
|
120
|
+
fail-on-red: "true"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
<!-- HUELLA-RELEASE-V013-20261031 -->
|
|
124
|
+
`v0.1.3`, `v0.1.2` y `v0.1.1` ejecutan el mismo `action.yml` (`9ef420ec`) y el mismo
|
|
125
|
+
`sim/gate_workflows_ci.py` (`ff9c69a9`): el código del gate no cambió entre los tres.
|
|
126
|
+
Lo que cambia es el envoltorio de adopción —`v0.1.2` añadió `templates/platino-gate.yml`
|
|
127
|
+
y `v0.1.3` añadió `action-selftest.yml`, la alineación del manifiesto y esta
|
|
128
|
+
documentación—. Si fijas por SHA, usa el commit —no el tag—.
|
|
129
|
+
|
|
130
|
+
Los cuatro inputs (`workflows-dir`, `root`, `python-bin`, `fail-on-red`) y los dos
|
|
131
|
+
outputs (`rc`, `veredicto`) conservan el contrato del `action.yml` que este repositorio
|
|
132
|
+
**reemplaza**: cualquier CI que apuntaba a la action de la raíz de un repositorio de
|
|
133
|
+
trabajo migra cambiando sólo el nombre del repositorio en `uses:`; el resto del bloque
|
|
134
|
+
`with:` no cambia.
|
|
135
|
+
|
|
136
|
+
La acción no adivina el dominio: el instrumento resuelve un único punto,
|
|
137
|
+
`<raiz>/.github/workflows`. Por eso `workflows-dir` se enruta a `--file` cuando el path
|
|
138
|
+
existe y a `--root` cuando no. Un `--root` que aceptara cualquier directorio con YAMLs
|
|
139
|
+
sería editar el metro para que quepa la demo.
|
|
140
|
+
|
|
141
|
+
Dependencia única: `PyYAML==6.0.3`, declarada como extra opcional
|
|
142
|
+
(`pip install ".[ci-authority-gate]"`). El resto es stdlib. El instrumento no lee red,
|
|
143
|
+
no escribe archivos, no usa aleatoriedad y no consulta el reloj.
|
|
144
|
+
|
|
145
|
+
Instalable sin clonar el repositorio, desde el wheel de la release. El extra
|
|
146
|
+
`ci-authority-gate` es obligatorio para el CLI: sin PyYAML el instrumento no mide y
|
|
147
|
+
sale con `RC=30` (fail-closed), no con verde.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install "git-preflight-gate[ci-authority-gate] @ https://github.com/alexisromtz-lab/git-preflight-gate/releases/download/v0.1.4/git_preflight_gate-0.1.4-py3-none-any.whl"
|
|
151
|
+
ci-authority-gate --self-test # banco must-fail incorporado; no toca disco
|
|
152
|
+
ci-authority-gate --root . # mide <raiz>/.github/workflows y devuelve su RC
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
<!-- HUELLA-README-V014-20261106 -->
|
|
156
|
+
`v0.1.4` no cambia el gate: `action.yml` (`9ef420ec`) y `sim/gate_workflows_ci.py`
|
|
157
|
+
(`ff9c69a9`) son los mismos blobs que en `v0.1.3`. Lo que cambia es el paquete:
|
|
158
|
+
`MANIFEST.in` hace que el sdist lleve el producto, y el manifiesto declara el paquete
|
|
159
|
+
`sim` y el script `ci-authority-gate`. Antes de este cambio el wheel eran 6 768 B de
|
|
160
|
+
solo `dist-info` y no existía ningún CLI. En PyPI todavía no está publicado: la única
|
|
161
|
+
vía de `pip` hoy es la URL de la release.
|
|
162
|
+
|
|
163
|
+
Control positivo medido: workflow con `permissions: contents: read` y `uses` fijado por
|
|
164
|
+
SHA completo ⇒ `CAUSAS PRESENTES: ninguna`, `VEREDICTO: PASA`, `RC=0`. Control negativo:
|
|
165
|
+
`pull_request_target` + `permissions: write-all` + `curl | bash` + `actions/checkout@v4`
|
|
166
|
+
⇒ `CAUSAS PRESENTES: [1, 2, 5]`, `RC=1`. Universo vacío ⇒ `CAUSAS PRESENTES: [31]`,
|
|
167
|
+
`RC=31`.
|
|
168
|
+
|
|
169
|
+
## Los dos instrumentos no se confunden
|
|
170
|
+
|
|
171
|
+
| instrumento | pregunta | forma |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `preflight-gate.sh` | ¿este agente puede escribir *este archivo* ahora? | CLI bash, tupla de destino |
|
|
174
|
+
| `sim/gate_workflows_ci.py` | ¿esta topología de CI concentra autoridad sin control? | action composite + CLI Python |
|
|
175
|
+
|
|
176
|
+
El primero protege una escritura; el segundo audita un árbol de workflows. Comparten la
|
|
177
|
+
doctrina (fail-closed, causa impresa, cero archivos = imposibilidad declarada), no el
|
|
178
|
+
código.
|
|
179
|
+
|
|
180
|
+
## Instalarlo en tu repositorio en 1 minuto
|
|
181
|
+
|
|
182
|
+
<!-- HUELLA-BADGE-PLATINO-README-20261031 -->
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
mkdir -p .github/workflows
|
|
186
|
+
curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/templates/platino-gate.yml \
|
|
187
|
+
-o .github/workflows/platino-gate.yml
|
|
188
|
+
git add .github/workflows/platino-gate.yml
|
|
189
|
+
git commit -m "ci: platino preflight gate"
|
|
190
|
+
git push
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Eso es todo: el workflow se dispara en el siguiente `push` o PR.
|
|
194
|
+
|
|
195
|
+
### El archivo debe llamarse `platino-gate.yml` (esto es un contrato, no un consejo)
|
|
196
|
+
|
|
197
|
+
Si lo renombras —`ci.yml`, `gate.yml`, `platino-gate.yaml`— el primer job falla
|
|
198
|
+
cerrado con `RC 20` y te dice por qué. La razón es medida, no estética: el badge
|
|
199
|
+
de estado se resuelve **por nombre de archivo** (`.../workflow/status/TU-REPO/TU-REPO/platino-gate.yml`).
|
|
200
|
+
Con otro nombre, el badge leería *tu* workflow de CI, no este gate, y publicaría un
|
|
201
|
+
verde que no mide nada. El contrato está dentro del workflow, no en esta página.
|
|
202
|
+
|
|
203
|
+
Falsado por `tests/test_platino_gate_template.py`: 5 casos de nombre y 5
|
|
204
|
+
estructurales, `RC 0` solo si los 10 concuerdan; un template mutado para aceptar
|
|
205
|
+
cualquier `.yml` devuelve `RC 1` nombrando la fila culpable.
|
|
206
|
+
|
|
207
|
+
## El badge
|
|
208
|
+
|
|
209
|
+
Al terminar el run, la pestaña **Summary** publica el badge ya rellenado con tu
|
|
210
|
+
`owner/repo`. Hay dos, y no son lo mismo:
|
|
211
|
+
|
|
212
|
+
| badge | qué afirma | permisos que pide |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| de **estado** (dinámico) | el resultado real del run más reciente de `platino-gate.yml` | `contents: read` |
|
|
215
|
+
| **sello** (estático) | «este repositorio pasó el gate» | `contents: read` |
|
|
216
|
+
|
|
217
|
+
```markdown
|
|
218
|
+
[](https://github.com/alexisromtz-lab/la-auditora)
|
|
219
|
+
|
|
220
|
+
[](https://github.com/alexisromtz-lab/la-auditora)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
El de estado no puede mentir: lo dibuja GitHub desde el run. **El sello, sí**: es un
|
|
224
|
+
literal que cualquiera puede pegar en verde sin haber corrido nada. Ponlo solo cuando
|
|
225
|
+
el run esté en verde — el gate no lo puede verificar por ti.
|
|
226
|
+
|
|
227
|
+
**Cero permisos de escritura, en los dos casos.** El workflow no toca tu README, no
|
|
228
|
+
pide `contents: write` y no necesita ningún servicio nuestro: el badge es una URL de
|
|
229
|
+
`shields.io` que lee el estado público de tu propio workflow. No hay endpoint que
|
|
230
|
+
hospedar, ni token que caducar.
|
|
231
|
+
|
|
232
|
+
### Qué mide y qué no
|
|
233
|
+
|
|
234
|
+
| mide | no mide |
|
|
235
|
+
|---|---|
|
|
236
|
+
| topología de autoridad en `.github/workflows/`: `permissions`, disparadores, `uses` por SHA, sumideros de ejecución | si tu código hace lo que dice |
|
|
237
|
+
| siete causas tipadas, `RC = min(causas)`, `UNIVERSO_VACIO` = causa `31` | los permisos por defecto del `GITHUB_TOKEN` a nivel de organización |
|
|
238
|
+
| el árbol que tienes, en el commit que corre | confianza OIDC / Trusted Publishing en el registro destino |
|
|
239
|
+
|
|
240
|
+
El segundo bloque está impreso en cada run, bajo `RESIDUO DECLARADO`. Un gate que no
|
|
241
|
+
declara sus límites no es un gate: es una promesa.
|
|
242
|
+
|
|
243
|
+
### Cómo se prueba el propio envoltorio
|
|
244
|
+
|
|
245
|
+
<!-- HUELLA-BADGE-SELFTEST-DOC-20261031 -->
|
|
246
|
+
El `action.yml` no se prueba leyendo su YAML: se ejecuta. En cada push a `main`, el
|
|
247
|
+
workflow [`action-selftest.yml`](.github/workflows/action-selftest.yml) corre la acción
|
|
248
|
+
dos veces con `uses: ./` —sobre este mismo repositorio, donde exige `rc=0` / `PASA`, y
|
|
249
|
+
sobre una fixture envenenada en `tests/fixtures/sumidero`, donde exige `rc=2` /
|
|
250
|
+
`SUMIDERO_IMPUDO`—. Si la instalación del extra (`pip install "<action_dir>[ci-authority-gate]"`)
|
|
251
|
+
o el enrutado de inputs se rompen, enrojece aquí antes de enrojecer en tu runner.
|
|
252
|
+
|
|
253
|
+
Un self-test que sólo sabe estar verde no discrimina nada: por eso el segundo caso exige
|
|
254
|
+
que la acción **falle** con la causa correcta.
|
|
255
|
+
|
|
256
|
+
## Licencia
|
|
257
|
+
|
|
258
|
+
MIT. Ver `LICENSE`.
|