Skip to content

Commit e139b72

Browse files
authored
Merge pull request #165 from reqcore-inc/feat/ai-chatbot
feat: add AI chatbot feature with configuration, access control, and attachment management
2 parents 8b9ea20 + f11a78f commit e139b72

85 files changed

Lines changed: 12787 additions & 940 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
7474
# POSTHOG_PUBLIC_KEY=phc_...
7575
# EU data center (default). Use https://us.i.posthog.com for US.
7676
# POSTHOG_HOST=https://eu.i.posthog.com
77+
# Personal API key with "Feature Flags: read" scope. When set, the server
78+
# evaluates feature flags locally (no per-request HTTP round trip).
79+
# POSTHOG_FEATURE_FLAGS_KEY=phx_...
80+
81+
# ─── Optional: Feature Flag Overrides (no PostHog required) ─────────────────
82+
# Force any flag on or off without running PostHog. The full list of available
83+
# flags lives in shared/feature-flags.ts. Variable name pattern:
84+
# FEATURE_FLAG_<UPPERCASE_KEY_WITH_UNDERSCORES>
85+
# Accepted values: true / false / 1 / 0 / on / off (or a variant key for
86+
# multivariate flags). Env overrides win over PostHog rollouts.
87+
# Example — enable the new chatbot experience for everyone on this instance:
88+
# FEATURE_FLAG_CHATBOT_EXPERIENCE=true
7789

7890
# ─── Optional: OIDC SSO (Keycloak, Authentik, Authelia, Okta, etc.) ──────────
7991
# Enable Single Sign-On via any OIDC-compliant identity provider.

.github/pull_request_template.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- What does this PR change?
44
- Why is this needed?
55

6+
> **PR title must follow [Conventional Commits](https://www.conventionalcommits.org/)** — e.g. `feat(jobs): add bulk import` or `fix: handle null salary`. The squash-merged title is what release-please uses to generate the changelog and pick the next version. PRs with non-conventional titles are blocked by CI.
7+
68
## Type of change
79

810
- [ ] Bug fix

.github/workflows/dependabot-automerge.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,21 @@ jobs:
5353
env:
5454
PR_URL: ${{ github.event.pull_request.html_url }}
5555
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56+
57+
# Production dependency PATCHES are also safe to automerge — patch
58+
# releases are by definition backwards-compatible bug fixes. The merge
59+
# still only happens once branch-protection checks pass (build,
60+
# typecheck, audit, E2E, docker-readme-validation), so a regression in
61+
# a patch will block the merge.
62+
#
63+
# MINOR production updates intentionally remain manual: even though
64+
# semver says they are backwards-compatible, in practice they often
65+
# introduce new behaviour worth a human glance for an ATS.
66+
- name: Enable automerge (prod deps, patch only)
67+
if: |
68+
steps.metadata.outputs.dependency-type == 'direct:production' &&
69+
steps.metadata.outputs.update-type == 'version-update:semver-patch'
70+
run: gh pr merge --auto --squash "$PR_URL"
71+
env:
72+
PR_URL: ${{ github.event.pull_request.html_url }}
73+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/docker-publish.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
permissions:
99
contents: read
1010
packages: write
11+
# id-token: needed by cosign for keyless OIDC signing.
12+
# attestations: needed to publish build attestations to GHCR.
13+
id-token: write
14+
attestations: write
1115

1216
concurrency:
1317
group: docker-publish-${{ github.ref }}
@@ -63,6 +67,7 @@ jobs:
6367
6468
# ── 5. Build & push ────────────────────────────────────────────────────
6569
- name: Build and push
70+
id: build
6671
uses: docker/build-push-action@v7
6772
with:
6873
context: .
@@ -77,3 +82,34 @@ jobs:
7782
sbom: true
7883
env:
7984
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
85+
86+
# ── 6. Keyless cosign signature (Sigstore OIDC) ────────────────────────
87+
# Self-hosters can verify the image they pulled was actually built by
88+
# this workflow with:
89+
# cosign verify ghcr.io/reqcore-inc/reqcore@<digest> \
90+
# --certificate-identity-regexp 'https://github.com/reqcore-inc/reqcore/.github/workflows/docker-publish.yml@.*' \
91+
# --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
92+
- name: Install cosign
93+
uses: sigstore/cosign-installer@v3
94+
95+
- name: Sign published images by digest
96+
env:
97+
DIGEST: ${{ steps.build.outputs.digest }}
98+
TAGS: ${{ steps.meta.outputs.tags }}
99+
run: |
100+
set -euo pipefail
101+
# cosign signs by immutable digest; the tags only resolve the digest.
102+
# We sign each unique image reference so verification works against
103+
# any tag the user pulled (latest, semver, edge, sha-…).
104+
# `${tag%:*}` (single %) strips only the trailing :tag so registries
105+
# with ports (e.g. registry.local:5000/repo:tag) survive intact.
106+
# Pipe through `sort -u` so the same image+digest is signed once
107+
# even when the tag list contains many aliases.
108+
printf '%s\n' $TAGS \
109+
| awk 'NF' \
110+
| sed -E 's#:[^:/]+$##' \
111+
| sort -u \
112+
| while IFS= read -r image; do
113+
echo "Signing ${image}@${DIGEST}"
114+
cosign sign --yes "${image}@${DIGEST}"
115+
done
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: PR Title Lint
2+
3+
# Enforce Conventional Commit syntax in PR titles so release-please can
4+
# always derive a clean changelog and the correct semver bump from the
5+
# squash-merged commit. Without this, a single mis-titled PR silently
6+
# disappears from the release notes.
7+
#
8+
# Examples that pass:
9+
# feat: add candidate bulk import
10+
# fix(jobs): handle null salary range
11+
# chore(deps): bump nuxt from 4.3.1 to 4.3.2
12+
#
13+
# Examples that fail:
14+
# Update stuff
15+
# WIP
16+
17+
on:
18+
pull_request:
19+
types: [opened, edited, synchronize, reopened]
20+
21+
permissions:
22+
pull-requests: read
23+
24+
jobs:
25+
lint:
26+
name: Validate PR title
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: amannn/action-semantic-pull-request@v6
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
with:
33+
# Keep types aligned with .github/release-please-config.json
34+
types: |
35+
feat
36+
fix
37+
perf
38+
security
39+
docs
40+
refactor
41+
test
42+
build
43+
ci
44+
chore
45+
requireScope: false
46+
subjectPattern: ^[A-Za-z0-9].+$
47+
subjectPatternError: |
48+
The PR title subject must start with a letter or number and not
49+
be empty. Example: `feat(jobs): add bulk import`.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
name: Release Verification
2+
3+
# Fires after release-please publishes a GitHub Release (which also pushes a
4+
# `v*` tag and triggers docker-publish.yml). This workflow is the last gate
5+
# in the chain and provides two guarantees:
6+
#
7+
# 1. smoke-test: the *published* image (not a locally-built one) actually
8+
# starts cleanly using the same setup.sh + docker-compose flow that
9+
# self-hosters follow. If this fails, the release is auto-marked as a
10+
# pre-release so it stops being advertised as the "Latest" release.
11+
#
12+
# 2. bundle: attach a self-hoster bundle (docker-compose.production.yml
13+
# with the image tag pinned + setup.sh) to the GitHub Release so users
14+
# can `curl -L .../releases/download/v1.4.0/reqcore-1.4.0.tar.gz` and
15+
# get a deterministic, version-locked install.
16+
17+
on:
18+
release:
19+
types: [published]
20+
workflow_dispatch:
21+
inputs:
22+
tag:
23+
description: "Release tag to verify (e.g. v1.4.0)"
24+
required: true
25+
type: string
26+
27+
permissions:
28+
contents: write
29+
30+
concurrency:
31+
group: release-verification-${{ github.event.release.tag_name || inputs.tag }}
32+
cancel-in-progress: false
33+
34+
jobs:
35+
smoke-test:
36+
name: Smoke-test published image
37+
runs-on: ubuntu-latest
38+
timeout-minutes: 35
39+
steps:
40+
- name: Resolve release tag
41+
id: tag
42+
run: |
43+
set -euo pipefail
44+
tag="${{ github.event.release.tag_name || inputs.tag }}"
45+
version="${tag#v}"
46+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
47+
echo "version=$version" >> "$GITHUB_OUTPUT"
48+
49+
- name: Checkout release tag
50+
uses: actions/checkout@v6
51+
with:
52+
ref: ${{ steps.tag.outputs.tag }}
53+
54+
- name: Pin compose file to the released image tag
55+
run: |
56+
set -euo pipefail
57+
sed -i \
58+
"s|ghcr.io/reqcore-inc/reqcore:latest|ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}|" \
59+
docker-compose.production.yml
60+
grep "ghcr.io/reqcore-inc/reqcore" docker-compose.production.yml
61+
62+
- name: Wait for the published image to be available on GHCR
63+
run: |
64+
set -euo pipefail
65+
# docker-publish.yml is triggered by the same tag push, so it may
66+
# still be running when this job starts. Poll for up to 20 minutes.
67+
for i in $(seq 60); do
68+
if docker manifest inspect "ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}" > /dev/null 2>&1; then
69+
echo "✅ Image is available"
70+
exit 0
71+
fi
72+
echo " attempt $i/60 — image not yet published, waiting 20s..."
73+
sleep 20
74+
done
75+
echo "❌ Image ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }} never appeared"
76+
exit 1
77+
78+
- name: Generate .env via setup.sh
79+
run: |
80+
chmod +x ./setup.sh
81+
./setup.sh
82+
83+
- name: Start full stack against the published image
84+
run: docker compose -f docker-compose.production.yml up -d
85+
86+
- name: Wait for app to be reachable
87+
run: |
88+
set -euo pipefail
89+
for i in $(seq 60); do
90+
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
91+
echo "✅ App reachable"
92+
exit 0
93+
fi
94+
sleep 3
95+
done
96+
echo "❌ App did not become reachable"
97+
docker compose -f docker-compose.production.yml logs app --tail=200
98+
exit 1
99+
100+
- name: Assert migrations + S3 bucket ready
101+
run: |
102+
set -euo pipefail
103+
# Startup messages can land slightly after the HTTP port opens, so
104+
# poll instead of one-shot grepping to avoid flaky failures.
105+
for i in $(seq 40); do
106+
logs="$(docker compose -f docker-compose.production.yml logs app || true)"
107+
if grep -q "Database migrations applied successfully" <<<"$logs" \
108+
&& grep -q 'S3 bucket "reqcore" is ready' <<<"$logs"; then
109+
echo "✅ Migrations + S3 ready messages found (attempt $i)"
110+
exit 0
111+
fi
112+
sleep 3
113+
done
114+
echo "❌ Required startup messages missing after polling"
115+
docker compose -f docker-compose.production.yml logs app
116+
exit 1
117+
118+
- name: Demote release to pre-release on failure
119+
if: failure() && github.event_name == 'release'
120+
env:
121+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122+
run: |
123+
gh release edit "${{ steps.tag.outputs.tag }}" --prerelease --latest=false
124+
gh release view "${{ steps.tag.outputs.tag }}" --json isPrerelease,isLatest
125+
126+
bundle:
127+
name: Attach self-hoster bundle
128+
needs: smoke-test
129+
runs-on: ubuntu-latest
130+
steps:
131+
- name: Resolve release tag
132+
id: tag
133+
run: |
134+
set -euo pipefail
135+
tag="${{ github.event.release.tag_name || inputs.tag }}"
136+
version="${tag#v}"
137+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
138+
echo "version=$version" >> "$GITHUB_OUTPUT"
139+
140+
- name: Checkout release tag
141+
uses: actions/checkout@v6
142+
with:
143+
ref: ${{ steps.tag.outputs.tag }}
144+
145+
- name: Build version-pinned bundle
146+
run: |
147+
set -euo pipefail
148+
mkdir -p "bundle/reqcore-${{ steps.tag.outputs.version }}"
149+
cp setup.sh "bundle/reqcore-${{ steps.tag.outputs.version }}/"
150+
cp SELF-HOSTING.md "bundle/reqcore-${{ steps.tag.outputs.version }}/"
151+
# Pin the compose file to the exact released image tag so users
152+
# who download the bundle get a deterministic install.
153+
sed \
154+
"s|ghcr.io/reqcore-inc/reqcore:latest|ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}|" \
155+
docker-compose.production.yml \
156+
> "bundle/reqcore-${{ steps.tag.outputs.version }}/docker-compose.production.yml"
157+
158+
cat > "bundle/reqcore-${{ steps.tag.outputs.version }}/INSTALL.txt" <<EOF
159+
Reqcore ${{ steps.tag.outputs.tag }} — Self-Hoster Bundle
160+
161+
1. ./setup.sh
162+
2. docker compose -f docker-compose.production.yml up -d
163+
3. Open http://localhost:3000
164+
165+
The image tag in docker-compose.production.yml is pinned to
166+
${{ steps.tag.outputs.version }}. To upgrade later, download the
167+
newer release bundle and re-run docker compose up -d.
168+
169+
Full guide: SELF-HOSTING.md
170+
EOF
171+
172+
tar -czf "reqcore-${{ steps.tag.outputs.version }}.tar.gz" -C bundle "reqcore-${{ steps.tag.outputs.version }}"
173+
sha256sum "reqcore-${{ steps.tag.outputs.version }}.tar.gz" > "reqcore-${{ steps.tag.outputs.version }}.tar.gz.sha256"
174+
175+
- name: Attach bundle to the GitHub Release
176+
env:
177+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178+
run: |
179+
gh release upload "${{ steps.tag.outputs.tag }}" \
180+
"reqcore-${{ steps.tag.outputs.version }}.tar.gz" \
181+
"reqcore-${{ steps.tag.outputs.version }}.tar.gz.sha256" \
182+
--clobber

.vscode/tasks.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
// VS Code tasks for the release / dependency workflow.
3+
//
4+
// Run any of these via: Ctrl+Shift+P → "Tasks: Run Task"
5+
//
6+
// The whole release flow is automated by GitHub Actions (release-please +
7+
// docker-publish + release-smoke-test). These tasks are only for visibility
8+
// and one-off local actions — you should never *need* them in steady state.
9+
"version": "2.0.0",
10+
"tasks": [
11+
{
12+
"label": "Release: watch latest run",
13+
"type": "shell",
14+
"command": "gh run list --workflow=release-please.yml --limit 5 && gh run watch",
15+
"problemMatcher": [],
16+
"presentation": { "reveal": "always", "panel": "dedicated" },
17+
"detail": "Show recent release-please runs and tail the latest."
18+
},
19+
{
20+
"label": "Release: open release-please PR",
21+
"type": "shell",
22+
"command": "gh pr list --label autorelease:pending --json url --jq '.[0].url' | ForEach-Object { Start-Process $_ }",
23+
"windows": {
24+
"command": "gh pr list --label \"autorelease: pending\" --json url --jq '.[0].url' | ForEach-Object { Start-Process $_ }"
25+
},
26+
"problemMatcher": [],
27+
"detail": "Open the pending release-please PR in the browser."
28+
},
29+
{
30+
"label": "Release: dry-run notes (local)",
31+
"type": "shell",
32+
"command": "npx release-please release-pr --dry-run --token=$env:GITHUB_TOKEN --repo-url=https://github.com/reqcore-inc/reqcore --config-file=.github/release-please-config.json --manifest-file=.release-please-manifest.json",
33+
"problemMatcher": [],
34+
"detail": "Preview what the next release would contain, without creating anything."
35+
},
36+
{
37+
"label": "Dependabot: list open PRs",
38+
"type": "shell",
39+
"command": "gh pr list --author 'app/dependabot' --state open",
40+
"problemMatcher": [],
41+
"detail": "Show all open Dependabot PRs and their auto-merge status."
42+
},
43+
{
44+
"label": "Dependabot: enable automerge on current branch PR",
45+
"type": "shell",
46+
"command": "gh pr merge --auto --squash",
47+
"problemMatcher": [],
48+
"detail": "Enable auto-merge for the PR associated with the current branch (gates on CI)."
49+
},
50+
{
51+
"label": "CI: tail latest workflow run",
52+
"type": "shell",
53+
"command": "gh run watch",
54+
"problemMatcher": [],
55+
"presentation": { "reveal": "always", "panel": "dedicated" },
56+
"detail": "Tail the most recently triggered workflow run for this repo."
57+
}
58+
]
59+
}

0 commit comments

Comments
 (0)