Skip to content

Implement: Use Koan with Gitlab and Codeberg.org?#992

Draft
Koan-Bot wants to merge 2 commits intoAnantys-oss:mainfrom
Koan-Bot:koan.atoomic/implement-968
Draft

Implement: Use Koan with Gitlab and Codeberg.org?#992
Koan-Bot wants to merge 2 commits intoAnantys-oss:mainfrom
Koan-Bot:koan.atoomic/implement-968

Conversation

@Koan-Bot
Copy link
Copy Markdown
Contributor

@Koan-Bot Koan-Bot commented Mar 21, 2026

Summary

Implements #968

  • feat: add ForgeProvider abstraction layer (Phase 1)

Closes #968


Generated by Kōan /implement


Quality Report

Changes: 7 files changed, 1196 insertions(+)

Code scan: clean

Tests: passed (10 PASSED)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

Introduce koan/app/forge/ package with ForgeProvider ABC, GitHubForge
thin delegation wrapper over app.github, a forge registry, and a
get_forge() factory. GitHubForge delegates all operations to existing
app.github/github_url_parser — zero behavior change. Supports GitHub
Enterprise via base_url parameter. Includes 70 tests covering base
class, GitHub delegation, registry, and factory.

Co-Authored-By: Claude <noreply@anthropic.com>
@atoomic
Copy link
Copy Markdown
Collaborator

atoomic commented Mar 21, 2026

@Koan-Bot review

@Koan-Bot
Copy link
Copy Markdown
Contributor Author

PR Review — Implement: Use Koan with Gitlab and Codeberg.org?

Solid Phase 1 foundation — clean separation, good test coverage, and proper delegation to existing modules. However, there are several issues that should be addressed before merge: (1) supports() claims features that aren't implemented, which will mislead callers; (2) pr_view returns an inconsistent dict shape on error; (3) the bare except Exception in config resolution will hide misconfigurations. The ABC without abstract methods is a design inconsistency worth resolving. None of these are hard to fix, and the overall architecture is sound.


🟡 Important

1. Bare except Exception swallows config errors silently (koan/app/forge/__init__.py, L119-120)
The except Exception in _resolve_forge_config silently catches everything including KeyError, TypeError, or bugs in get_project_config. If projects.yaml has a malformed forge field, the user will silently get GitHub instead of an error. Consider at minimum logging the exception so misconfigurations are diagnosable.

Suggested fix: add a logging.debug or logging.warning call inside the except block, or narrow the except to (FileNotFoundError, KeyError).

    except Exception:
        return DEFAULT_FORGE, None

2. forge_url passed only to GitHubForge, ignored for future forges (koan/app/forge/__init__.py, L54-56)
When forge_url is set but the resolved class is not GitHubForge (e.g., a future GitLabForge), the URL is silently dropped because the else branch calls cls() with no arguments. This will be a bug as soon as Phase 2a/2b lands.

Suggested fix: pass base_url unconditionally if the resolved class accepts it, or document this as a known Phase 1 limitation with a TODO.

    if forge_url and cls is GitHubForge:
        return cls(base_url=forge_url)
    return cls()

3. auth_env imports from app.github_auth but module is not in the PR (koan/app/forge/github.py, L460-462)
auth_env() does from app.github_auth import get_gh_env. While github_auth.py exists in the repo, the test for this method patches app.github_auth.get_gh_env — meaning the test never validates the actual import path works. If get_gh_env is renamed or moved, this will only fail at runtime. Not blocking, but worth noting the implicit coupling.

    def auth_env(self) -> Dict[str, str]:
        from app.github_auth import get_gh_env
        return get_gh_env()

4. pr_view returns untyped fallback dict on JSON parse failure (koan/app/forge/github.py, L515-518)
When run_gh returns non-JSON output, pr_view returns {"raw": output} — a completely different shape from the success case. Callers accessing result["number"] or result["title"] will get a KeyError. Consider raising an exception instead, or returning a dict with sentinel values for expected keys, so callers don't need to special-case the "raw" key.

        try:
            return json.loads(output)
        except (json.JSONDecodeError, TypeError):
            return {"raw": output}

5. CI status test doesn't exercise the error path correctly (koan/tests/test_forge_github.py, L1081-1087)
The test patches app.github.subprocess.run to return returncode=1, but run_gh() likely raises RuntimeError on non-zero exit codes (since it's a CLI wrapper). The test works because get_ci_status catches RuntimeError, but it's testing the exception path by accident rather than by design. A more explicit test would mock run_gh to raise RuntimeError directly.

    @patch("app.github.subprocess.run")
    def test_returns_unknown_on_failure(self, mock_run):
        mock_run.return_value = MagicMock(
            returncode=1, stderr="not found", stdout=""
        )

6. detect_fork tests mock too deep and assert on implementation detail (koan/tests/test_forge_github.py, L1121-1133)
The TestDetectFork tests patch app.github.subprocess.run and check for specific stdout like "null/null\n". This couples tests to detect_parent_repo's internal parsing of gh CLI output. It would be more robust to patch app.github.detect_parent_repo directly since GitHubForge.detect_fork is a pure delegation method — testing the delegation is sufficient.

    @patch("app.github.subprocess.run")
    def test_returns_none_when_not_fork(self, mock_run):
        mock_run.return_value = MagicMock(returncode=0, stdout="null/null\n")

🟢 Suggestions

1. "github.enterprise" substring match is too broad (koan/app/forge/__init__.py, L76)
"github.enterprise" in lower will match any URL containing that substring anywhere (e.g., not-github.enterprise-tools.com). GitHub Enterprise Server URLs use custom domains — there's no standard github.enterprise domain. This check is misleading and should be removed or replaced with a configurable GHE domain list.

if "github.com" in lower or "github.enterprise" in lower:

2. ABC with no abstract methods (koan/app/forge/base.py, L170)
ForgeProvider inherits from ABC and is imported with @abstractmethod, but no method is decorated @abstractmethod. This means ForgeProvider() can be instantiated directly, defeating the purpose of using ABC. Either mark at least cli_name() and supports() as @abstractmethod, or drop the ABC inheritance since the class relies on NotImplementedError for the protocol pattern anyway. The test file already creates a ConcreteForge subclass, so making key methods abstract wouldn't break tests.

class ForgeProvider(ABC):

3. supports() claims all features including notifications/reactions (koan/app/forge/github.py, L628-629)
GitHubForge.supports() returns True for ALL_FEATURES, but the class doesn't implement notifications or reactions methods anywhere. The supports() check is meant to let callers know what's actually available. Returning True for unimplemented features is misleading — callers that check supports(FEATURE_NOTIFICATIONS) and then call a notification method will still get NotImplementedError.

    def supports(self, feature: str) -> bool:
        return feature in ALL_FEATURES

Checklist


Summary

Solid Phase 1 foundation — clean separation, good test coverage, and proper delegation to existing modules. However, there are several issues that should be addressed before merge: (1) supports() claims features that aren't implemented, which will mislead callers; (2) pr_view returns an inconsistent dict shape on error; (3) the bare except Exception in config resolution will hide misconfigurations. The ABC without abstract methods is a design inconsistency worth resolving. None of these are hard to fix, and the overall architecture is sound.


Automated review by Kōan

@Koan-Bot
Copy link
Copy Markdown
Contributor Author

@atoomic: @Koan-Bot review

Here's the review. The PR introduces a well-structured ForgeProvider abstraction with clean delegation to existing app.github modules. Main concerns:

  1. supports() over-promisesGitHubForge.supports() returns True for ALL_FEATURES including notifications and reactions, but those methods aren't implemented. Callers checking supports() before calling will still hit NotImplementedError.

  2. Silent config error swallowing_resolve_forge_config catches all exceptions and silently falls back to GitHub. A misconfigured forge: field in projects.yaml will be invisible to the user.

  3. Inconsistent pr_view error shape — Returns {"raw": output} on JSON parse failure, which has a completely different shape from the success case and will cause KeyError in callers.

  4. ABC without abstract methodsForgeProvider extends ABC and imports @abstractmethod but never uses it, so the base class can be instantiated directly.

  5. forge_url silently dropped for non-GitHub forges — Will be a bug when Phase 2 lands.

See inline comments for details and suggested fixes.

@atoomic
Copy link
Copy Markdown
Collaborator

atoomic commented Mar 21, 2026

@Koan-Bot rebase

TODO for `forge_url` not passed to non-GitHub forges** (`koan/app/forge/__init__.py`): Documented the Phase 2 limitation with a `TODO(Phase 2)` comment so it won't be forgotten when GitLab/Gitea forges are added. Per reviewer comment Anantys-oss#2.

- **`pr_view` now raises `RuntimeError` on JSON parse failure** (`koan/app/forge/github.py`): Instead of returning `{"raw": output}` (inconsistent dict shape that would cause `KeyError` in callers), `pr_view` now raises `RuntimeError` with a descriptive message. Updated corresponding test. Per reviewer comment Anantys-oss#4.

- **`supports()` now only claims actually-implemented features** (`koan/app/forge/github.py`): Changed from `feature in ALL_FEATURES` to a curated `_SUPPORTED_FEATURES` frozenset containing only `pr`, `issues`, `ci_status`, and `pr_review_comments`. `notifications` and `reactions` are excluded since GitHubForge doesn't implement those methods. Updated tests accordingly. Per reviewer's top-level concern about `supports()` misleading callers.
@Koan-Bot
Copy link
Copy Markdown
Contributor Author

Rebase: Implement: Use Koan with Gitlab and Codeberg.org?

Branch koan.atoomic/implement-968 rebased onto main and force-pushed.

Diff: 7 files changed, 1217 insertions(+)

Review feedback was analyzed and applied.

Changes

TODO for forge_url not passed to non-GitHub forges** (koan/app/forge/__init__.py): Documented the Phase 2 limitation with a TODO(Phase 2) comment so it won't be forgotten when GitLab/Gitea forges are added. Per reviewer comment #2.

  • pr_view now raises RuntimeError on JSON parse failure (koan/app/forge/github.py): Instead of returning {"raw": output} (inconsistent dict shape that would cause KeyError in callers), pr_view now raises RuntimeError with a descriptive message. Updated corresponding test. Per reviewer comment Add mission lifecycle Telegram notifications (US 3.1) #4.

  • supports() now only claims actually-implemented features (koan/app/forge/github.py): Changed from feature in ALL_FEATURES to a curated _SUPPORTED_FEATURES frozenset containing only pr, issues, ci_status, and pr_review_comments. notifications and reactions are excluded since GitHubForge doesn't implement those methods. Updated tests accordingly. Per reviewer's top-level concern about supports() misleading callers.

Actions

  • Rebased koan.atoomic/implement-968 onto upstream/main
  • Applied review feedback
  • Force-pushed koan.atoomic/implement-968 to origin
  • CI passed

CI

CI passed.


Automated by Kōan

@atoomic
Copy link
Copy Markdown
Collaborator

atoomic commented Mar 24, 2026

Also view #977

@sukria
Copy link
Copy Markdown
Collaborator

sukria commented Mar 26, 2026

That's great, but we need a real-life end-to-end testing before merging to main.

@GeekRuthie : would you like to test this branch on your existing setup and report here? That would be great!

@GeekRuthie
Copy link
Copy Markdown

@sukria : I will do so, probably this weekend--I'll try to squeeze it in! :)

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.

Use Koan with Gitlab and Codeberg.org?

4 participants