Skip to content

fix(eslint-factory): require-async-entrypoint-catch — track async arrow/fn-expr entrypoints and flag .then() without .catch()#43356

Merged
pelikhan merged 4 commits into
mainfrom
copilot/eslint-factory-fix-async-entrypoint-catch
Jul 4, 2026
Merged

fix(eslint-factory): require-async-entrypoint-catch — track async arrow/fn-expr entrypoints and flag .then() without .catch()#43356
pelikhan merged 4 commits into
mainfrom
copilot/eslint-factory-fix-async-entrypoint-catch

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

require-async-entrypoint-catch had two false-negative gaps: it only tracked FunctionDeclaration entrypoints (missing const main = async () => {} and const run = async function() {}), and its call handler bailed early on any non-Identifier callee, silently accepting main().then(cb) chains with no rejection handler.

Rule changes (require-async-entrypoint-catch.ts)

  • Gap 1 — async variable entrypoints: added a VariableDeclaration handler that registers module-scope const/let/var X = async () => {} and const/let/var X = async function() {} into asyncFunctionNames, alongside existing FunctionDeclaration tracking.

  • Gap 2 — .then() without .catch(): replaced the early Identifier-only guard with a MemberExpression branch in the ExpressionStatement > CallExpression handler. Adds two helpers:

    • chainEndsWithCatch(node) — returns true if the outermost call's property is catch
    • getRootCallName(node) — recursively walks a chained CallExpression to resolve the root identifier

    Chains terminating in .catch(...) remain valid; chains ending in .then(...) (or any non-catch method) are flagged.

const main = async () => { /* ... */ };
main();                           // ❌ now flagged (was false negative)
main().then(() => process.exit(0)); // ❌ now flagged (was false negative)
main().catch(err => { ... });     // ✅ still valid
main().then(cb).catch(err => { ... }); // ✅ still valid

Tests (require-async-entrypoint-catch.test.ts)

Added four new test groups covering the acceptance criteria:

  • Valid: async arrow/fn-expr with .catch(), .then().catch() chain, async arrow awaited inside async context, sync arrow
  • Invalid: bare async arrow call, bare async fn-expr call, .then() without .catch() on both FunctionDeclaration and arrow entrypoints — each with expected suggestion output

Generated by 👨‍🍳 PR Sous Chef · 11.4 AIC · ⌖ 4.86 AIC · ⊞ 3.4K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · 8.26 AIC · ⌖ 3.14 AIC · ⊞ 3.4K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · 15.7 AIC · ⌖ 5.01 AIC · ⊞ 3.4K ·
Comment /souschef to run again

…then() without .catch()

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix eslint-factory to track async function expressions and arrow functions fix(eslint-factory): require-async-entrypoint-catch — track async arrow/fn-expr entrypoints and flag .then() without .catch() Jul 4, 2026
Copilot AI requested a review from pelikhan July 4, 2026 09:43
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — thanks for picking up the require-async-entrypoint-catch gaps! Fixing false-negative detection for arrow/function-expression entrypoints and unguarded .then() chains is a solid, well-scoped improvement to the eslint-factory rule.

This PR is still a draft with no committed changes yet, so a few things will be needed before it's ready for review:

If you'd like a hand completing the implementation, here's a ready-to-use prompt:

Fix the `require-async-entrypoint-catch` rule in eslint-factory to address two false-negative gaps:

1. **Gap 1 – async function expressions and arrow functions at module scope**
   Update the collector to also handle `VariableDeclarator` nodes where the init is an async `FunctionExpression` or `ArrowFunctionExpression` assigned at module scope (parent is `VariableDeclaration` whose parent is `Program`). Track the declared name the same way `FunctionDeclaration` names are tracked today.

2. **Gap 2 – `.then()` chains without a terminal `.catch()`**
   In the call handler, detect `ExpressionStatement` nodes where the callee is a MemberExpression ending in `.then(...)` on a tracked async function name. Flag it unless the chain terminates in a `.catch(...)` call.

Add RuleTester cases covering:
- Valid: tracked name called with `.catch(...)`, tracked name awaited inside another async function, sync arrow function (not tracked)
- Invalid: bare `main()` call where `main` is an async arrow, bare `run()` where `run` is an async function expression, `main().then(cb)` without `.catch`

Ensure no new false positives are introduced against the existing `actions/setup/js/**/*.cjs` corpus by running the relevant tests.

Generated by ✅ Contribution Check · 165.9 AIC · ⌖ 29.5 AIC · ⊞ 6.3K ·

@pelikhan pelikhan marked this pull request as ready for review July 4, 2026 10:15
Copilot AI review requested due to automatic review settings July 4, 2026 10:15
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #43356 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copilot AI 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.

Pull request overview

This PR improves the require-async-entrypoint-catch ESLint rule in eslint-factory by expanding what it considers an async “entrypoint” and by detecting promise chains (.then(...)) that omit a rejection handler, reducing false negatives when linting GitHub Actions scripts.

Changes:

  • Track module-scope async entrypoints declared via const/let/var X = async () => {} and const/let/var X = async function() {}.
  • Detect and flag main().then(...)-style chains without a .catch(...) handler, while allowing chains that are already caught.
  • Add new tests covering async arrow/function-expression entrypoints and .then()-without-.catch() cases.
Show a summary per file
File Description
eslint-factory/src/rules/require-async-entrypoint-catch.ts Adds module-scope async variable entrypoint tracking and extends call detection to handle chained promise calls.
eslint-factory/src/rules/require-async-entrypoint-catch.test.ts Adds new valid/invalid cases to verify the new entrypoint tracking and .then() chain behavior.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment on lines +83 to +85
VariableDeclaration(node) {
if (node.parent.type !== AST_NODE_TYPES.Program) return;
for (const declarator of node.declarations) {
Comment on lines +11 to +19
/** Returns true if the outermost call in the chain ends with `.catch(...)`. */
function chainEndsWithCatch(node: TSESTree.CallExpression): boolean {
const callee = node.callee;
if (callee.type === AST_NODE_TYPES.MemberExpression) {
const prop = callee.property;
return prop.type === AST_NODE_TYPES.Identifier && prop.name === "catch";
}
return false;
}
Comment on lines +110 to +112
// Chained call: main().then(...) etc.
// If the chain ends with .catch(...), it's handled — skip.
if (chainEndsWithCatch(node)) return;
@github-actions github-actions Bot mentioned this pull request Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

Analyzed 6 test(s): 6 design, 0 implementation, 0 violation(s).

📊 Metrics (6 tests)
Metric Value
Analyzed 6 (Go: 0, JS/TS: 6)
✅ Design 6 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 6 (100%)
Duplicate clusters 0
Inflation YES (test: +140 lines, prod: +60 lines → 2.33:1)
🚨 Violations 0
Test File Classification Issues
valid: async main chained with .catch() is not flagged (modified) require-async-entrypoint-catch.test.ts design_test None
valid: async main called with await inside an async context (modified) require-async-entrypoint-catch.test.ts design_test None
valid: sync arrow or non-async fn-expression call is not flagged (new) require-async-entrypoint-catch.test.ts design_test None
invalid: bare call to async arrow function is flagged (new) require-async-entrypoint-catch.test.ts design_test None
invalid: bare call to async function expression is flagged (new) require-async-entrypoint-catch.test.ts design_test None
invalid: .then() chain without .catch() on async function is flagged (new) require-async-entrypoint-catch.test.ts design_test None

i️ Inflation note: The 2.33:1 test-to-prod line ratio is expected here — ESLint RuleTester invalid cases require full code snippets for both code and expected output (autofix), which is inherently verbose. The content represents genuine coverage, not padding.

Verdict

Passed. 0% implementation tests (threshold: 30%). All new tests verify user-visible behavioral contracts via ESLint RuleTester: correct error messageId, autofix output, and valid/invalid case segregation. The four new it() blocks directly cover the two new features (async arrow/fn-expr tracking, .then()-without-.catch() detection).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 48.6 AIC · ⌖ 10.3 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 90/100. 0% implementation tests (threshold: 30%). All 6 new/modified test blocks are design tests with full edge-case and autofix output coverage.

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — commenting with test coverage gaps and one logic-clarity note. No blocking issues; the core fix is sound.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gaps (minor): The new VariableDeclaration handler's scope guard (parent.type !== Program) and the recursive getRootCallName path have partial test coverage. A few additional edge-case tests would lock in the invariants (see inline comments).
  • Logic clarity: The relationship between chainEndsWithCatch (checks the outermost node) and getRootCallName (walks inward) is non-obvious — a short comment would help future readers.

Positive Highlights

  • ✅ Both false-negative gaps are correctly identified in the PR description with clear before/after examples
  • chainEndsWithCatch + getRootCallName are clean, pure helper functions — easy to unit-test independently if needed
  • ✅ The VariableDeclaration handler correctly guards on node.parent.type === Program and declarator.init.async, mirroring the existing FunctionDeclaration guard precisely
  • ✅ 140 lines of tests added covering all acceptance criteria paths — the signal-to-noise ratio on the test descriptions is excellent
  • ✅ The autofix suggestion correctly appends .catch(...) after the full expression node, which works for both bare and chained calls without special-casing

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 109 AIC · ⌖ 6.35 AIC · ⊞ 6.6K
Comment /matt to run again

Comments that could not be inline-anchored

eslint-factory/src/rules/require-async-entrypoint-catch.test.ts:80

[/tdd] Missing valid test: non-module-scope async arrow should not be flagged — this directly validates the node.parent.type !== Program guard added to the new VariableDeclaration handler.

<details>
<summary>💡 Suggested test case to add to the existing "valid: async function called as part of an expression" group</summary>

// Nested async arrow is not a module-scope entrypoint — should not be tracked
`function setup() {
  const inner = async () =&gt; { return 42; }
  inner();
}`

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.ts:12</summary>

**[/diagnosing-bugs]** `chainEndsWithCatch` only checks the immediate callee  it misses the intermediate `.then()` in `main().then(cb).catch(err)`. However, the current logic treats that chain as **valid** (it returns early), which is correct behaviour. The concern is whether deeply nested chains like `main().then(cb1).then(cb2)` without `.catch()` are properly flagged.

&lt;details&gt;
&lt;summary&gt;💡 Trace through the logic for `main().then(cb1).then(cb2)`&lt;/summary&gt;

For `main().then(cb1).then(cb2)` a…

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.test.ts:296</summary>

**[/tdd]** Missing invalid test: a multi-step chain ending in `.then()` (e.g. `main().then(cb1).then(cb2)`) is not covered  this exercises `getRootCallName`&#39;s recursive path more thoroughly.

&lt;details&gt;
&lt;summary&gt;💡 Suggested test case to add to the &quot;invalid: .then() chain without .catch()&quot; group&lt;/summary&gt;

Add a third case with a double-chained `.then().then()` to the existing invalid test group:

```ts
{
  code: `async function main() {}\nmain().then(() =&gt; {}).then(() =&gt; process.exit(0));`,


</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.ts:101</summary>

**[/diagnosing-bugs]** The `ExpressionStatement &gt; CallExpression` handler only handles `Identifier` and `MemberExpression` callees, but silently ignores computed member expressions like `obj[&quot;catch&quot;](err)`. This is fine in practice (the rule targets simple entrypoint patterns), but worth a brief comment explaining the intentional narrowness so future maintainers don&#39;t add a false-positive case.

&lt;details&gt;
&lt;summary&gt;💡 Suggested clarifying comment&lt;/summary&gt;

```ts
// Only handle Identifier (bare …

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.test.ts:61</summary>

**[/tdd]** Missing valid test: `const main = async () =&gt; {}; main();` inside an async IIFE  i.e. `(async () =&gt; { await main(); })()` already exists (line 73), but a variant where the async arrow is called *without* await inside an async context is absent. That is the case exercised by the existing `FunctionDeclaration` test at line 62: `const run = async () =&gt; { main(); }`. Adding the async-arrow analogue would fully close the gap.

&lt;details&gt;
&lt;summary&gt;💡 Suggested test case to add to the &quot;vali…

</details>

@github-actions github-actions Bot 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.

Review: require-async-entrypoint-catch — async arrow/fn-expr tracking + .then() without .catch()

The approach is sound and the two gaps (missing arrow/fn-expression entrypoints, and .then() chains without .catch()) are correctly addressed. The implementation is clean and the tests cover the main acceptance criteria.

Issues found:

# Severity Location Summary
1 Low getRootCallName, line 26 Unbounded recursion on deep chains — add a depth guard
2 Low Call handler, line 130 Add a test for main().then(() => main()) to confirm no double-fire
3 Info Line 112 .catch(err => { throw err; }) is accepted — document intentional scope
4 Info Line 84 Reassigned-async-variable gap — document as known limitation
5 Info Line 109 Non-Identifier/MemberExpression callees silently skipped — add comment
Correctness assessment
  • The VariableDeclaration handler correctly gates on node.parent.type === AST_NODE_TYPES.Program — only module-scope declarations are tracked. ✅
  • The "ExpressionStatement > CallExpression" selector uses the CSS child combinator, so only direct-child CallExpression nodes match. Nested callbacks do not trigger the rule. ✅
  • chainEndsWithCatch looks only at the outermost method name; it does not traverse into the callback body. This is intentional and correct for a syntactic rule. ✅
  • The suggestion fixer calls fixer.insertTextAfter(node, ".catch(...)") where node is the outermost CallExpression. Because the semicolon belongs to the parent ExpressionStatement, the insertion lands before the semicolon — producing valid output. ✅ (verified by test expectations)
  • getRootCallName handles main().then(cb)"main", and main().then(cb).catch(err) → caught before getRootCallName is called. ✅

No blocking correctness bugs were found. Inline comments above are non-blocking improvements.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 133.6 AIC · ⌖ 6.6 AIC · ⊞ 4.9K

Comments that could not be inline-anchored

eslint-factory/src/rules/require-async-entrypoint-catch.ts:130

Potential double-fire: ExpressionStatement &gt; CallExpression matches every descendant CallExpression.

The selector &quot;ExpressionStatement &gt; CallExpression&quot; (with the CSS child combinator &gt;) only matches CallExpression nodes that are direct children of an ExpressionStatement, so nested calls like the inner main() inside main().then(() =&gt; main()) will NOT match — they are children of an ArrowFunctionExpression, not an ExpressionStatement. That is correct behaviour.

Howeve…

eslint-factory/src/rules/require-async-entrypoint-catch.ts:26

Unbounded recursion risk in getRootCallName.

For deeply nested method chains like main().a().b().c().d(), each level recurses into the next inner CallExpression. In practice chains are shallow, but there is no depth guard. For a linter called on arbitrary user code, an adversarially long chain (e.g. from generated code) could produce a stack overflow. Consider adding a max-depth counter (e.g. 20 levels) and returning null when exceeded.

function getRootCallName(node: TSESTre…

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.ts:112</summary>

**False negative: `main().catch(err =&gt; { throw err; })` re-throws but is accepted as valid.**

`chainEndsWithCatch` returns `true` for any call whose outermost method is `.catch()`, regardless of what the handler does (e.g. `throw err`, which re-throws). Re-throwing converts a rejected promise into a thrown exception rather than calling `process.exit`, which may silently fail in GitHub Actions scripts. This is intentional (the rule only enforces the presence of `.catch`, not its body), but it s…

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.ts:84</summary>

**Missing: reassigned async variable is not tracked.**

The `VariableDeclaration` handler only picks up declarations of the form `const/let/var X = async () =&gt; {}`. If code later reassigns an existing variable (`let X = () =&gt; {}; X = async () =&gt; {}`), the reassignment (an `AssignmentExpression`) is never seen. This is a pre-existing gap (same limitation existed for `FunctionDeclaration`), but since this PR is specifically expanding entrypoint tracking, it&#39;s worth noting as a known limitation wi…

</details>

<details><summary>eslint-factory/src/rules/require-async-entrypoint-catch.ts:109</summary>

**False negative: computed member access `main[method]()` is silently ignored.**

When `callee` is a `MemberExpression` with a computed property (`main[&#39;then&#39;]()`), `chainEndsWithCatch` checks `prop.type === AST_NODE_TYPES.Identifier` which returns `false` for a `Literal`, so it falls through to `return false`. Then `getRootCallName` is called and correctly walks back to find `&#39;main&#39;`. The final check `asyncFunctionNames.has(name)` returns `true`, so the node IS flagged  but this is correct be…

</details>

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

…lly chains

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address outstanding review feedback, refresh the branch if needed, and rerun checks once updated.

Unresolved review feedback to address:...

Addressed the outstanding Copilot feedback in commit 4416ef9.

Changes made:

  • Track exported module-scope async variable entrypoints (e.g. export const main = async () => {})
  • Accept chains that include .catch(...) anywhere (so .catch(...).finally(...) is no longer a false positive)
  • Updated the related inline comment and added focused tests for exported entrypoints and .catch(...).finally(...) / .then(...).catch(...).finally(...) chains

Validation run locally:

  • npx vitest run src/rules/require-async-entrypoint-catch.test.ts
  • make fmt
  • make lint
  • make test-unit
  • make test
  • make recompile
  • make agent-report-progress

I couldn’t refresh PR/check metadata from gh pr view in this environment due API 403, but the code/test updates requested in those unresolved threads are now applied.

Copilot AI requested a review from gh-aw-bot July 4, 2026 12:03
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the outstanding review feedback, refresh the branch if needed, and rerun checks once updated.

Unresolved review feedback to address:...

Addressed in 4416ef9.

I re-ran local checks on the current branch:

  • make fmt
  • make lint
  • make test-unit
  • make test
  • make recompile

No additional code changes were needed.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage — Run §28706384347

Field Value
Category bug
Risk 🟢 Low
Score 69 / 100
Action fast_track

Score breakdown: impact 35 + urgency 20 + quality 14

Closes two false-negative gaps in require-async-entrypoint-catch — arrow/fn-expr entrypoints were untracked, and .then() without .catch() was not flagged. +247/-4 in 2 files (impl + tests). Copilot check passed. Rebase if needed and fast-track review.

Generated by 🔧 PR Triage Agent · 56.5 AIC · ⌖ 8.62 AIC · ⊞ 5.5K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the outstanding review feedback below, refresh the branch if needed, and rerun checks once updated.

Unresolved review feedback to address:...

Addressed by 4416ef9; no further code changes were needed.

Re-ran:

  • make fmt
  • make lint
  • make test-unit
  • make test
  • make recompile

Also reviewed the referenced run (28707474242) and found no new actionable failures.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any remaining review feedback, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 15.7 AIC · ⌖ 5.01 AIC · ⊞ 3.4K ·
Comment /souschef to run again

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address any remaining review feedback, and rerun checks once the branch is up to date.

...

Addressed in 4416ef9 (already on this branch). I rechecked for remaining actionable review feedback and found none, then re-ran:

  • make fmt
  • make lint
  • make test-unit
  • make test
  • make recompile

All passed locally, and the latest PR check runs for this head are successful/in progress.

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please
run the pr-finisher skill, address any outstanding review feedback, and rerun checks once the branch is up to date.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · 5.38 AIC · ⌖ 4.28 AIC · ⊞ 3.4K ·
Comment /souschef to run again

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please
run the pr-finisher skill, address any outstanding review feedback, and rerun checks once the branch is up to date.
...

Reviewed the outstanding feedback and revalidated this branch; no additional code changes were needed beyond 4416ef9.

In this run I verified the rule/test updates are present, checked PR review thread status, inspected the latest CI signal, and reran:

  • make fmt
  • make lint
  • make test-unit
  • make test
  • make recompile

All local checks passed.

@pelikhan pelikhan merged commit 0e20a06 into main Jul 4, 2026
7 checks passed
@pelikhan pelikhan deleted the copilot/eslint-factory-fix-async-entrypoint-catch branch July 4, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants