Skip to content

fix: unify remediation orchestrator with conflict-aware groups - #681

Open
Strum355 wants to merge 4 commits into
guacsec:mainfrom
Strum355:feature/TC-7116-unified-remediation-groups
Open

Strum355 wants to merge 4 commits into
guacsec:mainfrom
Strum355:feature/TC-7116-unified-remediation-groups

Conversation

@Strum355

@Strum355 Strum355 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

The orchestrator had two divergent code paths: one that ran the updater per-dep to discover edit sites (perDependencyChanges: true), and one that ran it once with everything bundled. Neither detected shared-ref version conflicts — two deps sharing a Maven ${property} or Gradle version.ref could end up with different target versions, silently applying only one while reporting both as fixed (TC-6891).

Now every consumer — CLI, GH action, future tooling — receives the same truthful, conflict-resolved data regardless of how they plan to use it.

Test plan

  • Single-dep groups produce correct changeKey and content
  • Two independent deps produce distinct groups with isolated content
  • Shared Maven property / TOML version.ref collapse into one group
  • Conflicting versions resolve to highest, group marked divergent
  • Custom conflictResolution function overrides default policy
  • Transitive deps absent from manifest are dropped
  • Groups span multiple manifests independently
  • Flat remediations equals groups.flatMap(g => g.remediations)
  • All existing exclude filter, dry-run, apply, and CLI tests pass
  • 116 tests passing across remediate, remediation, remediation_report, and maven_updater suites

Implements: https://issues.redhat.com/browse/TC-7116

🤖 Generated with Claude Code

Summary by Sourcery

Unify remediation orchestration around conflict-aware edit-site groups so all consumers receive consistent, isolated, and truthful remediation results.

New Features:

  • Expose conflict-aware remediation groups containing isolated manifest content, applied versions, divergence status, and grouped remediations.
  • Support configurable version-conflict resolution with highest-version selection by default.

Bug Fixes:

  • Unify remediation processing across consumers so shared Maven properties and Gradle version references are applied truthfully without silently losing conflicting updates.
  • Drop remediations for dependencies that cannot be located in the target manifest.

Enhancements:

  • Replace the per-dependency changes option with a consistent groups-based result while retaining a flat remediation accessor.
  • Preserve exclusion behavior while preventing updates to edit sites shared with excluded dependencies.
  • Export version comparison for reuse in conflict resolution.

CI:

  • Run the workflow test suite on Python 3.13.

Tests:

  • Expand remediation coverage for independent and shared edit sites, version conflicts, custom conflict policies, transitive dependencies, multiple manifests, TOML version references, and flat-result consistency.

@sourcery-ai

sourcery-ai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR replaces the two remediation execution modes with one edit-site-aware pipeline that returns conflict-resolved groups and isolated content, while preserving a flat remediation view and strengthening behavior across Maven, TOML, exclusions, transitive dependencies, and multi-manifest workspaces.

Sequence diagram for conflict-aware remediation orchestration

sequenceDiagram
    participant Caller
    participant runRemediation
    participant Extractor as extractRemediations
    participant Updater
    participant Resolver as conflictResolution
    participant Filesystem

    Caller->>runRemediation: runRemediation(targetPath, options)
    runRemediation->>Extractor: extractRemediations(manifest)
    Extractor-->>runRemediation: remediations
    loop Each remediation
        runRemediation->>Updater: updater(originalContent, dependency fix)
        Updater-->>runRemediation: isolated content and applied edit
        runRemediation->>Updater: changeKey(manifestPath, applied edit)
    end
    runRemediation->>Resolver: resolve versions within each changeKey group
    Resolver-->>runRemediation: appliedVersion
    runRemediation->>Updater: updater(originalContent, grouped fixes)
    Updater-->>runRemediation: isolated group content
    opt Not dry run
        runRemediation->>Updater: updater(originalContent, all grouped fixes)
        Updater-->>runRemediation: combined content
        runRemediation->>Filesystem: writeFileSync(manifestPath, combined content)
    end
    runRemediation-->>Caller: groups and flat remediations
Loading

Flow diagram for edit-site grouping and conflict resolution

flowchart TD
    A[Extract remediations] --> B[Run updater once per remediation]
    B --> C{Updater located dependency?}
    C -- No --> D[Drop unlocatable remediation]
    C -- Yes --> E[Derive changeKey]
    E --> F{Excluded dependency shares edit site?}
    F -- Yes --> G[Record skipped remediation]
    F -- No --> H[Group by changeKey]
    H --> I{Group has conflicting versions?}
    I -- No --> J[Use shared target version]
    I -- Yes --> K[Resolve with conflictResolution]
    J --> L[Produce group content and divergent flag]
    K --> L
    L --> M[Flatten groups into remediations]
    M --> N{dryRun?}
    N -- Yes --> O[Return groups without writing]
    N -- No --> P[Apply combined changes and write manifest]
Loading

File-Level Changes

Change Details Files
Reworked remediation orchestration into a single conflict-aware grouping pipeline.
  • Always run isolated updater discovery to identify edit sites.
  • Group remediations by manifest-specific changeKey.
  • Resolve shared edit-site version conflicts using a configurable policy, defaulting to the highest version.
  • Generate isolated group content and combined content for disk application.
  • Drop remediations that cannot be located and applied in the manifest.
src/remediate.js
Introduced and exposed the grouped remediation result model.
  • Added groups containing changeKey, appliedVersion, remediations, isolated content, and divergence status.
  • Retained flat remediations as a groups-derived convenience result.
  • Removed the perDependencyChanges option and per-remediation changes payload.
  • Exported compareVersions for downstream consumers.
src/remediate.js
src/index.js
src/remediation.js
Expanded coverage for grouping, conflict resolution, manifest handling, and filtering behavior.
  • Tested independent and shared Maven/TOML edit sites.
  • Tested default and custom conflict policies, divergence flags, multi-manifest grouping, and flat-result equivalence.
  • Updated transitive dependency and exclude-filter expectations for the unified flow.
test/remediate.test.js

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

@Strum355
Strum355 requested review from a-oren and ruromero and removed request for a-oren October 28, 2026 20:09

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/remediate.js" line_range="216" />
<code_context>
+	if (resolve) {
+		return resolve(versions)
+	}
+	return versions.sort((a, b) => compareVersions(b, a))[0]
+}
+
</code_context>
<issue_to_address>
**Prerelease fixes are selected over stable releases**

When a shared edit site has both a prerelease fix and its corresponding stable release, `resolveConflict` sorts candidates with `compareVersions`, whose lexical comparison of qualified dot-separated segments ranks the prerelease above the stable release. It applies the prerelease version to the manifest, so users see the prerelease reported as the selected fix.

Use version ordering that ranks a prerelease below its corresponding stable release.
</issue_to_address>

### Comment 2
<location path="src/remediate.js" line_range="394" />
<code_context>
-				fs.writeFileSync(manifestPath, result.content, 'utf-8')
+			const combined = manifestType.updater(originalContent, allChanges)
+			if (combined.applied.length > 0) {
+				fs.writeFileSync(manifestPath, combined.content, 'utf-8')
 				appliedFiles.push(manifestPath)
 			}
</code_context>
<issue_to_address>
**Failed runs leave partial manifest updates**

When a later analysis request or conflict resolver throws after an earlier manifest has been updated, `runRemediation` writes earlier manifests before processing all manifests succeeds, then rejects without returning `appliedFiles`. The workspace is partially updated and the caller does not know which files changed.

Defer manifest writes until all analysis and conflict resolution for the run succeeds.

Also at `src/remediate.js:373`.
</issue_to_address>

### Comment 3
<location path="src/remediate.js" line_range="394" />
<code_context>
-				fs.writeFileSync(manifestPath, result.content, 'utf-8')
+			const combined = manifestType.updater(originalContent, allChanges)
+			if (combined.applied.length > 0) {
+				fs.writeFileSync(manifestPath, combined.content, 'utf-8')
 				appliedFiles.push(manifestPath)
 			}
</code_context>
<issue_to_address>
**Concurrent runs lose updates**

When two runs target the same manifest and both scan before either writes, each run reads the manifest at `src/remediate.js:315`, then writes a full-file result at `src/remediate.js:394`; the last writer overwrites edits made by the other run, so one remediation disappears.

Serialize writes per manifest or re-read and merge/check the file before writing.
</issue_to_address>

### Comment 4
<location path="src/remediate.js" line_range="383" />
<code_context>
+			allGroups.push({
+				changeKey,
+				appliedVersion,
+				remediations: members,
+				content: { path: manifestPath, after: result.content },
+				divergent: members.some(r => r.fixedInVersion !== appliedVersion),
</code_context>
<issue_to_address>
**Reports show versions that were not applied**

When a conflict resolver selects a version different from one or more member remediations, `runRemediation` retains each member's original `fixedInVersion` while recording the selected `appliedVersion` on the group. The CLI passes the flat remediations to `generateReport`, which prints `fixedInVersion`, so users see targets that were not written to the manifest.

Set each returned remediation's `fixedInVersion` to its group's `appliedVersion` before generating the report.

Also at `src/remediate.js:384`, `src/remediate.js:400`.
</issue_to_address>

### Comment 5
<location path="test/remediate.test.js" line_range="1047" />
<code_context>
 				const pomPath = path.join(dir, 'pom.xml')
 				matchStub.returns({ provideStack: stub().resolves({ content: '{}', contentType: 'application/json', ecosystem: 'maven' }) })
-				requestStackStub.resolves(buildExcludeReport([COMMONS_TEXT, JACKSON_CORE]))
+				requestStackStub.resolves(buildMultiDepReport([COMMONS_TEXT, JACKSON_CORE]))

 				const result = await runRemediation(pomPath, { dryRun: true, exclude: ['pkg:maven/org.apache.commons/commons-text'] })
</code_context>
<issue_to_address>
**Percent-encoding remains untested**

When purl component decoding is broken, when purl component decoding is broken, the `percent-encoded purls match decoded exclude patterns` test still passes: its fixture and exclude pattern use unencoded components, so the assertions only exercise case-insensitive type matching.

Use a fixture containing a percent-encoded purl component and an exclude pattern using its decoded form.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and when dependencies share an edit site, the selected conflict version is written into the manifest and may be consumed by later builds; a mistaken selection can therefore persist as a wrong dependency version. The impact is bounded and can be repaired by restoring the manifest or rerunning remediation.

Blocking findings: src/remediate.js:216, src/remediate.js:394, src/remediate.js:394, src/remediate.js:383, test/remediate.test.js:1047


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/remediate.js
Comment thread src/remediate.js
Comment thread src/remediate.js
Comment thread src/remediate.js
Comment thread test/remediate.test.js Outdated
@Strum355 Strum355 changed the title Unify remediation orchestrator with conflict-aware groups fix: unify remediation orchestrator with conflict-aware groups Oct 29, 2026
@Strum355
Strum355 force-pushed the feature/TC-7116-unified-remediation-groups branch 3 times, most recently from acd1f0b to 59a4eb4 Compare October 29, 2026 10:50
ruromero

This comment was marked as outdated.

@ruromero ruromero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for two small items; everything else is non-blocking (inline). This follows up on my earlier review. Findings were verified locally against 59a4eb4.

Blocking

  1. Mark the breaking change. CONTRIBUTING.md says PR titles must follow Conventional Commits. perDependencyChanges and remediation.changes / DependencyFix shipped in v0.4.0 (the current latest on npm, via #630), so removing them needs fix!: or a BREAKING CHANGE: footer. Releases are bumped by hand, so the main purpose is to make sure whoever cuts the next release picks the right bump and the changelog flags it. The GH action currently passes perDependencyChanges and reads remediation.changes, so it must move together with this (TC-7118).
  2. Add a test for the TC-6891 fix. See the inline comment on test/remediate.test.js.

No critical correctness issues found. In local probe runs, grouping, conflict resolution, exclude co-location and multi-manifest handling behave as intended. The TOML shared-ref conflict now writes the resolved version, where main overclaims.

Comment thread test/remediate.test.js
matchStub.returns({ provideStack: stub().resolves({ content: '{}', contentType: 'application/json', ecosystem: 'gradle' }) })
requestStackStub.resolves(buildMultiDepReport([
{ depRef: 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.14.0', fixedIn: ['pkg:maven/com.fasterxml.jackson.core/jackson-core@2.15.0'], issueId: 'CVE-2020-1000' },
{ depRef: 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0', fixedIn: ['pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.15.0'], issueId: 'CVE-2020-2000' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking: both deps here target 2.15.0, so this test never reaches the TC-6891 case this PR fixes. Please add a variant with conflicting targets (e.g. 2.15.0 / 2.17.0) that asserts:

  • group.appliedVersion === '2.17.0' and group.divergent === true
  • group.content.after contains jackson = "2.17.0"
  • in apply mode, the file on disk contains 2.17.0

I checked locally: this branch already gets all of these right, and main writes 2.15.0 while reporting both as applied. So the test only locks in behaviour that already works.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Test added

Comment thread src/remediate.js
artifactId: remediation.artifactId,
newVersion: remediation.fixedInVersion,
}])
if (isolated.applied.length === 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Transitive deps (not declared in this manifest) are now dropped without a trace. On main, a dry-run whose only finding is a transitive dep returns exit 2 with 1 remediation. On this branch it returns exit 0 with 0, and the CLI report loses those entries.

Keeping them out of remediations fits the "truthful result" goal. Could they go into skipped instead? Both updaters already give a reason in isolated.skipped (e.g. "Dependency not found in pom.xml or has no <version> element", "No library entry found for module …"), so consumers and the CLI would still see them. Either way, the behaviour change is worth calling out in the PR description.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added to skipped 👍

Comment thread src/remediate.js
* sources?: string,
* backendUrl?: string,
* perDependencyChanges?: boolean,
* conflictResolution?: (versions: string[]) => string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TC-7116: "The default should be 'highest'. Consider also supporting 'first' and a function form." Only the function form works today: conflictResolution: 'highest' throws resolve is not a function. Please either accept 'highest' | 'first' | (versions) => string, or at least fail with a clear error when the value isn't a function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intentional deviation from the ticket. We discussed this during implementation and decided string dispatch adds indirection for no real value — the default is highest (no option needed), and any custom policy is a one-liner function like v => v[0]. Accepting strings means maintaining a lookup table and validating unknown strings, which is more code for the same result.

If a consumer passes "highest" today it will be falsy and fall through to the default (highest), so it happens to work — but we are not documenting it as supported.

Comment thread src/remediate.js
})))
if (result.applied.length > 0) {
fs.writeFileSync(manifestPath, result.content, 'utf-8')
const combined = manifestType.updater(originalContent, allChanges)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: TC-7116 step 4 is "Produce combined content (all groups applied together) for consumers that want a single atomic write." Here the combined content is only computed when !dryRun, and it's written to disk but never returned. Is that intentional, i.e. apply mode is the only consumer? If a dry-run consumer should be able to get it, consider computing it regardless of dryRun and returning it per manifest (e.g. [{ path, after }]).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intentional. We discussed this with the TC-6891 agent who confirmed how the GH action works today: bundle mode runs with dryRun: false and uses the working tree as the combined content (stages changed files via git diff --name-only). Dependency mode also runs with dryRun: false, then discards the working tree and rebuilds branches from group.edit.after.

No consumer needs combined content as a return value — dryRun controls the side effect (disk write), groups is the return data. They are orthogonal. Computing combined content on dry-run would add an extra updater pass for a value nobody reads.

Comment thread src/remediate.js
if (resolve) {
return resolve(versions)
}
return versions.sort((a, b) => compareVersions(b, a))[0]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion (not a regression): compareVersions ranks pre-releases above the release (2.0.0-rc1 > 2.0.0), so with candidates {2.0.0, 2.0.0-rc1} the default policy picks 2.0.0-rc1. The action's compareVersions behaves the same, so parity is preserved. Since this is becoming the canonical policy, consider handling qualifiers, or at least adding a test that documents the current behaviour.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a test documenting the current behavior (compareVersions ranks 2.0.0-rc1 above 2.0.0).

Question for you: does the backend ever emit pre-release versions in fixedIn? If it does not today and is not expected to, this is a theoretical concern we can defer. If it does or might, we should handle qualifiers properly — but that would be a change to compareVersions in remediation.js, which is pre-existing and shared with the version selection strategies. Either way, not a regression in this PR.

Comment thread src/remediate.js Outdated
Comment thread src/remediate.js Outdated
Comment thread src/remediate.js

// Dry-run signals "changes available but not written" via exit code 2.
return { exitCode: dryRun ? 2 : 0, remediations: allRemediations, manifests: manifestPaths, appliedFiles, skipped: allSkipped }
return { exitCode: dryRun ? 2 : 0, groups: allGroups, remediations: flatRemediations, manifests: manifestPaths, appliedFiles, skipped: allSkipped }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: the result object { exitCode, groups, remediations, manifests, appliedFiles, skipped } is built in three places (:264, :403 and here). A small helper would keep the three copies from drifting apart.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The project has tsc set up with JSDoc types — the @returns typedef on runRemediation covers all three return sites, so tsc would catch a missing or misspelled field. Given that, the helper would add indirection without catching anything the type checker does not already catch. Leaving as-is.

Comment thread test/remediate.test.js Outdated
Comment thread test/remediate.test.js
Strum355 and others added 4 commits October 29, 2026 16:03
The orchestrator had two divergent code paths controlled by a
perDependencyChanges flag: one that ran the updater per-dep to discover
edit sites, and one that ran it once with everything bundled. Neither
detected shared-ref version conflicts — two deps sharing a Maven
property or Gradle version.ref could end up with different target
versions, silently applying only one while reporting both as fixed.
The per-dep path left conflict resolution to the GH action, and the
bundle path picked whichever version the iteration order landed on
first.

Now there is one flow. The orchestrator always discovers edit sites,
groups inseparable remediations by changeKey, resolves conflicts
(highest version wins by default, overridable), and returns structured
groups alongside the existing flat remediations list. Every consumer
gets the same truthful, conflict-resolved data regardless of whether
it plans to create one PR or many.

Implements: https://issues.redhat.com/browse/TC-7116
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous test only exercised case-insensitive type matching.
Now uses a percent-encoded exclude pattern (commons%2Dtext) to verify
normalizeExcludePattern decodes components before matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename RemediationGroup.content to .edit (clearer for { path, after })
- Add transitive deps to skipped with reason instead of dropping silently
- Guard conflictResolution output against non-candidate versions
- Add TOML conflicting-versions test (dry-run + apply mode)
- Add prerelease sorting behavior test (documents current semantics)
- Add exclude matcher unit tests (percent-encoding, ** vs *, case)
- Fix percent-encoding test comment (artifactId, not groupId)
- Export normalizeExcludePattern and compileExcludeMatcher for testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Strum355
Strum355 force-pushed the feature/TC-7116-unified-remediation-groups branch from 7e0c53b to ed50ee7 Compare October 29, 2026 16:03
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.15966% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.06%. Comparing base (b91037c) to head (ed50ee7).

Files with missing lines Patch % Lines
src/remediate.js 99.15% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #681      +/-   ##
==========================================
+ Coverage   92.03%   92.06%   +0.02%     
==========================================
  Files          43       43              
  Lines       11305    11312       +7     
  Branches     2265     2271       +6     
==========================================
+ Hits        10405    10414       +9     
+ Misses        900      898       -2     
Flag Coverage Δ
unit-tests 92.06% <99.15%> (+0.02%) ⬆️

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

Files with missing lines Coverage Δ
src/remediation.js 93.56% <100.00%> (ø)
src/remediate.js 95.22% <99.15%> (-0.41%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Strum355
Strum355 requested a review from ruromero October 29, 2026 16:24

This branch has not been deployed

No deployments
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