Skip to content

feat: self-register TPA budget group sync into SOCIAL_AUTH_PIPELINE - #197

Merged
pwnage101 merged 1 commit into
openedx:mainfrom
jono-booth:jb/register-tpa-pipeline-step
Aug 31, 2026
Merged

pwnage101 merged 1 commit into
openedx:mainfrom
jono-booth:jb/register-tpa-pipeline-step

Conversation

@jono-booth

Copy link
Copy Markdown
Contributor

Description

The edx-platform hook this repo relied on for inserting extra SOCIAL_AUTH_PIPELINE steps (insert_enterprise_pipeline_elements) has been removed upstream. Enterprise steps are now inserted by edx-enterprise's own entry_points-based plugin_settings(), which only inserts its own step and has no extension point for a sibling plugin. This repo isn't registered as an entry_points plugin either, so there's no external hook left to use.

This PR closes the gap from inside this repo: IntegratedChannelConfig.ready() now calls a new register_pipeline_step(), which inserts pipeline.sync_tpa_budget_group into settings.SOCIAL_AUTH_PIPELINE directly, right after handle_enterprise_logistration. This is safe because Django fully executes all settings modules - including edx-enterprise's plugin_settings() - before calling ready(), so that step is guaranteed to already be present. Only mutates an in-memory list, no DB access, so it's safe even before migrations run.

Safety properties (unchanged)

Idempotent, and never raises: no-ops if SOCIAL_AUTH_PIPELINE is undefined, our step is already registered, or handle_enterprise_logistration is missing. The actual sync logic is unchanged - still behind the enable_tpa_org_group_login_sync waffle switch (default off), still exception-isolated. Only how the step gets registered changed, not what it does.

Testing instructions

New tests in test_pipeline.py::TestRegisterPipelineStep cover insertion position, idempotency, both no-op cases, and exception safety. Full test_integrated_channel suite (352 tests) passes; no migrations required.

Merge checklist:
Check off if complete or not applicable:

  • Version bumped
  • Changelog record added
  • Documentation updated (not only docstrings)
  • Fixup commits are squashed away
  • Unit tests added/updated
  • Manual testing instructions provided
  • Noted any: Concerns, dependencies, migration issues, deadlines, tickets

Copilot AI lite review requested due to automatic review settings August 24, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR registers the TPA budget-group sync step during Django app startup, replacing the removed upstream pipeline extension hook.

Changes:

  • Adds idempotent pipeline-step registration after Enterprise logistration.
  • Invokes registration from AppConfig.ready().
  • Adds tests, bumps the package version, and updates the changelog.

Review finding: pipeline.py catches unexpected registration errors and only logs a warning, potentially silently disabling synchronization. Registration failures should be surfaced or narrowly handled.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Summary
tests/test_channel_integrations/test_integrated_channel/test_pipeline.py Tests registration behavior, ordering, idempotency, no-op cases, and exception handling.
channel_integrations/integrated_channel/pipeline.py Adds pipeline registration logic.
channel_integrations/integrated_channel/apps.py Registers the step during app startup.
channel_integrations/__init__.py Bumps the package version.
CHANGELOG.rst Documents the feature and release change.
Suppressed comments (1)

channel_integrations/integrated_channel/apps.py:27

  • The added tests call register_pipeline_step() directly but never exercise the new IntegratedChannelConfig.ready() wiring. test_settings.py does not define SOCIAL_AUTH_PIPELINE, so Django startup takes the no-op path in this suite; a regression removing this call would still leave all of TestRegisterPipelineStep green while the sync is absent from real logins. Add an AppConfig-level or startup-level test that verifies ready() registers the step after the enterprise pipeline is present.
        register_pipeline_step()

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread channel_integrations/integrated_channel/pipeline.py Outdated
Comment on lines +4 to +8
`sync_tpa_budget_group` needs to run as a SOCIAL_AUTH_PIPELINE step, right after
`enterprise.tpa_pipeline.handle_enterprise_logistration`. Runs on every SSO login on the platform,
not just Skillsoft's, so this module carries the outermost safety layer: a waffle switch (default off)
and a broad try/except that never raises. See the "Development spec" in the ENT-12084 plan for the
rollout sequencing this switch is used for.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I feel like this entire section is noise. It's implementation details, hard-coded module references, and rollout details that don't really help future engineers. It's also almost completely redundant with the step's docstring. Delete the whole paragraph.

Suggested change
`sync_tpa_budget_group` needs to run as a SOCIAL_AUTH_PIPELINE step, right after
`enterprise.tpa_pipeline.handle_enterprise_logistration`. Runs on every SSO login on the platform,
not just Skillsoft's, so this module carries the outermost safety layer: a waffle switch (default off)
and a broad try/except that never raises. See the "Development spec" in the ENT-12084 plan for the
rollout sequencing this switch is used for.

Comment on lines +57 to +65
def register_pipeline_step():
"""
Insert `sync_tpa_budget_group` into settings.SOCIAL_AUTH_PIPELINE, right after
`handle_enterprise_logistration`, if it isn't there already.

This only ever mutates a plain in-memory settings list - no database access, no waffle checks
- so it's safe to run this early, including during management commands that run before
migrations (e.g. `migrate` itself).
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The naming/docs of this function is not scalable and is potentially ambiguous. Just generalize it:

Suggested change
def register_pipeline_step():
"""
Insert `sync_tpa_budget_group` into settings.SOCIAL_AUTH_PIPELINE, right after
`handle_enterprise_logistration`, if it isn't there already.
This only ever mutates a plain in-memory settings list - no database access, no waffle checks
- so it's safe to run this early, including during management commands that run before
migrations (e.g. `migrate` itself).
"""
def register_pipeline_steps():
"""
Register all `channel_integrations` pipeline steps.
"""

ENABLE_TPA_ORG_GROUP_LOGIN_SYNC_SWITCH = 'enable_tpa_org_group_login_sync'

PIPELINE_STEP_PATH = 'channel_integrations.integrated_channel.pipeline.sync_tpa_budget_group'
ENTERPRISE_LOGISTRATION_STEP_PATH = 'enterprise.tpa_pipeline.handle_enterprise_logistration'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ This cross-dependency to another plugin should be done carefully. Soon I will remove edx-enterprise from edx-platform base requirements, and this injection will begin to fail on non-2U platform environments.

You probably should key off a non-enterprise step instead, if possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync_tpa_budget_group depends on the EnterpriseCustomerUser link that handle_enterprise_logistration creates. Moved the import to inside sync_tpa_budget_group so that importing this module doesn't require enterprise to be installed.

Comment thread CHANGELOG.rst Outdated
Comment on lines +20 to +26
* feat: self-register ``pipeline.sync_tpa_budget_group`` into ``SOCIAL_AUTH_PIPELINE`` from
``IntegratedChannelConfig.ready()``, right after ``enterprise.tpa_pipeline.handle_enterprise_logistration``.
edx-platform no longer exposes a generic extension point for enterprise pipeline steps (that
moved into edx-enterprise's own entry_points-based plugin_settings, which only inserts its own
step) and this repo isn't registered as an entry_points plugin itself, so there's no external
hook left to append to - this makes installing the package enough to wire up the login-time
sync, no edx-platform change required.

@pwnage101 pwnage101 Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Excessive noise in this message, and it doesn't even explain what the new pipeline step actually does. Something like this would be better:

Suggested change
* feat: self-register ``pipeline.sync_tpa_budget_group`` into ``SOCIAL_AUTH_PIPELINE`` from
``IntegratedChannelConfig.ready()``, right after ``enterprise.tpa_pipeline.handle_enterprise_logistration``.
edx-platform no longer exposes a generic extension point for enterprise pipeline steps (that
moved into edx-enterprise's own entry_points-based plugin_settings, which only inserts its own
step) and this repo isn't registered as an entry_points plugin itself, so there's no external
hook left to append to - this makes installing the package enough to wire up the login-time
sync, no edx-platform change required.
* feat: Register the new ``sync_tpa_budget_group`` pipeline step which syncs the logging-in
learner's EnterpriseGroup membership to the Learner Credit budget group mapped to the
EnterpriseCustomer for this active pipeline.

Comment on lines +18 to +19
Register signal handlers, configure structured logging, and register the TPA org budget
group sync step in SOCIAL_AUTH_PIPELINE.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Register signal handlers, configure structured logging, and register the TPA org budget
group sync step in SOCIAL_AUTH_PIPELINE.
Register signal handlers, configure structured logging, and register social-auth pipeline steps.

@@ -1,11 +1,11 @@
"""
Third-party-auth pipeline entry point for TPA org allowlist -> Learner Credit budget group sync.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Third-party-auth pipeline entry point for TPA org allowlist -> Learner Credit budget group sync.
Third-party-auth pipeline steps.

@pwnage101 pwnage101 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done reviewing, just some minor docs issues and one legitimate dependency on the enterprise plugin which we soon won't tolerate.

@jono-booth
jono-booth force-pushed the jb/register-tpa-pipeline-step branch from 2c5ae4e to 55fb531 Compare August 28, 2026 04:53
@jono-booth
jono-booth force-pushed the jb/register-tpa-pipeline-step branch from 55fb531 to 9c9142e Compare August 31, 2026 18:16
@pwnage101
pwnage101 merged commit 52a51dc into openedx:main Aug 31, 2026
8 checks passed
irfanuddinahmad added a commit that referenced this pull request Sep 7, 2026
The CHANGELOG.rst restoration commit recreated the file from a fixed
snapshot frozen at 0.1.62. Rebasing onto current main (now at 0.1.69)
exposed that the restoration doesn't track main's ongoing history --
it's a one-time static blob, not a merge. Replaced with main's actual
current content, restoring the 7 missing entries (0.1.63-0.1.69)
between Unreleased and 0.1.62, including PR #197's entry that
collided with this rebase.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
irfanuddinahmad added a commit that referenced this pull request Oct 12, 2026
…e) (#180)

* feat: consolidate package metadata into pyproject.toml

Replace setup.py/setup.cfg with PEP 621 [project] metadata and
setuptools-scm for git-tag-based versioning.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* feat: switch dependency management from pip-compile to uv

Replace requirements/*.in + *.txt with PEP 735 dependency-groups in
pyproject.toml and a single uv.lock. Update tox.ini to use tox-uv's
uv-venv-lock-runner, update Makefile targets, and switch CI to install
uv and run tests via `uv run tox`. Point .readthedocs.yaml at uv too,
since it referenced the now-deleted requirements/doc.txt.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* feat: add semantic-release for automated PyPI publishing

Replace the manual GitHub-release-triggered publish workflow with
python-semantic-release: pushes to main with conventional commits
now automatically bump the version, tag it, and publish to PyPI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: correct release tag format and drop stale MANIFEST.in entries

semantic-release defaults to a "v{version}" tag format, but this repo's
existing release tags are bare version numbers -- without tag_format set,
semantic-release wouldn't recognize any prior release. Also removes
MANIFEST.in references to requirements/base.in and requirements/constraints.txt,
which no longer exist after the uv migration.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: prevent uv sync from implicitly pulling in the dev group

Every uv sync/uv run invocation in this repo names an explicit --group,
but uv's implicit default group (named "dev") was still being synced
alongside it, silently pulling the entire dev/test/quality/ci superset
into every target. Verified with `uv sync --group ci`.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: publish to PyPI via OIDC instead of token

Parent issue openedx/public-engineering#506 asks for OIDC trusted-publisher
PyPI auth, not a stored token. Grant id-token: write on publish_to_pypi and
drop the explicit __token__/PYPI_UPLOAD_TOKEN credentials -- pypa/gh-action-pypi-publish
uses OIDC automatically once the permission is present and no credentials are given.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: prevent premature 1.0 bump and pin pypa/gh-action-pypi-publish

- major_on_zero defaults to True in python-semantic-release, which would
  auto-bump this still-0.x repo straight to 1.0.0 on the first breaking-change
  commit. 1.0.0 is supposed to be a deliberate "first stable release" signal,
  not an accidental side effect -- keep 0.x bumps within 0.x until that's an
  intentional decision (allow_zero_version stays true so 0.x remains valid).
- @release/v1 is a floating branch ref -- xblocks-core's release just failed
  with "docker: manifest unknown" because the Docker image tag it resolved to
  at checkout time wasn't published on ghcr.io yet. Pin to the exact commit
  backing the current v1.14.0 release instead.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* refactor: adopt src-layout, matching openedx/sample-plugin

Moves channel_integrations/ to src/channel_integrations/, in line with
the reference implementation for this modernization effort
(openedx/sample-plugin) and openedx/forum#281.

- pyproject.toml: add where = ["src"] to packages.find
- tox.ini: prefix src/ onto pylint/pycodestyle targets in the quality env
- Makefile: prefix src/ onto all 5 localization targets (extract,
  compile, detect_changed, pull, dummy); atlas pull's local destination
  path updated, remote Transifex identifier left unchanged
- test_settings.py: LOCALE_PATHS root() call updated
- docs/conf.py: sphinx-apidoc call updated to point at src/channel_integrations
- MANIFEST.in: recursive-include path updated

Verified: uv sync (no lock drift), full django42 test run (893 passed),
quality and pii_check envs pass, uv build --wheel + twine check pass.

The docs tox env fails both before and after this change (a pre-existing
sphinx/autodoc ValueError in object_description, unrelated to src-layout
and unrelated to this repo's file layout) -- confirmed by reproducing the
identical failure on the pre-migration commit via git stash.

* fix: pattern-gap audit (uv setup, package=true, coverage, changelog)

Same category of fixes farhan flagged on the batch's other reviewed PRs:

- ci.yml: add enable-cache/python-version to astral-sh/setup-uv and drop
  the now-redundant actions/setup-python step
- pyproject.toml: add [tool.uv] package = true
- Drop CHANGELOG.rst from the dynamic readme file list and delete it --
  python-semantic-release + GitHub Releases is the changelog of record.
  Also removes the now-stale MANIFEST.in include and the docs/changelog.rst
  page (and its toctree entry).
- Migrate .coveragerc into [tool.coverage.run] and delete the old file

Verified: uv sync (no lock drift), full django42 test run (893 passed),
quality env passes. docs env still fails on the same pre-existing
sphinx/autodoc ValueError already documented in this repo's src-layout
commit (confirmed via git stash at that time it predates and is
unrelated to any of our changes) -- docs isn't part of the CI matrix.

* fix: pattern-gap audit round 2 (uv run wrapping, dynamic readme, tox envlist, readthedocs os)

Cross-checked against review comments/fixes from openedx-ledger#242,
edx-enterprise-subsidy-client#222, enterprise-access#1015, and
enterprise-subsidy#441.

- Wrap remaining bare tool invocations in the Makefile (coverage erase,
  pytest --cov-report html, PYTHONPATH pytest, diff-cover, i18n_tool,
  manage.py compilemessages) with `uv run`. Transifex's tx binary is left
  bare since it's installed via curl, not uv/pip.
- Drop the redundant dynamic readme field; keep only version as dynamic.
- Match the py{311}-django{42,52} brace convention in the tox envlist.
- Bump Read the Docs build image to ubuntu-26.04.

* fix: correct Makefile recipe indentation (tabs, not spaces)

Make requires recipe lines to be indented with a literal tab
character. An earlier edit in this migration accidentally used
spaces for several targets and dropped the ifeq(OPENEDX_ATLAS_PULL)
conditional line, breaking 'make selfcheck' and CI.

* fix: restore CHANGELOG.rst and re-enable changelog generation

A previous migration pass deleted CHANGELOG.rst and disabled
python-semantic-release's changelog generation (changelog: "false" in
release.yml). Neither was required by the tooling-modernization ticket,
and deleting the file discarded real historical release notes.

- Restore CHANGELOG.rst from the commit before it was deleted
- Add the .. changelog-insertion-marker as the first line so PSR's
  "update" mode inserts new release notes above existing history
- Remove changelog: "false" from release.yml's PSR step
- Add [tool.semantic_release.changelog] config to pyproject.toml,
  pointing PSR at the restored CHANGELOG.rst in RST format
- tag_format was already correctly set to "{version}" matching this
  repo's bare version tags (e.g. 0.1.65), no change needed there

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* docs: remove stale version-bump/changelog checklist items from PR template

python-semantic-release now handles versioning and changelog generation
automatically, so asking PR authors to manually bump the version or add
a changelog record is obsolete and was left over from before that
automation was wired up.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: use stable pypa publish tag and drop tox from Makefile

- pypa/gh-action-pypi-publish: revert the hash-pinned SHA back to the
  stable @release/v1 tag. A hash-pinned version of this action broke
  PyPI publishing previously, which is why the org standardized on the
  stable tag for this specific action.
- Makefile: inline the actual doc8/sphinx/pylint/pycodestyle/
  code_annotations commands from tox.ini's docs/quality/pii_check envs
  instead of shelling out to `uv run tox -e ...`, matching the
  no-tox-in-Makefile convention already used elsewhere (e.g.
  enterprise-catalog). test-all now depends on quality/pii_check/test/
  docs directly. tox.ini itself is untouched; CI's own matrix testing
  still uses tox directly.

* fix: use int-parseable fallback_version instead of 0.0.0.dev0

"0.0.0.dev0" is the exact fallback_version value that crashed 17 tests
in a sibling repo (openedx-events) -- runtime code that parses
__version__ via tuple(map(int, __version__.split("."))) chokes on the
non-numeric "dev0" segment. No current consumer here does that (this
repo's own __version__ usage, if any, only interpolates it as a
string), but there's no reason to keep a fallback value from the exact
banned-pattern class when a plain int-parseable "0.0.0" is equally
valid and strictly safer.

* fix: disable python-semantic-release changelog generation

Set changelog: "false" on the PSR release step and remove the
[tool.semantic_release.changelog] config / insertion marker.

Checked against openedx/XBlock's actual production release.yml (the one
repo in this effort that has cut real automated releases) -- every run
passes changelog: false and invokes `semantic-release -v version
--no-changelog`, and the repo has zero github-actions[bot] commits ever.
The auto-changelog config this migration previously added was only ever
verified via a local dry-run prototype, never against a real release.

feanil flagged the same issue on openedx/DoneXBlock#388: "I thought we
were not going to add changelogs since they can't be updated by
python-semantic-release the way we have it setup."

* fix: create GitHub release with assets attached, not after publish

This repo has immutable releases enabled, which freezes a release's assets the moment it's published. The old flow (main PSR step publishes the release, a separate publish-action step attaches assets afterward) can never work under that constraint -- it would 422 on the first real release. Matches the fix already proven and merged on openedx/sample-plugin#57 and validated end-to-end on openedx/event-tracking#434: build without publishing (vcs_release: false), then create the release with dist/* attached in one gh release create call.

* fix: conform release.yml action pins to sample-plugin gold standard

release.yml's actions/checkout, python-semantic-release, upload-artifact,
and download-artifact pins were all stale relative to
openedx/sample-plugin's release.yml, this org's designated reference for
this file. Also re-pins pypa/gh-action-pypi-publish to a SHA
(dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2), reversing an earlier
revert to @release/v1 that was made on this PR at salman2013's request
after a prior SHA-pin caused issues. That guidance has since been
superseded: salman2013 has since directed (on a sibling repo's PR) to
match sample-plugin exactly, which now itself SHA-pins
gh-action-pypi-publish at v1.14.2.

Every pin below was independently verified against the GitHub API before
use (commit resolves, and matches its claimed tag, dereferencing through
the tag object for annotated tags):
- actions/checkout -> 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- python-semantic-release/python-semantic-release ->
  9a026e9303981c866c3425723009becb2437c757 # v10.6.2
- actions/upload-artifact -> 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- actions/download-artifact -> 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- pypa/gh-action-pypi-publish -> dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: reconcile CHANGELOG.rst with main after rebase

The CHANGELOG.rst restoration commit recreated the file from a fixed
snapshot frozen at 0.1.62. Rebasing onto current main (now at 0.1.69)
exposed that the restoration doesn't track main's ongoing history --
it's a one-time static blob, not a merge. Replaced with main's actual
current content, restoring the 7 missing entries (0.1.63-0.1.69)
between Unreleased and 0.1.62, including PR #197's entry that
collided with this rebase.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: use v-prefixed tag_format for release tag consistency

* fix: use secrets.GITHUB_TOKEN instead of custom org secret for release job

Matches openedx/sample-plugin's current release.yml; the custom
OPENEDX_SEMANTIC_RELEASE_GITHUB_TOKEN secret isn't needed.

* fix: restore tox delegation for docs/quality/pii_check targets

Makefile targets were fully inlining commands that tox.ini already
defines as standalone envs (docs, quality, pii_check), restoring the
pre-migration one-liner tox -e <env> delegation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: drop uv run from Makefile per feanil's review feedback

Makefile now assumes an already-synced/activated local env; uv run
stays in CI workflow steps only. See openedx/ccx-keys#190 (review
comment r4097501942).

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: bump python-semantic-release pin to v10.7.0, add doc group to dev, add signxml dep

- Bump python-semantic-release action pin from v10.6.2 to v10.7.0
  (b700dbeb1f431a0fcc30a79f3f0869407fe07278), matching openedx/sample-plugin's
  current live pin.
- Fold {include-group = "doc"} into the dev dependency-group so `make
  requirements` (uv sync --group dev) provisions doc8/Sphinx, matching the
  pattern already correct in openedx/edx-enterprise's pyproject.toml.
- Add signxml (pulled in transitively via the SAML assertion generator added
  on main in #201/#206) to [project.dependencies], since this migration
  replaced requirements/base.in with pyproject.toml as the source of truth
  for runtime dependencies and base.in's new direct dependency needs an
  equivalent entry there. Re-lock uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

---------

Co-authored-by: Irfan Ahmad <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants