Skip to content

RHINENG-25147: refactor workspaces#2176

Open
Dugowitch wants to merge 10 commits into
RedHatInsights:masterfrom
Dugowitch:workspaces
Open

RHINENG-25147: refactor workspaces#2176
Dugowitch wants to merge 10 commits into
RedHatInsights:masterfrom
Dugowitch:workspaces

Conversation

@Dugowitch
Copy link
Copy Markdown
Contributor

@Dugowitch Dugowitch commented Apr 28, 2026

Secure Coding Practices Checklist GitHub Link

Secure Coding Checklist

  • Input Validation
  • Output Encoding
  • Authentication and Password Management
  • Session Management
  • Access Control
  • Cryptographic Practices
  • Error Handling and Logging
  • Data Protection
  • Communication Security
  • System Configuration
  • Database Security
  • File Management
  • Memory Management
  • General Coding Practices

Summary by Sourcery

Refactor workspace handling across the service to replace JSONB-based group/workspace storage with explicit workspace_id and workspace_name columns, and propagate the new workspace-based filtering through queries, middleware, models, and exports.

Enhancements:

  • Simplify workspace representation in system_inventory by introducing dedicated workspace_id and workspace_name columns and indexing them.
  • Update database queries, controllers, and middleware to use workspace ID lists instead of inventory group JSON for scoping systems, advisories, packages, and templates.
  • Adjust system upload and model mapping to persist a single validated workspace per host and log anomalies like multiple workspaces.
  • Extend API response and export payloads to include explicit workspace identifiers and names alongside existing group data.
  • Revise tests and test fixtures to align with the new workspace schema and selection semantics, including updated test data and helper context setup.

Build:

  • Bump schema migration version and add a migration that backfills workspace_id and workspace_name from existing JSONB workspaces before dropping the old column.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Sorry @Dugowitch, your pull request is larger than the review limit of 150000 diff characters

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 82.30769% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.06%. Comparing base (6d0becb) to head (19291c2).
⚠️ Report is 30 commits behind head on master.

Files with missing lines Patch % Lines
base/database/utils.go 18.18% 8 Missing and 1 partial ⚠️
listener/upload.go 60.00% 6 Missing and 2 partials ⚠️
manager/controllers/system_detail.go 50.00% 2 Missing ⚠️
manager/controllers/template_systems.go 71.42% 2 Missing ⚠️
manager/middlewares/kessel.go 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2176      +/-   ##
==========================================
- Coverage   59.13%   59.06%   -0.07%     
==========================================
  Files         134      136       +2     
  Lines        8738     8761      +23     
==========================================
+ Hits         5167     5175       +8     
- Misses       3028     3040      +12     
- Partials      543      546       +3     
Flag Coverage Δ
unittests 59.06% <82.30%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread database_admin/migrations/153_simplify_workspaces.up.sql Outdated
Comment thread manager/controllers/common_attributes.go Outdated
Comment thread base/models/models.go Outdated
Comment thread manager/controllers/advisory_systems.go
@MichaelMraka MichaelMraka self-assigned this May 4, 2026
@Dugowitch Dugowitch marked this pull request as ready for review May 7, 2026 12:28
@Dugowitch Dugowitch requested a review from a team as a code owner May 7, 2026 12:28
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 7, 2026

Reviewer's Guide

Refactors workspace handling across the service by replacing JSONB-based workspaces groups with explicit workspace_id/workspace_name columns, updating all query helpers, controllers, middleware, schema, migrations, test data, and tests to use workspace IDs (and names) instead of inventory-group JSON, and wiring workspace information consistently from uploads through RBAC/Kessel to API responses and exports.

File-Level Changes

Change Details Files
Replace JSONB workspaces field with scalar workspace_id/workspace_name in schema, models, data access layer, and test data.
  • Update system_inventory schema to drop workspaces JSONB and add workspace_id UUID and workspace_name TEXT with indexes.
  • Add migration 153 to backfill workspace_id/workspace_name from existing workspaces JSONB and then drop the old column.
  • Adjust SystemInventory model struct to expose WorkspaceID and WorkspaceName instead of Workspaces, and update related JSON/query tags for workspace-based selection and ordering.
  • Rewrite dev test_data.sql seed rows to populate workspace_id and workspace_name columns instead of workspaces JSON arrays.
database_admin/schema/create_schema.sql
database_admin/migrations/153_simplify_workspaces.up.sql
base/models/models.go
dev/test_data.sql
Refactor workspace filtering and system/advisory/package queries to operate on workspace IDs instead of group JSON.
  • Change Systems, SystemAdvisories, SystemPackages, and SystemAdvisoriesByInventoryID helpers to take []string workspaceIDs and delegate to a simplified ApplyInventoryWorkspaceFilter that filters by si.workspace_id IN (?).
  • Simplify ApplyInventoryWorkspaceFilter to drop grouped/ungrouped semantics and log a warning when called with an empty workspace slice.
  • Update all controller query builders (systems, advisories, packages, package versions, system advisories, system packages, templates, template systems, systems-advisories view, template systems update/delete/subscribed-systems, system tags, exports) to accept/read workspace ID slices from Gin context instead of inventory group maps.
  • Adjust caching decisions (shouldUseCache, advisory cached counts) to use workspace-based inputs, with TODOs noting conditions that are now always true.
  • Update group-name filter construction to filter on workspace_name rather than JSONB workspaces content.
base/database/utils.go
manager/controllers/systems.go
manager/controllers/advisories.go
manager/controllers/advisories_export.go
manager/controllers/advisory_systems.go
manager/controllers/advisory_systems_export.go
manager/controllers/system_advisories.go
manager/controllers/system_advisories_export.go
manager/controllers/system_detail.go
manager/controllers/system_packages.go
manager/controllers/system_packages_export.go
manager/controllers/packages.go
manager/controllers/packages_export.go
manager/controllers/package_versions.go
manager/controllers/package_systems.go
manager/controllers/package_systems_export.go
manager/controllers/systems_advisories_view.go
manager/controllers/template_systems.go
manager/controllers/template_systems_update.go
manager/controllers/template_systems_delete.go
manager/controllers/template_subscribed_systems_update.go
manager/controllers/template_systems_export.go
manager/controllers/templates.go
manager/controllers/systemtags.go
manager/controllers/utils.go
Change how workspaces are propagated from RBAC/Kessel and uploads into requests and persisted systems, ensuring single-workspace semantics per host.
  • Replace KeyInventoryGroups/group map usage with KeyInventoryWorkspaces and a []string of workspace IDs in Gin context and testing helpers.
  • Remove Kessel processWorkspaces JSON-group builder; instead, collect workspace IDs directly from Kessel responses, validate non-empty, and place them under KeyInventoryWorkspaces.
  • Keep RBAC inventory-group discovery but store it under local middleware constants (KeyInventoryGroups, KeyGrouped, KeyUngrouped) to decouple from the new workspace key used elsewhere.
  • In upload pipeline, stop storing Groups as JSONB workspaces; instead, validate and parse the first host.Groups entry into workspace_id (UUID) and workspace_name, log and error on invalid UUIDs, and log a warning if multiple workspaces are received.
  • Ensure SystemPlatformV2 upsert paths write WorkspaceID and WorkspaceName and update their column update lists accordingly.
base/utils/gin.go
base/core/gintesting.go
manager/middlewares/kessel.go
manager/middlewares/rbac.go
listener/upload.go
listener/upload_test.go
listener/common_test.go
manager/middlewares/kessel_test.go
Expose workspace information in API DTOs and CSV exports while preserving existing groups JSON for backward compatibility.
  • Introduce SystemWorkspace struct with workspace_id and workspace_name fields and embed it where system attributes are exposed (systems, template systems, advisory systems, package systems).
  • Change SystemGroups query spec to synthesize groups JSON from workspace_id/workspace_name via jsonb_build_array(jsonb_build_object(...)), keeping the previous groups representation as a derived value.
  • Extend CSV headers and expectations in systems, template systems, advisory systems, and package systems export tests to include workspace ID and name columns alongside the groups field, and adjust line expectations to use UUID-based IDs instead of legacy inventory-group names.
  • Update tests that use group-name filters to use workspace IDs and new expectations for counts and order.
manager/controllers/common_attributes.go
manager/controllers/systems_export.go
manager/controllers/systems_export_test.go
manager/controllers/template_systems.go
manager/controllers/template_systems_export.go
manager/controllers/advisory_systems.go
manager/controllers/advisory_systems_export.go
manager/controllers/package_systems.go
manager/controllers/package_systems_export.go
manager/controllers/utils_test.go
Remove obsolete JSONB workspace helper types and update tests to the new workspace model and data.
  • Delete the inventory.Groups JSONB valuer/scanner type now that system_inventory no longer stores workspaces as JSONB.
  • Adjust listener and controller tests to assert on WorkspaceID/WorkspaceName instead of Workspaces, including creation helpers (e.g., createTestUploadEvent) and store/update assertions.
  • Update workspace filter tests in utils_test.go and systems_advisories_view_test.go to use workspace IDs, new expectations for grouped/ungrouped counts, and the new test router context default for workspaces.
  • Drop now-unused Kessel processWorkspaces tests and update Kessel middleware permission tests to read KeyInventoryWorkspaces and assert workspace IDs instead of grouped JSON.
base/inventory/inventory.go
listener/common_test.go
listener/upload_test.go
base/database/utils_test.go
manager/middlewares/kessel_test.go
manager/controllers/template_systems_export_test.go
manager/controllers/advisory_systems_export_test.go
manager/controllers/package_systems_export_test.go
manager/controllers/systems_advisories_view_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • ApplyInventoryWorkspaceFilter now always applies WHERE si.workspace_id IN (?) even when workspaceIDs is empty (and only logs a warning); consider short‑circuiting and returning the unmodified tx (or an always‑false predicate) to avoid unexpected SQL/behavior on empty slices.
  • RBAC still computes and stores inventory groups under its own KeyInventoryGroups, but the rest of the code now reads workspace IDs from utils.KeyInventoryWorkspaces and ApplyInventoryWorkspaceFilter no longer accepts groups; you should either adapt RBAC to emit workspace IDs or add a translation layer, otherwise RBAC group restrictions are effectively ignored in queries.
  • The TODOs around caching conditions (e.g., advisoriesCommon and AdvisoriesExportHandler) indicate the cache path is now always disabled because there is always at least the root workspace; it would be good to either remove the dead cache branch or update the conditions to make caching usable again under the new workspace model.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- ApplyInventoryWorkspaceFilter now always applies `WHERE si.workspace_id IN (?)` even when `workspaceIDs` is empty (and only logs a warning); consider short‑circuiting and returning the unmodified tx (or an always‑false predicate) to avoid unexpected SQL/behavior on empty slices.
- RBAC still computes and stores inventory groups under its own KeyInventoryGroups, but the rest of the code now reads workspace IDs from utils.KeyInventoryWorkspaces and ApplyInventoryWorkspaceFilter no longer accepts groups; you should either adapt RBAC to emit workspace IDs or add a translation layer, otherwise RBAC group restrictions are effectively ignored in queries.
- The TODOs around caching conditions (e.g., advisoriesCommon and AdvisoriesExportHandler) indicate the cache path is now always disabled because there is always at least the root workspace; it would be good to either remove the dead cache branch or update the conditions to make caching usable again under the new workspace model.

## Individual Comments

### Comment 1
<location path="listener/upload.go" line_range="363-372" />
<code_context>

 	updatesReqJSONString := string(updatesReqJSON)
-	hostWorkspaces := inventory.Groups(host.Groups)
+	var workspaceID uuid.UUID
+	var workspaceName *string
+	if l := len(host.Groups); l >= 1 {
+		if idString := host.Groups[0].ID; idString != "" {
+			workspaceID, err = uuid.Parse(idString)
+			if err != nil {
+				utils.LogError("workspaceID", idString, "invalid workspace UUID")
+				return nil, errors.New("received invalid workspace UUID")
+			}
+		}
+		if host.Groups[0].Name != "" {
+			workspaceName = &host.Groups[0].Name
+		}
+		if l != 1 {
+			utils.LogWarn(
+				"host_id", host.ID, "org_id", host.OrgID, "workspaces", host.Groups,
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid persisting a zero UUID when no or invalid workspace ID is present

Because `workspaceID` is always initialized and passed by address, `SystemInventory` will always see a non-nil pointer:

- With no groups, you persist `00000000-0000-0000-0000-000000000000` instead of `NULL`.
- With an empty `host.Groups[0].ID`, you silently use the zero UUID, which is indistinguishable in the DB from a real value and doesn’t clearly mean “no workspace”.

Instead, either:
- Declare `var workspaceID *uuid.UUID` and only set it on successful parse, or
- Keep `workspaceID uuid.UUID` but only set `WorkspaceID` to a non-nil pointer when a valid ID was parsed.

This preserves the intended `NULL` semantics for missing/unknown workspaces and avoids treating the zero UUID as a valid value.
</issue_to_address>

### Comment 2
<location path="manager/middlewares/kessel_test.go" line_range="112" />
<code_context>
-	inventoryGroups, found := c.Get(utils.KeyInventoryGroups)
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a negative test for missing or empty workspaces in hasPermissionKessel

The updated `hasPermissionKessel` now aborts with `http.StatusUnauthorized` when the collected workspace IDs slice is empty, but `TestHasPermissionKessel` only covers the success path. Please add a test where the `workspaces` stream is empty (or decoded to an empty slice) and assert that the handler returns 401 and does not set `utils.KeyInventoryWorkspaces` to ensure this failure path is covered and guarded against regressions.

Suggested implementation:

```golang
func TestBuildPermission(t *testing.T) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = &http.Request{Method: http.MethodGet, Header: http.Header{}}

	permission := buildPermission(c)
	require.NotNil(t, permission)

	c.Request.Header.Set("x-rh-identity", "ewogICAgImVudGl0bGVtZW50cyI6IHsKICAgICAgICAiaW5zaWdodHMiOiB7CiAgICAgICAgICAgICJpc19lbnRpdGxlZCI6IHRydWUKICAgICAgICB9LAogICAgICAgICJjb3N0X21hbmFnZW1lbnQiOiB7CiAgICAgICAgICAgICJpc19lbnRpdGxlZCI6IHRydWUKICAgICAgICB9LAogICAgICAgICJhbnNpYmxlIjogewogICAgICAgICAgICAiaXNfZW50aXRsZWQiOiB0cnVlCiAgICAgICAgfSwKICAgICAgICAib3BlbnNoaWZ0IjogewogICAgICAgICAgICAiaXNfZW50aXRsZWQiOiB0cnVlCiAgICAgICAgfSwKICAgICAgICAic21hcnRfbWFuYWdlbWVudCI6IHsKICAgICAgICAgICAgImlzX2VudGl0bGVkIjogdHJ1ZQogICAgICAgIH0sCiAgICAgICAgIm1pZ3JhdGlvbnMiOiB7CiAgICAgICAgICAgICJpc19lbnRpdGxlZCI6IHRydWUKICAgICAgICB9CiAgICB9LAogICAgImlkZW50aXR5IjogewogICAgICAgICJpbnRlcm5hbCI6IHsKICAgICAgICAgICAgImF1dGhfdGltZSI6IDI5OSwKICAgICAgICAgICAgImF1dGhfdHlwZSI6ICJiYXNpYy1hdXRoIiwKICAgICAgICAgICAgIm9yZ19pZCI6ICIxMTc4OTc3MiIKICAgICAgICB9LAogICAgICAgICJhY2NvdW50X251bWJlciI6ICI2MDg5NzE5IiwKICAgICAgICAidXNlciI6IHsKICAgICAgICAgICAgImZpcnN0X25hbWUiOiAiSW5zaWdodHMiLAogICAgICAgICAgICAiaXNfYWN0aXZlIjogdHJ1ZSwKICAgICAgICAgICAgImlzX2ludGVybmFsIjogZmFsc2UsCiAgICAgICAgICAgICJsYXN0X25hbWUiOiAiUUEiLAogICAgICAgICAgICAibG9jYWxlIjogImVuX1VTIiwKICAgICAgICAgICAgImlzX29yZ19hZG1pbiI6IHRydWUsCiAgICAgICAgICAgICJ1c2VybmFtZSI6ICJpbnNpZ2h0cy1xYSIsCiAgICAgICAgICAgICJlbWFpbCI6ICJqbmVlZGxlK3FhQHJlZGhhdC5jb20iLAogICAgICAgICAgICAidXNlcl9pZCI6ICI2MDg5NzE5IgogICAgICAgIH0sCiAgICAgICAgInR5cGUiOiAiVXNlciIKICAgIH0KfQ==") //nolint:lll

	hasPermissionKessel(c)

	assert.Equal(t, http.StatusOK, w.Code)

	workspaces, found := c.Get(utils.KeyInventoryWorkspaces)
	require.True(t, found)
	workspaceIDs, ok := workspaces.([]string)
	require.True(t, ok)
	require.Greater(t, len(workspaceIDs), 0)
	assert.Equal(t, "inventory-group-1", workspaceIDs[0])
}

func TestHasPermissionKessel_NoWorkspaces(t *testing.T) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = &http.Request{Method: http.MethodGet, Header: http.Header{}}

	// Do not set any identity / workspaces so that hasPermissionKessel
	// collects an empty workspace IDs slice and should abort as unauthorized.
	hasPermissionKessel(c)

	assert.Equal(t, http.StatusUnauthorized, w.Code)

	_, found := c.Get(utils.KeyInventoryWorkspaces)
	require.False(t, found, "inventory workspaces should not be set when authorization fails")
}

```

To compile these tests, ensure `manager/middlewares/kessel_test.go` imports `net/http/httptest` (and `net/http` if not already imported):

1. Add `httptest` to the import list:

<<<<<<< SEARCH
import (
	"net/http"

	"github.com/gin-gonic/gin"
=======
import (
	"net/http"
	"net/http/httptest"

	"github.com/gin-gonic/gin"
>>>>>>> REPLACE

If the actual import block differs, adjust the import edits accordingly to include `net/http/httptest`.
</issue_to_address>

### Comment 3
<location path="listener/common_test.go" line_range="89-92" />
<code_context>
 // assertSystemInventoryProfileMatchesHost checks host-derived system_inventory columns written by
 // storeOrUpdateSysPlatform (must stay in sync on ON CONFLICT DO UPDATE, not only on first insert).
-// nolint: unparam
+// nolint: unparam,funlen
 func assertSystemInventoryProfileMatchesHost(t *testing.T, inventoryID string, host *Host) {
 	t.Helper()
</code_context>
<issue_to_address>
**suggestion:** Test (and make helper robust for) hosts without any workspace groups

The helper currently assumes `host.Groups[0]` exists, but `updateSystemPlatform` now allows hosts with zero groups (nil `WorkspaceID`/`WorkspaceName`). Please guard against `len(host.Groups) == 0` (asserting `inv.WorkspaceID`/`WorkspaceName` are nil in that case), and add a test with `Groups: nil`/`[]` to confirm the values stay nil and no panic occurs.

Suggested implementation:

```golang
 // assertSystemInventoryProfileMatchesHost checks host-derived system_inventory columns written by
 // storeOrUpdateSysPlatform (must stay in sync on ON CONFLICT DO UPDATE, not only on first insert).
 // nolint: unparam,funlen
 func assertSystemInventoryProfileMatchesHost(t *testing.T, inventoryID string, host *Host) {
 	t.Helper()
 	var inv models.SystemInventory

 	assert.JSONEq(t, string(utils.MarshalNilToJSONB(host.Tags)), string(inv.Tags))

+	// A host may legitimately have no workspace groups; in that case the inventory workspace
+	// fields should remain nil and we must not index into host.Groups[0].
+	if len(host.Groups) == 0 {
+		assert.Nil(t, inv.WorkspaceID)
+		// NOTE: if inv.WorkspaceName is a *string / sql.NullString or similar, it should also be nil/invalid here.
+		// See additional_changes below for aligning this with the actual type.
+	} else if hostWorkspaceID := host.Groups[0].ID; hostWorkspaceID != "" {
+		require.NotNil(t, inv.WorkspaceID)
+		assert.Equal(t, hostWorkspaceID, inv.WorkspaceID.String())
+	} else {

```

1. In `assertSystemInventoryProfileMatchesHost`, you should also assert the correct behavior for `inv.WorkspaceName` in the `len(host.Groups) == 0` branch, matching its actual type:
   - If it's a pointer: `assert.Nil(t, inv.WorkspaceName)`
   - If it's an `sql.NullString`: `assert.False(t, inv.WorkspaceName.Valid)`
   - Or equivalent for whatever type is used.
2. Anywhere else in this helper where `host.Groups[0]` is accessed (e.g., to derive workspace name), wrap that logic in a `len(host.Groups) > 0` guard in the same pattern.
3. Add tests in `listener/common_test.go` that exercise the helper with no groups, e.g.:
   - One test with `host := &Host{Groups: nil, /* other required fields */}` that eventually calls `assertSystemInventoryProfileMatchesHost` and verifies no panic and that `inv.WorkspaceID`/`WorkspaceName` remain nil.
   - Another test with `host := &Host{Groups: []*HostGroup{}, /* other required fields */}` doing the same.
   These tests should mirror the existing tests that cover the non-empty-groups case, reusing whatever setup helpers you already have for inserting a `Host`, calling `storeOrUpdateSysPlatform`, and retrieving the resulting `models.SystemInventory`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread listener/upload.go Outdated
Comment thread manager/middlewares/kessel_test.go
Comment thread listener/common_test.go
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