Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions .github/release-control-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
{
"schema_version": 1,
"contract_edition": "wp562-f1-c",
"repository": {
"owner": "aisystant",
"name": "aist_bot"
},
"source_refs": {
"pilot": "pilot",
"production": "new-architecture"
},
"artifact": {
"digest_kind": "oci_manifest",
"oci_repository": "registry.example.invalid/team/app",
"platform": {
"os": "linux",
"architecture": "amd64"
},
"immutable_reference_required": true,
"embedded_manifest_excludes": [
"artifact_digest",
"manifest_hash"
]
},
"authority": {
"audience": "iwe.release-control.runner",
"independent_approver_required": true,
"max_uses": 1,
"purpose": "iwe.release-control.authority.v1",
"trust_root_identity": "release-authority-root",
"required_signed_fields": [
"approver_identity",
"audience",
"authority_id",
"contract_digest",
"contract_edition",
"expires_at",
"issued_at",
"key_id",
"max_uses",
"nonce",
"operation_fingerprint",
"purpose",
"release_id",
"repository",
"required_checks_digest",
"runner_identity",
"schema_version",
"target"
],
"ttl_seconds": 3600
},
"ledger": {
"append_only_required": true,
"claim_field_allowlist": [
"artifact_digest",
"authorization_id",
"contract_digest",
"evidence_hash",
"external_id",
"operation_fingerprint",
"operation_kind",
"target_key"
],
"durable_claim_required": true,
"external_receipt_required": true,
"raw_evidence_forbidden": true,
"target_version_cas_required": true
},
"promotion": {
"build_once": true,
"check_suite_max_age_seconds": 3600,
"check_suite_receipt_required": true,
"independent_review_required": true,
"pilot_qualification_independent_verifier_required": true,
"pilot_qualification_max_age_seconds": 300,
"pilot_qualification_receipt_required": true,
"pilot_qualification_required_signals": [
"canary",
"data-invariants",
"readiness",
"runtime-attestation"
],
"provider_deadline_required": true,
"provider_fencing_required": true,
"provider_idempotency_required": true,
"same_digest_required": true,
"source_candidate_max_age_seconds": 3600,
"source_candidate_receipt_required": true,
"reconciliation": {
"zero": "observed_not_applied",
"one": "observed_applied",
"many": "manual_review"
},
"reconciliation_settling_seconds": 60,
"zero_settlement_min_interval_seconds": 30,
"zero_settlement_observations": 2
},
"rollback": {
"compatibility_max_age_seconds": 1800,
"compatibility_receipt_required": true,
"historical_build_contract_receipt_required": true,
"independent_verifier_required": true,
"same_previous_good_digest": true,
"rebuild_forbidden": true,
"require_can_rollback": true,
"require_schema_compatibility": true,
"retention_recheck_seconds": 3600,
"reattestation_seconds": 1800
},
"required_checks": [
"bandit-sast",
"full-regression",
"l1-l2-smoke-regression",
"pilot-production-semantic-parity",
"pip-audit",
"release-candidate-build",
"release-control-contract",
"scenario-compliance"
],
"forbidden_paths": [
"hashlib.py",
"hashlib/__init__.py",
"pytest.py",
"pytest/__init__.py",
"pytest/__main__.py",
"release_manifest/__init__.py",
"scripts.py",
"scripts/__init__.py",
"scripts/pytest.py",
"scripts/release_control/__init__.py",
"scripts/release_manifest.py",
"scripts/sitecustomize.py",
"scripts/usercustomize.py",
"sitecustomize.py",
"tests/conftest.py",
"usercustomize.py"
],
"protected_paths": [
".dockerignore",
".gitattributes",
".githooks/pre-push",
".github/CODEOWNERS",
".github/actions/reject-python-shadows/action.yml",
".github/release-control-contract.json",
".github/release-metadata.json",
".github/workflows/check-dt-indicators.yml",
".github/workflows/pilot-prod-sync.yml",
".github/workflows/release-control-dry-run.yml",
".github/workflows/scenario-compliance.yml",
".github/workflows/security.yml",
".github/workflows/smoke-tests.yml",
".gitmodules",
"CLAUDE.md",
"Dockerfile",
"PROCESSES.md",
"bot.py",
"clients/github_content.py",
"config/settings.py",
"conftest.py",
"core/autofix.py",
"core/health_check.py",
"core/scheduler.py",
"db/migrations/**",
"db/models.py",
"db/queries/autofix.py",
"oauth_server.py",
"pytest.ini",
"railway.json",
"readiness.py",
"release-manifest.unavailable.json",
"release_attestation.py",
"release_manifest.py",
"requirements.in",
"requirements.txt",
"scripts/build_release_manifest.py",
"scripts/check_pilot_prod_parity.py",
"scripts/check_release_metadata.py",
"scripts/record_pilot_prod_correspondence.py",
"scripts/release_control.py",
"tests/__init__.py",
"tests/test_autofix_security.py",
"tests/test_build_release_manifest.py",
"tests/test_health_check_l3.py",
"tests/test_pilot_prod_correspondence.py",
"tests/test_pilot_prod_parity.py",
"tests/test_readiness.py",
"tests/test_release_attestation.py",
"tests/test_release_control.py",
"tests/test_release_control_workflows.py",
"tests/test_release_manifest.py",
"tests/test_release_metadata.py",
"tests/test_repo/__init__.py",
"tests/test_repo/requirements-scenarios.yaml",
"tests/test_repo/scripts/__init__.py",
"tests/test_repo/scripts/checker.py",
"tests/test_repo/scripts/code_analyzer.py",
"tests/test_repo/scripts/report_generator.py",
"wheels/activity_hub-0.1.0-py3-none-any.whl"
],
"cutover": {
"enabled": false,
"negative_matrix_required": true,
"negative_matrix_max_age_seconds": 86400,
"negative_matrix_evidence_ref": null
}
}
44 changes: 44 additions & 0 deletions .github/workflows/release-control-dry-run.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Release Control Contract

on:
push:
branches: [pilot, new-architecture]
pull_request:

permissions:
contents: read

jobs:
release-control-contract:
name: release-control-contract
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out the candidate
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.11'

- name: Install test dependencies
run: python -I -m pip install pytest

- name: Require production mutations to remain disabled
run: >-
python -I -B scripts/release_control.py validate-contract
.github/release-control-contract.json
--require-cutover-disabled

- name: Check release planning and rejected operations
env:
PYTEST_DISABLE_PLUGIN_AUTOLOAD: '1'
run: >-
python -I -B -m pytest --import-mode=importlib
-p no:cacheprovider
tests/test_release_control.py
tests/test_release_manifest.py
-q --tb=short
80 changes: 80 additions & 0 deletions docs/processes/process-18-release-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,86 @@ digest в production. Docker Image source пропускает сборку; о
проверенным планом с решением пилота. [Railway IaC](https://docs.railway.com/infrastructure-as-code#apply-changes),
[настройки источника](https://docs.railway.com/infrastructure-as-code/reference#sources).

## 10. Подготовка Ф3 без переключения сред

Ядро `scripts/release_control.py` и схема `release_manifest.py` перенесены
отдельно от продуктовых и миграционных изменений старого прототипа.
`.github/release-control-contract.json` сохраняет `cutover.enabled=false`.
CI `release-control-contract` проверяет это ограничение и поведение ядра;
проверка не выполняет сборку, публикацию образа или развёртывание.

Предварительный просмотр принимает два JSON-файла и выводит канонический
JSON в stdout; пути передаёт вызывающий:

```bash
python -I -B scripts/release_control.py preview-release \
.github/release-control-contract.json \
--embedded-manifest EMBEDDED_MANIFEST_JSON \
--configuration-manifest CONFIGURATION_MANIFEST_JSON
```

Необязательный `--artifact-reference REGISTRY/REPOSITORY@sha256:DIGEST`
допускает только точный registry/repository из контракта. Без него digest
остаётся неизвестным. Код 0 означает согласованность деклараций;
`readiness=not_verified` и `mutation_authorized=false` сохраняются всегда.
Список `missing_evidence` перечисляет необходимые внешние проверки,
включая фактическое содержимое профилей и классификацию дельт. Предварительный
просмотр не читает профили, не обращается в registry и не создаёт разрешения.
Его `preview_digest` не является fingerprint исполняемой операции.

Конфигурационный manifest хранится вне образа и связывает release ID,
хеш embedded manifest, оба профиля и реестр допустимых различий. Каждый
профиль адресуется полным commit, относительным путём и digest. Эти данные
задают ожидаемую конфигурацию; для допуска требуется отдельная проверенная
квитанция фактического состояния среды. Сам JSON квитанцией не является.

При финализации production можно обновить наблюдение текущего состояния
среды после pilot. Менять принятую пару профилей или реестр различий нельзя:
это новый manifest и новая операция с новым подтверждением. Хеш
конфигурационного manifest входит в идентичность операции. Откат также
должен ссылаться на конфигурационный manifest возвращаемого выпуска.

Проверки с подставными провайдерами доказывают правила ядра, но не свойства
Railway. Реальные адаптеры должны отдельно обеспечить проверенное
происхождение образа, полномочия, устойчивую запись журнала и безопасное
разрешение неопределённого результата. Статус применённой операции не
заменяет проверку production readiness, canary и инвариантов данных.

### Предлагаемый источник образов

Предлагаемый адрес — `ghcr.io/aisystant/aist_bot@sha256:<digest>`.
Наличие пакета и доступ Railway к нему пока не подтверждены.
Сборка должна использовать полный принятый commit в Git context,
`linux/amd64`, закреплённые Actions и публикацию по digest без пересборки
между pilot и production. [Git context Docker](https://github.com/docker/build-push-action),
[публикация по digest](https://docs.docker.com/build/exporters/image-registry/).

Для сборочного CI нужны `contents: read` и `packages: write`; вход в GHCR
возможен через `GITHUB_TOKEN`. Подписанное GitHub-свидетельство происхождения
добавляет `id-token: write` и `attestations: write`; для private/internal
репозитория его доступность требует Enterprise Cloud. Наличие нужного
тарифа и выбранного способа подписи ещё нужно установить. Неподписанная
BuildKit provenance не заменяет проверку доверенного подписанта.
[GHCR](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry),
[GitHub attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations).

Если результат сборки — OCI index, нужно сохранить его digest и связь с
исполняемым манифестом `linux/amd64`. Они не взаимозаменяемы. Поддержка
index со свидетельствами и источник связи deployment ID с работающим
digest должны быть проверены до переключения.
[Хранение attestations](https://docs.docker.com/build/metadata/attestations/attestation-storage/).

Для private registry Railway требует Pro и учётные данные чтения пакета;
для GHCR — отдельный PAT classic с `read:packages` и ограниченным доступом
учётной записи. Значение секрета вводится в Railway, а профиль хранит
только ссылку на его версию. Нужные тариф, права и источник runtime digest
пока не подтверждены. [Railway private registries](https://docs.railway.com/builds/private-registries).

Следующее решение должно включать владельца пакета и выпуска, доступный
способ подписания, принятые профили/различия и точный кандидат. Затем можно
готовить конкретный Railway change set с актуальным `configEtag`.
Наличие подготовленного кода не разрешает это изменение автоматически.

## Связанные материалы

- РП562 (`DS-my-strategy/inbox/WP-562/WP-562.md`) — постановка проблемы, гипотеза H-314, фазы Ф2-Ф6.
Expand Down
Loading
Loading