Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 122 additions & 14 deletions .github/workflows/devin-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,136 @@ name: Devin Review

on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to review
required: true
type: string

jobs:
devin-review:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
pull-requests: read

steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Resolve Devin Review URL
id: review
uses: actions/github-script@v8
env:
WORKFLOW_PR_NUMBER: ${{ inputs.pr_number }}
with:
fetch-depth: 0
script: |
const prNumber = context.eventName === 'workflow_dispatch'
? Number(process.env.WORKFLOW_PR_NUMBER)
: context.payload.pull_request.number;

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
if (!Number.isInteger(prNumber) || prNumber <= 0) {
core.setFailed(`Invalid pull request number: ${prNumber}`);
return;
}

const { owner, repo } = context.repo;
const reviewUrl = `https://devinreview.com/${owner}/${repo}/pull/${prNumber}`;
const prUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}`;

core.setOutput('number', String(prNumber));
core.setOutput('pr_url', prUrl);
core.setOutput('review_url', reviewUrl);

- name: Warm Devin Review page
env:
REVIEW_URL: ${{ steps.review.outputs.review_url }}
run: |
curl --fail --silent --show-error --location "$REVIEW_URL" --output /dev/null

- name: Publish Devin Review summary
env:
PR_NUMBER: ${{ steps.review.outputs.number }}
PR_URL: ${{ steps.review.outputs.pr_url }}
REVIEW_URL: ${{ steps.review.outputs.review_url }}
run: |
{
echo "Devin Review is available for PR #${PR_NUMBER}."
echo
echo "- GitHub PR: ${PR_URL}"
echo "- Devin Review: ${REVIEW_URL}"
echo
echo "This workflow intentionally does not use DEVIN_API_KEY."
echo "For automatic Devin statuses or comments inside GitHub, connect the Devin GitHub integration and enable auto-review in Devin settings."
} >> "$GITHUB_STEP_SUMMARY"

- name: Run Devin Review
# Use script to emulate a TTY as devin-review requires terminal features
# The -q flag suppresses script output, -e exits with command exit code,
# and -c runs the command. /dev/null discards script's own output.
- name: Upsert PR comment with Devin Review link
id: comment
uses: actions/github-script@v8
env:
CI: true # Ensures the tool runs in non-interactive CI mode
PR_NUMBER: ${{ steps.review.outputs.number }}
PR_URL: ${{ steps.review.outputs.pr_url }}
REVIEW_URL: ${{ steps.review.outputs.review_url }}
with:
script: |
const marker = '<!-- devin-review-link -->';
const issue_number = Number(process.env.PR_NUMBER);
core.setOutput('posted', 'false');
const body = [
marker,
'Devin Review is available for this pull request.',
'',
`- GitHub PR: ${process.env.PR_URL}`,
`- Devin Review: ${process.env.REVIEW_URL}`,
'',
'This link opens the hosted Devin Review page for the current PR.',
].join('\n');

try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
per_page: 100,
});

const existing = comments.find((comment) => comment.body && comment.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated existing Devin Review comment: ${existing.html_url}`);
} else {
const created = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
core.info(`Created Devin Review comment: ${created.data.html_url}`);
}

core.setOutput('posted', 'true');
} catch (error) {
if (error && error.status === 403) {
core.warning('PR comment was not posted because this repository grants GitHub Actions a read-only GITHUB_TOKEN.');
} else {
throw error;
}
}

- name: Report comment permission limitation
if: ${{ steps.comment.outputs.posted != 'true' }}
run: |
script -q -e -c "npx devin-review ${{ github.event.pull_request.html_url }}" /dev/null
{
echo
echo "PR comment was not posted automatically."
echo
echo "This repository currently sets GitHub Actions GITHUB_TOKEN to read-only permissions, so the workflow cannot create issue comments."
echo "If repository workflow permissions are changed to read/write, this step will start posting or updating the PR comment automatically."
} >> "$GITHUB_STEP_SUMMARY"
4 changes: 2 additions & 2 deletions src/hyperview/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ def _build_parser() -> argparse.ArgumentParser:
)
parser.add_argument(
"--method",
choices=["umap"],
choices=["umap", "pca"],
default="umap",
help="Projection method (currently only 'umap')",
help="Projection method: 'umap' (default) or 'pca'",
)
parser.add_argument(
"--geometry",
Expand Down
9 changes: 5 additions & 4 deletions src/hyperview/core/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,12 @@ def compute_visualization(

Args:
space_key: Embedding space to project. If None, uses the first available.
method: Projection method ('umap' supported).
method: Projection method ('umap' or 'pca'). PCA is faster and
deterministic; UMAP captures nonlinear structure better.
geometry: Output geometry type ('euclidean' or 'poincare').
n_neighbors: Number of neighbors for UMAP.
min_dist: Minimum distance for UMAP.
metric: Distance metric for UMAP.
n_neighbors: Number of neighbors for UMAP (ignored for PCA).
min_dist: Minimum distance for UMAP (ignored for PCA).
metric: Distance metric for UMAP (ignored for PCA).
force: Force recomputation even if layout exists.

Returns:
Expand Down
29 changes: 18 additions & 11 deletions src/hyperview/embeddings/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def compute_layout(
Args:
storage: Storage backend with embeddings.
space_key: Embedding space to project. If None, uses the first available.
method: Projection method ('umap' supported).
method: Projection method ('umap' or 'pca').
geometry: Output geometry type ('euclidean' or 'poincare').
n_neighbors: Number of neighbors for UMAP.
min_dist: Minimum distance for UMAP.
Expand All @@ -122,8 +122,8 @@ def compute_layout(
"""
from hyperview.embeddings.projection import ProjectionEngine

if method != "umap":
raise ValueError(f"Invalid method: {method}. Only 'umap' is supported.")
if method not in ("umap", "pca"):
raise ValueError(f"Invalid method: {method}. Supported methods: 'umap', 'pca'.")

if geometry not in ("euclidean", "poincare"):
raise ValueError(f"Invalid geometry: {geometry}. Must be 'euclidean' or 'poincare'.")
Expand Down Expand Up @@ -154,14 +154,21 @@ def compute_layout(
if len(ids) == 0:
raise ValueError(f"No embeddings in space '{space_key}'. Call compute_embeddings() first.")

if len(ids) < 3:
raise ValueError(f"Need at least 3 samples for visualization, have {len(ids)}")

layout_params = {
"n_neighbors": n_neighbors,
"min_dist": min_dist,
"metric": metric,
}
min_samples = 3 if method == "umap" else 2
if len(ids) < min_samples:
raise ValueError(
f"Need at least {min_samples} samples for {method} visualization, have {len(ids)}"
)

if method == "umap":
layout_params = {
"n_neighbors": n_neighbors,
"min_dist": min_dist,
"metric": metric,
}
else:
# PCA is deterministic with no tuning parameters
layout_params = {}
layout_key = make_layout_key(space_key, method, geometry, layout_params)

if not force:
Expand Down
Loading