Skip to content
Merged
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
20 changes: 15 additions & 5 deletions .github/workflows/check-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,38 @@ jobs:
check-schema:
name: Check schema is in sync
runs-on: ubuntu-latest
env:
BOUNDLESS_NESTJS_TOKEN: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
Comment on lines 17 to 18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect checkout steps and whether persist-credentials is set
rg -n -C3 'uses:\s*actions/checkout@' .github/workflows/*.yml .github/workflows/*.yaml
rg -n 'persist-credentials:\s*false' .github/workflows/*.yml .github/workflows/*.yaml

Repository: boundlessfi/bounties

Length of output: 1670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show the relevant section of the workflow with line numbers
nl -ba .github/workflows/check-schema.yml | sed -n '1,120p'

# 2) Search only .yml files for persist-credentials on checkout steps
rg -n -C2 'uses:\s*actions/checkout@' .github/workflows/*.yml
rg -n 'persist-credentials:\s*false' .github/workflows/*.yml || true
rg -n 'persist-credentials:' .github/workflows/*.yml || true

Repository: boundlessfi/bounties

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show the beginning of the workflow with line numbers
cat -n .github/workflows/check-schema.yml | sed -n '1,140p'

# 2) Search only .yml files for persist-credentials on checkout steps
rg -n -C2 'uses:\s*actions/checkout@' .github/workflows/*.yml
rg -n 'persist-credentials:\s*false' .github/workflows/*.yml || true
rg -n 'persist-credentials:' .github/workflows/*.yml || true

Repository: boundlessfi/bounties

Length of output: 2727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm persist-credentials usage only in check-schema.yml
rg -n 'persist-credentials' .github/workflows/check-schema.yml || true
rg -n -C2 'uses:\s*actions/checkout@' .github/workflows/check-schema.yml

# Reprint the relevant section cleanly
cat -n .github/workflows/check-schema.yml | sed -n '10,40p'

Repository: boundlessfi/bounties

Length of output: 1689


Disable credential persistence on checkout steps in .github/workflows/check-schema.yml.

actions/checkout@v4 should set persist-credentials: false on both checkout steps (lines 17-18 and 20-26) to prevent the token/credentials from being written to the runner’s local git config for subsequent steps.

Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
 
       - name: Checkout canonical schema repo (private)
         if: ${{ env.BOUNDLESS_NESTJS_TOKEN != '' }}
         uses: actions/checkout@v4
         with:
           repository: boundlessfi/boundless-nestjs
           path: boundless-nestjs
           token: ${{ env.BOUNDLESS_NESTJS_TOKEN }}
+          persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 17-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check-schema.yml around lines 17 - 18, Update both
checkout steps that use actions/checkout@v4 to set persist-credentials: false so
the runner does not persist the token to local git config; locate the checkout
invocations (the steps using actions/checkout@v4) and add a persist-credentials:
false entry under each step's with: block (ensure YAML indentation is correct).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all workflow `uses:` entries are SHA pinned (40-hex)
rg -n '^\s*uses:\s*[^@]+@' .github/workflows/*.yml .github/workflows/*.yaml

Repository: boundlessfi/bounties

Length of output: 931


Pin GitHub Actions to commit SHAs instead of mutable tags.

.github/workflows/check-schema.yml uses mutable action tags (actions/checkout@v4 at lines 18 and 22, actions/setup-node@v4 at line 35); pin to full 40-hex commit SHAs for reproducible, safer CI.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 17-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check-schema.yml at line 18, The workflow uses mutable
tags "actions/checkout@v4" and "actions/setup-node@v4"; update those uses
entries to the corresponding full 40-character commit SHAs (replace each "`@v4`"
occurrence for checkout and setup-node with the commit SHA from the respective
GitHub Actions repo) so the CI references an exact immutable revision; ensure
you update all occurrences in the file (both checkout usages and the setup-node
usage) and commit the updated workflow.


- name: Checkout canonical schema repo (private)
if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN != '' }}
if: ${{ env.BOUNDLESS_NESTJS_TOKEN != '' }}
uses: actions/checkout@v4
with:
repository: boundlessfi/boundless-nestjs
path: boundless-nestjs
token: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }}
token: ${{ env.BOUNDLESS_NESTJS_TOKEN }}

- name: Note about private repo
if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN == '' }}
if: ${{ env.BOUNDLESS_NESTJS_TOKEN == '' }}
run: |
echo "No secret BOUNDLESS_NESTJS_TOKEN provided; skipping checkout of private repo."
echo "If the canonical schema is in the private repo, add a repository secret named BOUNDLESS_NESTJS_TOKEN with a PAT that has access to boundlessfi/boundless-nestjs."
echo "This is expected for forked pull requests where repository secrets are not exposed."

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "20"

- name: Run schema check
if: ${{ env.BOUNDLESS_NESTJS_TOKEN != '' }}
run: node ./scripts/sync-schema.js --check

- name: Skip schema check without private repo token
if: ${{ env.BOUNDLESS_NESTJS_TOKEN == '' }}
run: |
echo "Skipping schema sync check because BOUNDLESS_NESTJS_TOKEN is not available."
echo "Maintainers can run the full schema check from a trusted branch with the secret available."
129 changes: 120 additions & 9 deletions components/bounty/application-review-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
"use client";

import { useState } from "react";
import { useMemo, useState } from "react";

Check warning on line 2 in components/bounty/application-review-dashboard.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (22.x)

'useMemo' is defined but never used
import {
Users,
CheckCircle,
Clock,
Star,
Trophy,
ArrowRight,
XCircle,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

import { useSelectApplicant } from "@/hooks/use-bounty-application";
import { Textarea } from "@/components/ui/textarea";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
import {
useDeclineApplicant,
useSelectApplicant,
} from "@/hooks/use-bounty-application";

export interface Application {
id: string;
Expand Down Expand Up @@ -45,9 +59,14 @@
applications,
}: ApplicationReviewDashboardProps) {
const [selectedForCompare, setSelectedForCompare] = useState<string[]>([]);
const [reviewApplications, setReviewApplications] = useState(applications);
const [declineTarget, setDeclineTarget] = useState<Application | null>(null);
const [declineReason, setDeclineReason] = useState("");
const { mutate: selectApplicant, isPending: isSelecting } =
useSelectApplicant();

useEffect(() => {
setReviewApplications(applications);
}, [applications]);
Comment on lines +67 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: useEffect is not imported — this call hits the throwing stub at Lines 317-319 and crashes the component on every render.

react is imported as { useMemo, useState } (Line 2), so useEffect resolves to the hoisted local function useEffect (Lines 317-319) which does throw new Error("Function not implemented."). The component will throw immediately.

🐛 Proposed fix: import the real `useEffect` and delete the stub

Line 2 (also drops the now-unused useMemo):

-import { useMemo, useState } from "react";
+import { useEffect, useState } from "react";

Lines 317-319:

-function useEffect(arg0: () => void, arg1: Application[][]) {
-  throw new Error("Function not implemented.");
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty/application-review-dashboard.tsx` around lines 67 - 69, The
component is crashing because useEffect is not imported from React and the local
hoisted stub function useEffect (the one that does `throw new Error("Function
not implemented.")` around lines 317-319) is being called; update the React
import to include useEffect (and drop unused useMemo if appropriate), remove the
local stub function named useEffect, and ensure the effect call inside the
component that calls setReviewApplications(applications) uses the real React
useEffect.

const handleSelectApplicant = (applicantAddress: string) => {
selectApplicant({
bountyId,
Expand All @@ -56,6 +75,44 @@
});
};

const { mutate: declineApplicant, isPending: isDeclining } =
useDeclineApplicant();

const handleDeclineApplicant = () => {
if (!declineTarget) return;

const previousApplications = reviewApplications;
const applicantAddress = declineTarget.applicantAddress;
const reason = declineReason.trim();

setReviewApplications((current) =>
current.filter((app) => app.applicantAddress !== applicantAddress),
);

setSelectedForCompare((current) =>
current.filter((id) => id !== declineTarget.id),
);

declineApplicant(
{
bountyId,
applicantAddress,
reason,
},
{
onSuccess: () => {
toast.success("Applicant declined");
setDeclineTarget(null);
setDeclineReason("");
},
onError: () => {
setReviewApplications(previousApplications);
toast.error("Failed to decline applicant");
},
},
);
};
const visibleApplications = reviewApplications;
const toggleCompare = (id: string) => {
if (selectedForCompare.includes(id)) {
setSelectedForCompare(selectedForCompare.filter((i) => i !== id));
Expand Down Expand Up @@ -113,6 +170,19 @@
{selectedForCompare.includes(app.id) ? "Comparing" : "Compare"}
</Button>
)}
<Button
variant="outline"
size="sm"
className="border-red-500/50 text-red-500 hover:bg-red-500/10 hover:text-red-400"
onClick={() => {
setDeclineTarget(app);
setDeclineReason("");
}}
disabled={isDeclining}
>
<XCircle className="size-4 mr-1" />
Decline
</Button>
<Button
size="sm"
onClick={() => handleSelectApplicant(app.applicantAddress)}
Expand Down Expand Up @@ -159,7 +229,7 @@
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold flex items-center gap-2">
<Users className="size-5 text-primary" />
Review Applications ({applications.length})
Review Applications ({visibleApplications.length})
</h2>
{selectedForCompare.length > 0 && (
<Badge variant="outline" className="text-sm">
Expand All @@ -168,7 +238,7 @@
)}
</div>

{applications.length === 0 ? (
{visibleApplications.length === 0 ? (
<Card className="border-dashed border-gray-800 bg-transparent">
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<Users className="size-12 text-gray-600 mb-4" />
Expand All @@ -194,16 +264,57 @@
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{applications
{visibleApplications
.filter((app) => selectedForCompare.includes(app.id))
.map((app) => renderApplicationCard(app, false))}
</div>
</div>
) : (
<div className="grid gap-4">
{applications.map((app) => renderApplicationCard(app, false))}
{visibleApplications.map((app) => renderApplicationCard(app, false))}
</div>
)}
<AlertDialog
open={!!declineTarget}
onOpenChange={(open) => {
if (!open) {
setDeclineTarget(null);
setDeclineReason("");
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Decline applicant?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the applicant from the review queue. You can
include an optional reason for internal records.
</AlertDialogDescription>
</AlertDialogHeader>

<Textarea
value={declineReason}
onChange={(event) => setDeclineReason(event.target.value)}
placeholder="Optional reason for declining this applicant"
className="min-h-24"
/>

<AlertDialogFooter>
<AlertDialogCancel disabled={isDeclining}>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 text-white hover:bg-red-700"
disabled={isDeclining}
onClick={handleDeclineApplicant}
>
Decline applicant
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
function useEffect(arg0: () => void, arg1: Application[][]) {

Check warning on line 317 in components/bounty/application-review-dashboard.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (22.x)

'arg1' is defined but never used

Check warning on line 317 in components/bounty/application-review-dashboard.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (22.x)

'arg0' is defined but never used
throw new Error("Function not implemented.");
}

91 changes: 91 additions & 0 deletions hooks/use-bounty-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,97 @@ export function useSelectApplicant() {
},
});
}
// ---------------------------------------------------------------------------
// Hook: decline applicant
// ---------------------------------------------------------------------------

type DeclinedApplicationRecord = {
id?: string;
bountyId?: string;
applicantAddress?: string;
status?: string;
declinedReason?: string;
declineReason?: string;
declinedAt?: string;
};

type BountyWithApplications = BountyQuery & {
bounty?: BountyQuery["bounty"] & {
applications?: DeclinedApplicationRecord[];
};
};

export function useDeclineApplicant() {
const qc = useQueryClient();

return useMutation({
mutationFn: async ({
bountyId,
applicantAddress,
reason,
}: {
bountyId: string;
applicantAddress: string;
reason?: string;
}) => {
return {
bountyId,
applicantAddress,
reason: reason?.trim() || undefined,
declinedAt: new Date().toISOString(),
};
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.

onMutate: async ({ bountyId, applicantAddress, reason }) => {
await qc.cancelQueries({ queryKey: bountyKeys.detail(bountyId) });

const prev = qc.getQueryData<BountyWithApplications>(
bountyKeys.detail(bountyId),
);

if (prev?.bounty?.applications) {
const declinedAt = new Date().toISOString();

qc.setQueryData<BountyWithApplications>(bountyKeys.detail(bountyId), {
...prev,
bounty: {
...prev.bounty,
applications: prev.bounty.applications
.map((application) =>
application.applicantAddress === applicantAddress
? {
...application,
status: "DECLINED",
declineReason: reason?.trim() || undefined,
declinedReason: reason?.trim() || undefined,
declinedAt,
}
: application,
)
.filter(
(application) =>
application.applicantAddress !== applicantAddress,
),
updatedAt: declinedAt,
Comment on lines +214 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

.filter() discards the record you just marked DECLINED — the decline reason is never recorded in cache.

The .map() marks the matching application as DECLINED and stores declineReason/declinedReason, but the chained .filter() then removes that exact entry, throwing away all of that work. This violates the requirement to record the decline reason in the cache.

Drop the filter so the declined record (with its reason) persists; the dashboard already removes the applicant from its own visible list.

🐛 Proposed fix: keep the declined record instead of filtering it out
             applications: prev.bounty.applications
               .map((application) =>
                 application.applicantAddress === applicantAddress
                   ? {
                       ...application,
                       status: "DECLINED",
                       declineReason: reason?.trim() || undefined,
                       declinedReason: reason?.trim() || undefined,
                       declinedAt,
                     }
                   : application,
-              )
-              .filter(
-                (application) =>
-                  application.applicantAddress !== applicantAddress,
-              ),
+              ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
...prev.bounty,
applications: prev.bounty.applications
.map((application) =>
application.applicantAddress === applicantAddress
? {
...application,
status: "DECLINED",
declineReason: reason?.trim() || undefined,
declinedReason: reason?.trim() || undefined,
declinedAt,
}
: application,
)
.filter(
(application) =>
application.applicantAddress !== applicantAddress,
),
updatedAt: declinedAt,
...prev.bounty,
applications: prev.bounty.applications
.map((application) =>
application.applicantAddress === applicantAddress
? {
...application,
status: "DECLINED",
declineReason: reason?.trim() || undefined,
declinedReason: reason?.trim() || undefined,
declinedAt,
}
: application,
),
updatedAt: declinedAt,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 214 - 231, The current update
to prev.bounty.applications maps the matching application to status "DECLINED"
with declineReason/declinedReason and declinedAt but then immediately chains
.filter(...) which removes that declined record from the cache; remove the
chained .filter so the mapped DECLINED application (with reason and declinedAt)
is kept in prev.bounty.applications, ensuring the declineReason persists in
cache (update the code around the applications mapping that references
applicantAddress, reason, and declinedAt in hooks/use-bounty-application.ts).

},
});
}

return { prev, bountyId };
},

onError: (_error, _variables, context) => {
if (context?.prev) {
qc.setQueryData(bountyKeys.detail(context.bountyId), context.prev);
}
},

onSettled: (_result, _error, variables) => {
qc.invalidateQueries({ queryKey: bountyKeys.detail(variables.bountyId) });
qc.invalidateQueries({ queryKey: bountyKeys.lists() });
},
});
}

// ---------------------------------------------------------------------------
// Hook: submit work (BountyRegistry.submit_work)
Expand Down
Loading