src-auth-perms-sync 0.4.0__tar.gz → 0.5.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.
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.gitignore +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/AGENTS.md +8 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/PKG-INFO +42 -7
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/README.md +40 -5
- src_auth_perms_sync-0.5.0/dev/PLAN.md +458 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/TODO.md +9 -21
- src_auth_perms_sync-0.5.0/dev/engineering-requests.md +781 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/pyproject.toml +2 -2
- src_auth_perms_sync-0.5.0/src/src_auth_perms_sync/__init__.py +36 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/cli.py +244 -92
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/sync.py +39 -50
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/command.py +189 -170
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/full_set.py +157 -177
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/queries.py +46 -5
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/restore.py +133 -171
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/snapshot.py +133 -16
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/sourcegraph.py +103 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/workflow.py +51 -64
- src_auth_perms_sync-0.5.0/src/src_auth_perms_sync/shared/backups.py +159 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/run_context.py +10 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/README.md +15 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/case_runner.py +182 -87
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/before.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-bad-regex → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/empty-maps-noop}/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-dry-run/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/after.json +74 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/before.json +74 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/maps.yaml +8 -0
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-with-backup → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant}/after.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-with-backup → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant}/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/after.json +7 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/before.json +7 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-with-backup}/after.json +4 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-with-backup}/before.json +4 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/get-user-grants → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/get-full-snapshot}/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/get-repos-without-explicit-perms/before.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/get-full-snapshot → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/get-user-grants}/before.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/empty-maps-noop → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-bad-regex}/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-missing-repos-section/before.json +47 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +47 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-created-after-date/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-repo/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-user/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +47 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/match-provider-and-host-fields/after.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/saml-group-filter → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/match-provider-and-host-fields}/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/no-match-noop/before.json +47 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/snapshot.json +2 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-dry-run-noop/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-dry-run-noop/snapshot.json +2 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-missing-file/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/after.json +57 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/before.json +57 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/snapshot.json +30 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-filter/after.json +1 -2
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/match-provider-and-host-fields → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/saml-group-filter}/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-noop/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/after.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/before.json +1 -2
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after-noop/before.json +1 -2
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/after.json +97 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/before.json +94 -0
- src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/maps.yaml +10 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/test_local_cases.py +46 -5
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/integration/test_cli_entrypoint.py +25 -1
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/run.py +823 -53
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/setup.py +127 -75
- src_auth_perms_sync-0.5.0/tests/setup.yaml +56 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/tests.yaml +224 -72
- src_auth_perms_sync-0.5.0/tests/unit/test_backups.py +297 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_cli_config.py +177 -55
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_command_additive.py +46 -32
- src_auth_perms_sync-0.5.0/tests/unit/test_command_files.py +241 -0
- src_auth_perms_sync-0.5.0/tests/unit/test_module_api.py +119 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_permissions_sourcegraph.py +61 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_restore.py +76 -1
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_snapshot.py +90 -14
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/uv.lock +24 -24
- src_auth_perms_sync-0.4.0/dev/engineering-requests.md +0 -270
- src_auth_perms_sync-0.4.0/src/src_auth_perms_sync/__init__.py +0 -11
- src_auth_perms_sync-0.4.0/src/src_auth_perms_sync/shared/backups.py +0 -112
- src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-missing-repos-section/before.json +0 -48
- src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +0 -48
- src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +0 -48
- src_auth_perms_sync-0.4.0/tests/e2e/fixtures/no-match-noop/before.json +0 -48
- src_auth_perms_sync-0.4.0/tests/unit/test_backups.py +0 -83
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.env.example +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/CODEOWNERS +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/ci.yml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/release.yml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/validate.yml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.markdownlint-cli2.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.python-version +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/LICENSE +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/SECURITY.md +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/audit-dead-code.md +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/hooks/pre-commit +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/mapping-efficiency.md +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-analyze.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-generate.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-monitor-sourcegraph.sh +0 -0
- {src_auth_perms_sync-0.4.0/dev → src_auth_perms_sync-0.5.0/dev/memory-analysis}/memory-efficiency.md +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/update-python-versions.md +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/maps-example.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/renovate.json +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/__main__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/command.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/queries.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/types.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/apply.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/mapping.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/maps.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/types.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/queries.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/saml_groups.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/site_config.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/sourcegraph.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/types.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/empty-maps-noop/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-with-backup/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-bad-regex/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-missing-repos-section/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/snapshot.json +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-created-after-date/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-repo/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-user/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-unknown-selector-field/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/match-provider-and-host-fields/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/no-match-noop/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-filter/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-full-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-noop/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after-noop/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/after.json +0 -0
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/before.json +0 -0
- {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/integration/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/__init__.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_apply.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_maps.py +0 -0
- {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_saml_groups.py +0 -0
|
@@ -201,10 +201,16 @@ organization sync maps SAML groups to Sourcegraph org membership. Read
|
|
|
201
201
|
CLI lives in `src/src_auth_perms_sync/`; invoke with `uv run src-auth-perms-sync`.
|
|
202
202
|
Strict pyright covers the package. Root modules are entrypoints only:
|
|
203
203
|
|
|
204
|
-
- `cli.py` — `main()`, arg parsing, owns the CLI description.
|
|
204
|
+
- `cli.py` — `main()`, arg parsing, owns the CLI description. Module
|
|
205
|
+
wrappers (`Get`/`Set`/`Restore`/`SyncSamlOrgs`) return result dataclasses
|
|
206
|
+
and never install logging handlers; only `main()` runs CLI-mode logging.
|
|
205
207
|
- `shared/` — cross-workflow helpers: Sourcegraph auth-provider/user list
|
|
206
208
|
helpers, shared GraphQL operations and TypedDicts, site-config validation,
|
|
207
|
-
and SAML group parsing.
|
|
209
|
+
and SAML group parsing. `shared/backups.py` defines `RunPaths`: every
|
|
210
|
+
filesystem path for one run, resolved once at the edge
|
|
211
|
+
(`resolve_run_paths`) and threaded explicitly — never recompute paths
|
|
212
|
+
from cwd or globals below the edge, and honor `run_paths.write_files`
|
|
213
|
+
(False under `--no-files`) before any disk write.
|
|
208
214
|
|
|
209
215
|
Business workflows live in packages:
|
|
210
216
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: src-auth-perms-sync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Set Sourcegraph permissions from authentication provider data
|
|
5
5
|
Project-URL: Homepage, https://github.com/sourcegraph/src-auth-perms-sync
|
|
6
6
|
Project-URL: Issues, https://github.com/sourcegraph/src-auth-perms-sync/issues
|
|
@@ -18,7 +18,7 @@ Classifier: Typing :: Typed
|
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Requires-Dist: json5>=0.14.0
|
|
20
20
|
Requires-Dist: pyyaml>=6.0.3
|
|
21
|
-
Requires-Dist: src-py-lib[otel]==0.
|
|
21
|
+
Requires-Dist: src-py-lib[otel]==0.3.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# src-auth-perms-sync
|
|
@@ -160,14 +160,44 @@ config = src.Config(
|
|
|
160
160
|
apply=False, # Dry run (default), set to True to make changes
|
|
161
161
|
)
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
result = src.Set(config) # truthy on success; result.paths has run artifacts
|
|
164
|
+
|
|
165
|
+
# Discovery returns the auth provider and code host data in memory, so you
|
|
166
|
+
# can assemble mapping rules without re-parsing the generated YAML files:
|
|
167
|
+
get_result = src.Get(config)
|
|
168
|
+
for provider in get_result.auth_providers:
|
|
169
|
+
...
|
|
170
|
+
for code_host in get_result.code_hosts:
|
|
171
|
+
...
|
|
164
172
|
|
|
165
173
|
# Other command wrappers:
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
|
|
174
|
+
# result = src.Restore(config)
|
|
175
|
+
# result = src.SyncSamlOrgs(config)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Module mode never touches your `logging` handlers or the root logger — your
|
|
179
|
+
application's logging config stays in charge. To see progress messages:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import logging
|
|
183
|
+
|
|
184
|
+
logging.basicConfig(level=logging.INFO) # or your own handlers
|
|
185
|
+
logging.getLogger("src_auth_perms_sync").setLevel(logging.INFO)
|
|
186
|
+
logging.getLogger("src_py_lib").setLevel(logging.INFO)
|
|
169
187
|
```
|
|
170
188
|
|
|
189
|
+
To receive structured wide events programmatically, pass an event sink:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
events = src.InMemoryEventSink()
|
|
193
|
+
src.Get(config, event_sink=events) # or src.CallbackEventSink(my_function)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
To run fully disk-free (no generated YAML, snapshots, or log file), set
|
|
197
|
+
`no_files=True`. Combined with `apply=True` this also requires
|
|
198
|
+
`no_backup=True`, because skipping files gives up the before/after
|
|
199
|
+
snapshots that make `--apply` reversible.
|
|
200
|
+
|
|
171
201
|
## Inputs
|
|
172
202
|
|
|
173
203
|
- Environment variables (CLI), or src.Config args (Python import)
|
|
@@ -177,7 +207,12 @@ succeeded = src.Set(config)
|
|
|
177
207
|
|
|
178
208
|
- YAML maps file
|
|
179
209
|
- By default: `src-auth-perms-sync-runs/<src_endpoint>/maps.yaml`
|
|
180
|
-
- Or pass `--maps-path ./path/to/maps.yaml`
|
|
210
|
+
- Or pass `--maps-path ./path/to/maps.yaml` (works for both `get` and `set`,
|
|
211
|
+
so the maps file can live outside the generated artifacts tree)
|
|
212
|
+
- `--artifacts-dir DIR` moves the whole artifacts tree (generated YAML,
|
|
213
|
+
snapshots, logs); the default is `./src-auth-perms-sync-runs`
|
|
214
|
+
- `--no-files` writes nothing to disk; with `--apply` it also requires
|
|
215
|
+
`--no-backup`
|
|
181
216
|
- A list of mapping rules
|
|
182
217
|
- Each mapping rule takes
|
|
183
218
|
- A map of filters for users
|
|
@@ -137,14 +137,44 @@ config = src.Config(
|
|
|
137
137
|
apply=False, # Dry run (default), set to True to make changes
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
result = src.Set(config) # truthy on success; result.paths has run artifacts
|
|
141
|
+
|
|
142
|
+
# Discovery returns the auth provider and code host data in memory, so you
|
|
143
|
+
# can assemble mapping rules without re-parsing the generated YAML files:
|
|
144
|
+
get_result = src.Get(config)
|
|
145
|
+
for provider in get_result.auth_providers:
|
|
146
|
+
...
|
|
147
|
+
for code_host in get_result.code_hosts:
|
|
148
|
+
...
|
|
141
149
|
|
|
142
150
|
# Other command wrappers:
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
|
|
151
|
+
# result = src.Restore(config)
|
|
152
|
+
# result = src.SyncSamlOrgs(config)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Module mode never touches your `logging` handlers or the root logger — your
|
|
156
|
+
application's logging config stays in charge. To see progress messages:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import logging
|
|
160
|
+
|
|
161
|
+
logging.basicConfig(level=logging.INFO) # or your own handlers
|
|
162
|
+
logging.getLogger("src_auth_perms_sync").setLevel(logging.INFO)
|
|
163
|
+
logging.getLogger("src_py_lib").setLevel(logging.INFO)
|
|
146
164
|
```
|
|
147
165
|
|
|
166
|
+
To receive structured wide events programmatically, pass an event sink:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
events = src.InMemoryEventSink()
|
|
170
|
+
src.Get(config, event_sink=events) # or src.CallbackEventSink(my_function)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
To run fully disk-free (no generated YAML, snapshots, or log file), set
|
|
174
|
+
`no_files=True`. Combined with `apply=True` this also requires
|
|
175
|
+
`no_backup=True`, because skipping files gives up the before/after
|
|
176
|
+
snapshots that make `--apply` reversible.
|
|
177
|
+
|
|
148
178
|
## Inputs
|
|
149
179
|
|
|
150
180
|
- Environment variables (CLI), or src.Config args (Python import)
|
|
@@ -154,7 +184,12 @@ succeeded = src.Set(config)
|
|
|
154
184
|
|
|
155
185
|
- YAML maps file
|
|
156
186
|
- By default: `src-auth-perms-sync-runs/<src_endpoint>/maps.yaml`
|
|
157
|
-
- Or pass `--maps-path ./path/to/maps.yaml`
|
|
187
|
+
- Or pass `--maps-path ./path/to/maps.yaml` (works for both `get` and `set`,
|
|
188
|
+
so the maps file can live outside the generated artifacts tree)
|
|
189
|
+
- `--artifacts-dir DIR` moves the whole artifacts tree (generated YAML,
|
|
190
|
+
snapshots, logs); the default is `./src-auth-perms-sync-runs`
|
|
191
|
+
- `--no-files` writes nothing to disk; with `--apply` it also requires
|
|
192
|
+
`--no-backup`
|
|
158
193
|
- A list of mapping rules
|
|
159
194
|
- Each mapping rule takes
|
|
160
195
|
- A map of filters for users
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# PLAN: file handling + observability redesign
|
|
2
|
+
|
|
3
|
+
> **Status (2026-06-12): implemented**, except Track A Phase A4
|
|
4
|
+
> (in-memory mapping rules for `Set`), which the plan marks optional —
|
|
5
|
+
> tracked in [TODO.md](./TODO.md). Shipped as src-py-lib v0.3.0 and the
|
|
6
|
+
> src-auth-perms-sync `refactor-logging-and-files` PR. Phases were
|
|
7
|
+
> compressed into one PR per repo (no ContextVar bridge phase was
|
|
8
|
+
> needed); everything else landed as specified.
|
|
9
|
+
|
|
10
|
+
Spans both repos (we own both, both greenfield, no external users):
|
|
11
|
+
|
|
12
|
+
- `src-py-lib` — reusable plumbing (logging, events, OTel, HTTP, config)
|
|
13
|
+
- `src-auth-perms-sync` — consumer; runs as a CLI and as an importable
|
|
14
|
+
Python module (`src.Get(config)` etc.)
|
|
15
|
+
|
|
16
|
+
Two tracks. Track A fixes how files are handled. Track B fixes how logs and
|
|
17
|
+
structured events are handled. They meet where the JSONL event sink takes its
|
|
18
|
+
path from `RunPaths`.
|
|
19
|
+
|
|
20
|
+
## Goals
|
|
21
|
+
|
|
22
|
+
1. Module-import customers get data back in memory, control over (or freedom
|
|
23
|
+
from) disk writes, and their own `logging` config left untouched.
|
|
24
|
+
2. CLI customers get explicit control over where files land, with default
|
|
25
|
+
paths unchanged.
|
|
26
|
+
3. Structured events become first-class, OTel-standard wide events instead of
|
|
27
|
+
stowaways on the stdlib logging pipe.
|
|
28
|
+
4. Net code reduction: delete the path ContextVars, the logging demux filters,
|
|
29
|
+
and the magic record attributes.
|
|
30
|
+
|
|
31
|
+
## Current state (verified in code, this thread)
|
|
32
|
+
|
|
33
|
+
### Files written today
|
|
34
|
+
|
|
35
|
+
All output roots at `Path.cwd()/src-auth-perms-sync-runs/<endpoint>/` via
|
|
36
|
+
`backups.endpoint_artifacts_directory()`. Three species share that tree:
|
|
37
|
+
|
|
38
|
+
| Species | Files | Nature |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Human-edited input | `maps.yaml` | `get` creates empty if missing; `set` reads |
|
|
41
|
+
| Regenerated reference | `code-hosts.yaml`, `auth-providers.yaml` | overwritten by every `get` |
|
|
42
|
+
| Append-only audit | `runs/<ts>-<cmd>/{before,after,diff}.json` + copies | gate reversibility |
|
|
43
|
+
|
|
44
|
+
The audit run directory also carries a `maps.yaml` copy and `log.json`.
|
|
45
|
+
|
|
46
|
+
Path plumbing: `cli.py:_run_or_raise` computes a run directory, then sets two
|
|
47
|
+
ContextVars (`backups.run_artifacts_context`). Deep code in
|
|
48
|
+
`permissions/restore.py`, `permissions/full_set.py`, `orgs/sync.py` calls
|
|
49
|
+
`backups.backup_timestamp()` / `backups.backup_path(name, ts, endpoint, cmd,
|
|
50
|
+
state)`, which read the ContextVars or silently recompute from cwd.
|
|
51
|
+
|
|
52
|
+
### What `--no-backup` does and does not do
|
|
53
|
+
|
|
54
|
+
`--no-backup` skips only the reversibility artifacts (before/after snapshots,
|
|
55
|
+
maps.yaml backup copy). Even with it set, the tool still writes:
|
|
56
|
+
|
|
57
|
+
- `maps.yaml` creation (`run_get`, unconditional)
|
|
58
|
+
- `code-hosts.yaml` / `auth-providers.yaml` (`cmd_get`, unconditional)
|
|
59
|
+
- `log.json` (`_run_or_raise` always passes a log file path)
|
|
60
|
+
- dry-run snapshots/diffs — `full_set.py` gates on
|
|
61
|
+
`if not (dry_run or do_backup)`, so dry runs write regardless, because the
|
|
62
|
+
diff is the dry run's output
|
|
63
|
+
|
|
64
|
+
### Logging today
|
|
65
|
+
|
|
66
|
+
- Two record species ride one stdlib logging pipe: human messages
|
|
67
|
+
(`log.info(...)` on module loggers) and structured events
|
|
68
|
+
(`span()`/`log_event()`), the latter with message `event=%s` and the payload
|
|
69
|
+
smuggled in record attrs `_src_py_lib_structured_event` /
|
|
70
|
+
`_src_py_lib_structured_fields`, emitted on the ROOT logger
|
|
71
|
+
(`logger_name=""` default).
|
|
72
|
+
- Demux at sinks: terminal handler carries `_DropStructuredEvents`;
|
|
73
|
+
`JSONLogFileHandler` knows the magic attrs and renders both species into
|
|
74
|
+
`log.json` (human records become `{"event": "log", ...}`).
|
|
75
|
+
- OTel spans open in parallel inside `span()`; OTel SDK is a hard dependency.
|
|
76
|
+
|
|
77
|
+
### Bugs / hazards found during investigation
|
|
78
|
+
|
|
79
|
+
1. **Module import nukes customer logging.** `configure_logging()` runs on
|
|
80
|
+
every `src.Get(config)` call and, with `logger_name=""`, clears the ROOT
|
|
81
|
+
logger's handlers and sets `propagate = False`. A host application's
|
|
82
|
+
logging config is destroyed.
|
|
83
|
+
2. **Snapshot filename collision risk.** In a combined `set
|
|
84
|
+
--sync-saml-orgs` run, permission snapshots and org snapshots both resolve
|
|
85
|
+
to `before.json` in the same run directory (`backup_path` ignores
|
|
86
|
+
`source_name` when `state` is given).
|
|
87
|
+
3. **Run-directory collision.** Timestamps are second-precision; concurrent
|
|
88
|
+
module calls or rapid CLI invocations can collide.
|
|
89
|
+
4. **HTTP metric reset misplaced.** `reset_observability_metrics()` lives in
|
|
90
|
+
`configure_logging()`; skip handler config and a long-running script's
|
|
91
|
+
second run reports cumulative counters.
|
|
92
|
+
5. **Audit events ride logger levels.** A host that sets root level WARNING
|
|
93
|
+
silently drops our info-level audit events.
|
|
94
|
+
|
|
95
|
+
## Hard invariants (do not break; see AGENTS.md)
|
|
96
|
+
|
|
97
|
+
All four AGENTS.md invariants apply. The one this plan touches most is
|
|
98
|
+
invariant 3 — snapshots gate reversibility: `--apply` defaults to
|
|
99
|
+
before/after snapshots; `--no-backup` stays an explicit escape hatch, never
|
|
100
|
+
the default.
|
|
101
|
+
|
|
102
|
+
New corollary adopted in this plan: any flag combination that removes the
|
|
103
|
+
undo path must be stated explicitly twice (`--no-files` + `--apply` requires
|
|
104
|
+
`--no-backup`).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Track A: file handling (`RunPaths`)
|
|
109
|
+
|
|
110
|
+
### Design decisions
|
|
111
|
+
|
|
112
|
+
- **One knob**: `artifacts_dir` Config field, CLI `--artifacts-dir`, env
|
|
113
|
+
`SRC_AUTH_PERMS_SYNC_ARTIFACTS_DIR`. Semantics: the directory that contains
|
|
114
|
+
endpoint subdirectories. Default `./src-auth-perms-sync-runs` — existing
|
|
115
|
+
default paths do not move. Resolve to absolute once at startup
|
|
116
|
+
(`Path.resolve(strict=False)`) so later `os.chdir()` in a host script
|
|
117
|
+
cannot redirect writes. No XDG / platformdirs: these are audit artifacts in
|
|
118
|
+
secured offline environments; explicit operator paths beat OS conventions.
|
|
119
|
+
- **One value object**: frozen `RunPaths`, built once at the edge
|
|
120
|
+
(`_run_or_raise`) by `resolve_run_paths(config, endpoint, command)`:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class RunPaths:
|
|
125
|
+
timestamp: str
|
|
126
|
+
artifacts_dir: Path # resolved artifacts root
|
|
127
|
+
endpoint_directory: Path # artifacts_dir / <endpoint-dirname>
|
|
128
|
+
maps_path: Path # respects --maps-path override
|
|
129
|
+
code_hosts_path: Path
|
|
130
|
+
auth_providers_path: Path
|
|
131
|
+
run_directory: Path # endpoint_directory / runs / <ts>-<artifact-name>
|
|
132
|
+
log_path: Path
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Threaded explicitly down `run_command` → `cmd_*` → workflow helpers. Deep
|
|
136
|
+
code receives concrete `Path`s; nothing recomputes from
|
|
137
|
+
`(timestamp, endpoint, command)`.
|
|
138
|
+
- **`maps.yaml` default stays endpoint-scoped.** Endpoint scoping prevents
|
|
139
|
+
applying one instance's mappings to another; a silent default move creates
|
|
140
|
+
two plausible maps files. Instead make `--maps-path` symmetric: `get` gains
|
|
141
|
+
the flag (today only `set` has it), so `get --maps-path ./maps.yaml`
|
|
142
|
+
creates/leaves that file and `set --maps-path ./maps.yaml` reads it.
|
|
143
|
+
- **Run directories are created exclusively**; on collision append a numeric
|
|
144
|
+
suffix (`...-set-apply-2`). Never overwrite run artifacts.
|
|
145
|
+
- **Artifact families are named.** Permission snapshots keep `before.json` /
|
|
146
|
+
`after.json` / `diff.json`; org-sync snapshots become
|
|
147
|
+
`saml-organizations-before.json` etc., so combined runs cannot collide.
|
|
148
|
+
- **Writability preflight.** Apply-mode commands resolve paths, create the
|
|
149
|
+
run directory, and verify the snapshot destination is writable BEFORE the
|
|
150
|
+
first mutation. Never mutate first and discover backups can't be written.
|
|
151
|
+
- **`no_files` knob** (CLI `--no-files`, Config field): suppress all disk
|
|
152
|
+
output. Distinct axis from `no_backup` (reversibility) — keep both flags.
|
|
153
|
+
- Guardrail 1: `no_files=True` + `apply=True` is a config error unless
|
|
154
|
+
`no_backup=True` is also set (give up the undo button explicitly, twice).
|
|
155
|
+
- Guardrail 2: `no_files` governs output only; `set` still reads its
|
|
156
|
+
`maps_path` input (until Track A Phase 4 in-memory maps).
|
|
157
|
+
- `no_files` implies `log_file=None` (lib already supports no file handler)
|
|
158
|
+
and no JSONL event sink.
|
|
159
|
+
- For CLI dry runs `--no-files` discards `diff.json` — operator's explicit
|
|
160
|
+
choice; the real audience is module customers, but offer uniformly.
|
|
161
|
+
- **Result objects.** Command wrappers return dataclasses; `__bool__` returns
|
|
162
|
+
`succeeded` so existing `if src.Get(config):` keeps working. Note
|
|
163
|
+
`__bool__` is only partial compat (`Get(c) is True` breaks); acceptable —
|
|
164
|
+
no external users yet. `GetResult` carries the same dicts dumped to YAML
|
|
165
|
+
(`cmd_get` already has them in memory; thread out via
|
|
166
|
+
`run_context.CommandData`):
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class GetResult:
|
|
171
|
+
succeeded: bool
|
|
172
|
+
auth_providers: list[dict[str, Any]] # as written to auth-providers.yaml
|
|
173
|
+
code_hosts: list[dict[str, Any]] # as written to code-hosts.yaml
|
|
174
|
+
maps_path: Path
|
|
175
|
+
maps_created: bool
|
|
176
|
+
paths: RunPaths | None
|
|
177
|
+
|
|
178
|
+
def __bool__(self) -> bool: ...
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Same pattern for `SetResult` (planned/applied diff), `RestoreResult`,
|
|
182
|
+
`SyncSamlOrgsResult`. Export the path helpers (`default_maps_path`,
|
|
183
|
+
`endpoint_artifacts_directory`) from the package `__init__`.
|
|
184
|
+
|
|
185
|
+
### Track A phases (each ships independently)
|
|
186
|
+
|
|
187
|
+
- **A0 — characterization tests first.** Pin current behavior before
|
|
188
|
+
touching paths: default `get` output locations; default `set` maps path;
|
|
189
|
+
relative `--maps-path` is cwd-relative; `--no-backup` opt-in only; apply
|
|
190
|
+
fails before mutation when artifact paths are unwritable; combined
|
|
191
|
+
`set --sync-saml-orgs` snapshot naming (exposes the collision).
|
|
192
|
+
- **A1 — `artifacts_dir` + `RunPaths` at the edge.** Add the config field,
|
|
193
|
+
`resolve_run_paths()`, exclusive run-dir creation, `no_files` knob with
|
|
194
|
+
guardrails, `--maps-path` on `get`. Keep the two ContextVars as a
|
|
195
|
+
temporary bridge (`run_artifacts_context(paths.run_directory, ...)`). QoL
|
|
196
|
+
lands immediately; refactor risk stays low.
|
|
197
|
+
- **A2 — thread `RunPaths` down; delete the ContextVars.** Change
|
|
198
|
+
signatures: `run_command(..., paths)` → `run_get/run_set/run_restore/
|
|
199
|
+
run_sync_saml_organizations(..., paths)` → workflow helpers take concrete
|
|
200
|
+
paths (`write_maps_backup(paths)`, `write_snapshot_pair(paths, ...)`).
|
|
201
|
+
Apply artifact-family naming. When no deep code calls `backup_path()` /
|
|
202
|
+
`backup_timestamp()`, delete `run_artifacts_context`, both ContextVars,
|
|
203
|
+
and the cwd fallback. Net code reduction.
|
|
204
|
+
- **A3 — result dataclasses + exported helpers.** As specified above. CLI
|
|
205
|
+
ignores the result; module callers stop re-parsing YAML the library just
|
|
206
|
+
wrote.
|
|
207
|
+
- **A4 — in-memory mapping rules for `Set` (optional, later).** Module
|
|
208
|
+
customers pass parsed mapping rules instead of a maps file; the full
|
|
209
|
+
get → assemble → dry-run set loop never touches disk. Snapshots still go
|
|
210
|
+
to disk on `--apply` unless `no_files` + `no_backup` are both explicit.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Track B: observability (`EventSink` + OTel wide events)
|
|
215
|
+
|
|
216
|
+
### Design decisions: three concerns, three channels
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
Human messages Structured events Tracing
|
|
220
|
+
log.info("Wrote %s") span() / log_event() OTel spans
|
|
221
|
+
│ │ │
|
|
222
|
+
▼ ▼ │
|
|
223
|
+
stdlib logging EventSink (explicit, per run) │
|
|
224
|
+
module loggers only ├ JSONLEventSink → log.json │
|
|
225
|
+
│ ├ InMemoryEventSink (tests, │
|
|
226
|
+
│ │ module customers) │
|
|
227
|
+
│ ├ CallbackEventSink │
|
|
228
|
+
│ ├ OtelLogsSink (--otel) │
|
|
229
|
+
│ └ NullEventSink (default) │
|
|
230
|
+
CLI mode only: ▲ │
|
|
231
|
+
terminal handler EventBridgeHandler forwards human │
|
|
232
|
+
on package loggers, log records INTO the sink so │
|
|
233
|
+
never root log.json keeps everything │
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- **Bridge direction reversed.** Today rich events are flattened onto the
|
|
237
|
+
logging pipe (magic attrs) and unpacked at the file handler. Instead:
|
|
238
|
+
events go to the sink directly as dicts; ONE `EventBridgeHandler`
|
|
239
|
+
(CLI-installed) wraps lossy human log records into `{"event_name": "log"}`
|
|
240
|
+
events. The adapter direction matches data richness.
|
|
241
|
+
- **Entrypoint is the mode signal — no `manage_logging` knob.**
|
|
242
|
+
`main()` runs CLI mode (terminal handler + bridge + JSONL sink, handlers
|
|
243
|
+
attached only to `("src_auth_perms_sync", "src_py_lib")`, never root, no
|
|
244
|
+
`handlers.clear()`, no `propagate` changes on host loggers).
|
|
245
|
+
`Get/Set/Restore/SyncSamlOrgs` run guest mode: no handler changes
|
|
246
|
+
anywhere, `NullEventSink` default, optional caller-provided sink. Standard
|
|
247
|
+
etiquette `logging.NullHandler()` on the package loggers.
|
|
248
|
+
- **Sink resolution via ContextVar (`EventRuntime`).** One deliberate
|
|
249
|
+
exception to the "kill ContextVars" rule: the event runtime
|
|
250
|
+
(`run`, `sink`, `min_level`, `base_fields`) is genuinely ambient execution
|
|
251
|
+
context, like the current OTel span. Guardrails: only
|
|
252
|
+
`observability_context()` sets it; default is `NullEventSink`; thread
|
|
253
|
+
pools keep using `contextvars.copy_context()` via
|
|
254
|
+
`submit_with_log_context`; no import-time side effects.
|
|
255
|
+
(`RunPaths` stays explicitly threaded — paths are not ambient.)
|
|
256
|
+
- **`span()` / `log_event()` / `event()` / `stage()` signatures unchanged.**
|
|
257
|
+
Only the backend moves; consumer call sites in src-auth-perms-sync are
|
|
258
|
+
untouched.
|
|
259
|
+
- **Split `logging_context()`** (currently does too much) into:
|
|
260
|
+
- `observability_context(name, *, sink, run, run_fields, open_telemetry,
|
|
261
|
+
resource_sample_interval_seconds, ...)` — sets the EventRuntime, OTel
|
|
262
|
+
only if explicitly requested, run start/end events, resource sampler,
|
|
263
|
+
metric reset (moved here from `configure_logging`), sink lifecycle.
|
|
264
|
+
- `cli_logging_handlers(*, sink, run, logger_names, terminal_level,
|
|
265
|
+
audit_log_level)` — adds/removes only its own handlers.
|
|
266
|
+
- **Teardown ordering** (baked into one context manager): stop resource
|
|
267
|
+
sampler → wait for worker threads → emit run-end event → flush/close JSONL
|
|
268
|
+
sink → force-flush OTel → remove CLI handlers. Run-end event must land
|
|
269
|
+
even on `SystemExit` / exceptions; OTel flush failure must not prevent
|
|
270
|
+
JSON flush; no logging from inside sink error handling (recursion).
|
|
271
|
+
- **OTel global provider hygiene.** Never configure OTel in guest mode
|
|
272
|
+
unless the caller asks; if the host already configured OTel, `span()`
|
|
273
|
+
uses it. Add an `owned` flag to `OpenTelemetryRuntime`; only force-flush
|
|
274
|
+
providers this run configured.
|
|
275
|
+
- **Level policy.** Audit completeness is governed by the sink, not logger
|
|
276
|
+
levels: `log.json` defaults to complete (debug); `--quiet` affects
|
|
277
|
+
terminal only. Lifecycle events (run start/end, startup config snapshot,
|
|
278
|
+
errors) are always emitted.
|
|
279
|
+
- **Thread safety.** JSONL sink locks around writes; copy event dicts before
|
|
280
|
+
customer callbacks; worker pools complete inside the observability
|
|
281
|
+
context; document that `ProcessPoolExecutor` does not inherit context.
|
|
282
|
+
|
|
283
|
+
### OTel standards adoption (decided inside B1/B2, not a separate phase)
|
|
284
|
+
|
|
285
|
+
- **Event schema = OTel Logs Data Model field names:**
|
|
286
|
+
|
|
287
|
+
| Today | OTel standard |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| `ts` (string) | `time_unix_nano` |
|
|
290
|
+
| `level` | `severity_text` + `severity_number` (INFO=9, WARN=13, ERROR=17) |
|
|
291
|
+
| `event` key | `event_name` |
|
|
292
|
+
| `message` | `body` |
|
|
293
|
+
| `trace` / `span` / `parent_span` | `trace_id` / `span_id` (hex from our OTel spans) |
|
|
294
|
+
| other fields | `attributes` |
|
|
295
|
+
| run-level fields | `resource` attributes, stamped once per run |
|
|
296
|
+
|
|
297
|
+
- **Semantic conventions for attribute names**, pinned in one
|
|
298
|
+
`semconv.py` constants module (semconv is versioned and partly
|
|
299
|
+
experimental; an upstream rename must be a one-file change):
|
|
300
|
+
`error_type` → `error.type`; pid → `process.pid`; Python version →
|
|
301
|
+
`process.runtime.version`; `service.name` / `service.version`; HTTP
|
|
302
|
+
run-summary counters aligned to `http.client.*`. App-specific fields get
|
|
303
|
+
our own namespace so they can never collide: `sync.command`,
|
|
304
|
+
`sync.endpoint`, `sync.run.id`, `sync.parallelism`, etc.
|
|
305
|
+
- **Wide-event discipline: one event per unit of work.** The span-END event
|
|
306
|
+
is THE wide event (the codebase already accumulates attributes onto
|
|
307
|
+
`cmd_event[...]` during work — keep that pattern). Demote span-START
|
|
308
|
+
events to debug; demote run-start once `startup_event` carries the config
|
|
309
|
+
snapshot. `log.json` gets ~half the lines and every line is worth reading.
|
|
310
|
+
- **File format: flat JSONL with OTel field names** (human-greppable,
|
|
311
|
+
audit-friendly, one trivial transform from OTLP). NOT per-line OTLP/JSON
|
|
312
|
+
envelopes (`resourceLogs[...]` — three envelopes deep, miserable to audit
|
|
313
|
+
by eye). True-OTLP interop is a separate sink: when `--otel` is on,
|
|
314
|
+
`OtelLogsSink` emits wide events through the OTel Logs SDK
|
|
315
|
+
(`LoggerProvider` → OTLP/HTTP) — already a dependency. Offline customers
|
|
316
|
+
keep the readable file; connected ones get standard wire logs correlated
|
|
317
|
+
with our traces via `trace_id`/`span_id`.
|
|
318
|
+
|
|
319
|
+
### Custom logic carry-over inventory (audited 2026-06-12)
|
|
320
|
+
|
|
321
|
+
Existing custom logic in `src_py_lib/utils/logging.py` and its fate. Most
|
|
322
|
+
survives because `span()` / `log_event()` / context machinery keep their
|
|
323
|
+
signatures; four items live in code Track B deletes and MUST be carried over
|
|
324
|
+
explicitly:
|
|
325
|
+
|
|
326
|
+
1. **HTTP header secret redaction** (`_http_headers` +
|
|
327
|
+
`SECRET_FIELD_FRAGMENTS`): scrubs authorization/cookie/token headers
|
|
328
|
+
before they reach `log.json`. Lives in the handler path B6 deletes. Move
|
|
329
|
+
the redaction (and `_is_sensitive_log_field`) into the bridge/mining path.
|
|
330
|
+
Losing this writes bearer tokens to disk during wire debugging.
|
|
331
|
+
2. **httpcore/httpx wire-debug mining** (`_structured_log_fields`):
|
|
332
|
+
`ast.literal_eval`s httpcore debug messages into structured
|
|
333
|
+
`status_code` / `http_version` / redacted-header fields; demotes httpx
|
|
334
|
+
"HTTP Request:" lines to debug. The bridge attaches to package loggers
|
|
335
|
+
only, so httpx/httpcore records never reach the sink — the opt-in
|
|
336
|
+
wire-debug mode (`suppress_http_dependency_logs=False`) would silently
|
|
337
|
+
die. Carry-over: when suppression is disabled, also attach the bridge to
|
|
338
|
+
`("httpx", "httpcore")` and keep the mining + redaction in the bridge.
|
|
339
|
+
3. **Log-file retention** (`_prune_old_log_files`, `retain_log_files`):
|
|
340
|
+
inert for src-auth-perms-sync (per-run directories don't match the
|
|
341
|
+
prune glob) but active for other src-py-lib consumers using the default
|
|
342
|
+
`logs/` dir. Keep pruning where the sink's file is created
|
|
343
|
+
(`logs_dir`-style construction helper), not in the sink itself.
|
|
344
|
+
4. **`exc_info` traceback formatting and field ordering**
|
|
345
|
+
(`_ordered_payload`, `LOG_FIELD_ORDER`): move traceback rendering into
|
|
346
|
+
`EventBridgeHandler`; move ordering into the JSONL sink, updated to the
|
|
347
|
+
OTel field names (`time_unix_nano`, `severity_text`, `event_name`,
|
|
348
|
+
`trace_id`, `span_id` first, attributes alphabetical).
|
|
349
|
+
|
|
350
|
+
Verified safe without action (mechanism unchanged by the plan):
|
|
351
|
+
`log_context`/`stage` inherited fields; `submit_with_log_context`;
|
|
352
|
+
trace-field stamping; HTTP metric counters wired into `HTTPClient`;
|
|
353
|
+
`ResourceSampler` (emits via `log_event`, so it follows the sink);
|
|
354
|
+
`startup_event` git hash + `sanitized_config_snapshot` (incl. `op://` →
|
|
355
|
+
"reference" secret-state detection); run-end `SystemExit(0)`/exit-code
|
|
356
|
+
semantics; `-v/-q/-s` alias validation and level parsing; OTel span
|
|
357
|
+
attributes/status and traceparent propagation for `fetch_sg_traces`.
|
|
358
|
+
|
|
359
|
+
`_DropHTTPDependencyLogs` on the terminal handler becomes unnecessary by
|
|
360
|
+
construction (handlers sit on package loggers, so httpx/httpcore noise
|
|
361
|
+
never reaches them) — delete it; behavior is preserved.
|
|
362
|
+
|
|
363
|
+
### Track B steps (lib first, consumer second)
|
|
364
|
+
|
|
365
|
+
- **B1 — sinks beside the old code (src-py-lib).** `EventSink` protocol;
|
|
366
|
+
`NullEventSink`, `JSONLEventSink` (locked writes, flush/close),
|
|
367
|
+
`CompositeEventSink`, `InMemoryEventSink`, `CallbackEventSink`;
|
|
368
|
+
`EventRuntime` ContextVar; `semconv.py`; tests for JSONL output shape
|
|
369
|
+
(OTel field names, severity numbers, resource stamping).
|
|
370
|
+
- **B2 — move `log_event()` off stdlib logging (src-py-lib).** Build the
|
|
371
|
+
OTel-shaped payload, emit to the current sink, keep OTel span-event
|
|
372
|
+
emission; delete the `LogRecord.extra` magic-attr path. Apply wide-event
|
|
373
|
+
discipline (start events → debug).
|
|
374
|
+
- **B3 — `EventBridgeHandler` (src-py-lib).** Human records → sink as
|
|
375
|
+
`event_name="log"` events. CLI-installed only, attached to
|
|
376
|
+
`("src_auth_perms_sync", "src_py_lib")`.
|
|
377
|
+
- **B4 — split `logging_context()` (src-py-lib).** Into
|
|
378
|
+
`observability_context()` and `cli_logging_handlers()` as above; move
|
|
379
|
+
`reset_observability_metrics()` into `observability_context()`; OTel
|
|
380
|
+
`owned` flag.
|
|
381
|
+
- **B5 — consumer wiring (src-auth-perms-sync).** `_run_or_raise(...,
|
|
382
|
+
cli_mode: bool)`; `main()` passes `cli_mode=True` (JSONL sink at
|
|
383
|
+
`RunPaths.log_path`, terminal + bridge handlers); `Get/Set/Restore/
|
|
384
|
+
SyncSamlOrgs` pass `cli_mode=False` (guest mode; optional `event_sink`
|
|
385
|
+
parameter for customers who want events programmatically). README gains a
|
|
386
|
+
module-logging snippet (root WARNING default means a long sync looks
|
|
387
|
+
silent — show `logging.getLogger("src_auth_perms_sync")` setup).
|
|
388
|
+
- **B6 — delete the old demux (src-py-lib).** Remove
|
|
389
|
+
`_DropStructuredEvents`, `_STRUCTURED_EVENT_ATTR`,
|
|
390
|
+
`_STRUCTURED_FIELDS_ATTR`, the structured branch in `JSONLogFileHandler`
|
|
391
|
+
(or the whole handler, replaced by the sink), and the destructive root
|
|
392
|
+
`handlers.clear()`. Regression tests: CLI produces `log.json`;
|
|
393
|
+
`log.info()` appears as `event_name="log"`; structured events appear with
|
|
394
|
+
full attributes; module `Get(config)` does not mutate root handlers or
|
|
395
|
+
levels; thread-pool events retain run/context fields; run-end event
|
|
396
|
+
appears on exceptions; second module run in one process reports per-run
|
|
397
|
+
(not cumulative) HTTP counters.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Track interaction points
|
|
402
|
+
|
|
403
|
+
- `JSONLEventSink` path comes from `RunPaths.log_path` (A1 must land before
|
|
404
|
+
B5's consumer wiring, or B5 temporarily uses the current log path helper).
|
|
405
|
+
- `no_files` (A1) disables the JSONL sink and the log file; guest mode (B5)
|
|
406
|
+
defaults to no file output anyway — `no_files` mainly matters for CLI and
|
|
407
|
+
for module callers who set `artifacts_dir`.
|
|
408
|
+
- Result dataclasses (A3) and `InMemoryEventSink` / `CallbackEventSink` (B1)
|
|
409
|
+
together complete the module story: data back in memory, events back in
|
|
410
|
+
memory, zero disk unless asked (and snapshots on `--apply`, always, unless
|
|
411
|
+
doubly opted out).
|
|
412
|
+
|
|
413
|
+
## Verification
|
|
414
|
+
|
|
415
|
+
- `uv run tests/run.py` (lint, format, pyright, unit + fixture tests, CLI
|
|
416
|
+
rejection matrix, randomized permission invariants) after every phase.
|
|
417
|
+
- `uv run tests/run.py --live` for mutation-path phases (A2 especially), with
|
|
418
|
+
before/after snapshot inspection under `src-auth-perms-sync-runs/`.
|
|
419
|
+
- A0 characterization tests are the safety net for every later phase: default
|
|
420
|
+
paths must not move.
|
|
421
|
+
- New fixture cases for: `--artifacts-dir`, `get --maps-path`, `--no-files`
|
|
422
|
+
rejection matrix (`--no-files --apply` without `--no-backup` is an error),
|
|
423
|
+
combined-run snapshot naming, log.json schema goldens.
|
|
424
|
+
- Per AGENTS.md: dry-run against the .env test instance and read the planned
|
|
425
|
+
changes before any `--apply`; inspect snapshots afterward.
|
|
426
|
+
|
|
427
|
+
## Explicitly out of scope (revisit triggers noted)
|
|
428
|
+
|
|
429
|
+
- **Moving the `maps.yaml` default** out of the artifacts tree — revisit only
|
|
430
|
+
if compatibility constraints loosen; `--maps-path` symmetry covers the need.
|
|
431
|
+
- **XDG / platformdirs** — explicit flag + env is correct for secured offline
|
|
432
|
+
environments.
|
|
433
|
+
- **`QueueEventSink` / async writer thread** — only if event volume, slow
|
|
434
|
+
customer callbacks, or OTLP backpressure measurably hurt sync runs.
|
|
435
|
+
- **Artifact manifest / `ArtifactStore` abstraction** — only if artifact
|
|
436
|
+
families multiply beyond the current three.
|
|
437
|
+
- **Strict per-line OTLP/JSON file format** — only if a customer needs
|
|
438
|
+
collector `otlpjsonfile` ingestion of `log.json` as-is; ship as a sink
|
|
439
|
+
variant, not a default.
|
|
440
|
+
|
|
441
|
+
## Sequencing summary
|
|
442
|
+
|
|
443
|
+
```text
|
|
444
|
+
A0 characterization tests
|
|
445
|
+
A1 artifacts_dir + RunPaths (bridge) + no_files + get --maps-path
|
|
446
|
+
B1 sinks + OTel schema + semconv (src-py-lib, parallel with A1/A2)
|
|
447
|
+
B2 log_event → sink, wide events
|
|
448
|
+
A2 thread RunPaths, delete ContextVars
|
|
449
|
+
B3 bridge handler
|
|
450
|
+
B4 observability_context split
|
|
451
|
+
B5 consumer cli_mode wiring (needs A1 for RunPaths.log_path)
|
|
452
|
+
A3 result dataclasses + exports
|
|
453
|
+
B6 delete demux + regression tests
|
|
454
|
+
A4 in-memory maps for Set (optional, last)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Each step lands as its own PR from a clean worktree off `origin/main`, lint
|
|
458
|
+
and tests green, docs updated.
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
# TODO
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Follow-up: in-memory mapping rules for Set (PLAN.md Track A Phase A4)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Decide: pendingBindIDs / usersWithPendingPermissions
|
|
13
|
-
|
|
14
|
-
The CLI cannot create pending permissions (it validates users exist), but
|
|
15
|
-
snapshots record `pending_bindIDs`, and setup.py / the live hygiene check
|
|
16
|
-
report (never delete) any that appear. Decide whether "grant before first
|
|
17
|
-
login" is a customer need; if not, consider dropping the snapshot field.
|
|
18
|
-
See the thread discussion 2026-06-11.
|
|
5
|
+
The rest of [PLAN.md](./PLAN.md) is implemented (src-py-lib v0.3.0 +
|
|
6
|
+
the consumer refactor-logging-and-files PR). The one deliberately
|
|
7
|
+
deferred piece, marked optional in the plan: let module callers pass
|
|
8
|
+
parsed mapping rules to `Set` instead of a maps file, so the full
|
|
9
|
+
get → assemble → dry-run loop never touches disk. Snapshots must stay
|
|
10
|
+
on disk for `--apply` (reversibility invariant); `no_files` + `apply`
|
|
11
|
+
must keep requiring `no_backup`.
|
|
19
12
|
|
|
20
13
|
## High priority: Remote trigger on demand
|
|
21
14
|
|
|
@@ -30,12 +23,7 @@ See the thread discussion 2026-06-11.
|
|
|
30
23
|
- How do we avoid stampedes (e.g., bulk repo sync triggering thousands
|
|
31
24
|
of re-runs)?
|
|
32
25
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
- If a user gets added to a new SAML group, which hits a mapping, ensure they
|
|
36
|
-
get the new perms
|
|
37
|
-
|
|
38
|
-
## High priority: Reduce worst-case full-permission sync load
|
|
26
|
+
## Medium priority: Reduce worst-case full-permission sync load
|
|
39
27
|
|
|
40
28
|
- Use the stress-run evidence in
|
|
41
29
|
[engineering-requests.md](./engineering-requests.md)
|