Skip to content

Commit 8dacd55

Browse files
iamzifeiclaude
andcommitted
feat: scaffold OrrisTech org-level code quality automation
Centralized .github org repo with: - 8 reusable CI workflows (lint, test, build, security, react-doctor, e2e, lighthouse, dead-links) - Synced files (org-rules, CI caller, PR template, lefthook, vscode settings) - Reference templates (CLAUDE.md, eslint, vitest, playwright, lighthouse, 6 doc templates) - Scripts (detect-pkg-manager, bootstrap-repo, audit-all-repos) - File sync via BetaHuhn/repo-file-sync-action (3 pilot repos) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 8dacd55

31 files changed

+3243
-0
lines changed

.github/workflows/ci-build.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Reusable workflow: Build
2+
# Runs a production build with auto-detected or custom build command.
3+
# Sets common dummy env vars for Next.js builds so they don't fail on missing secrets.
4+
name: CI - Build
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
node_version:
10+
description: "Node.js version to use"
11+
required: false
12+
type: string
13+
default: "22"
14+
working_directory:
15+
description: "Working directory for monorepo support"
16+
required: false
17+
type: string
18+
default: "."
19+
build_command:
20+
description: "Custom build command (default: auto-detect from package.json)"
21+
required: false
22+
type: string
23+
default: ""
24+
25+
jobs:
26+
build:
27+
name: Build
28+
runs-on: ubuntu-latest
29+
defaults:
30+
run:
31+
working-directory: ${{ inputs.working_directory }}
32+
# Provide dummy env vars so Next.js builds don't fail on missing public env
33+
env:
34+
NEXT_PUBLIC_SITE_URL: "https://example.com"
35+
NEXT_PUBLIC_API_URL: "https://api.example.com"
36+
NEXT_PUBLIC_SUPABASE_URL: "https://placeholder.supabase.co"
37+
NEXT_PUBLIC_SUPABASE_ANON_KEY: "placeholder-anon-key"
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v4
41+
42+
- name: Detect package manager
43+
id: detect-pm
44+
run: |
45+
if [ -f "pnpm-lock.yaml" ]; then
46+
echo "manager=pnpm" >> "$GITHUB_OUTPUT"
47+
echo "install_cmd=pnpm install --frozen-lockfile" >> "$GITHUB_OUTPUT"
48+
elif [ -f "bun.lockb" ] || [ -f "bun.lock" ]; then
49+
echo "manager=bun" >> "$GITHUB_OUTPUT"
50+
echo "install_cmd=bun install --frozen-lockfile" >> "$GITHUB_OUTPUT"
51+
else
52+
echo "manager=npm" >> "$GITHUB_OUTPUT"
53+
echo "install_cmd=npm ci" >> "$GITHUB_OUTPUT"
54+
fi
55+
56+
- name: Setup pnpm
57+
if: steps.detect-pm.outputs.manager == 'pnpm'
58+
uses: pnpm/action-setup@v4
59+
60+
- name: Setup Bun
61+
if: steps.detect-pm.outputs.manager == 'bun'
62+
uses: oven-sh/setup-bun@v2
63+
64+
- name: Setup Node.js
65+
uses: actions/setup-node@v4
66+
with:
67+
node-version: ${{ inputs.node_version }}
68+
cache: ${{ steps.detect-pm.outputs.manager != 'bun' && steps.detect-pm.outputs.manager || '' }}
69+
70+
- name: Install dependencies
71+
run: ${{ steps.detect-pm.outputs.install_cmd }}
72+
73+
# Determine which build command to run
74+
- name: Detect build command
75+
id: detect-build
76+
run: |
77+
if [ -n "${{ inputs.build_command }}" ]; then
78+
echo "cmd=${{ inputs.build_command }}" >> "$GITHUB_OUTPUT"
79+
else
80+
PM="${{ steps.detect-pm.outputs.manager }}"
81+
# Check for a 'build' script in package.json
82+
if node -e "const p=require('./package.json'); process.exit(p.scripts && p.scripts.build ? 0 : 1)"; then
83+
if [ "$PM" = "npm" ]; then
84+
echo "cmd=npm run build" >> "$GITHUB_OUTPUT"
85+
else
86+
echo "cmd=$PM run build" >> "$GITHUB_OUTPUT"
87+
fi
88+
else
89+
echo "::error::No 'build' script found in package.json and no custom build_command provided"
90+
exit 1
91+
fi
92+
fi
93+
94+
- name: Run production build
95+
run: ${{ steps.detect-build.outputs.cmd }}
96+
97+
# Upload build output for downstream jobs (e.g., e2e, lighthouse)
98+
- name: Upload build artifacts
99+
uses: actions/upload-artifact@v4
100+
with:
101+
name: build-output
102+
path: |
103+
${{ inputs.working_directory }}/.next/
104+
${{ inputs.working_directory }}/dist/
105+
${{ inputs.working_directory }}/build/
106+
${{ inputs.working_directory }}/out/
107+
if-no-files-found: ignore
108+
retention-days: 1
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Reusable workflow: Dead Link Checker
2+
# Runs linkinator to find broken links in the built site or provided URL.
3+
# Posts results as a PR comment if broken links are found.
4+
name: CI - Dead Link Checker
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
node_version:
10+
description: "Node.js version to use"
11+
required: false
12+
type: string
13+
default: "22"
14+
working_directory:
15+
description: "Working directory for monorepo support"
16+
required: false
17+
type: string
18+
default: "."
19+
urls:
20+
description: "URL or path to check for broken links (e.g., https://example.com or ./out)"
21+
required: false
22+
type: string
23+
default: ""
24+
25+
jobs:
26+
dead-links:
27+
name: Dead Link Checker
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: read
31+
pull-requests: write
32+
defaults:
33+
run:
34+
working-directory: ${{ inputs.working_directory }}
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@v4
38+
39+
# Determine the target URL/path to scan
40+
- name: Determine scan target
41+
id: target
42+
run: |
43+
if [ -n "${{ inputs.urls }}" ]; then
44+
echo "url=${{ inputs.urls }}" >> "$GITHUB_OUTPUT"
45+
echo "should_run=true" >> "$GITHUB_OUTPUT"
46+
else
47+
# Auto-detect common build output directories
48+
for dir in out dist build .next/server/pages public; do
49+
if [ -d "$dir" ]; then
50+
echo "url=./$dir" >> "$GITHUB_OUTPUT"
51+
echo "should_run=true" >> "$GITHUB_OUTPUT"
52+
echo "Found build output directory: $dir"
53+
exit 0
54+
fi
55+
done
56+
echo "should_run=false" >> "$GITHUB_OUTPUT"
57+
echo "::notice::No URL provided and no build output directory found — skipping dead link check"
58+
fi
59+
60+
- name: Setup Node.js
61+
if: steps.target.outputs.should_run == 'true'
62+
uses: actions/setup-node@v4
63+
with:
64+
node-version: ${{ inputs.node_version }}
65+
66+
# Run linkinator and capture output
67+
- name: Run linkinator
68+
if: steps.target.outputs.should_run == 'true'
69+
id: linkinator
70+
continue-on-error: true
71+
run: |
72+
TARGET="${{ steps.target.outputs.url }}"
73+
echo "Scanning: $TARGET"
74+
75+
# Run linkinator and capture output. Use --retry for transient network failures.
76+
npx linkinator "$TARGET" \
77+
--retry \
78+
--timeout 30000 \
79+
--verbosity error \
80+
2>&1 | tee /tmp/linkinator-output.txt
81+
82+
EXIT_CODE=${PIPESTATUS[0]}
83+
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
84+
85+
# Check if there are broken links in the output
86+
if [ "$EXIT_CODE" -ne 0 ]; then
87+
echo "has_broken_links=true" >> "$GITHUB_OUTPUT"
88+
else
89+
echo "has_broken_links=false" >> "$GITHUB_OUTPUT"
90+
fi
91+
92+
# Post broken link results as a PR comment
93+
- name: Post results as PR comment
94+
if: steps.target.outputs.should_run == 'true' && steps.linkinator.outputs.has_broken_links == 'true' && github.event_name == 'pull_request'
95+
uses: actions/github-script@v7
96+
with:
97+
script: |
98+
const fs = require('fs');
99+
const output = fs.readFileSync('/tmp/linkinator-output.txt', 'utf8');
100+
101+
// Truncate if too long for a PR comment
102+
const maxLength = 60000;
103+
const truncated = output.length > maxLength
104+
? output.substring(0, maxLength) + '\n\n... (output truncated)'
105+
: output;
106+
107+
const body = `## Dead Link Checker Report
108+
109+
Broken links were detected by [linkinator](https://github.com/JustinBeckwith/linkinator):
110+
111+
\`\`\`
112+
${truncated}
113+
\`\`\`
114+
115+
> Please fix the broken links listed above.`;
116+
117+
// Find and update existing comment or create a new one
118+
const { data: comments } = await github.rest.issues.listComments({
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
issue_number: context.issue.number,
122+
});
123+
124+
const botComment = comments.find(c =>
125+
c.body.includes('## Dead Link Checker Report')
126+
);
127+
128+
if (botComment) {
129+
await github.rest.issues.updateComment({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
comment_id: botComment.id,
133+
body,
134+
});
135+
} else {
136+
await github.rest.issues.createComment({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
issue_number: context.issue.number,
140+
body,
141+
});
142+
}
143+
144+
# Fail the job if broken links were found
145+
- name: Fail if broken links found
146+
if: steps.target.outputs.should_run == 'true' && steps.linkinator.outputs.has_broken_links == 'true'
147+
run: |
148+
echo "::error::Broken links were detected — see output above or PR comment for details"
149+
exit 1

.github/workflows/ci-e2e.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Reusable workflow: End-to-End Tests (Playwright)
2+
# Only runs if a Playwright config file exists in the project.
3+
# Installs Playwright browsers, runs tests, and uploads results as artifacts.
4+
name: CI - E2E Tests
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
node_version:
10+
description: "Node.js version to use"
11+
required: false
12+
type: string
13+
default: "22"
14+
working_directory:
15+
description: "Working directory for monorepo support"
16+
required: false
17+
type: string
18+
default: "."
19+
20+
jobs:
21+
e2e:
22+
name: E2E Tests (Playwright)
23+
runs-on: ubuntu-latest
24+
defaults:
25+
run:
26+
working-directory: ${{ inputs.working_directory }}
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
31+
# Check if Playwright config exists before proceeding
32+
- name: Check for Playwright config
33+
id: check-pw
34+
run: |
35+
if [ -f "playwright.config.ts" ] || [ -f "playwright.config.js" ] || [ -f "playwright.config.mjs" ]; then
36+
echo "has_config=true" >> "$GITHUB_OUTPUT"
37+
else
38+
echo "has_config=false" >> "$GITHUB_OUTPUT"
39+
echo "::notice::No Playwright config found — skipping E2E tests"
40+
fi
41+
42+
- name: Detect package manager
43+
if: steps.check-pw.outputs.has_config == 'true'
44+
id: detect-pm
45+
run: |
46+
if [ -f "pnpm-lock.yaml" ]; then
47+
echo "manager=pnpm" >> "$GITHUB_OUTPUT"
48+
echo "install_cmd=pnpm install --frozen-lockfile" >> "$GITHUB_OUTPUT"
49+
elif [ -f "bun.lockb" ] || [ -f "bun.lock" ]; then
50+
echo "manager=bun" >> "$GITHUB_OUTPUT"
51+
echo "install_cmd=bun install --frozen-lockfile" >> "$GITHUB_OUTPUT"
52+
else
53+
echo "manager=npm" >> "$GITHUB_OUTPUT"
54+
echo "install_cmd=npm ci" >> "$GITHUB_OUTPUT"
55+
fi
56+
57+
- name: Setup pnpm
58+
if: steps.check-pw.outputs.has_config == 'true' && steps.detect-pm.outputs.manager == 'pnpm'
59+
uses: pnpm/action-setup@v4
60+
61+
- name: Setup Bun
62+
if: steps.check-pw.outputs.has_config == 'true' && steps.detect-pm.outputs.manager == 'bun'
63+
uses: oven-sh/setup-bun@v2
64+
65+
- name: Setup Node.js
66+
if: steps.check-pw.outputs.has_config == 'true'
67+
uses: actions/setup-node@v4
68+
with:
69+
node-version: ${{ inputs.node_version }}
70+
cache: ${{ steps.detect-pm.outputs.manager != 'bun' && steps.detect-pm.outputs.manager || '' }}
71+
72+
- name: Install dependencies
73+
if: steps.check-pw.outputs.has_config == 'true'
74+
run: ${{ steps.detect-pm.outputs.install_cmd }}
75+
76+
# Install Playwright browsers and their system dependencies
77+
- name: Install Playwright browsers
78+
if: steps.check-pw.outputs.has_config == 'true'
79+
run: npx playwright install --with-deps
80+
81+
# Run Playwright tests
82+
- name: Run Playwright tests
83+
if: steps.check-pw.outputs.has_config == 'true'
84+
run: npx playwright test
85+
86+
# Upload test results (HTML report + trace files) as artifacts
87+
- name: Upload Playwright report
88+
if: steps.check-pw.outputs.has_config == 'true' && always()
89+
uses: actions/upload-artifact@v4
90+
with:
91+
name: playwright-report
92+
path: |
93+
${{ inputs.working_directory }}/playwright-report/
94+
${{ inputs.working_directory }}/test-results/
95+
if-no-files-found: ignore
96+
retention-days: 7

0 commit comments

Comments
 (0)