celik-muhammed commited on
Commit
dcd69c7
·
verified ·
1 Parent(s): 5d868aa

Delete ci

Browse files
ci/README.md DELETED
@@ -1,111 +0,0 @@
1
- # Provider-artifact Redis chaos CI
2
-
3
- Run 147 makes the Run 146 live Redis gate reproducible without changing the
4
- provider-artifact public API.
5
-
6
- The portable entrypoint is:
7
-
8
- ```bash
9
- python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all
10
- ```
11
-
12
- It is intentionally **fail closed**: Redis 7/8 `redis-server`, `pytest`, and the
13
- pinned `redis-py` cluster client must exist. The entrypoint sets the mandatory
14
- no-skip flags for the live tests itself.
15
-
16
- Modes:
17
-
18
- - `standalone` — executes the Run 146 real-Lua/restart tests plus the Run 147
19
- standalone binary smoke.
20
- - `cluster` — bootstraps six real Redis nodes (3 primaries + 3 replicas), assigns
21
- all 16,384 slots without `redis-cli`, races two OS processes for one lifecycle
22
- reservation, kills the primary that owns the `{provider-artifact}` slot, waits
23
- for replica promotion, and proves terminal non-replay plus new lifecycle work.
24
- - `all` — runs both gates.
25
-
26
- For exact Redis-major CI, build the included `redis-chaos.Dockerfile` from the
27
- repository root. It copies `redis-server` from the selected official Redis
28
- bookworm image into a Python image, installs the proxy's pinned requirements,
29
- and runs the portable entrypoint. Reference GitHub Actions and CircleCI job
30
- fragments are included beside this file for Redis 7.4.11 and 8.2.9.
31
-
32
- The container examples are read-only and grant Redis writable storage only via
33
- bounded `/tmp` and `/var/tmp` tmpfs mounts. They drop Linux capabilities and set
34
- `no-new-privileges`.
35
-
36
- The chaos gate never needs production Redis URLs, credentials, provider tokens,
37
- prompts, generated media, or ZIP contents. Every Redis process binds only to
38
- loopback inside the disposable CI container.
39
-
40
-
41
- ## Run 148 release attestation output
42
-
43
- For release CI, invoke the same gate with a destination outside the source tree:
44
-
45
- ```bash
46
- python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all \
47
- --attestation-dir /secure/evidence \
48
- --source-revision "$SOURCE_REVISION" \
49
- --redis-image 'redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d'
50
- ```
51
-
52
- Run Redis 7 and Redis 8 separately so release evidence contains all four required
53
- major/mode combinations. `--redis-image` must contain both the reviewed version tag
54
- and an immutable SHA-256 index digest. Attestation output inside the source tree is
55
- rejected to avoid self-referential source subjects.
56
-
57
- The emitted JSON is a canonical **subject**, not a self-authenticating signature. The
58
- parent CI must cryptographically sign/attest it, verify the signer identity, and only
59
- then create a sanitized signature-verification record. GitHub Actions reference jobs
60
- show this with the SHA-pinned `actions/attest` action plus `gh attestation verify`;
61
- the verification command additionally pins the exact `GITHUB_SHA` and `GITHUB_REF`,
62
- derives the signing-workflow identity from `GITHUB_WORKFLOW_REF`, pins its signer
63
- digest, and rejects self-hosted-runner attestations. CircleCI environments should use
64
- the organization's approved Sigstore/KMS/HSM flow,
65
- then call `redis_chaos_attestation.py signature-record` only after that verifier exits
66
- successfully.
67
-
68
- ## Run 149 promotion handoff
69
-
70
- Redis chaos and schema-v2 evidence are inputs to the transactional promotion layer;
71
- they are not permission to publish arbitrary rebuilt bytes. Parent CI should run
72
- `security/promote_release.py prepare`, sign/verify the emitted canonical statement
73
- with the approved release trust root, create a sanitized signature record bound to the
74
- actual verifier output, and run `promote_release.py finalize`. Upload/publish only the
75
- exact receipt-listed objects from the final promotion directory.
76
-
77
-
78
- ## Run 151 post-publication handoff
79
-
80
- After Run 150 publishes the receipt-authorized objects, use
81
- `security/finalize_publication.py prepare` with credentials that are distinct from the
82
- publisher and read-only for the release target. Cryptographically sign/attest the
83
- resulting `publication-attestation.json`, verify the exact expected signer identity with
84
- the organization's release trust root, and create the sanitized signature-verification
85
- record only after that verifier succeeds.
86
-
87
- Then run `finalize_publication.py finalize` with the same independent verifier identity
88
- and a create-only final-record binder. The finalizer re-reads every release object after
89
- signature verification and permits the binder to attach only the fixed deterministic
90
- `release-publication-record.json` to the original target. GitHub deployments should use
91
- a read-only release-asset verifier identity distinct from the upload identity; object
92
- stores and registries should use equivalent separate read-only principals. Do not place
93
- signing keys, publisher tokens, verifier tokens, signed URLs, or raw credential-bearing
94
- provider output in the generated evidence.
95
-
96
- ## Run 152 transparency witness handoff
97
-
98
- After Run 151 creates and binds `release-publication-record.json`, submit those exact
99
- bytes to the approved append-only transparency service using
100
- `security/witness_publication.py`. CI must provide a previously trusted checkpoint for
101
- the same log, a log submitter identity, a separate read-only primary verifier, and the
102
- configured witness quorum. The default policy requires at least two distinct witness
103
- identities operated by at least two distinct operators.
104
-
105
- The primary verifier and every witness must independently verify the log checkpoint
106
- signature, exact entry inclusion, integrated entry, and consistency from the pinned
107
- previous checkpoint. Do not satisfy quorum with aliases sharing one credential or one
108
- mutable trust root. After quorum, the release-evidence anchor may create-only attach the
109
- deterministic witnessed record and must read it back before success. Persist the emitted canonical `accepted-transparency-checkpoint.json` for the next
110
- release only after this gate succeeds; pass those exact bytes back as the next
111
- `--previous-checkpoint`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ci/circleci.redis-chaos.reference.yml DELETED
@@ -1,68 +0,0 @@
1
- # Reference CircleCI job fragment. Merge under the parent repository's jobs/workflows keys.
2
- # The jobs emit canonical Run 148 attestations. Before production promotion, an
3
- # organization-approved cryptographic signer/verifier MUST create the paired
4
- # *.signature-verification.json records consumed by verify_release_evidence.py.
5
- version: 2.1
6
-
7
- jobs:
8
- provider_artifact_redis_chaos_7:
9
- machine:
10
- image: ubuntu-2404:current
11
- resource_class: medium
12
- steps:
13
- - checkout
14
- - run:
15
- name: Redis 7.4.11 standalone + cluster live-Lua chaos gate
16
- command: |
17
- REDIS_IMAGE='redis:7.4.11-bookworm@sha256:71da9275c5f3fcb97d0fa0c8c5b36cc995327265420f17a04bfd544f458059f7'
18
- mkdir -p /tmp/redis-chaos-evidence
19
- docker build \
20
- -f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
21
- --build-arg REDIS_IMAGE="$REDIS_IMAGE" \
22
- -t scikitplot-redis-chaos:7 .
23
- docker run --rm --read-only \
24
- --tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
25
- --tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
26
- --mount type=bind,src=/tmp/redis-chaos-evidence,dst=/evidence \
27
- --cap-drop ALL --security-opt no-new-privileges \
28
- scikitplot-redis-chaos:7 --mode all \
29
- --attestation-dir /evidence \
30
- --source-revision "$CIRCLE_SHA1" \
31
- --redis-image "$REDIS_IMAGE"
32
- - store_artifacts:
33
- path: /tmp/redis-chaos-evidence
34
- destination: redis-chaos-evidence-7
35
-
36
- provider_artifact_redis_chaos_8:
37
- machine:
38
- image: ubuntu-2404:current
39
- resource_class: medium
40
- steps:
41
- - checkout
42
- - run:
43
- name: Redis 8.2.9 standalone + cluster live-Lua chaos gate
44
- command: |
45
- REDIS_IMAGE='redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d'
46
- mkdir -p /tmp/redis-chaos-evidence
47
- docker build \
48
- -f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
49
- --build-arg REDIS_IMAGE="$REDIS_IMAGE" \
50
- -t scikitplot-redis-chaos:8 .
51
- docker run --rm --read-only \
52
- --tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
53
- --tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
54
- --mount type=bind,src=/tmp/redis-chaos-evidence,dst=/evidence \
55
- --cap-drop ALL --security-opt no-new-privileges \
56
- scikitplot-redis-chaos:8 --mode all \
57
- --attestation-dir /evidence \
58
- --source-revision "$CIRCLE_SHA1" \
59
- --redis-image "$REDIS_IMAGE"
60
- - store_artifacts:
61
- path: /tmp/redis-chaos-evidence
62
- destination: redis-chaos-evidence-8
63
-
64
- workflows:
65
- redis_chaos_reference:
66
- jobs:
67
- - provider_artifact_redis_chaos_7
68
- - provider_artifact_redis_chaos_8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ci/github-actions.redis-chaos.reference.yml DELETED
@@ -1,81 +0,0 @@
1
- # Reference job. Merge into the parent repository workflow rather than running this file directly.
2
- name: redis-chaos-reference
3
- on:
4
- workflow_dispatch:
5
-
6
- permissions:
7
- contents: read
8
- id-token: write
9
- attestations: write
10
-
11
- jobs:
12
- provider-artifact-redis-chaos:
13
- runs-on: ubuntu-24.04
14
- timeout-minutes: 25
15
- strategy:
16
- fail-fast: false
17
- matrix:
18
- include:
19
- - redis_major: 7
20
- redis_image: redis:7.4.11-bookworm@sha256:71da9275c5f3fcb97d0fa0c8c5b36cc995327265420f17a04bfd544f458059f7
21
- - redis_major: 8
22
- redis_image: redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d
23
- steps:
24
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
- with:
26
- persist-credentials: false
27
- - name: Build isolated digest-pinned Redis chaos image
28
- run: |
29
- docker build \
30
- -f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
31
- --build-arg REDIS_IMAGE='${{ matrix.redis_image }}' \
32
- -t scikitplot-redis-chaos:ci .
33
- - name: Run standalone + cluster live-Lua chaos gate and emit canonical evidence
34
- run: |
35
- mkdir -p "$RUNNER_TEMP/redis-chaos-evidence"
36
- docker run --rm \
37
- --read-only \
38
- --tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
39
- --tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
40
- --mount "type=bind,src=$RUNNER_TEMP/redis-chaos-evidence,dst=/evidence" \
41
- --cap-drop ALL \
42
- --security-opt no-new-privileges \
43
- scikitplot-redis-chaos:ci \
44
- --mode all \
45
- --attestation-dir /evidence \
46
- --source-revision '${{ github.sha }}' \
47
- --redis-image '${{ matrix.redis_image }}'
48
- - name: Cryptographically attest canonical Redis chaos evidence
49
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
50
- with:
51
- subject-path: '${{ runner.temp }}/redis-chaos-evidence/*.json'
52
- - name: Verify signatures and emit sanitized release records
53
- env:
54
- GH_TOKEN: ${{ github.token }}
55
- run: |
56
- set -eu
57
- signer_workflow="${GITHUB_WORKFLOW_REF%@*}"
58
- test -n "$signer_workflow"
59
- for attestation in "$RUNNER_TEMP"/redis-chaos-evidence/redis-chaos-redis${{ matrix.redis_major }}-*.json; do
60
- gh attestation verify "$attestation" \
61
- -R "$GITHUB_REPOSITORY" \
62
- --source-digest "$GITHUB_SHA" \
63
- --source-ref "$GITHUB_REF" \
64
- --signer-workflow "$signer_workflow" \
65
- --signer-digest "$GITHUB_SHA" \
66
- --deny-self-hosted-runners \
67
- --format json >/dev/null
68
- stem="${attestation%.json}"
69
- python scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis_chaos_attestation.py \
70
- signature-record \
71
- --attestation "$attestation" \
72
- --output "${stem}.signature-verification.json" \
73
- --source-revision '${{ github.sha }}'
74
- done
75
- - name: Upload signed Redis chaos release evidence
76
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
77
- with:
78
- name: redis-chaos-evidence-${{ matrix.redis_major }}
79
- path: '${{ runner.temp }}/redis-chaos-evidence/'
80
- if-no-files-found: error
81
- retention-days: 7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ci/redis-chaos.Dockerfile DELETED
@@ -1,19 +0,0 @@
1
- # Reproducible Run 147 Redis chaos image.
2
- # Build from the repository root so the complete scikitplot package is copied.
3
- # Example:
4
- # docker build -f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
5
- # --build-arg REDIS_IMAGE=redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d -t sphinx-ai-redis-chaos:8 .
6
- ARG REDIS_IMAGE=redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d
7
- FROM ${REDIS_IMAGE} AS redis_runtime
8
-
9
- FROM python:3.13-slim-bookworm
10
- COPY --from=redis_runtime /usr/local/bin/redis-server /usr/local/bin/redis-server
11
- WORKDIR /workspace
12
- COPY scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/requirements.txt /tmp/proxy-requirements.txt
13
- RUN python -m pip install --no-cache-dir -r /tmp/proxy-requirements.txt pytest
14
- COPY . /workspace
15
- ENV PYTHONPATH=/workspace \
16
- PYTHONDONTWRITEBYTECODE=1 \
17
- PYTHONUNBUFFERED=1
18
- ENTRYPOINT ["python", "scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/run_redis_chaos.py"]
19
- CMD ["--mode", "all"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ci/redis_chaos_attestation.py DELETED
@@ -1,189 +0,0 @@
1
- """
2
- Canonical Redis chaos attestation payloads for Run 148 release evidence.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import argparse
8
- import hashlib
9
- import json
10
- import logging
11
- import re
12
- import sys
13
- from datetime import datetime, timezone
14
- from pathlib import Path
15
- from typing import Any
16
-
17
- HERE = Path(__file__).resolve().parent
18
- EXTENSION_ROOT = HERE.parents[1]
19
- SECURITY = EXTENSION_ROOT / "_hf_spaces_proxy" / "security"
20
- if str(SECURITY) not in sys.path:
21
- sys.path.insert(0, str(SECURITY))
22
-
23
- from source_tree import source_tree_sha256 # noqa: E402
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- SCHEMA_VERSION = 1
28
- PREDICATE_TYPE = (
29
- "https://scikit-plots.org/attestations/provider-artifact-redis-chaos/v1"
30
- )
31
- TOOL_NAME = "scikitplot-provider-artifact-redis-chaos"
32
- TOOL_VERSION = "148"
33
- _REVISION = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$")
34
- _IMAGE = re.compile(r"^redis:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}$")
35
-
36
-
37
- class AttestationError(RuntimeError):
38
- """Attestation inputs violate the fail-closed Run 148 contract."""
39
-
40
-
41
- def validate_source_revision( # ruff: ignore[undocumented-public-function]
42
- value: str,
43
- ) -> str:
44
- rendered = str(value or "").strip().lower()
45
- if _REVISION.fullmatch(rendered) is None:
46
- raise AttestationError("SOURCE_REVISION_INVALID")
47
- return rendered
48
-
49
-
50
- def validate_redis_image( # ruff: ignore[undocumented-public-function]
51
- value: str,
52
- ) -> str:
53
- rendered = str(value or "").strip()
54
- if _IMAGE.fullmatch(rendered) is None:
55
- raise AttestationError("REDIS_IMAGE_NOT_DIGEST_PINNED")
56
- return rendered
57
-
58
-
59
- def _outside_source_tree(path: Path) -> Path:
60
- target = path.expanduser().resolve()
61
- root = EXTENSION_ROOT.resolve()
62
- if target == root or root in target.parents:
63
- raise AttestationError("ATTESTATION_OUTPUT_INSIDE_SOURCE_TREE")
64
- return target
65
-
66
-
67
- def build_attestation( # ruff: ignore[undocumented-public-function]
68
- *,
69
- mode: str,
70
- redis_major: int,
71
- redis_image: str,
72
- source_revision: str,
73
- generated_at: datetime | None = None,
74
- ) -> dict[str, Any]:
75
- if mode not in {"standalone", "cluster"}:
76
- raise AttestationError("REDIS_CHAOS_MODE_INVALID")
77
- if redis_major not in {7, 8}:
78
- raise AttestationError("REDIS_MAJOR_INVALID")
79
- revision = validate_source_revision(source_revision)
80
- image = validate_redis_image(redis_image)
81
- now = (generated_at or datetime.now(timezone.utc)).astimezone(timezone.utc)
82
- return {
83
- "schemaVersion": SCHEMA_VERSION,
84
- "predicateType": PREDICATE_TYPE,
85
- "generatedAt": now.isoformat(timespec="seconds").replace("+00:00", "Z"),
86
- "subject": {
87
- "sourceRevision": revision,
88
- "sourceTreeSha256": source_tree_sha256(EXTENSION_ROOT),
89
- },
90
- "redis": {
91
- "major": redis_major,
92
- "image": image,
93
- "mode": mode,
94
- },
95
- "result": {"status": "pass"},
96
- "tool": {"name": TOOL_NAME, "version": TOOL_VERSION},
97
- }
98
-
99
-
100
- def write_attestation( # ruff: ignore[undocumented-public-function]
101
- directory: Path,
102
- *,
103
- mode: str,
104
- redis_major: int,
105
- redis_image: str,
106
- source_revision: str,
107
- ) -> Path:
108
- root = _outside_source_tree(directory)
109
- root.mkdir(parents=True, exist_ok=True)
110
- target = root / f"redis-chaos-redis{redis_major}-{mode}.json"
111
- payload = build_attestation(
112
- mode=mode,
113
- redis_major=redis_major,
114
- redis_image=redis_image,
115
- source_revision=source_revision,
116
- )
117
- target.write_text(
118
- json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
119
- )
120
- return target
121
-
122
-
123
- def _sha256(path: Path) -> str:
124
- h = hashlib.sha256()
125
- with path.open("rb") as handle:
126
- for chunk in iter(lambda: handle.read(1024 * 1024), b""):
127
- h.update(chunk)
128
- return h.hexdigest()
129
-
130
-
131
- def write_signature_verification_record( # ruff: ignore[undocumented-public-function]
132
- attestation: Path,
133
- output: Path,
134
- *,
135
- source_revision: str,
136
- ) -> Path:
137
- revision = validate_source_revision(source_revision)
138
- source = attestation.expanduser().resolve()
139
- if not source.is_file() or source.is_symlink():
140
- raise AttestationError("ATTESTATION_FILE_INVALID")
141
- target = _outside_source_tree(output)
142
- target.parent.mkdir(parents=True, exist_ok=True)
143
- payload = {
144
- "schemaVersion": 1,
145
- "verified": True,
146
- "verifiedAt": (
147
- datetime.now(timezone.utc)
148
- .isoformat(timespec="seconds")
149
- .replace("+00:00", "Z")
150
- ),
151
- "attestationSha256": _sha256(source),
152
- "sourceRevision": revision,
153
- "signerIdentityVerified": True,
154
- }
155
- target.write_text(
156
- json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
157
- )
158
- return target
159
-
160
-
161
- def main( # ruff: ignore[undocumented-public-function]
162
- argv: list[str] | None = None,
163
- ) -> int:
164
- parser = argparse.ArgumentParser(description=__doc__)
165
- sub = parser.add_subparsers(dest="command", required=True)
166
- sig = sub.add_parser(
167
- "signature-record",
168
- help="emit sanitized record only after an external signature verifier succeeds",
169
- )
170
- sig.add_argument("--attestation", type=Path, required=True)
171
- sig.add_argument("--output", type=Path, required=True)
172
- sig.add_argument("--source-revision", required=True)
173
- args = parser.parse_args(argv)
174
- try:
175
- if args.command == "signature-record":
176
- write_signature_verification_record(
177
- args.attestation,
178
- args.output,
179
- source_revision=args.source_revision,
180
- )
181
- return 0
182
- raise AttestationError("COMMAND_INVALID")
183
- except (AttestationError, OSError) as exc:
184
- logger.error("[run148] FAIL: %s", exc)
185
- return 2
186
-
187
-
188
- if __name__ == "__main__":
189
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ci/run_redis_chaos.py DELETED
@@ -1,208 +0,0 @@
1
- # Authors: The scikit-plots developers
2
- # SPDX-License-Identifier: BSD-3-Clause
3
-
4
- """
5
- Mandatory Redis lifecycle chaos gate for CI.
6
-
7
- This entrypoint is intentionally standard-library only. It validates the Redis
8
- server major, verifies that the configured Python can import the pinned Redis
9
- client, and then executes the live Redis lifecycle doctors with skip-as-failure
10
- environment flags enabled.
11
-
12
- Run from any working directory::
13
-
14
- python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all
15
-
16
- The test suite itself starts disposable standalone/cluster Redis processes. No
17
- Redis authority URL, password, provider token, prompt, or generated bytes are
18
- printed by this runner.
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import argparse
24
- import logging
25
- import os
26
- import re
27
- import shutil
28
- import subprocess
29
- import sys
30
- from pathlib import Path
31
-
32
- _CI_DIR = Path(__file__).resolve().parent
33
- if str(_CI_DIR) not in sys.path:
34
- sys.path.insert(0, str(_CI_DIR))
35
- from redis_chaos_attestation import write_attestation # noqa: E402
36
-
37
- _SUPPORTED_REDIS_MAJORS = {7, 8}
38
- _TRUE = {"1", "true", "yes", "on"}
39
- logger = logging.getLogger(__name__)
40
-
41
-
42
- def _extension_root() -> Path:
43
- return Path(__file__).resolve().parents[2]
44
-
45
-
46
- def _repo_pythonpath() -> Path:
47
- # .../scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci
48
- # parents[6] is the directory containing the top-level scikitplot package.
49
- return Path(__file__).resolve().parents[6]
50
-
51
-
52
- def _resolve_binary(explicit: str, env_name: str, default_name: str) -> str:
53
- candidate = str(explicit or "").strip() or os.environ.get(env_name, "").strip()
54
- if candidate:
55
- path = Path(candidate).expanduser()
56
- if not path.is_file() or not os.access(path, os.X_OK):
57
- raise RuntimeError(f"{env_name} does not name an executable file")
58
- return str(path.resolve())
59
- found = shutil.which(default_name)
60
- if not found:
61
- raise RuntimeError(f"required executable is unavailable: {default_name}")
62
- return found
63
-
64
-
65
- def _redis_major(binary: str) -> int:
66
- result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
67
- [binary, "--version"],
68
- capture_output=True,
69
- text=True,
70
- timeout=10,
71
- check=False,
72
- )
73
- if result.returncode != 0:
74
- raise RuntimeError("redis-server --version failed")
75
- rendered = (result.stdout or "") + " " + (result.stderr or "")
76
- match = re.search(r"(?:v=|Redis server v=)(\d+)(?:\.|\b)", rendered)
77
- if not match:
78
- match = re.search(r"\b(\d+)\.\d+(?:\.\d+)?\b", rendered)
79
- if not match:
80
- raise RuntimeError("could not determine redis-server major version")
81
- major = int(match.group(1))
82
- if major not in _SUPPORTED_REDIS_MAJORS:
83
- raise RuntimeError(
84
- f"unsupported redis-server major {major}; expected Redis 7 or 8"
85
- )
86
- return major
87
-
88
-
89
- def _verify_python_runtime(python: str) -> None:
90
- code = (
91
- "import pytest, redis; "
92
- "from redis.asyncio.cluster import RedisCluster; "
93
- "print(getattr(redis, '__version__', 'unknown'))"
94
- )
95
- result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
96
- [python, "-c", code],
97
- capture_output=True,
98
- text=True,
99
- timeout=20,
100
- check=False,
101
- )
102
- if result.returncode != 0:
103
- raise RuntimeError(
104
- "CI Python must provide pytest, redis, and redis.asyncio.cluster.RedisCluster"
105
- )
106
-
107
-
108
- def _run_pytest(*, python: str, redis_server: str, mode: str) -> None:
109
- root = _extension_root()
110
- env = os.environ.copy()
111
- existing = env.get("PYTHONPATH", "").strip()
112
- repo_path = str(_repo_pythonpath())
113
- env["PYTHONPATH"] = repo_path if not existing else repo_path + os.pathsep + existing
114
- env["RUN146_REDIS_SERVER"] = redis_server
115
- env["RUN147_REDIS_SERVER"] = redis_server
116
-
117
- if mode == "standalone":
118
- env["RUN146_REDIS_CHAOS_REQUIRED"] = "1"
119
- env["RUN147_REDIS_STANDALONE_REQUIRED"] = "1"
120
- command = [
121
- python,
122
- "-m",
123
- "pytest",
124
- "-q",
125
- "-p",
126
- "no:cacheprovider",
127
- "tests/_hf_spaces_proxy/_utils/test__provider_artifact_lifecycle__redis_chaos.py",
128
- "tests/_hf_spaces_proxy/ci/test_run_redis_chaos.py",
129
- "-k",
130
- "live_redis or live_standalone",
131
- ]
132
- elif mode == "cluster":
133
- env["RUN147_REDIS_CLUSTER_REQUIRED"] = "1"
134
- command = [
135
- python,
136
- "-m",
137
- "pytest",
138
- "-q",
139
- "-p",
140
- "no:cacheprovider",
141
- "tests/_hf_spaces_proxy/ci/test_run_redis_chaos.py",
142
- "-k",
143
- "live_cluster",
144
- ]
145
- else: # pragma: no cover - argparse constrains this
146
- raise RuntimeError("unsupported chaos mode")
147
-
148
- result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
149
- command,
150
- cwd=root,
151
- env=env,
152
- check=False,
153
- )
154
- if result.returncode != 0:
155
- raise RuntimeError(f"Redis {mode} chaos gate failed")
156
-
157
-
158
- def main( # ruff: ignore[undocumented-public-function]
159
- argv: list[str] | None = None,
160
- ) -> int:
161
- parser = argparse.ArgumentParser(description=__doc__)
162
- parser.add_argument(
163
- "--mode", choices=("standalone", "cluster", "all"), default="all"
164
- )
165
- parser.add_argument("--redis-server", default="")
166
- parser.add_argument("--python", default=sys.executable)
167
- parser.add_argument("--attestation-dir", type=Path)
168
- parser.add_argument("--source-revision", default="")
169
- parser.add_argument("--redis-image", default="")
170
- args = parser.parse_args(argv)
171
-
172
- try:
173
- redis_server = _resolve_binary(
174
- args.redis_server, "RUN147_REDIS_SERVER", "redis-server"
175
- )
176
- major = _redis_major(redis_server)
177
- _verify_python_runtime(args.python)
178
- modes = ("standalone", "cluster") if args.mode == "all" else (args.mode,)
179
- if args.attestation_dir is not None and (
180
- not args.source_revision or not args.redis_image
181
- ):
182
- raise RuntimeError(
183
- "attestation output requires --source-revision and digest-pinned --redis-image"
184
- )
185
- for mode in modes:
186
- logger.info("[run147] Redis %s %s chaos gate", major, mode)
187
- _run_pytest(python=args.python, redis_server=redis_server, mode=mode)
188
- if args.attestation_dir is not None:
189
- logger.info(
190
- "[run147] writing Redis %s attestation for mode=%s", major, mode
191
- )
192
- write_attestation(
193
- args.attestation_dir,
194
- mode=mode,
195
- redis_major=major,
196
- redis_image=args.redis_image,
197
- source_revision=args.source_revision,
198
- )
199
- logger.info("[run147] PASS Redis %s mode=%s", major, args.mode)
200
- return 0
201
- except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
202
- # Never include Redis URLs or secret environment values in failures.
203
- logger.error("[run147] FAIL: %s", exc)
204
- return 2
205
-
206
-
207
- if __name__ == "__main__":
208
- raise SystemExit(main())