Skip to content

Commit 1ed3409

Browse files
committed
merge(main): bring OSS hotfixes through next
Port the main-only fixes onto next's rewritten paths: Jira JWT/JQL status-sync, outbound hook config, Slack support notify, portal-invite callback parsing, session revoke on portal-user removal, and prompt=login as the OIDC default. Keep next's invitation transaction, email inbound, and sentiment stack. Cloud Agent env files come across as-is.
2 parents a2b05bc + d031f1d commit 1ed3409

35 files changed

Lines changed: 1220 additions & 71 deletions

.cursor/Dockerfile

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Base image for the Quackback Cloud Agent environment.
2+
#
3+
# Provides the slow, stable system layer: Bun (pinned to the repo's
4+
# packageManager version), Docker Engine + Compose (used to run the local
5+
# datastores from docker-compose.yml), and the userspace overlay tooling that
6+
# lets dockerd run nested inside the Cloud Agent VM. Repo dependencies and
7+
# per-boot services are handled by install.sh / start.sh, not here.
8+
FROM ubuntu:24.04
9+
10+
ENV DEBIAN_FRONTEND=noninteractive
11+
12+
# Core tooling + Docker's nested-container prerequisites. fuse-overlayfs lets
13+
# the Docker daemon use a userspace overlay storage driver, which is required
14+
# because the native overlay driver cannot create whiteout device nodes inside
15+
# an unprivileged nested container.
16+
RUN apt-get update \
17+
&& apt-get install -y --no-install-recommends \
18+
ca-certificates \
19+
curl \
20+
git \
21+
gnupg \
22+
openssl \
23+
sudo \
24+
unzip \
25+
fuse-overlayfs \
26+
uidmap \
27+
iptables \
28+
&& update-alternatives --set iptables /usr/sbin/iptables-legacy \
29+
&& update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy \
30+
&& rm -rf /var/lib/apt/lists/*
31+
32+
# Install Docker Engine + CLI + Compose plugin from Docker's official apt repo.
33+
RUN install -m 0755 -d /etc/apt/keyrings \
34+
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
35+
&& chmod a+r /etc/apt/keyrings/docker.asc \
36+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
37+
> /etc/apt/sources.list.d/docker.list \
38+
&& apt-get update \
39+
&& apt-get install -y --no-install-recommends \
40+
docker-ce \
41+
docker-ce-cli \
42+
containerd.io \
43+
docker-buildx-plugin \
44+
docker-compose-plugin \
45+
&& rm -rf /var/lib/apt/lists/*
46+
47+
# Configure the Docker daemon to use the classic image store with the
48+
# fuse-overlayfs storage driver so image builds and container filesystems work
49+
# in the nested VM. dockerd itself is started per-boot by start.sh.
50+
RUN mkdir -p /etc/docker \
51+
&& printf '%s\n' '{"features":{"containerd-snapshotter":false},"storage-driver":"fuse-overlayfs"}' \
52+
> /etc/docker/daemon.json
53+
54+
# The `ubuntu` user (uid 1000) already exists in the base image. Give it
55+
# passwordless sudo (start.sh needs it to launch dockerd) and Docker access.
56+
RUN usermod -aG docker ubuntu \
57+
&& echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu \
58+
&& chmod 0440 /etc/sudoers.d/ubuntu
59+
60+
# Install Bun, pinned to the version in the repo's package.json "packageManager".
61+
ARG BUN_VERSION=1.3.7
62+
RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local/bun bash -s "bun-v${BUN_VERSION}" \
63+
&& ln -s /usr/local/bun/bin/bun /usr/local/bin/bun \
64+
&& ln -s /usr/local/bun/bin/bunx /usr/local/bin/bunx
65+
ENV BUN_INSTALL=/usr/local/bun
66+
ENV PATH=/usr/local/bun/bin:$PATH
67+
68+
USER ubuntu

.cursor/environment.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "Quackback",
3+
"user": "ubuntu",
4+
"build": {
5+
"dockerfile": "Dockerfile"
6+
},
7+
"install": "bash .cursor/install.sh",
8+
"start": "bash .cursor/start.sh",
9+
"terminals": [
10+
{
11+
"name": "dev",
12+
"command": "bun run dev",
13+
"description": "Quackback web app (TanStack Start) — http://localhost:3000"
14+
}
15+
],
16+
"ports": [
17+
{ "name": "web", "port": 3000 },
18+
{ "name": "mailpit", "port": 8025 },
19+
{ "name": "minio-console", "port": 9001 }
20+
]
21+
}

.cursor/install.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# Repository bootstrap for the Quackback Cloud Agent environment.
3+
#
4+
# Runs after the source is checked out. Installs JS dependencies and builds the
5+
# widget bundle that the web build imports (packages/widget/dist/browser.js).
6+
# Must be idempotent and must NOT start long-running processes or depend on the
7+
# datastores (those are handled per-boot by start.sh).
8+
set -euo pipefail
9+
10+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11+
cd "$REPO_ROOT"
12+
13+
echo "[install] Installing dependencies with Bun..."
14+
bun install --frozen-lockfile
15+
16+
# apps/web imports packages/widget/dist/browser.js via Vite `?raw`, so the
17+
# widget must be built before any web build. This is fast (tsup) and safe to
18+
# re-run.
19+
echo "[install] Building @quackback/widget bundle..."
20+
bun run --filter @quackback/widget build
21+
22+
# Snapshot the datastore images so start.sh is not paying pull/build on every
23+
# agent VM. dockerd is only up for this step.
24+
echo "[install] Prefetching datastore images..."
25+
if ! sudo docker info >/dev/null 2>&1; then
26+
sudo bash -c 'nohup dockerd >/var/log/dockerd.log 2>&1 &'
27+
for i in $(seq 1 60); do
28+
if sudo docker info >/dev/null 2>&1; then break; fi
29+
sleep 1
30+
done
31+
fi
32+
if sudo docker info >/dev/null 2>&1; then
33+
sudo chmod 666 /var/run/docker.sock 2>/dev/null || true
34+
docker compose pull postgres minio dragonfly mailpit minio-init
35+
docker compose build postgres
36+
sudo kill "$(pidof dockerd)" 2>/dev/null || true
37+
for i in $(seq 1 30); do
38+
if ! sudo docker info >/dev/null 2>&1; then break; fi
39+
sleep 1
40+
done
41+
else
42+
echo "[install] Docker daemon failed to start; images will be pulled on first start" >&2
43+
fi
44+
45+
echo "[install] Done."

.cursor/start.sh

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
# Per-boot startup for the Quackback Cloud Agent environment.
3+
#
4+
# Brings up the local datastores the app depends on (PostgreSQL with pg_cron +
5+
# pgvector, MinIO, Dragonfly, Mailpit) via docker-compose, then applies database
6+
# migrations. Idempotent: safe to run on every boot, tolerates an already-running
7+
# daemon/containers, and only creates databases when missing.
8+
set -euo pipefail
9+
10+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11+
cd "$REPO_ROOT"
12+
13+
DB_URL_DEV="postgresql://postgres:password@localhost:5432/quackback"
14+
DB_URL_TEST="postgresql://postgres:password@localhost:5432/quackback_test"
15+
16+
echo "[start] Ensuring Docker daemon is running..."
17+
if ! sudo docker info >/dev/null 2>&1; then
18+
sudo bash -c 'nohup dockerd >/var/log/dockerd.log 2>&1 &'
19+
for i in $(seq 1 60); do
20+
if sudo docker info >/dev/null 2>&1; then break; fi
21+
sleep 1
22+
done
23+
fi
24+
sudo docker info >/dev/null 2>&1 || { echo "[start] Docker daemon failed to start" >&2; exit 1; }
25+
# Let the non-root user talk to the daemon without sudo for the rest of the session.
26+
sudo chmod 666 /var/run/docker.sock 2>/dev/null || true
27+
echo "[start] Docker is up."
28+
29+
# Create .env from the example if absent, and fill in a SECRET_KEY.
30+
if [ ! -f .env ]; then
31+
cp .env.example .env
32+
echo "[start] Created .env from .env.example"
33+
fi
34+
if grep -q '^SECRET_KEY=$' .env 2>/dev/null; then
35+
SECRET="$(openssl rand -hex 32)"
36+
sed -i "s/^SECRET_KEY=$/SECRET_KEY=$SECRET/" .env
37+
echo "[start] Generated SECRET_KEY"
38+
fi
39+
40+
echo "[start] Starting datastores (postgres, minio, dragonfly, mailpit)..."
41+
docker compose up -d --wait postgres minio minio-init dragonfly mailpit
42+
43+
# Create the dev and test databases if they do not exist yet. The test DB is
44+
# used by the DB-integration parts of `bun run test`.
45+
for DBNAME in quackback quackback_test; do
46+
if ! docker compose exec -T postgres psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$DBNAME'" | grep -q 1; then
47+
docker compose exec -T postgres psql -U postgres -c "CREATE DATABASE $DBNAME;"
48+
echo "[start] Created database $DBNAME"
49+
fi
50+
done
51+
52+
echo "[start] Applying migrations (dev + test databases)..."
53+
DATABASE_URL="$DB_URL_DEV" bun run db:migrate
54+
DATABASE_URL="$DB_URL_TEST" bun run db:migrate
55+
56+
echo "[start] Ready. Run 'bun run db:seed' for demo data; 'bun run dev' serves http://localhost:3000"

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@quackback/web",
3-
"version": "0.13.1",
3+
"version": "0.13.2",
44
"private": true,
55
"license": "AGPL-3.0",
66
"type": "module",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import type { EventData, PostCreatedEvent } from '@/lib/server/events/types'
3+
4+
vi.mock('../api', () => ({
5+
createWorkItem: vi.fn(),
6+
}))
7+
8+
import { azureDevOpsHook } from '../hook'
9+
import { createWorkItem } from '../api'
10+
11+
function makePostCreatedEvent(): PostCreatedEvent {
12+
return {
13+
id: 'evt-1',
14+
type: 'post.created',
15+
timestamp: '2025-01-01T00:00:00Z',
16+
actor: { type: 'user', userId: 'user_1', email: 'test@test.com' },
17+
data: {
18+
post: {
19+
id: 'post_1',
20+
title: 'Bug report',
21+
content: '<p>Something broke</p>',
22+
boardId: 'board_1',
23+
boardSlug: 'bugs',
24+
voteCount: 0,
25+
},
26+
},
27+
}
28+
}
29+
30+
beforeEach(() => {
31+
vi.clearAllMocks()
32+
})
33+
34+
describe('azureDevOpsHook', () => {
35+
it('does not treat a missing organization name as an auth failure', async () => {
36+
const result = await azureDevOpsHook.run(
37+
makePostCreatedEvent(),
38+
{ channelId: 'Proj:Task' },
39+
{ accessToken: 'pat', rootUrl: 'https://app.example.com' }
40+
)
41+
42+
expect(result).toEqual({
43+
success: false,
44+
error: 'Azure DevOps organization name is missing from integration config',
45+
shouldRetry: false,
46+
})
47+
expect(createWorkItem).not.toHaveBeenCalled()
48+
})
49+
50+
it('creates a work item when organizationName is present', async () => {
51+
vi.mocked(createWorkItem).mockResolvedValue({
52+
id: 42,
53+
url: 'https://dev.azure.com/acme/Proj/_workitems/edit/42',
54+
})
55+
56+
const result = await azureDevOpsHook.run(
57+
makePostCreatedEvent(),
58+
{ channelId: 'Proj:Task' },
59+
{
60+
accessToken: 'pat',
61+
organizationName: 'acme',
62+
rootUrl: 'https://app.example.com',
63+
}
64+
)
65+
66+
expect(result.success).toBe(true)
67+
expect(result.externalId).toBe('42')
68+
expect(createWorkItem).toHaveBeenCalledWith('pat', 'acme', 'Proj', 'Task', expect.any(Object))
69+
})
70+
71+
it('maps HTTP 401 to reconnect only when organizationName is present', async () => {
72+
vi.mocked(createWorkItem).mockRejectedValue(
73+
Object.assign(new Error('Unauthorized'), { status: 401 })
74+
)
75+
76+
const result = await azureDevOpsHook.run(
77+
makePostCreatedEvent(),
78+
{ channelId: 'Proj:Task' },
79+
{
80+
accessToken: 'pat',
81+
organizationName: 'acme',
82+
rootUrl: 'https://app.example.com',
83+
}
84+
)
85+
86+
expect(result).toEqual({
87+
success: false,
88+
error: 'Authentication failed. Please reconnect Azure DevOps.',
89+
shouldRetry: false,
90+
})
91+
})
92+
93+
it('skips non post.created events', async () => {
94+
const event = { type: 'comment.created' } as unknown as EventData
95+
expect(await azureDevOpsHook.run(event, { channelId: 'Proj:Task' }, {})).toEqual({
96+
success: true,
97+
})
98+
})
99+
})

apps/web/src/integrations/azure-devops/server/hook.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ export const azureDevOpsHook: HookHandler = {
3232
return { success: true }
3333
}
3434

35+
if (!organizationName) {
36+
return {
37+
success: false,
38+
error: 'Azure DevOps organization name is missing from integration config',
39+
shouldRetry: false,
40+
}
41+
}
42+
if (!accessToken) {
43+
return {
44+
success: false,
45+
error: 'Azure DevOps access token is missing',
46+
shouldRetry: false,
47+
}
48+
}
49+
3550
const [project, workItemType] = channelId.split(':')
3651
if (!project || !workItemType) {
3752
return {

0 commit comments

Comments
 (0)