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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
# POSTHOG_PUBLIC_KEY=phc_...
# EU data center (default). Use https://us.i.posthog.com for US.
# POSTHOG_HOST=https://eu.i.posthog.com
# Personal API key with "Feature Flags: read" scope. When set, the server
# evaluates feature flags locally (no per-request HTTP round trip).
# POSTHOG_FEATURE_FLAGS_KEY=phx_...

# ─── Optional: Feature Flag Overrides (no PostHog required) ─────────────────
# Force any flag on or off without running PostHog. The full list of available
# flags lives in shared/feature-flags.ts. Variable name pattern:
# FEATURE_FLAG_<UPPERCASE_KEY_WITH_UNDERSCORES>
# Accepted values: true / false / 1 / 0 / on / off (or a variant key for
# multivariate flags). Env overrides win over PostHog rollouts.
# Example — enable the new chatbot experience for everyone on this instance:
# FEATURE_FLAG_CHATBOT_EXPERIENCE=true

# ─── Optional: OIDC SSO (Keycloak, Authentik, Authelia, Okta, etc.) ──────────
# Enable Single Sign-On via any OIDC-compliant identity provider.
Expand Down
2 changes: 2 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
- What does this PR change?
- Why is this needed?

> **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.

## Type of change

- [ ] Bug fix
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,21 @@ jobs:
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Production dependency PATCHES are also safe to automerge — patch
# releases are by definition backwards-compatible bug fixes. The merge
# still only happens once branch-protection checks pass (build,
# typecheck, audit, E2E, docker-readme-validation), so a regression in
# a patch will block the merge.
#
# MINOR production updates intentionally remain manual: even though
# semver says they are backwards-compatible, in practice they often
# introduce new behaviour worth a human glance for an ATS.
- name: Enable automerge (prod deps, patch only)
if: |
steps.metadata.outputs.dependency-type == 'direct:production' &&
steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 changes: 36 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
permissions:
contents: read
packages: write
# id-token: needed by cosign for keyless OIDC signing.
# attestations: needed to publish build attestations to GHCR.
id-token: write
attestations: write

concurrency:
group: docker-publish-${{ github.ref }}
Expand Down Expand Up @@ -63,6 +67,7 @@ jobs:

# ── 5. Build & push ────────────────────────────────────────────────────
- name: Build and push
id: build
uses: docker/build-push-action@v7
with:
context: .
Expand All @@ -77,3 +82,34 @@ jobs:
sbom: true
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index

# ── 6. Keyless cosign signature (Sigstore OIDC) ────────────────────────
# Self-hosters can verify the image they pulled was actually built by
# this workflow with:
# cosign verify ghcr.io/reqcore-inc/reqcore@<digest> \
# --certificate-identity-regexp 'https://github.com/reqcore-inc/reqcore/.github/workflows/docker-publish.yml@.*' \
# --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
- name: Install cosign
uses: sigstore/cosign-installer@v3

- name: Sign published images by digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
# cosign signs by immutable digest; the tags only resolve the digest.
# We sign each unique image reference so verification works against
# any tag the user pulled (latest, semver, edge, sha-…).
# `${tag%:*}` (single %) strips only the trailing :tag so registries
# with ports (e.g. registry.local:5000/repo:tag) survive intact.
# Pipe through `sort -u` so the same image+digest is signed once
# even when the tag list contains many aliases.
printf '%s\n' $TAGS \
| awk 'NF' \
| sed -E 's#:[^:/]+$##' \
| sort -u \
| while IFS= read -r image; do
echo "Signing ${image}@${DIGEST}"
cosign sign --yes "${image}@${DIGEST}"
done
49 changes: 49 additions & 0 deletions .github/workflows/pr-title-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: PR Title Lint

# Enforce Conventional Commit syntax in PR titles so release-please can
# always derive a clean changelog and the correct semver bump from the
# squash-merged commit. Without this, a single mis-titled PR silently
# disappears from the release notes.
#
# Examples that pass:
# feat: add candidate bulk import
# fix(jobs): handle null salary range
# chore(deps): bump nuxt from 4.3.1 to 4.3.2
#
# Examples that fail:
# Update stuff
# WIP

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read

jobs:
lint:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
# Keep types aligned with .github/release-please-config.json
types: |
feat
fix
perf
security
docs
refactor
test
build
ci
chore
requireScope: false
subjectPattern: ^[A-Za-z0-9].+$
subjectPatternError: |
The PR title subject must start with a letter or number and not
be empty. Example: `feat(jobs): add bulk import`.
182 changes: 182 additions & 0 deletions .github/workflows/release-verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: Release Verification

# Fires after release-please publishes a GitHub Release (which also pushes a
# `v*` tag and triggers docker-publish.yml). This workflow is the last gate
# in the chain and provides two guarantees:
#
# 1. smoke-test: the *published* image (not a locally-built one) actually
# starts cleanly using the same setup.sh + docker-compose flow that
# self-hosters follow. If this fails, the release is auto-marked as a
# pre-release so it stops being advertised as the "Latest" release.
#
# 2. bundle: attach a self-hoster bundle (docker-compose.production.yml
# with the image tag pinned + setup.sh) to the GitHub Release so users
# can `curl -L .../releases/download/v1.4.0/reqcore-1.4.0.tar.gz` and
# get a deterministic, version-locked install.

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to verify (e.g. v1.4.0)"
required: true
type: string

permissions:
contents: write

concurrency:
group: release-verification-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false

jobs:
smoke-test:
name: Smoke-test published image
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- name: Resolve release tag
id: tag
run: |
set -euo pipefail
tag="${{ github.event.release.tag_name || inputs.tag }}"
version="${tag#v}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Checkout release tag
uses: actions/checkout@v6
with:
ref: ${{ steps.tag.outputs.tag }}

- name: Pin compose file to the released image tag
run: |
set -euo pipefail
sed -i \
"s|ghcr.io/reqcore-inc/reqcore:latest|ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}|" \
docker-compose.production.yml
grep "ghcr.io/reqcore-inc/reqcore" docker-compose.production.yml

- name: Wait for the published image to be available on GHCR
run: |
set -euo pipefail
# docker-publish.yml is triggered by the same tag push, so it may
# still be running when this job starts. Poll for up to 20 minutes.
for i in $(seq 60); do
if docker manifest inspect "ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}" > /dev/null 2>&1; then
echo "✅ Image is available"
exit 0
fi
echo " attempt $i/60 — image not yet published, waiting 20s..."
sleep 20
done
echo "❌ Image ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }} never appeared"
exit 1

- name: Generate .env via setup.sh
run: |
chmod +x ./setup.sh
./setup.sh

- name: Start full stack against the published image
run: docker compose -f docker-compose.production.yml up -d

- name: Wait for app to be reachable
run: |
set -euo pipefail
for i in $(seq 60); do
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
echo "✅ App reachable"
exit 0
fi
sleep 3
done
echo "❌ App did not become reachable"
docker compose -f docker-compose.production.yml logs app --tail=200
exit 1

- name: Assert migrations + S3 bucket ready
run: |
set -euo pipefail
# Startup messages can land slightly after the HTTP port opens, so
# poll instead of one-shot grepping to avoid flaky failures.
for i in $(seq 40); do
logs="$(docker compose -f docker-compose.production.yml logs app || true)"
if grep -q "Database migrations applied successfully" <<<"$logs" \
&& grep -q 'S3 bucket "reqcore" is ready' <<<"$logs"; then
echo "✅ Migrations + S3 ready messages found (attempt $i)"
exit 0
fi
sleep 3
done
echo "❌ Required startup messages missing after polling"
docker compose -f docker-compose.production.yml logs app
exit 1

- name: Demote release to pre-release on failure
if: failure() && github.event_name == 'release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit "${{ steps.tag.outputs.tag }}" --prerelease --latest=false
gh release view "${{ steps.tag.outputs.tag }}" --json isPrerelease,isLatest

bundle:
name: Attach self-hoster bundle
needs: smoke-test
runs-on: ubuntu-latest
steps:
- name: Resolve release tag
id: tag
run: |
set -euo pipefail
tag="${{ github.event.release.tag_name || inputs.tag }}"
version="${tag#v}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Checkout release tag
uses: actions/checkout@v6
with:
ref: ${{ steps.tag.outputs.tag }}

- name: Build version-pinned bundle
run: |
set -euo pipefail
mkdir -p "bundle/reqcore-${{ steps.tag.outputs.version }}"
cp setup.sh "bundle/reqcore-${{ steps.tag.outputs.version }}/"
cp SELF-HOSTING.md "bundle/reqcore-${{ steps.tag.outputs.version }}/"
# Pin the compose file to the exact released image tag so users
# who download the bundle get a deterministic install.
sed \
"s|ghcr.io/reqcore-inc/reqcore:latest|ghcr.io/reqcore-inc/reqcore:${{ steps.tag.outputs.version }}|" \
docker-compose.production.yml \
> "bundle/reqcore-${{ steps.tag.outputs.version }}/docker-compose.production.yml"

cat > "bundle/reqcore-${{ steps.tag.outputs.version }}/INSTALL.txt" <<EOF
Reqcore ${{ steps.tag.outputs.tag }} — Self-Hoster Bundle

1. ./setup.sh
2. docker compose -f docker-compose.production.yml up -d
3. Open http://localhost:3000

The image tag in docker-compose.production.yml is pinned to
${{ steps.tag.outputs.version }}. To upgrade later, download the
newer release bundle and re-run docker compose up -d.

Full guide: SELF-HOSTING.md
EOF

tar -czf "reqcore-${{ steps.tag.outputs.version }}.tar.gz" -C bundle "reqcore-${{ steps.tag.outputs.version }}"
sha256sum "reqcore-${{ steps.tag.outputs.version }}.tar.gz" > "reqcore-${{ steps.tag.outputs.version }}.tar.gz.sha256"

- name: Attach bundle to the GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ steps.tag.outputs.tag }}" \
"reqcore-${{ steps.tag.outputs.version }}.tar.gz" \
"reqcore-${{ steps.tag.outputs.version }}.tar.gz.sha256" \
--clobber
59 changes: 59 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
// VS Code tasks for the release / dependency workflow.
//
// Run any of these via: Ctrl+Shift+P → "Tasks: Run Task"
//
// The whole release flow is automated by GitHub Actions (release-please +
// docker-publish + release-smoke-test). These tasks are only for visibility
// and one-off local actions — you should never *need* them in steady state.
"version": "2.0.0",
"tasks": [
{
"label": "Release: watch latest run",
"type": "shell",
"command": "gh run list --workflow=release-please.yml --limit 5 && gh run watch",
"problemMatcher": [],
"presentation": { "reveal": "always", "panel": "dedicated" },
"detail": "Show recent release-please runs and tail the latest."
},
{
"label": "Release: open release-please PR",
"type": "shell",
"command": "gh pr list --label autorelease:pending --json url --jq '.[0].url' | ForEach-Object { Start-Process $_ }",
"windows": {
"command": "gh pr list --label \"autorelease: pending\" --json url --jq '.[0].url' | ForEach-Object { Start-Process $_ }"
},
"problemMatcher": [],
"detail": "Open the pending release-please PR in the browser."
},
{
"label": "Release: dry-run notes (local)",
"type": "shell",
"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",
"problemMatcher": [],
"detail": "Preview what the next release would contain, without creating anything."
},
{
"label": "Dependabot: list open PRs",
"type": "shell",
"command": "gh pr list --author 'app/dependabot' --state open",
"problemMatcher": [],
"detail": "Show all open Dependabot PRs and their auto-merge status."
},
{
"label": "Dependabot: enable automerge on current branch PR",
"type": "shell",
"command": "gh pr merge --auto --squash",
"problemMatcher": [],
"detail": "Enable auto-merge for the PR associated with the current branch (gates on CI)."
},
{
"label": "CI: tail latest workflow run",
"type": "shell",
"command": "gh run watch",
"problemMatcher": [],
"presentation": { "reveal": "always", "panel": "dedicated" },
"detail": "Tail the most recently triggered workflow run for this repo."
}
]
}
Loading
Loading