subroutine 0.1.2__tar.gz → 0.1.3__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.
- {subroutine-0.1.2 → subroutine-0.1.3}/.github/workflows/ci.yml +10 -10
- {subroutine-0.1.2 → subroutine-0.1.3}/.github/workflows/release.yml +7 -7
- {subroutine-0.1.2 → subroutine-0.1.3}/CHANGELOG.md +16 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/CONTRIBUTING.md +4 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/PKG-INFO +13 -7
- {subroutine-0.1.2 → subroutine-0.1.3}/README.md +12 -6
- subroutine-0.1.3/SECURITY.md +74 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/.claude-plugin/plugin.json +1 -1
- subroutine-0.1.3/scripts/release.py +198 -0
- subroutine-0.1.3/tests/test_release_script.py +233 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/.claude-plugin/marketplace.json +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/.gitignore +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/CLA.md +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/LICENSE +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/alembic.ini +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/docs/errors.md +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/docs/hosting.md +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/.mcp.json +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/skills/subroutine/SKILL.md +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/pyproject.toml +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/scripts/check_licences.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/scripts/check_release_notes.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/__main__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/addressing.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/admin.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/agenda.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/app.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/comments.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/concurrency.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/dependencies.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/documents.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/events.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/health.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/identity.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/meta.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/middleware.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/pagination.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/problems.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/projects.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/query.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/routing.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/schemas.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/security.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/shaping.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/subjects.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/tasks.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/tokens.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/users.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/workspaces.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/auth.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/main.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/personal.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/topics.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/base.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/http.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/local.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/opening.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/config.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/connections.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/context.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/credentials.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/backup.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/base.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrate.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/env.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/script.py.mako +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/0c8f7a7027e6_refs_are_workspace_sequential_integers.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/233f898a2bee_instance_timezone_workspace_timezone_.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/2fee457e5b0b_initial_schema.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/547fe53b263c_an_event_carries_the_subject_it_.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/ea3e86ad12c4_workspace_slug_frees_on_soft_delete.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/mixins.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/activity.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/identity.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/project.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/system.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/vocabulary.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/work.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/seed.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/session.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/transfer.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/types.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/directory.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/agenda.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/authentication.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/authorization.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/bootstrap.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/capture.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/comments.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/dates.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/documents.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/durations.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/events.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/hierarchy.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/instances.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/links.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/local.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/mentions.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/ordering.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/paging.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/patch.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/projects.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/readiness.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/refs.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/schedule.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/scoping.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/search.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/selection.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tags.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tasks.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/text.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tokens.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/users.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/versions.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/workspaces.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/errors.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/fanout.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/__init__.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/protocol.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/session.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/tools.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/permissions.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/py.typed +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/views.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/api_support.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/conftest.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/sample_models.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_actor_discipline.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_agenda.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_app.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_authentication.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_comments.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_concurrency.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_documents.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_events.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_examples.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_meta.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_problems.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_projects.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_routing.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_shaping.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_tasks.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_tokens.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_workspaces.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_writability.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_auth.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_authentication.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_authorization.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_bootstrap.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_capture.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_cli_connections.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_cli_help.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_config.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_connections.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_context.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_dates.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_db_types.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_directory.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_documentation.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_durations.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_errors.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_fanout.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_fixtures.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_imports.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_instances.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_isolation.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_local_mode.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_mcp.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_migrations.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_models.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_multi_user.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_ordering.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_personal_path.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_plugin.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_reach.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_release_notes.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_schedule.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_scoping.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_seed.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_services.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_smoke.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_spec_endpoints.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_sqlite_concurrency.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_transfer.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_transport_equivalence.py +0 -0
- {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_upgrade.py +0 -0
|
@@ -29,11 +29,11 @@ jobs:
|
|
|
29
29
|
# Full history and tags, because the release check compares this commit's migration
|
|
30
30
|
# head against the one at the most recent tag. A shallow clone has neither, and the
|
|
31
31
|
# check would find no previous tag and cheerfully pass every time.
|
|
32
|
-
- uses: actions/checkout@
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
33
|
with:
|
|
34
34
|
fetch-depth: 0
|
|
35
35
|
|
|
36
|
-
- uses: actions/setup-python@
|
|
36
|
+
- uses: actions/setup-python@v7
|
|
37
37
|
with:
|
|
38
38
|
python-version: "3.12"
|
|
39
39
|
cache: pip
|
|
@@ -67,14 +67,14 @@ jobs:
|
|
|
67
67
|
runs-on: ubuntu-latest
|
|
68
68
|
|
|
69
69
|
steps:
|
|
70
|
-
- uses: actions/checkout@
|
|
70
|
+
- uses: actions/checkout@v7
|
|
71
71
|
with:
|
|
72
72
|
# Tags, because the version is derived from them (`SR#234`). A shallow clone makes
|
|
73
73
|
# the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
|
|
74
74
|
# install a package quietly claiming to be a version nobody released.
|
|
75
75
|
fetch-depth: 0
|
|
76
76
|
|
|
77
|
-
- uses: actions/setup-python@
|
|
77
|
+
- uses: actions/setup-python@v7
|
|
78
78
|
with:
|
|
79
79
|
python-version: "3.12"
|
|
80
80
|
cache: pip
|
|
@@ -102,9 +102,9 @@ jobs:
|
|
|
102
102
|
runs-on: ubuntu-latest
|
|
103
103
|
|
|
104
104
|
steps:
|
|
105
|
-
- uses: actions/checkout@
|
|
105
|
+
- uses: actions/checkout@v7
|
|
106
106
|
|
|
107
|
-
- uses: actions/setup-node@
|
|
107
|
+
- uses: actions/setup-node@v7
|
|
108
108
|
with:
|
|
109
109
|
node-version: "22"
|
|
110
110
|
|
|
@@ -143,14 +143,14 @@ jobs:
|
|
|
143
143
|
--health-retries 10
|
|
144
144
|
|
|
145
145
|
steps:
|
|
146
|
-
- uses: actions/checkout@
|
|
146
|
+
- uses: actions/checkout@v7
|
|
147
147
|
with:
|
|
148
148
|
# Tags, because the version is derived from them (`SR#234`). A shallow clone makes
|
|
149
149
|
# the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
|
|
150
150
|
# install a package quietly claiming to be a version nobody released.
|
|
151
151
|
fetch-depth: 0
|
|
152
152
|
|
|
153
|
-
- uses: actions/setup-python@
|
|
153
|
+
- uses: actions/setup-python@v7
|
|
154
154
|
with:
|
|
155
155
|
python-version: ${{ matrix.python-version }}
|
|
156
156
|
cache: pip
|
|
@@ -177,14 +177,14 @@ jobs:
|
|
|
177
177
|
runs-on: ubuntu-latest
|
|
178
178
|
|
|
179
179
|
steps:
|
|
180
|
-
- uses: actions/checkout@
|
|
180
|
+
- uses: actions/checkout@v7
|
|
181
181
|
with:
|
|
182
182
|
# Tags, because the version is derived from them (`SR#234`). A shallow clone makes
|
|
183
183
|
# the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
|
|
184
184
|
# install a package quietly claiming to be a version nobody released.
|
|
185
185
|
fetch-depth: 0
|
|
186
186
|
|
|
187
|
-
- uses: actions/setup-python@
|
|
187
|
+
- uses: actions/setup-python@v7
|
|
188
188
|
with:
|
|
189
189
|
python-version: "3.12"
|
|
190
190
|
cache: pip
|
|
@@ -51,14 +51,14 @@ jobs:
|
|
|
51
51
|
--health-retries 10
|
|
52
52
|
|
|
53
53
|
steps:
|
|
54
|
-
- uses: actions/checkout@
|
|
54
|
+
- uses: actions/checkout@v7
|
|
55
55
|
with:
|
|
56
56
|
# Tags, because the version is derived from them (`SR#234`). A shallow clone makes
|
|
57
57
|
# the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
|
|
58
58
|
# install a package quietly claiming to be a version nobody released.
|
|
59
59
|
fetch-depth: 0
|
|
60
60
|
|
|
61
|
-
- uses: actions/setup-python@
|
|
61
|
+
- uses: actions/setup-python@v7
|
|
62
62
|
with:
|
|
63
63
|
python-version: "3.12"
|
|
64
64
|
cache: pip
|
|
@@ -83,11 +83,11 @@ jobs:
|
|
|
83
83
|
steps:
|
|
84
84
|
# Full history and tags: `check_release_notes.py` compares this commit's migration head
|
|
85
85
|
# against the head at the previous tag, and a shallow clone has neither.
|
|
86
|
-
- uses: actions/checkout@
|
|
86
|
+
- uses: actions/checkout@v7
|
|
87
87
|
with:
|
|
88
88
|
fetch-depth: 0
|
|
89
89
|
|
|
90
|
-
- uses: actions/setup-python@
|
|
90
|
+
- uses: actions/setup-python@v7
|
|
91
91
|
with:
|
|
92
92
|
python-version: "3.12"
|
|
93
93
|
|
|
@@ -137,7 +137,7 @@ jobs:
|
|
|
137
137
|
- name: Release notes
|
|
138
138
|
run: python scripts/check_release_notes.py
|
|
139
139
|
|
|
140
|
-
- uses: actions/upload-artifact@
|
|
140
|
+
- uses: actions/upload-artifact@v7
|
|
141
141
|
with:
|
|
142
142
|
name: dist
|
|
143
143
|
path: dist/
|
|
@@ -159,7 +159,7 @@ jobs:
|
|
|
159
159
|
id-token: write
|
|
160
160
|
|
|
161
161
|
steps:
|
|
162
|
-
- uses: actions/download-artifact@
|
|
162
|
+
- uses: actions/download-artifact@v8
|
|
163
163
|
with:
|
|
164
164
|
name: dist
|
|
165
165
|
path: dist/
|
|
@@ -186,7 +186,7 @@ jobs:
|
|
|
186
186
|
id-token: write
|
|
187
187
|
|
|
188
188
|
steps:
|
|
189
|
-
- uses: actions/download-artifact@
|
|
189
|
+
- uses: actions/download-artifact@v8
|
|
190
190
|
with:
|
|
191
191
|
name: dist
|
|
192
192
|
path: dist/
|
|
@@ -12,6 +12,22 @@ The point of it is that you can *plan* a database upgrade instead of meeting one
|
|
|
12
12
|
through installing something. See [docs/hosting.md](docs/hosting.md#upgrading) for what the
|
|
13
13
|
upgrade involves.
|
|
14
14
|
|
|
15
|
+
## 0.1.3 — 2026-08-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **The first install line no longer fails on the machine most people have.** `pip install
|
|
20
|
+
subroutine` outside a virtualenv is refused by Debian, Ubuntu and Fedora — PEP 668's
|
|
21
|
+
`externally-managed-environment` — and that was the first command in this README, the first
|
|
22
|
+
on the PyPI page, and the first thing anybody read. It is now `uv tool install subroutine`
|
|
23
|
+
or `pipx install subroutine`: they work on those systems, they put `subroutine` on your
|
|
24
|
+
`PATH` where an editor or an agent can launch it, and pipx is what Debian's own error
|
|
25
|
+
message tells you to use.
|
|
26
|
+
|
|
27
|
+
**This corrects the note at the end of 0.1.2 below**, which said `pip install subroutine`
|
|
28
|
+
remained right if you were only going to type commands yourself. That holds inside a
|
|
29
|
+
virtualenv you have activated, and nowhere else — which is not what it implied.
|
|
30
|
+
|
|
15
31
|
## 0.1.2 — 2026-08-02
|
|
16
32
|
|
|
17
33
|
No migration notice: the schema head has not moved since 0.1.0. Nothing in the package itself
|
|
@@ -16,6 +16,10 @@ That will change once the shape has settled, and this file will say so when it d
|
|
|
16
16
|
- **Telling us it does not fit.** If you tried it and stopped, we would rather know why than
|
|
17
17
|
not. An issue saying "I wanted X and there was no way to do it" is worth more than a guess.
|
|
18
18
|
|
|
19
|
+
**A vulnerability is the exception, and please treat it as one.** An issue is public from the
|
|
20
|
+
moment it is filed, so it is the one report that must not start there —
|
|
21
|
+
[SECURITY.md](SECURITY.md) says where it should go instead.
|
|
22
|
+
|
|
19
23
|
The rest of this file is kept for two reasons: an unsolicited pull request still needs the
|
|
20
24
|
licence agreement below if it is ever taken, and the conventions are worth having if you are
|
|
21
25
|
reading the code to decide whether to trust it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subroutine
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Project management for people and agents, in equal measure.
|
|
5
5
|
Project-URL: Homepage, https://github.com/simonholliday/subroutine
|
|
6
6
|
Project-URL: Issues, https://github.com/simonholliday/subroutine/issues
|
|
@@ -73,7 +73,7 @@ Three things you might want. Pick one; they compose.
|
|
|
73
73
|
**A to-do list on your own machine.** Nothing to configure.
|
|
74
74
|
|
|
75
75
|
```console
|
|
76
|
-
$
|
|
76
|
+
$ uv tool install subroutine # or: pipx install subroutine
|
|
77
77
|
$ subroutine init
|
|
78
78
|
$ subroutine add "Call the dentist before Sunday"
|
|
79
79
|
$ subroutine today
|
|
@@ -84,7 +84,7 @@ practice — it keeps the backlog, records what it did, and adopts Subroutine in
|
|
|
84
84
|
are already working on. **You never have to learn the CLI.**
|
|
85
85
|
|
|
86
86
|
```console
|
|
87
|
-
$ uv tool install subroutine # or pipx
|
|
87
|
+
$ uv tool install subroutine # or: pipx install subroutine
|
|
88
88
|
$ claude plugin marketplace add simonholliday/subroutine
|
|
89
89
|
$ claude plugin install subroutine@subroutine
|
|
90
90
|
```
|
|
@@ -119,16 +119,20 @@ commands and `subroutine explain dates` covers the ideas behind them.
|
|
|
119
119
|
Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
|
|
120
120
|
start — SQLite is the default and `subroutine init` makes it.
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
**Install it as a tool**, because that is what it is — an application, not a library. It puts
|
|
123
|
+
`subroutine` on your `PATH`, which is what lets an editor or an agent launch it. It is also the
|
|
124
|
+
only thing that works on a current Linux: Debian, Ubuntu and Fedora now refuse a bare
|
|
125
|
+
`pip install` outside a virtualenv and tell you to use pipx instead.
|
|
125
126
|
|
|
126
127
|
```console
|
|
127
128
|
$ uv tool install subroutine
|
|
128
129
|
$ pipx install subroutine # the same thing, if you have pipx rather than uv
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
`pip install subroutine` is still right *inside* a virtualenv you have activated — embedding it
|
|
133
|
+
in something else, or working on it.
|
|
134
|
+
|
|
135
|
+
PostgreSQL when you outgrow SQLite — the extra goes on whichever you used:
|
|
132
136
|
|
|
133
137
|
```console
|
|
134
138
|
$ uv tool install "subroutine[postgres]"
|
|
@@ -352,6 +356,8 @@ Named plainly, because a tool that overstates itself wastes your afternoon:
|
|
|
352
356
|
- **[docs/errors.md](docs/errors.md)** — every error code the API can return. Generated from
|
|
353
357
|
the registry, so it cannot drift from the code.
|
|
354
358
|
- **`GET /v1/docs/agent`** — the guide an agent should read first, written for that reader.
|
|
359
|
+
- **[SECURITY.md](SECURITY.md)** — how to report a vulnerability privately, and what is in
|
|
360
|
+
scope. Not through an issue: an issue is public from the moment it is filed.
|
|
355
361
|
|
|
356
362
|
The full specification — data model, API, permissions and agent design — is written but
|
|
357
363
|
not yet published. It lands here once the API has settled enough to be worth reading.
|
|
@@ -28,7 +28,7 @@ Three things you might want. Pick one; they compose.
|
|
|
28
28
|
**A to-do list on your own machine.** Nothing to configure.
|
|
29
29
|
|
|
30
30
|
```console
|
|
31
|
-
$
|
|
31
|
+
$ uv tool install subroutine # or: pipx install subroutine
|
|
32
32
|
$ subroutine init
|
|
33
33
|
$ subroutine add "Call the dentist before Sunday"
|
|
34
34
|
$ subroutine today
|
|
@@ -39,7 +39,7 @@ practice — it keeps the backlog, records what it did, and adopts Subroutine in
|
|
|
39
39
|
are already working on. **You never have to learn the CLI.**
|
|
40
40
|
|
|
41
41
|
```console
|
|
42
|
-
$ uv tool install subroutine # or pipx
|
|
42
|
+
$ uv tool install subroutine # or: pipx install subroutine
|
|
43
43
|
$ claude plugin marketplace add simonholliday/subroutine
|
|
44
44
|
$ claude plugin install subroutine@subroutine
|
|
45
45
|
```
|
|
@@ -74,16 +74,20 @@ commands and `subroutine explain dates` covers the ideas behind them.
|
|
|
74
74
|
Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
|
|
75
75
|
start — SQLite is the default and `subroutine init` makes it.
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
**Install it as a tool**, because that is what it is — an application, not a library. It puts
|
|
78
|
+
`subroutine` on your `PATH`, which is what lets an editor or an agent launch it. It is also the
|
|
79
|
+
only thing that works on a current Linux: Debian, Ubuntu and Fedora now refuse a bare
|
|
80
|
+
`pip install` outside a virtualenv and tell you to use pipx instead.
|
|
80
81
|
|
|
81
82
|
```console
|
|
82
83
|
$ uv tool install subroutine
|
|
83
84
|
$ pipx install subroutine # the same thing, if you have pipx rather than uv
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
`pip install subroutine` is still right *inside* a virtualenv you have activated — embedding it
|
|
88
|
+
in something else, or working on it.
|
|
89
|
+
|
|
90
|
+
PostgreSQL when you outgrow SQLite — the extra goes on whichever you used:
|
|
87
91
|
|
|
88
92
|
```console
|
|
89
93
|
$ uv tool install "subroutine[postgres]"
|
|
@@ -307,6 +311,8 @@ Named plainly, because a tool that overstates itself wastes your afternoon:
|
|
|
307
311
|
- **[docs/errors.md](docs/errors.md)** — every error code the API can return. Generated from
|
|
308
312
|
the registry, so it cannot drift from the code.
|
|
309
313
|
- **`GET /v1/docs/agent`** — the guide an agent should read first, written for that reader.
|
|
314
|
+
- **[SECURITY.md](SECURITY.md)** — how to report a vulnerability privately, and what is in
|
|
315
|
+
scope. Not through an issue: an issue is public from the moment it is filed.
|
|
310
316
|
|
|
311
317
|
The full specification — data model, API, permissions and agent design — is written but
|
|
312
318
|
not yet published. It lands here once the API has settled enough to be worth reading.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
**Please do not open an issue.** An issue is public from the moment it is filed, which for a
|
|
6
|
+
vulnerability is the one thing that must not happen first.
|
|
7
|
+
|
|
8
|
+
Use **[GitHub's private vulnerability reporting](https://github.com/simonholliday/subroutine/security/advisories/new)**
|
|
9
|
+
— the *Report a vulnerability* button on the Security tab. It is private between you and the
|
|
10
|
+
maintainer, it needs no email address on either side, and it keeps the whole conversation with
|
|
11
|
+
the eventual advisory.
|
|
12
|
+
|
|
13
|
+
If that is not available to you, write to **simon.holliday@protonmail.com** with `Subroutine` in
|
|
14
|
+
the subject.
|
|
15
|
+
|
|
16
|
+
**What helps:** the version (`subroutine --version`), whether it was reached over the HTTP API
|
|
17
|
+
or the CLI, and the smallest sequence that shows the problem. A proof of concept is welcome and
|
|
18
|
+
not required — a clear description of the flaw is worth more than a working exploit.
|
|
19
|
+
|
|
20
|
+
## What to expect
|
|
21
|
+
|
|
22
|
+
Subroutine is maintained by one person, so these are honest intentions rather than a
|
|
23
|
+
service-level agreement:
|
|
24
|
+
|
|
25
|
+
- **An acknowledgement within a few days.** If you have heard nothing in a week, assume it went
|
|
26
|
+
astray and send a reminder.
|
|
27
|
+
- **An assessment, and the reasoning.** If it turns out not to be a vulnerability you will be
|
|
28
|
+
told why, rather than left waiting.
|
|
29
|
+
- **Credit in the advisory** unless you would rather not be named.
|
|
30
|
+
|
|
31
|
+
Please give a reasonable window before disclosing publicly. If a fix is taking longer than you
|
|
32
|
+
think reasonable, say so — a deadline you have stated is easier to work to than one nobody
|
|
33
|
+
mentioned.
|
|
34
|
+
|
|
35
|
+
## What is in scope
|
|
36
|
+
|
|
37
|
+
The code in this repository, and the current release on PyPI.
|
|
38
|
+
|
|
39
|
+
Things that are worth reporting even though they may look like design decisions, because the
|
|
40
|
+
boundary is exactly where mistakes hide:
|
|
41
|
+
|
|
42
|
+
- **A credential doing more than it should.** A token that is wider than the person who issued
|
|
43
|
+
it, or that survives something meant to end it.
|
|
44
|
+
- **Reading or writing across a boundary** — another workspace, a private project, another
|
|
45
|
+
user's items.
|
|
46
|
+
- **Anything that reaches the database or the filesystem through an input** that was not
|
|
47
|
+
supposed to.
|
|
48
|
+
- **A refusal that discloses.** "Forbidden" where the answer should be "not found" tells a
|
|
49
|
+
stranger something exists; the API is written to avoid that and a place it does not is a bug.
|
|
50
|
+
|
|
51
|
+
## What is not
|
|
52
|
+
|
|
53
|
+
- **Anything requiring an account you were given.** A member of a workspace can read that
|
|
54
|
+
workspace; that is the product working.
|
|
55
|
+
- **`serve` bound to a wider interface with `--insecure`.** It refuses without TLS in front of
|
|
56
|
+
it and says why; overriding that is a deliberate act.
|
|
57
|
+
- **A self-hosted instance's own configuration** — file permissions, a database on a shared
|
|
58
|
+
volume, a reverse proxy that terminates TLS and forwards over a network you do not trust.
|
|
59
|
+
- **Dependency advisories with no reachable path here.** Do report one if you can show the path.
|
|
60
|
+
|
|
61
|
+
## Supported versions
|
|
62
|
+
|
|
63
|
+
**The most recent release**, which is the only version that gets a fix. Subroutine is `0.x`: it
|
|
64
|
+
is early, the surface is still moving, and pretending to maintain several lines at once would be
|
|
65
|
+
a promise nobody could keep. Upgrading is `uv tool upgrade subroutine` or `pipx upgrade
|
|
66
|
+
subroutine` — whichever you installed it with — and then `subroutine upgrade` for the database.
|
|
67
|
+
|
|
68
|
+
## One thing this project cannot do for you
|
|
69
|
+
|
|
70
|
+
**Subroutine authenticates with bearer tokens and stores their SHA-256 hashes.** A token is
|
|
71
|
+
shown once, at creation, and is unrecoverable afterwards by design. Nothing here can tell you
|
|
72
|
+
what a token was, and nobody who has the database can either — but anybody who can *read* an
|
|
73
|
+
instance's database can read every item in it. The database is a secret. `docs/hosting.md` says
|
|
74
|
+
where it should live and what should be backing it up.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subroutine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Project management for people and agents, in equal measure. Wires this session to a Subroutine instance and carries the working practice for using it well.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Simon Holliday",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Cut a release — one version, written everywhere it has to appear. Item ``#239``.
|
|
2
|
+
|
|
3
|
+
**The number is a judgement and stays a human's.** Nothing here guesses whether a change is a
|
|
4
|
+
patch or a minor; what it removes is the chance to decide 0.1.3 and then say it in only some of
|
|
5
|
+
the places. ``#234`` took ``pyproject.toml`` out of that list by deriving the package version
|
|
6
|
+
from the tag. Three remain and every one has to agree on a single commit:
|
|
7
|
+
|
|
8
|
+
1. the git tag, which is what the package version is built from;
|
|
9
|
+
2. ``plugin.json``, which Claude Code reads straight from a clone with no build step, so
|
|
10
|
+
nothing can derive it;
|
|
11
|
+
3. the changelog heading, which is how a person finds out what they are upgrading into.
|
|
12
|
+
|
|
13
|
+
**Two of the first three releases needed a corrective commit for exactly this** — 0.1.0's
|
|
14
|
+
changelog still said *Unreleased*, and 0.1.2 was tagged with the manifest reading 0.1.1, which
|
|
15
|
+
CI refused. Neither was carelessness; both were one step in a sequence somebody was holding in
|
|
16
|
+
their head.
|
|
17
|
+
|
|
18
|
+
**It refuses rather than repairs.** A dirty tree, a tag that exists, a version that goes
|
|
19
|
+
backwards and a changelog with nothing unreleased in it are all reasons to stop and let a
|
|
20
|
+
person look, because each means the release is not the thing they think it is.
|
|
21
|
+
|
|
22
|
+
**It never pushes.** Publishing is outward-facing and belongs to whoever owns the repository;
|
|
23
|
+
this stops at a commit and a tag, and prints the two commands that finish the job.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import datetime
|
|
28
|
+
import json
|
|
29
|
+
import pathlib
|
|
30
|
+
import re
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
#: The repository, resolved from this file rather than from the working directory — the script
|
|
35
|
+
#: is run from wherever somebody happens to be standing.
|
|
36
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
37
|
+
|
|
38
|
+
#: The two files that carry a version. The changelog's heading and the plugin's manifest; the
|
|
39
|
+
#: tag is made rather than written, and `pyproject.toml` no longer has one at all (`#234`).
|
|
40
|
+
CHANGELOG = ROOT / "CHANGELOG.md"
|
|
41
|
+
PLUGIN = ROOT / "plugins" / "subroutine" / ".claude-plugin" / "plugin.json"
|
|
42
|
+
|
|
43
|
+
#: What a version may look like. Deliberately narrow: three numbers, optionally a pre-release
|
|
44
|
+
#: suffix. A tag is not the place to discover that somebody typed `v0.1.3` or `0.1` — the first
|
|
45
|
+
#: would produce `vv0.1.3` and the second a version PyPI sorts in a way nobody expects.
|
|
46
|
+
VERSION = re.compile(r"^\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.]+)?$")
|
|
47
|
+
|
|
48
|
+
#: The heading this promotes. **An `Unreleased` section has to already exist**, which is the
|
|
49
|
+
#: point rather than a limitation: it means the changelog was written while the work was fresh,
|
|
50
|
+
#: by whoever did it, instead of at the moment of release by whoever is in a hurry. The
|
|
51
|
+
#: changelog's own preamble already describes the file this way.
|
|
52
|
+
UNRELEASED = re.compile(r"^##\s+Unreleased\b.*$", re.IGNORECASE | re.MULTILINE)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main (argv: list[str] | None = None) -> int:
|
|
56
|
+
"""Write ``version`` into every place a release names itself, then commit and tag it."""
|
|
57
|
+
|
|
58
|
+
parsed = _arguments(argv)
|
|
59
|
+
version = parsed.version
|
|
60
|
+
|
|
61
|
+
if not VERSION.match(version):
|
|
62
|
+
return _refuse(f"{version!r} is not a version. Write it as 1.2.3, with no leading 'v'.")
|
|
63
|
+
|
|
64
|
+
problem = _reasons_to_stop(version)
|
|
65
|
+
|
|
66
|
+
if problem is not None:
|
|
67
|
+
return _refuse(problem)
|
|
68
|
+
|
|
69
|
+
on = parsed.date or datetime.date.today().isoformat()
|
|
70
|
+
changelog = UNRELEASED.sub(f"## {version} — {on}", CHANGELOG.read_text(encoding="utf-8"), 1)
|
|
71
|
+
|
|
72
|
+
if parsed.dry_run:
|
|
73
|
+
print(f"Would release {version} ({on}):")
|
|
74
|
+
print(f" {CHANGELOG.name}: the Unreleased heading becomes '## {version} — {on}'")
|
|
75
|
+
print(f" {PLUGIN.name}: version becomes {version}")
|
|
76
|
+
print(f" commit both, then tag v{version}")
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
CHANGELOG.write_text(changelog, encoding="utf-8")
|
|
81
|
+
_write_plugin_version(version)
|
|
82
|
+
|
|
83
|
+
# **`check_release_notes.py` is deliberately not run here.** It compares this commit's
|
|
84
|
+
# migration head against the head at the most recent tag — which is the same comparison CI
|
|
85
|
+
# makes on every push to main, against the same previous tag. So a missing migration notice
|
|
86
|
+
# is already refused before anybody reaches this script, and running it again would couple
|
|
87
|
+
# two scripts for an answer that has been available since the commit that moved the head.
|
|
88
|
+
_git("add", str(CHANGELOG), str(PLUGIN))
|
|
89
|
+
_git("commit", "-m", f"Release {version}", "-m", f"See CHANGELOG.md for what {version} contains.")
|
|
90
|
+
_git("tag", "-a", f"v{version}", "-m", f"Subroutine {version}")
|
|
91
|
+
|
|
92
|
+
print(f"Committed and tagged v{version}. Nothing has been pushed. To publish:")
|
|
93
|
+
print(" git push")
|
|
94
|
+
print(f" git push origin v{version}")
|
|
95
|
+
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _arguments (argv: list[str] | None) -> argparse.Namespace:
|
|
100
|
+
"""Read the version to cut, and the two options that exist for testing it."""
|
|
101
|
+
|
|
102
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
103
|
+
parser.add_argument("version", help="the release, as 1.2.3, with no leading 'v'")
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--dry-run", action="store_true", help="say what would change and touch nothing"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Passed in rather than always read off the clock, so a test can assert the heading it
|
|
109
|
+
# expects instead of building today's date twice and comparing a program to itself.
|
|
110
|
+
parser.add_argument("--date", help="the date to stamp, as YYYY-MM-DD. Defaults to today.")
|
|
111
|
+
|
|
112
|
+
return parser.parse_args(argv)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _reasons_to_stop (version: str) -> str | None:
|
|
116
|
+
"""Return why this release must not be cut, or ``None`` if there is no reason."""
|
|
117
|
+
|
|
118
|
+
if _git("status", "--porcelain").strip():
|
|
119
|
+
return "the working tree has changes. Commit or stash them, so the tag names a state."
|
|
120
|
+
|
|
121
|
+
if _git("tag", "--list", f"v{version}").strip():
|
|
122
|
+
return f"v{version} already exists. A released version is never re-cut — choose the next one."
|
|
123
|
+
|
|
124
|
+
if not UNRELEASED.search(CHANGELOG.read_text(encoding="utf-8")):
|
|
125
|
+
return (
|
|
126
|
+
f"{CHANGELOG.name} has no '## Unreleased' section, so this release would say "
|
|
127
|
+
f"nothing about itself. Write what changed under one, then cut it."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
latest = _latest_version()
|
|
131
|
+
proposed = _ordered(version)
|
|
132
|
+
previous = _ordered(latest) if latest is not None else None
|
|
133
|
+
|
|
134
|
+
# **Only when both parse as plain numbers.** A pre-release suffix makes ordering a question
|
|
135
|
+
# with more than one defensible answer, and guessing it here would refuse a release
|
|
136
|
+
# somebody meant. Refusing what is unambiguously backwards is the whole value.
|
|
137
|
+
if proposed is not None and previous is not None and proposed <= previous:
|
|
138
|
+
return f"{version} is not ahead of {latest}, the most recent tag."
|
|
139
|
+
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ordered (version: str) -> tuple[int, ...] | None:
|
|
144
|
+
"""Return a version as numbers for comparison, or ``None`` if it is not purely numeric."""
|
|
145
|
+
|
|
146
|
+
parts = version.split(".")
|
|
147
|
+
|
|
148
|
+
if len(parts) != 3 or not all(part.isdigit() for part in parts):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
return tuple(int(part) for part in parts)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _latest_version () -> str | None:
|
|
155
|
+
"""Return the newest release tag with its ``v`` removed, or ``None`` on a repository with none."""
|
|
156
|
+
|
|
157
|
+
found = subprocess.run(
|
|
158
|
+
["git", "describe", "--tags", "--abbrev=0", "--match", "v*"],
|
|
159
|
+
cwd=ROOT, capture_output=True, text=True, check=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if found.returncode != 0 or not found.stdout.strip():
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
return found.stdout.strip().removeprefix("v")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write_plugin_version (version: str) -> None:
|
|
169
|
+
"""Set the plugin manifest's version, leaving every other key and the formatting alone.
|
|
170
|
+
|
|
171
|
+
Rewritten through ``json`` rather than by pattern, because a manifest that stops being
|
|
172
|
+
valid JSON is one nobody can install and the failure arrives at a stranger.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
manifest = json.loads(PLUGIN.read_text(encoding="utf-8"))
|
|
176
|
+
manifest["version"] = version
|
|
177
|
+
|
|
178
|
+
PLUGIN.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _git (*arguments: str) -> str:
|
|
182
|
+
"""Run one git command in the repository and return its output, raising if it fails."""
|
|
183
|
+
|
|
184
|
+
return subprocess.run(
|
|
185
|
+
["git", *arguments], cwd=ROOT, capture_output=True, text=True, check=True
|
|
186
|
+
).stdout
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _refuse (message: str) -> int:
|
|
190
|
+
"""Say why the release is not being cut, and exit non-zero."""
|
|
191
|
+
|
|
192
|
+
print(f"Not releasing: {message}", file=sys.stderr)
|
|
193
|
+
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
raise SystemExit(main())
|