integrate generalized quality-rails into mosaic bootstrap
This commit is contained in:
30
README.md
30
README.md
@@ -103,6 +103,30 @@ Inside any compatible repository:
|
||||
|
||||
Wrapper commands call local repo scripts under `scripts/agent/`.
|
||||
|
||||
## Quality Rails (Generalized)
|
||||
|
||||
Mosaic includes vendored quality-rails templates and scripts at:
|
||||
|
||||
- `~/.mosaic/rails/quality/`
|
||||
|
||||
Apply to a repo:
|
||||
|
||||
```bash
|
||||
~/.mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/repo
|
||||
```
|
||||
|
||||
Verify enforcement:
|
||||
|
||||
```bash
|
||||
~/.mosaic/bin/mosaic-quality-verify --target /path/to/repo
|
||||
```
|
||||
|
||||
Templates currently supported:
|
||||
|
||||
- `typescript-node`
|
||||
- `typescript-nextjs`
|
||||
- `monorepo`
|
||||
|
||||
## Bootstrap Any Repo (Slave Linkage)
|
||||
|
||||
Attach any repository/workspace to the master layer:
|
||||
@@ -111,6 +135,12 @@ Attach any repository/workspace to the master layer:
|
||||
~/.mosaic/bin/mosaic-bootstrap-repo /path/to/repo
|
||||
```
|
||||
|
||||
Attach and apply quality rails in one step:
|
||||
|
||||
```bash
|
||||
~/.mosaic/bin/mosaic-bootstrap-repo --quality-template typescript-node /path/to/repo
|
||||
```
|
||||
|
||||
This creates/updates:
|
||||
|
||||
- `.mosaic/` (repo-specific hook/config surface)
|
||||
|
||||
@@ -20,6 +20,7 @@ Master/slave model:
|
||||
- Pull before edits when collaborating in shared repos.
|
||||
- Run validation checks before claiming completion.
|
||||
- Apply quality rails from `~/.mosaic/rails/` when relevant (review, QA, git workflow).
|
||||
- For project-level mechanical enforcement templates, use `~/.mosaic/rails/quality/` via `~/.mosaic/bin/mosaic-quality-apply`.
|
||||
- Avoid hardcoded secrets and token leakage in remotes/commits.
|
||||
- Do not perform destructive git/file actions without explicit instruction.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
||||
|
||||
TARGET_DIR="$(pwd)"
|
||||
FORCE=0
|
||||
QUALITY_TEMPLATE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -10,6 +11,10 @@ while [[ $# -gt 0 ]]; do
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--quality-template)
|
||||
QUALITY_TEMPLATE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
TARGET_DIR="$1"
|
||||
shift
|
||||
@@ -48,6 +53,7 @@ copy_file() {
|
||||
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/README.md" "$TARGET_DIR/.mosaic/README.md"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/repo-hooks.sh" "$TARGET_DIR/.mosaic/repo-hooks.sh"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/quality-rails.yml" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
|
||||
for file in "$TEMPLATE_ROOT"/scripts/agent/*.sh; do
|
||||
base="$(basename "$file")"
|
||||
@@ -90,3 +96,17 @@ fi
|
||||
|
||||
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
|
||||
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
|
||||
echo "[mosaic] Optional: apply quality rails via ~/.mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
||||
|
||||
if [[ -n "$QUALITY_TEMPLATE" ]]; then
|
||||
if [[ -x "$MOSAIC_HOME/bin/mosaic-quality-apply" ]]; then
|
||||
"$MOSAIC_HOME/bin/mosaic-quality-apply" --template "$QUALITY_TEMPLATE" --target "$TARGET_DIR"
|
||||
if [[ -f "$TARGET_DIR/.mosaic/quality-rails.yml" ]]; then
|
||||
sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
fi
|
||||
echo "[mosaic] Applied quality rails template: $QUALITY_TEMPLATE"
|
||||
else
|
||||
echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality rails apply" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -115,12 +115,15 @@ echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME"
|
||||
expect_file "$MOSAIC_HOME/STANDARDS.md"
|
||||
expect_dir "$MOSAIC_HOME/guides"
|
||||
expect_dir "$MOSAIC_HOME/rails"
|
||||
expect_dir "$MOSAIC_HOME/rails/quality"
|
||||
expect_dir "$MOSAIC_HOME/profiles"
|
||||
expect_dir "$MOSAIC_HOME/templates/agent"
|
||||
expect_dir "$MOSAIC_HOME/skills"
|
||||
expect_dir "$MOSAIC_HOME/skills-local"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify"
|
||||
|
||||
# Claude runtime file checks (copied, non-symlink).
|
||||
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
|
||||
|
||||
65
bin/mosaic-quality-apply
Executable file
65
bin/mosaic-quality-apply
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.mosaic}"
|
||||
TARGET_DIR="$(pwd)"
|
||||
TEMPLATE=""
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") --template <name> [--target <dir>]
|
||||
|
||||
Apply Mosaic quality rails templates into a project.
|
||||
|
||||
Templates:
|
||||
typescript-node
|
||||
typescript-nextjs
|
||||
monorepo
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --template typescript-node --target ~/src/my-project
|
||||
$(basename "$0") --template monorepo
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template)
|
||||
TEMPLATE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
TARGET_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
echo "[mosaic-quality] Missing required --template" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/install.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing install script: $SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[mosaic-quality] Applying template '$TEMPLATE' to $TARGET_DIR"
|
||||
"$SCRIPT" --template "$TEMPLATE" --target "$TARGET_DIR"
|
||||
52
bin/mosaic-quality-verify
Executable file
52
bin/mosaic-quality-verify
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.mosaic}"
|
||||
TARGET_DIR="$(pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [--target <dir>]
|
||||
|
||||
Run quality-rails verification checks inside a target repository.
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --target ~/src/my-project
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
TARGET_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/verify.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[mosaic-quality] Running verification in $TARGET_DIR"
|
||||
(
|
||||
cd "$TARGET_DIR"
|
||||
"$SCRIPT"
|
||||
)
|
||||
189
rails/quality/PHILOSOPHY.md
Normal file
189
rails/quality/PHILOSOPHY.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Why Hard Rails Matter
|
||||
|
||||
## The Problem We Discovered
|
||||
|
||||
In AI-assisted development, we found:
|
||||
|
||||
1. **Process adherence fails** - Agents claim to do code review but miss critical issues
|
||||
2. **Manual review insufficient** - Even AI-assisted review missed hardcoded passwords, SQL injection
|
||||
3. **Scale breaks quality** - 50 issues in a single patch release despite explicit QA processes
|
||||
|
||||
### Real-World Case Study
|
||||
|
||||
**Production patch validation:**
|
||||
|
||||
After explicit code review and QA processes, we discovered **50 issues**:
|
||||
|
||||
**Security Issues (9):**
|
||||
- 4 hardcoded passwords committed to repository
|
||||
- 1 SQL injection vulnerability
|
||||
- World-readable .env files
|
||||
- XSS vulnerabilities (CSP unsafe-inline)
|
||||
|
||||
**Type Safety Issues (11):**
|
||||
- TypeScript strict mode DISABLED (`"strict": false`)
|
||||
- ESLint explicitly ALLOWING any types (`no-explicit-any: 'off'`)
|
||||
- Missing return types
|
||||
- Type assertion overuse
|
||||
|
||||
**Silent Failures (9):**
|
||||
- Errors swallowed in try/catch blocks
|
||||
- Functions returning wrong types on error
|
||||
- No error logging
|
||||
- Network failures treated as false instead of errors
|
||||
|
||||
**Test Coverage Gaps (10):**
|
||||
- No test coverage requirements
|
||||
- No testing framework setup
|
||||
- Code shipped with 0% coverage
|
||||
|
||||
**Build Failures (2):**
|
||||
- Code committed that doesn't compile
|
||||
- Tests committed that fail
|
||||
|
||||
**Dependency Issues (6):**
|
||||
- Critical CVEs not caught
|
||||
- Version conflicts between packages
|
||||
|
||||
## The Solution: Mechanical Enforcement
|
||||
|
||||
Don't **ask** agents to:
|
||||
- "Please do code review"
|
||||
- "Make sure to run tests"
|
||||
- "Check for security issues"
|
||||
|
||||
Instead, **BLOCK** commits that:
|
||||
- Have type errors
|
||||
- Contain hardcoded secrets
|
||||
- Don't pass tests
|
||||
- Have security vulnerabilities
|
||||
|
||||
### Why This Works
|
||||
|
||||
**Example: Type Safety**
|
||||
|
||||
❌ **Process-based (fails):**
|
||||
```
|
||||
Human: "Please avoid using 'any' types"
|
||||
Agent: "I'll make sure to use proper types"
|
||||
*Agent uses any types anyway*
|
||||
```
|
||||
|
||||
✅ **Mechanically enforced (works):**
|
||||
```
|
||||
Agent writes: const x: any = 123;
|
||||
Git hook runs: ❌ Error: no-explicit-any
|
||||
Commit blocked
|
||||
Agent must fix to proceed
|
||||
```
|
||||
|
||||
The agent doesn't get to **claim** it followed the process. The automated gate **determines** if code is acceptable.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Fail Fast
|
||||
|
||||
Detect issues at commit time, not in CI, not in code review, not in production.
|
||||
|
||||
**Timeline:**
|
||||
- ⚡ Commit time: Type errors, lint errors, secrets → **BLOCKED**
|
||||
- 🔄 CI time: Build failures, test failures, CVEs → **BLOCKED**
|
||||
- 👀 Code review: Architecture, design, business logic
|
||||
- 🚀 Production: (Issues should never reach here)
|
||||
|
||||
### 2. Non-Negotiable
|
||||
|
||||
No agent can bypass enforcement. No "skip hooks" flag. No emergency override.
|
||||
|
||||
If the code doesn't pass gates, it doesn't get committed. Period.
|
||||
|
||||
### 3. Portable
|
||||
|
||||
Same enforcement across:
|
||||
- All projects
|
||||
- All developers (human + AI)
|
||||
- All environments (local, CI, production)
|
||||
|
||||
### 4. Minimal Friction
|
||||
|
||||
Auto-fix where possible:
|
||||
- Prettier formats code automatically
|
||||
- ESLint --fix corrects simple issues
|
||||
- Only block when can't auto-fix
|
||||
|
||||
### 5. Clear Feedback
|
||||
|
||||
When enforcement blocks a commit, tell the agent:
|
||||
- ❌ What's wrong (type error, lint violation, etc.)
|
||||
- 📍 Where it is (file:line)
|
||||
- ✅ How to fix it (expected type, remove 'any', etc.)
|
||||
|
||||
## Impact Prediction
|
||||
|
||||
Based on a 50-issue production analysis:
|
||||
|
||||
| Phase | Enforcement | Issues Prevented |
|
||||
|-------|-------------|------------------|
|
||||
| **Phase 1** | Pre-commit + strict mode + ESLint | 25 of 50 (50%) |
|
||||
| **Phase 2** | + CI expansion + npm audit | 35 of 50 (70%) |
|
||||
| **Phase 3** | + OWASP + coverage gates | 45 of 50 (90%) |
|
||||
|
||||
**The remaining 10%** require human judgment:
|
||||
- Architecture decisions
|
||||
- Business logic correctness
|
||||
- User experience
|
||||
- Performance optimization
|
||||
|
||||
## Agent Behavior Evolution
|
||||
|
||||
### Before Quality Rails
|
||||
```
|
||||
Agent: "I've completed the feature and run all tests"
|
||||
Reality: Code has type errors, no tests written, hardcoded password
|
||||
Result: 50 issues discovered in code review
|
||||
```
|
||||
|
||||
### After Quality Rails
|
||||
```
|
||||
Agent writes code with 'any' type
|
||||
Git hook: ❌ no-explicit-any
|
||||
Agent rewrites with proper type
|
||||
Git hook: ✅ Pass
|
||||
|
||||
Agent writes code with hardcoded password
|
||||
Git hook: ❌ Secret detected
|
||||
Agent moves to environment variable
|
||||
Git hook: ✅ Pass
|
||||
|
||||
Agent commits without tests
|
||||
CI: ❌ Coverage below 80%
|
||||
Agent writes tests
|
||||
CI: ✅ Pass
|
||||
```
|
||||
|
||||
**The agent learns:** Good code passes gates, bad code is rejected.
|
||||
|
||||
## Why This Matters for AI Development
|
||||
|
||||
AI agents are **deterministically bad** at self-enforcement:
|
||||
- They claim to follow processes
|
||||
- They **believe** they're following processes
|
||||
- Output proves otherwise
|
||||
|
||||
But AI agents are **good** at responding to mechanical feedback:
|
||||
- Clear error messages
|
||||
- Specific line numbers
|
||||
- Concrete fix requirements
|
||||
|
||||
Quality Rails exploits this strength and avoids the weakness.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Process compliance:** Agents claim → Output fails
|
||||
**Mechanical enforcement:** Gates determine → Output succeeds
|
||||
|
||||
This is not philosophical. This is pragmatic. Based on 50 real issues from production code.
|
||||
|
||||
Quality Rails exists because **process-based quality doesn't work at scale with AI agents.**
|
||||
|
||||
Mechanical enforcement does.
|
||||
166
rails/quality/README.md
Normal file
166
rails/quality/README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Quality Rails
|
||||
|
||||
Portable quality enforcement for TypeScript, Python, and Node.js projects.
|
||||
|
||||
## 🎯 What This Prevents
|
||||
|
||||
Based on real-world validation of 50 issues in a production codebase:
|
||||
- ❌ Hardcoded passwords
|
||||
- ❌ SQL injection vulnerabilities
|
||||
- ❌ Type safety violations (`any` types)
|
||||
- ❌ Missing test coverage
|
||||
- ❌ Build failures
|
||||
- ❌ Dependency vulnerabilities
|
||||
|
||||
**70% of these issues are prevented mechanically with quality-rails.**
|
||||
|
||||
## ⚡ Quick Start (Mosaic)
|
||||
|
||||
### New Project
|
||||
```bash
|
||||
# Apply template from Mosaic
|
||||
~/.mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/project
|
||||
|
||||
# Install dependencies
|
||||
cd /path/to/project
|
||||
npm install
|
||||
|
||||
# Initialize git hooks
|
||||
npx husky install
|
||||
|
||||
# Verify enforcement is working
|
||||
~/.mosaic/bin/mosaic-quality-verify --target /path/to/project
|
||||
```
|
||||
|
||||
### Existing Project
|
||||
```bash
|
||||
# Same as above - works for new or existing projects
|
||||
~/.mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/existing-project
|
||||
```
|
||||
|
||||
## 🛡️ What You Get
|
||||
|
||||
✅ **TypeScript strict mode** - All type checks enabled
|
||||
✅ **ESLint blocking `any` types** - no-explicit-any: error
|
||||
✅ **Pre-commit hooks** - Type check + lint + format before commit
|
||||
✅ **Secret scanning** - Block hardcoded passwords/API keys
|
||||
✅ **CI/CD templates** - Woodpecker, GitHub Actions, GitLab
|
||||
✅ **Test coverage enforcement** - 80% threshold
|
||||
✅ **Security scanning** - npm audit, OWASP checks
|
||||
|
||||
## 📦 Available Templates
|
||||
|
||||
| Template | Language | Framework | Status |
|
||||
|----------|----------|-----------|--------|
|
||||
| `typescript-node` | TypeScript | Node.js | ✅ Ready |
|
||||
| `typescript-nextjs` | TypeScript | Next.js | ✅ Ready |
|
||||
| `monorepo` | TypeScript | TurboRepo + pnpm | ✅ Ready |
|
||||
| `python` | Python | - | 🚧 Coming Soon |
|
||||
|
||||
### Monorepo Template
|
||||
|
||||
Perfect for projects combining **Next.js frontend** + **NestJS backend** in one repository.
|
||||
|
||||
Features:
|
||||
- 🎯 **Multi-package aware** - lint-staged only checks changed packages
|
||||
- ⚡ **TurboRepo caching** - Faster builds and tests
|
||||
- 🔀 **Parallel dev servers** - Run web + API simultaneously
|
||||
- 📦 **pnpm workspaces** - Efficient dependency management
|
||||
- 🛡️ **Package-specific rules** - Next.js and NestJS get appropriate ESLint configs
|
||||
|
||||
Example structure:
|
||||
```
|
||||
monorepo/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js frontend
|
||||
│ └── api/ # NestJS backend
|
||||
└── packages/
|
||||
├── shared-types/
|
||||
├── ui/
|
||||
└── config/
|
||||
```
|
||||
|
||||
## 🧪 How It Works
|
||||
|
||||
### Pre-Commit (Local Enforcement)
|
||||
```bash
|
||||
# You try to commit code with a type error
|
||||
git commit -m "Add feature"
|
||||
|
||||
# Quality rails blocks it:
|
||||
❌ Type error: Type 'number' is not assignable to type 'string'
|
||||
❌ ESLint: Unexpected any. Specify a different type.
|
||||
✋ Commit blocked - fix errors and try again
|
||||
```
|
||||
|
||||
### CI/CD (Remote Enforcement)
|
||||
```yaml
|
||||
# Woodpecker pipeline runs:
|
||||
✓ npm audit (dependency security)
|
||||
✓ eslint (code quality)
|
||||
✓ tsc --noEmit (type checking)
|
||||
✓ jest --coverage (tests + coverage)
|
||||
✓ npm run build (compilation)
|
||||
|
||||
# If any step fails, merge is blocked
|
||||
```
|
||||
|
||||
## 🎓 Philosophy
|
||||
|
||||
**Process compliance doesn't work.**
|
||||
|
||||
Instructing AI agents to "do code review" or "run tests" fails. They claim to follow processes but output quality doesn't match claims.
|
||||
|
||||
**Mechanical enforcement works.**
|
||||
|
||||
Quality rails don't ask agents to follow processes. They **block commits** that don't pass automated checks.
|
||||
|
||||
- Type errors? → **Commit blocked**
|
||||
- Hardcoded secrets? → **Commit blocked**
|
||||
- Test failures? → **Commit blocked**
|
||||
- Missing coverage? → **Commit blocked**
|
||||
|
||||
This works for **any agent runtime** (Codex, Claude, OpenCode, Gemini, etc.) because enforcement is mechanical, not instructional.
|
||||
|
||||
[Read more: PHILOSOPHY.md](./PHILOSOPHY.md)
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- [TypeScript Setup Guide](./docs/TYPESCRIPT-SETUP.md)
|
||||
- [CI/CD Configuration](./docs/CI-SETUP.md)
|
||||
|
||||
## 🔧 Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/install.sh` | Install template to project (Linux/Mac) |
|
||||
| `scripts/install.ps1` | Install template to project (Windows) |
|
||||
| `scripts/verify.sh` | Verify enforcement is working (Linux/Mac) |
|
||||
| `scripts/verify.ps1` | Verify enforcement is working (Windows) |
|
||||
|
||||
## 🚀 Roadmap
|
||||
|
||||
- [x] TypeScript/Node template
|
||||
- [x] Pre-commit enforcement (husky + lint-staged)
|
||||
- [x] CI/CD templates (Woodpecker, GitHub Actions)
|
||||
- [x] Installation scripts
|
||||
- [x] Verification testing
|
||||
- [x] Next.js template
|
||||
- [x] Monorepo template
|
||||
- [ ] Python template
|
||||
- [ ] Coverage visualization
|
||||
- [ ] IDE integration (VSCode extension)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Quality Rails is based on lessons learned from real production codebases. Contributions welcome!
|
||||
|
||||
## 📝 License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
Built to solve real problems discovered in AI-assisted development workflows.
|
||||
|
||||
Based on validation findings from a production patch milestone.
|
||||
174
rails/quality/docs/CI-SETUP.md
Normal file
174
rails/quality/docs/CI-SETUP.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# CI/CD Configuration Guide
|
||||
|
||||
Configure Woodpecker CI, GitHub Actions, or GitLab CI for quality enforcement.
|
||||
|
||||
## Woodpecker CI
|
||||
|
||||
Quality Rails includes `.woodpecker.yml` template.
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
1. **Install** - Dependencies
|
||||
2. **Security Audit** - npm audit for CVEs
|
||||
3. **Lint** - ESLint checks
|
||||
4. **Type Check** - TypeScript compilation
|
||||
5. **Test** - Jest with coverage thresholds
|
||||
6. **Build** - Production build
|
||||
|
||||
### Configuration
|
||||
|
||||
No additional configuration needed. Push to repository and Woodpecker runs automatically.
|
||||
|
||||
### Blocking Merges
|
||||
|
||||
Configure Woodpecker to block merges on pipeline failure:
|
||||
1. Repository Settings → Protected Branches
|
||||
2. Require Woodpecker pipeline to pass
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
Copy from `templates/typescript-node/.github/workflows/quality.yml`:
|
||||
|
||||
```yaml
|
||||
name: Quality Enforcement
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm audit --audit-level=high
|
||||
- run: npm run lint
|
||||
- run: npm run type-check
|
||||
- run: npm run test -- --coverage
|
||||
- run: npm run build
|
||||
```
|
||||
|
||||
### Blocking Merges
|
||||
|
||||
1. Repository Settings → Branches → Branch protection rules
|
||||
2. Require status checks to pass: `quality`
|
||||
|
||||
## GitLab CI
|
||||
|
||||
Copy from `templates/typescript-node/.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- install
|
||||
- audit
|
||||
- quality
|
||||
- build
|
||||
|
||||
install:
|
||||
stage: install
|
||||
script:
|
||||
- npm ci
|
||||
|
||||
audit:
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=high
|
||||
|
||||
lint:
|
||||
stage: quality
|
||||
script:
|
||||
- npm run lint
|
||||
|
||||
typecheck:
|
||||
stage: quality
|
||||
script:
|
||||
- npm run type-check
|
||||
|
||||
test:
|
||||
stage: quality
|
||||
script:
|
||||
- npm run test -- --coverage
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- npm run build
|
||||
```
|
||||
|
||||
## Coverage Enforcement
|
||||
|
||||
Configure Jest coverage thresholds in `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
CI will fail if coverage drops below threshold.
|
||||
|
||||
## Security Scanning
|
||||
|
||||
### npm audit
|
||||
|
||||
Runs automatically in CI. Adjust sensitivity:
|
||||
|
||||
```bash
|
||||
npm audit --audit-level=moderate # Block moderate+
|
||||
npm audit --audit-level=high # Block high+critical only
|
||||
npm audit --audit-level=critical # Block critical only
|
||||
```
|
||||
|
||||
### Snyk Integration
|
||||
|
||||
Add to CI for additional security:
|
||||
|
||||
```yaml
|
||||
- run: npx snyk test
|
||||
```
|
||||
|
||||
Requires `SNYK_TOKEN` environment variable.
|
||||
|
||||
## Notification Setup
|
||||
|
||||
### Woodpecker
|
||||
|
||||
Configure in Woodpecker UI:
|
||||
- Slack/Discord webhooks
|
||||
- Email notifications
|
||||
- Status badges
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Add notification step:
|
||||
|
||||
```yaml
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -X POST $WEBHOOK_URL -d "Build failed"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Pipeline fails but pre-commit passed:**
|
||||
- CI runs all packages, pre-commit only checks changed files
|
||||
- Fix issues in all packages, not just changed files
|
||||
|
||||
**npm audit blocks on low-severity:**
|
||||
- Adjust `--audit-level` to `moderate` or `high`
|
||||
|
||||
**Coverage threshold too strict:**
|
||||
- Lower thresholds in package.json
|
||||
- Add coverage exceptions for specific files
|
||||
164
rails/quality/docs/TYPESCRIPT-SETUP.md
Normal file
164
rails/quality/docs/TYPESCRIPT-SETUP.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# TypeScript Project Setup Guide
|
||||
|
||||
Step-by-step guide to add Quality Rails to a TypeScript project.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ and npm/pnpm
|
||||
- Git repository initialized
|
||||
- TypeScript project (or create with `npm init` + `tsc --init`)
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Clone Quality Rails
|
||||
|
||||
```bash
|
||||
git clone git@git.mosaicstack.dev:mosaic/quality-rails.git
|
||||
```
|
||||
|
||||
### 2. Run Installation Script
|
||||
|
||||
```bash
|
||||
# From your project directory
|
||||
../quality-rails/scripts/install.sh --template typescript-node --target .
|
||||
```
|
||||
|
||||
This copies:
|
||||
- `.husky/pre-commit` - Git hooks
|
||||
- `.lintstagedrc.js` - Pre-commit checks
|
||||
- `.eslintrc.js` - Strict ESLint rules
|
||||
- `tsconfig.json` - TypeScript strict mode
|
||||
- `.woodpecker.yml` - CI pipeline
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
Add to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint 'src/**/*.{ts,tsx}' --max-warnings=0",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-security": "^3.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
```bash
|
||||
npm install
|
||||
npx husky install
|
||||
```
|
||||
|
||||
### 4. Verify Enforcement
|
||||
|
||||
```bash
|
||||
../quality-rails/scripts/verify.sh
|
||||
```
|
||||
|
||||
Should output:
|
||||
```
|
||||
✅ PASS: Type errors blocked
|
||||
✅ PASS: 'any' types blocked
|
||||
✅ PASS: Lint errors blocked
|
||||
```
|
||||
|
||||
## What Gets Enforced
|
||||
|
||||
### TypeScript Strict Mode
|
||||
|
||||
All strict checks enabled in `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint Rules
|
||||
|
||||
Key rules in `.eslintrc.js`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
'@typescript-eslint/no-explicit-any': 'error', // Block 'any' types
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn', // Require return types
|
||||
'@typescript-eslint/no-floating-promises': 'error', // Catch unhandled promises
|
||||
'@typescript-eslint/no-misused-promises': 'error', // Prevent promise misuse
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-Commit Checks
|
||||
|
||||
On every `git commit`, runs:
|
||||
1. ESLint with --max-warnings=0
|
||||
2. TypeScript type check
|
||||
3. Prettier formatting
|
||||
4. Secret scanning (if git-secrets installed)
|
||||
|
||||
If any fail → **commit blocked**.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "husky - pre-commit hook exited with code 1"
|
||||
|
||||
This means pre-commit checks failed. Read the error output:
|
||||
|
||||
```
|
||||
src/example.ts:5:14 - error TS2322: Type 'number' is not assignable to type 'string'
|
||||
```
|
||||
|
||||
Fix the error and commit again.
|
||||
|
||||
### "Cannot find module '@typescript-eslint/parser'"
|
||||
|
||||
Dependencies not installed:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Pre-commit hooks not running
|
||||
|
||||
Husky not initialized:
|
||||
```bash
|
||||
npx husky install
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
See [CUSTOMIZATION.md](./CUSTOMIZATION.md) for adjusting strictness levels.
|
||||
|
||||
## CI/CD Setup
|
||||
|
||||
See [CI-SETUP.md](./CI-SETUP.md) for Woodpecker/GitHub Actions configuration.
|
||||
53
rails/quality/scripts/install.ps1
Normal file
53
rails/quality/scripts/install.ps1
Normal file
@@ -0,0 +1,53 @@
|
||||
# Quality Rails Installation Script (Windows)
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Template,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$TargetDir = "."
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = Split-Path -Parent $ScriptDir
|
||||
$TemplateDir = Join-Path $RepoRoot "templates\$Template"
|
||||
|
||||
if (-not (Test-Path $TemplateDir)) {
|
||||
Write-Error "Template '$Template' not found at $TemplateDir"
|
||||
Write-Host "Available templates: typescript-node, typescript-nextjs, python, monorepo"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Installing Quality Rails: $Template"
|
||||
Write-Host "Target directory: $TargetDir"
|
||||
Write-Host ""
|
||||
|
||||
# Copy template files
|
||||
Write-Host "Copying template files..."
|
||||
if (Test-Path "$TemplateDir\.husky") {
|
||||
Copy-Item -Path "$TemplateDir\.husky" -Destination $TargetDir -Recurse -Force
|
||||
}
|
||||
Copy-Item -Path "$TemplateDir\.lintstagedrc.js" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path "$TemplateDir\.eslintrc.strict.js" -Destination "$TargetDir\.eslintrc.js" -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path "$TemplateDir\tsconfig.strict.json" -Destination "$TargetDir\tsconfig.json" -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path "$TemplateDir\.woodpecker.yml" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "✓ Files copied"
|
||||
|
||||
if (Test-Path "$TargetDir\package.json") {
|
||||
Write-Host ""
|
||||
Write-Host "⚠ package.json exists. Please manually merge dependencies from:"
|
||||
Write-Host " $TemplateDir\package.json.snippet"
|
||||
} else {
|
||||
Write-Host "⚠ No package.json found. Create one and add dependencies from:"
|
||||
Write-Host " $TemplateDir\package.json.snippet"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✓ Quality Rails installed successfully!"
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host "1. Install dependencies: npm install"
|
||||
Write-Host "2. Initialize husky: npx husky install"
|
||||
Write-Host "3. Run verification: ..\quality-rails\scripts\verify.ps1"
|
||||
75
rails/quality/scripts/install.sh
Executable file
75
rails/quality/scripts/install.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Quality Rails Installation Script
|
||||
# Usage: ./install.sh --template typescript-node [--target /path/to/project]
|
||||
|
||||
TEMPLATE=""
|
||||
TARGET_DIR="."
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--template)
|
||||
TEMPLATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
TARGET_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 --template <template-name> [--target <directory>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$TEMPLATE" ]; then
|
||||
echo "Error: --template is required"
|
||||
echo "Available templates: typescript-node, typescript-nextjs, python, monorepo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
TEMPLATE_DIR="$REPO_ROOT/templates/$TEMPLATE"
|
||||
|
||||
if [ ! -d "$TEMPLATE_DIR" ]; then
|
||||
echo "Error: Template '$TEMPLATE' not found at $TEMPLATE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Quality Rails: $TEMPLATE"
|
||||
echo "Target directory: $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
# Copy template files
|
||||
echo "Copying template files..."
|
||||
cp -r "$TEMPLATE_DIR/.husky" "$TARGET_DIR/" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/.lintstagedrc.js" "$TARGET_DIR/" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/.eslintrc.strict.js" "$TARGET_DIR/.eslintrc.js" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/tsconfig.strict.json" "$TARGET_DIR/tsconfig.json" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/.woodpecker.yml" "$TARGET_DIR/" 2>/dev/null || true
|
||||
|
||||
echo "✓ Files copied"
|
||||
|
||||
# Check if package.json exists
|
||||
if [ -f "$TARGET_DIR/package.json" ]; then
|
||||
echo ""
|
||||
echo "⚠ package.json exists. Please manually merge dependencies from:"
|
||||
echo " $TEMPLATE_DIR/package.json.snippet"
|
||||
else
|
||||
echo "⚠ No package.json found. Create one and add dependencies from:"
|
||||
echo " $TEMPLATE_DIR/package.json.snippet"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Quality Rails installed successfully!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Install dependencies: npm install"
|
||||
echo "2. Initialize husky: npx husky install"
|
||||
echo "3. Run verification: ~/.mosaic/bin/mosaic-quality-verify --target $TARGET_DIR"
|
||||
echo ""
|
||||
57
rails/quality/scripts/verify.ps1
Normal file
57
rails/quality/scripts/verify.ps1
Normal file
@@ -0,0 +1,57 @@
|
||||
# Quality Rails Verification Script (Windows)
|
||||
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
Write-Host "Quality Rails Enforcement Verification"
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
Write-Host ""
|
||||
|
||||
$Passed = 0
|
||||
$Failed = 0
|
||||
|
||||
# Test 1: Type error blocked
|
||||
Write-Host "Test 1: Type errors should be blocked..."
|
||||
"const x: string = 123;" | Out-File -FilePath test-file.ts -Encoding utf8
|
||||
git add test-file.ts 2>$null
|
||||
$output = git commit -m "Test commit" 2>&1 | Out-String
|
||||
if ($output -match "error") {
|
||||
Write-Host "✅ PASS: Type errors blocked" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: Type errors NOT blocked" -ForegroundColor Red
|
||||
$Failed++
|
||||
}
|
||||
git reset HEAD test-file.ts 2>$null
|
||||
Remove-Item test-file.ts -ErrorAction SilentlyContinue
|
||||
|
||||
# Test 2: any type blocked
|
||||
Write-Host ""
|
||||
Write-Host "Test 2: 'any' types should be blocked..."
|
||||
"const x: any = 123;" | Out-File -FilePath test-file.ts -Encoding utf8
|
||||
git add test-file.ts 2>$null
|
||||
$output = git commit -m "Test commit" 2>&1 | Out-String
|
||||
if ($output -match "no-explicit-any") {
|
||||
Write-Host "✅ PASS: 'any' types blocked" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: 'any' types NOT blocked" -ForegroundColor Red
|
||||
$Failed++
|
||||
}
|
||||
git reset HEAD test-file.ts 2>$null
|
||||
Remove-Item test-file.ts -ErrorAction SilentlyContinue
|
||||
|
||||
# Summary
|
||||
Write-Host ""
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
Write-Host "Verification Summary"
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
Write-Host "✅ Passed: $Passed"
|
||||
Write-Host "❌ Failed: $Failed"
|
||||
Write-Host ""
|
||||
|
||||
if ($Failed -eq 0) {
|
||||
Write-Host "🎉 All tests passed! Quality enforcement is working." -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "⚠ Some tests failed. Review configuration." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
92
rails/quality/scripts/verify.sh
Executable file
92
rails/quality/scripts/verify.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Quality Rails Verification Script
|
||||
# Tests that enforcement actually works
|
||||
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo "Quality Rails Enforcement Verification"
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# Test 1: Type error blocked
|
||||
echo "Test 1: Type errors should be blocked..."
|
||||
echo "const x: string = 123;" > test-file.ts
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q "error"; then
|
||||
echo "✅ PASS: Type errors blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "❌ FAIL: Type errors NOT blocked"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
|
||||
# Test 2: any type blocked
|
||||
echo ""
|
||||
echo "Test 2: 'any' types should be blocked..."
|
||||
echo "const x: any = 123;" > test-file.ts
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q "no-explicit-any"; then
|
||||
echo "✅ PASS: 'any' types blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "❌ FAIL: 'any' types NOT blocked"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
|
||||
# Test 3: Hardcoded secret blocked (if git-secrets installed)
|
||||
echo ""
|
||||
echo "Test 3: Hardcoded secrets should be blocked..."
|
||||
if command -v git-secrets &> /dev/null; then
|
||||
echo "const password = 'SuperSecret123!';" > test-file.ts
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q -i "secret\|password"; then
|
||||
echo "✅ PASS: Secrets blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "⚠ WARN: Secrets NOT blocked (git-secrets may need configuration)"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
else
|
||||
echo "⚠ SKIP: git-secrets not installed"
|
||||
fi
|
||||
|
||||
# Test 4: Lint error blocked
|
||||
echo ""
|
||||
echo "Test 4: Lint errors should be blocked..."
|
||||
echo "const x=123" > test-file.ts # Missing semicolon
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q "prettier"; then
|
||||
echo "✅ PASS: Lint errors blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "❌ FAIL: Lint errors NOT blocked"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo "Verification Summary"
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo "✅ Passed: $PASSED"
|
||||
echo "❌ Failed: $FAILED"
|
||||
echo ""
|
||||
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo "🎉 All tests passed! Quality enforcement is working."
|
||||
exit 0
|
||||
else
|
||||
echo "⚠ Some tests failed. Review configuration."
|
||||
exit 1
|
||||
fi
|
||||
64
rails/quality/templates/monorepo/.eslintrc.strict.js
Normal file
64
rails/quality/templates/monorepo/.eslintrc.strict.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Root ESLint config for monorepo
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'security'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:security/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
// Type Safety - STRICT
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
|
||||
// Promise/Async Safety
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
|
||||
// Code Quality
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
|
||||
// Prettier
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.next',
|
||||
'out',
|
||||
'coverage',
|
||||
'.turbo',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
// Next.js apps
|
||||
files: ['apps/**/app/**/*.{ts,tsx}', 'apps/**/pages/**/*.{ts,tsx}'],
|
||||
extends: ['next/core-web-vitals'],
|
||||
},
|
||||
{
|
||||
// NestJS apps
|
||||
files: ['apps/**/*.controller.ts', 'apps/**/*.service.ts', 'apps/**/*.module.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
2
rails/quality/templates/monorepo/.husky/pre-commit
Normal file
2
rails/quality/templates/monorepo/.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
29
rails/quality/templates/monorepo/.lintstagedrc.js
Normal file
29
rails/quality/templates/monorepo/.lintstagedrc.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Monorepo-aware lint-staged configuration
|
||||
module.exports = {
|
||||
// TypeScript files across all packages
|
||||
'**/*.{ts,tsx}': (filenames) => {
|
||||
const commands = [
|
||||
`eslint ${filenames.join(' ')} --fix --max-warnings=0`,
|
||||
];
|
||||
|
||||
// Only run tsc in packages that have tsconfig.json
|
||||
// NOTE: lint-staged passes absolute paths, so we need to match both:
|
||||
// - Relative: apps/api/src/file.ts
|
||||
// - Absolute: /home/user/project/apps/api/src/file.ts
|
||||
const packages = [...new Set(filenames.map(f => {
|
||||
const match = f.match(/(?:^|\/)(apps|packages)\/([^/]+)\//);
|
||||
return match ? `${match[1]}/${match[2]}` : null;
|
||||
}))].filter(Boolean);
|
||||
|
||||
packages.forEach(pkg => {
|
||||
commands.push(`tsc --project ${pkg}/tsconfig.json --noEmit`);
|
||||
});
|
||||
|
||||
return commands;
|
||||
},
|
||||
|
||||
// Format all files
|
||||
'**/*.{js,jsx,ts,tsx,json,md,yml,yaml}': [
|
||||
'prettier --write',
|
||||
],
|
||||
};
|
||||
67
rails/quality/templates/monorepo/.woodpecker.yml
Normal file
67
rails/quality/templates/monorepo/.woodpecker.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# Woodpecker CI Quality Enforcement Pipeline - Monorepo
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
|
||||
security-audit:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm audit --audit-level=high
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run lint
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
typecheck:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run type-check
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
test:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
NODE_ENV: "production"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
96
rails/quality/templates/monorepo/README-STRUCTURE.md
Normal file
96
rails/quality/templates/monorepo/README-STRUCTURE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Monorepo Structure
|
||||
|
||||
This quality-rails monorepo template supports the following structure:
|
||||
|
||||
```
|
||||
monorepo/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js frontend
|
||||
│ │ ├── package.json
|
||||
│ │ ├── tsconfig.json # extends ../../tsconfig.base.json
|
||||
│ │ └── .eslintrc.js # extends ../../.eslintrc.strict.js
|
||||
│ └── api/ # NestJS backend
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ └── .eslintrc.js
|
||||
├── packages/
|
||||
│ ├── shared-types/ # Shared TypeScript types
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ └── config/ # Shared configuration
|
||||
├── .husky/
|
||||
│ └── pre-commit
|
||||
├── .lintstagedrc.js # Multi-package aware
|
||||
├── .eslintrc.strict.js # Root ESLint config
|
||||
├── tsconfig.base.json # Base TypeScript config
|
||||
├── turbo.json # TurboRepo configuration
|
||||
├── pnpm-workspace.yaml # pnpm workspaces
|
||||
└── package.json # Root package with scripts
|
||||
```
|
||||
|
||||
## Package-Specific Configs
|
||||
|
||||
Each package extends the root configuration:
|
||||
|
||||
**apps/web/tsconfig.json:**
|
||||
```json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"jsx": "preserve",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".next"]
|
||||
}
|
||||
```
|
||||
|
||||
**apps/api/tsconfig.json:**
|
||||
```json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
```
|
||||
|
||||
## Running Commands
|
||||
|
||||
**All packages:**
|
||||
```bash
|
||||
npm run lint # Lint all packages
|
||||
npm run type-check # Type check all packages
|
||||
npm run test # Test all packages
|
||||
npm run build # Build all packages
|
||||
```
|
||||
|
||||
**Single package:**
|
||||
```bash
|
||||
npm run lint --workspace=apps/web
|
||||
npm run dev --workspace=apps/api
|
||||
```
|
||||
|
||||
**With TurboRepo:**
|
||||
```bash
|
||||
turbo run build # Build with caching
|
||||
turbo run dev --parallel # Run dev servers in parallel
|
||||
```
|
||||
|
||||
## Pre-Commit Enforcement
|
||||
|
||||
lint-staged automatically detects which packages contain modified files and runs:
|
||||
- ESLint on changed files
|
||||
- TypeScript check on affected packages
|
||||
- Prettier on all changed files
|
||||
|
||||
Only runs checks on packages that have changes (efficient).
|
||||
30
rails/quality/templates/monorepo/package.json.snippet
Normal file
30
rails/quality/templates/monorepo/package.json.snippet
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "monorepo",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"type-check": "turbo run type-check",
|
||||
"test": "turbo run test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-security": "^3.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"turbo": "^2.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
39
rails/quality/templates/monorepo/tsconfig.base.json
Normal file
39
rails/quality/templates/monorepo/tsconfig.base.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
// STRICT MODE - All enabled
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
// Additional Checks
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "build", ".next", "out"]
|
||||
}
|
||||
23
rails/quality/templates/monorepo/turbo.json.snippet
Normal file
23
rails/quality/templates/monorepo/turbo.json.snippet
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "build/**"]
|
||||
},
|
||||
"lint": {
|
||||
"cache": false
|
||||
},
|
||||
"type-check": {
|
||||
"cache": false
|
||||
},
|
||||
"test": {
|
||||
"cache": false
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:security/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'security'],
|
||||
rules: {
|
||||
// Type Safety - STRICT
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
|
||||
// Promise/Async Safety
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
|
||||
// React/Next.js specific
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
|
||||
// Prettier
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: ['.next', 'out', 'public', '.eslintrc.js'],
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
'*.{ts,tsx}': [
|
||||
'eslint --fix --max-warnings=0',
|
||||
'bash -c "tsc --noEmit"',
|
||||
],
|
||||
'*.{js,jsx,ts,tsx,json,md,css}': [
|
||||
'prettier --write',
|
||||
],
|
||||
};
|
||||
67
rails/quality/templates/typescript-nextjs/.woodpecker.yml
Normal file
67
rails/quality/templates/typescript-nextjs/.woodpecker.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# Woodpecker CI Quality Enforcement Pipeline - Next.js
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
|
||||
security-audit:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm audit --audit-level=high
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run lint
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
typecheck:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run type-check
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
test:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
NODE_ENV: "production"
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
@@ -0,0 +1,50 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
|
||||
// Security headers
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'X-DNS-Prefetch-Control',
|
||||
value: 'on'
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload'
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'SAMEORIGIN'
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff'
|
||||
},
|
||||
{
|
||||
key: 'X-XSS-Protection',
|
||||
value: '1; mode=block'
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'origin-when-cross-origin'
|
||||
},
|
||||
{
|
||||
key: 'Permissions-Policy',
|
||||
value: 'camera=(), microphone=(), geolocation=()'
|
||||
},
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self';"
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings=0",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "^14.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-security": "^3.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".next", "out"]
|
||||
}
|
||||
53
rails/quality/templates/typescript-node/.eslintrc.strict.js
Normal file
53
rails/quality/templates/typescript-node/.eslintrc.strict.js
Normal file
@@ -0,0 +1,53 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'security'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:security/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
// Type Safety - STRICT
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
|
||||
// Promise/Async Safety
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
|
||||
// Type Assertions
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' },
|
||||
],
|
||||
|
||||
// Code Quality
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'warn',
|
||||
|
||||
// Prettier
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
9
rails/quality/templates/typescript-node/.lintstagedrc.js
Normal file
9
rails/quality/templates/typescript-node/.lintstagedrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
'*.{ts,tsx}': [
|
||||
'eslint --fix --max-warnings=0',
|
||||
'bash -c "tsc --noEmit"',
|
||||
],
|
||||
'*.{js,jsx,ts,tsx,json,md}': [
|
||||
'prettier --write',
|
||||
],
|
||||
};
|
||||
66
rails/quality/templates/typescript-node/.woodpecker.yml
Normal file
66
rails/quality/templates/typescript-node/.woodpecker.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
# Woodpecker CI Quality Enforcement Pipeline
|
||||
# Runs on: push, pull_request, manual
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Stage 1: Install
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
|
||||
# Stage 2: Security Audit
|
||||
security-audit:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm audit --audit-level=high
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# Stage 3: Lint
|
||||
lint:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run lint
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# Stage 4: Type Check
|
||||
typecheck:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run type-check
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# Stage 5: Test with Coverage
|
||||
test:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# Stage 6: Build
|
||||
build:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- npm run build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
22
rails/quality/templates/typescript-node/package.json.snippet
Normal file
22
rails/quality/templates/typescript-node/package.json.snippet
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint 'src/**/*.{ts,tsx}' --max-warnings=0",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-security": "^3.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
42
rails/quality/templates/typescript-node/tsconfig.strict.json
Normal file
42
rails/quality/templates/typescript-node/tsconfig.strict.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"removeComments": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
// STRICT MODE - All enabled
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
// Additional Checks
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
||||
@@ -13,3 +13,19 @@ This repository is attached to the machine-wide Mosaic framework.
|
||||
- Keep universal standards in `~/.mosaic`
|
||||
- Keep repo-specific behavior in this repo
|
||||
- Avoid copying large runtime configs into each project
|
||||
|
||||
## Optional Quality Rails
|
||||
|
||||
Use `.mosaic/quality-rails.yml` to track whether quality rails are enabled for this repo.
|
||||
|
||||
Apply a template:
|
||||
|
||||
```bash
|
||||
~/.mosaic/bin/mosaic-quality-apply --template <template> --target .
|
||||
```
|
||||
|
||||
Verify enforcement:
|
||||
|
||||
```bash
|
||||
~/.mosaic/bin/mosaic-quality-verify --target .
|
||||
```
|
||||
|
||||
10
templates/repo/.mosaic/quality-rails.yml
Normal file
10
templates/repo/.mosaic/quality-rails.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
enabled: false
|
||||
template: ""
|
||||
|
||||
# Set enabled: true and choose one template:
|
||||
# - typescript-node
|
||||
# - typescript-nextjs
|
||||
# - monorepo
|
||||
#
|
||||
# Apply manually:
|
||||
# ~/.mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
Reference in New Issue
Block a user