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
80 changes: 80 additions & 0 deletions .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: CLI Release

on:
workflow_dispatch:
Comment on lines +3 to +4
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

Restrict release execution to main to avoid accidental off-branch releases.

workflow_dispatch can be run on arbitrary refs. Without a branch guard, Line 3 and Line 48 can release from non-main commits.

🔒 Proposed guard
 jobs:
   build:
+    if: github.ref == 'refs/heads/main'
     name: Build ${{ matrix.target }}
@@
   release:
+    if: github.ref == 'refs/heads/main'
     name: Create Release

Also applies to: 48-51

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cli-release.yml around lines 3 - 4, The workflow currently
allows workflow_dispatch to run on any ref; restrict manual releases to main by
updating the on.workflow_dispatch trigger to include a branch filter (e.g., for
refs/heads/main) or add a top-level conditional that checks github.ref ==
'refs/heads/main' so that the dispatch only executes on main; apply the same
guard to the other manual dispatch block referenced by the second
workflow_dispatch occurrence.


jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
matrix:
include:
- target: bun-darwin-arm64
os: macos-latest
artifact: chp-darwin-arm64
- target: bun-darwin-x64
os: macos-latest
artifact: chp-darwin-x64
- target: bun-linux-x64
os: ubuntu-latest
artifact: chp-linux-x64
- target: bun-linux-arm64
os: ubuntu-latest
artifact: chp-linux-arm64

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
Comment on lines +28 to +31
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify workflow action refs that are not SHA-pinned
set -euo pipefail
rg -n 'uses:\s*[^@]+@([vV].+|main|master)$' .github/workflows/cli-release.yml

Repository: techulus/changes-page

Length of output: 314


Pin action references to immutable commit SHAs.

Using mutable major version tags in a release workflow is a supply-chain risk. Lines 30, 33, 45, 59, 66, and 71 should be pinned to full commit SHAs:

  • Line 30: actions/checkout@v6
  • Line 33: oven-sh/setup-bun@v2
  • Line 45: actions/upload-artifact@v6
  • Line 59: actions/checkout@v6
  • Line 66: actions/download-artifact@v6
  • Line 71: softprops/action-gh-release@v2
🧷 Example hardening pattern
-      - name: Checkout code
-        uses: actions/checkout@v6
+      - name: Checkout code
+        uses: actions/checkout@<full_commit_sha> # v6

-      - name: Setup Bun
-        uses: oven-sh/setup-bun@v2
+      - name: Setup Bun
+        uses: oven-sh/setup-bun@<full_commit_sha> # v2

-      - name: Upload artifact
-        uses: actions/upload-artifact@v6
+      - name: Upload artifact
+        uses: actions/upload-artifact@<full_commit_sha> # v6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cli-release.yml around lines 28 - 31, Replace mutable
version tags with immutable commit SHAs for all GitHub Actions used in this
workflow: pin actions/checkout (both occurrences), oven-sh/setup-bun,
actions/upload-artifact, actions/download-artifact, and
softprops/action-gh-release to their respective full commit SHA references
instead of `@v6/`@v2/@v6/@v6/@v2; locate the lines using the identifiers
actions/checkout, oven-sh/setup-bun, actions/upload-artifact,
actions/download-artifact, and softprops/action-gh-release in the
.github/workflows/cli-release.yml and update each "uses:" value to the exact
commit SHA (e.g., actions/checkout@<commit-sha>) ensuring consistency and
immutability across the workflow.


- name: Install dependencies
run: cd packages/cli && bun install
Comment on lines +30 to +34
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 | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

According to Bun's official docs, what is the recommended CI command for lockfile-enforced installs: bun installorbun ci?

💡 Result:

According to Bun's official documentation, the recommended CI command for lockfile-enforced installs is bun ci. This command is equivalent to bun install --frozen-lockfile, installs exact versions from bun.lock, and fails if package.json does not match the lockfile. The docs explicitly state: "For CI/CD environments that want to enforce reproducible builds, use bun ci to fail the build if the package.json is out of sync with the lockfile" and "instead of running bun install, run bun ci". You must commit bun.lock to version control for this to work.

Citations:


🏁 Script executed:

# Check for .bun-version file
fd -i '\.bun-version$' || echo "No .bun-version file found"

# Check for bun.lock
fd -i 'bun\.lock$'

# Check package.json files for any bun version specification
fd 'package\.json$' -x grep -l "bun" {} \; 2>/dev/null | head -10

Repository: techulus/changes-page

Length of output: 113


🏁 Script executed:

# Search for bun.lock files in the repository
find . -name "bun.lock*" -type f

# Check the packages/cli directory structure and look for package.json and bun.lock
ls -la packages/cli/ 2>/dev/null | head -20

# Check if there's a .bun-version or similar configuration in root or packages/cli
find . -maxdepth 2 -name ".*bun*" -o -name "*bun-version*"

# Check the content of packages/cli/package.json for bun references
cat packages/cli/package.json 2>/dev/null | head -50

Repository: techulus/changes-page

Length of output: 1086


🏁 Script executed:

# Comprehensive search for bun.lock anywhere in repo
find . -type f -name "*bun*" | grep -v node_modules | grep -v ".git"

# Check root directory explicitly
ls -la | grep bun

# Search for any references to bun.lock or bun.ci in workflow files
rg "bun\.lock|bun ci" --type yaml

Repository: techulus/changes-page

Length of output: 47


🏁 Script executed:

# Check if bun.lock exists in root
ls -la bun.lock 2>&1

# Look at the entire file list to see what lockfiles exist
ls -la *.lock 2>&1 || echo "No lockfiles found"

# Check git-tracked files for anything with 'bun' in name
git ls-files | grep -i bun || echo "No bun-related files in git"

# Verify the current workflow file content to understand full context
cat .github/workflows/cli-release.yml

Repository: techulus/changes-page

Length of output: 2441


Pin Bun version and ensure lockfile-enforced CI installs.

Line 31 installs the latest Bun implicitly, and Line 34 uses bun install without lockfile enforcement, making release outputs non-reproducible. Pin the Bun version in the workflow setup. To use bun ci for reproducible installs, first commit bun.lock to the repository (generated locally via bun install).

  • Add bun-version to the setup step (e.g., bun-version: "1.2.20")
  • Commit bun.lock to the repository if not already present
  • Switch to bun ci in the install step to enforce exact versions from the lockfile
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cli-release.yml around lines 30 - 34, Update the GitHub
Actions steps named "Setup Bun" and "Install dependencies": pin the Bun runner
by adding the `bun-version` input (e.g., `bun-version: "1.2.20"`) to the
oven-sh/setup-bun@v2 step and switch the install step from `bun install` to `bun
ci` to enforce the lockfile; also ensure `bun.lock` is committed to the repo so
`bun ci` produces reproducible installs.


- name: Build binary
run: |
cd packages/cli
VERSION=$(jq -r .version package.json)
bun build --compile --target=${{ matrix.target }} --define "__CLI_VERSION__=\"${VERSION}\"" ./src/index.ts --outfile ${{ matrix.artifact }}

- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact }}
path: packages/cli/${{ matrix.artifact }}

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Read version
id: version
run: echo "version=$(jq -r .version packages/cli/package.json)" >> "$GITHUB_OUTPUT"

- name: Download all artifacts
uses: actions/download-artifact@v6
with:
path: artifacts

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: cli-v${{ steps.version.outputs.version }}
name: CLI v${{ steps.version.outputs.version }}
generate_release_notes: true
files: |
artifacts/chp-darwin-arm64/chp-darwin-arm64
artifacts/chp-darwin-x64/chp-darwin-x64
artifacts/chp-linux-x64/chp-linux-x64
artifacts/chp-linux-arm64/chp-linux-arm64
85 changes: 78 additions & 7 deletions apps/web/pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import {
createToastWrapper,
notifyError,
notifySuccess,
} from "../components/core/toast.component";
import { DEFAULT_TITLE, SUBTITLE } from "../data/marketing.data";
import { ROUTES } from "../data/routes.data";
import logoImage from "../public/images/logo.png";
Expand All @@ -19,6 +25,39 @@ export default function Login() {
const { supabase } = useUserData();
const prefersColorScheme = usePrefersColorScheme();

const [email, setEmail] = useState("");
const [sending, setSending] = useState(false);
const [submitted, setSubmitted] = useState(false);

const redirectTo =
getAppBaseURL() +
ROUTES.LOGIN_CALLBACK +
(redirectedFrom ? `?redirectedFrom=${redirectedFrom}` : "");
Comment on lines +32 to +35
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

Build redirectTo via URL to avoid malformed/unsafe query composition.

Current string concatenation does not normalize redirectedFrom (it can be string[]) and does not encode query content.

Suggested fix
-  const redirectTo =
-    getAppBaseURL() +
-    ROUTES.LOGIN_CALLBACK +
-    (redirectedFrom ? `?redirectedFrom=${redirectedFrom}` : "");
+  const redirectedFromValue = Array.isArray(redirectedFrom)
+    ? redirectedFrom[0]
+    : redirectedFrom;
+  const redirectUrl = new URL(ROUTES.LOGIN_CALLBACK, getAppBaseURL());
+  if (redirectedFromValue) {
+    redirectUrl.searchParams.set("redirectedFrom", redirectedFromValue);
+  }
+  const redirectTo = redirectUrl.toString();
📝 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
const redirectTo =
getAppBaseURL() +
ROUTES.LOGIN_CALLBACK +
(redirectedFrom ? `?redirectedFrom=${redirectedFrom}` : "");
const redirectedFromValue = Array.isArray(redirectedFrom)
? redirectedFrom[0]
: redirectedFrom;
const redirectUrl = new URL(ROUTES.LOGIN_CALLBACK, getAppBaseURL());
if (redirectedFromValue) {
redirectUrl.searchParams.set("redirectedFrom", redirectedFromValue);
}
const redirectTo = redirectUrl.toString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/pages/login.tsx` around lines 32 - 35, redirectTo is currently built
via string concatenation and doesn't handle redirectedFrom being a string[] or
URL-encode the query; replace the concat with a URL-based builder: create a new
URL using ROUTES.LOGIN_CALLBACK and getAppBaseURL(), normalize redirectedFrom
(e.g. const rf = Array.isArray(redirectedFrom) ? redirectedFrom[0] :
redirectedFrom), and if rf is present call
url.searchParams.set('redirectedFrom', rf); then use url.toString() for
redirectTo to ensure safe, normalized query composition.


async function handleEmailLogin(e: React.FormEvent) {
e.preventDefault();
if (!email || sending) return;
setSending(true);
const { error } = await supabase.auth.signInWithOtp({
email,
options: { shouldCreateUser: false, emailRedirectTo: redirectTo },
});
setSending(false);
if (error) {
const msg = error.message.toLowerCase();
if (msg.includes("signups not allowed") || msg.includes("not found")) {
notifyError(
"No account found for this email. Sign up with Google or GitHub."
);
} else {
notifyError(error.message);
}
return;
}
setSubmitted(true);
notifySuccess("Check your email for the login link.");
}
Comment on lines +37 to +59
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

Harden OTP submit flow: always clear loading and avoid account-enumeration messaging.

Two issues here:

  1. sending is not reset if the async call throws.
  2. "No account found..." reveals account existence; use a generic success-style response for both success and not-found cases.
Suggested fix
 async function handleEmailLogin(e: React.FormEvent) {
   e.preventDefault();
-  if (!email || sending) return;
-  setSending(true);
-  const { error } = await supabase.auth.signInWithOtp({
-    email,
-    options: { shouldCreateUser: false, emailRedirectTo: redirectTo },
-  });
-  setSending(false);
-  if (error) {
-    const msg = error.message.toLowerCase();
-    if (msg.includes("signups not allowed") || msg.includes("not found")) {
-      notifyError(
-        "No account found for this email. Sign up with Google or GitHub."
-      );
-    } else {
-      notifyError(error.message);
-    }
-    return;
-  }
-  setSubmitted(true);
-  notifySuccess("Check your email for the login link.");
+  const normalizedEmail = email.trim();
+  if (!normalizedEmail || sending) return;
+
+  setSending(true);
+  try {
+    const { error } = await supabase.auth.signInWithOtp({
+      email: normalizedEmail,
+      options: { shouldCreateUser: false, emailRedirectTo: redirectTo },
+    });
+
+    const msg = error?.message.toLowerCase() ?? "";
+    const isNotFound =
+      msg.includes("signups not allowed") || msg.includes("not found");
+
+    if (error && !isNotFound) {
+      notifyError(error.message);
+      return;
+    }
+
+    setSubmitted(true);
+    notifySuccess("If an account exists for this email, you'll receive a login link.");
+  } catch {
+    notifyError("Unable to send login link. Please try again.");
+  } finally {
+    setSending(false);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/pages/login.tsx` around lines 37 - 59, In handleEmailLogin, ensure
sending is always cleared by wrapping the async supabase.auth.signInWithOtp call
in try/catch/finally and calling setSending(false) in the finally block;
additionally, avoid account-enumeration by not revealing "No account found" —
when signInWithOtp returns an error that indicates "signups not allowed" or "not
found" treat it the same as success (setSubmitted(true) and show the generic
success notification via notifySuccess("Check your email for the login link." or
similar), while still surfacing other unexpected errors with
notifyError(error.message)); reference handleEmailLogin, sending/setSending,
supabase.auth.signInWithOtp, setSubmitted, notifySuccess/notifyError.


const fontFamily =
"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif";

Expand Down Expand Up @@ -53,6 +92,8 @@ export default function Login() {
}}
/>

{createToastWrapper(prefersColorScheme)}

<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-800 py-12 px-4 sm:px-6 lg:px-8 overflow-y-auto">
<div className="max-w-md w-full space-y-4">
<div className="flex flex-col items-center justify-center mb-8">
Expand Down Expand Up @@ -111,15 +152,45 @@ export default function Login() {
},
}}
providers={["github", "google"]}
redirectTo={
getAppBaseURL() +
ROUTES.LOGIN_CALLBACK +
(redirectedFrom ? `?redirectedFrom=${redirectedFrom}` : "")
}
redirectTo={redirectTo}
theme={prefersColorScheme === "dark" ? "dark" : "light"}
view="magic_link"
showLinks={false}
onlyThirdPartyProviders
/>

<div className="relative my-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300 dark:border-gray-700" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-gray-50 dark:bg-gray-800 px-2 text-gray-500 dark:text-gray-400">
Or sign in with email
</span>
</div>
</div>

<form onSubmit={handleEmailLogin} className="space-y-3">
<input
type="email"
required
autoComplete="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={sending || submitted}
className="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 disabled:opacity-60"
/>
<button
type="submit"
disabled={sending || submitted}
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-60"
>
{submitted
? "Check your email"
: sending
? "Sending…"
: "Send magic link"}
</button>
</form>
</div>
</div>
</>
Expand Down