Skip to content

Commit afc023c

Browse files
feat(config): implement commitlint + husky with controlled github actions
- add commitlint with conventional commit format requiring scopes - set up husky pre-commit hook to block multi-project commits - update github actions to run conditionally based on commit scope - firebase admin deploy only runs for (admin|config) commits - cloud run web deploy only runs for (web|config) commits - add npm scripts for commit format examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 1ef5715 commit afc023c

File tree

9 files changed

+951
-33
lines changed

9 files changed

+951
-33
lines changed

.github/workflows/firebase-admin-deploy.yml

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,49 @@ on:
44
push:
55
branches:
66
- main
7-
paths:
8-
- 'projects/admin/**'
9-
- 'firebase.json'
10-
- '.firebaserc'
11-
- 'package*.json'
127
pull_request:
138
branches:
149
- main
15-
paths:
16-
- 'projects/admin/**'
17-
- 'firebase.json'
18-
- '.firebaserc'
19-
- 'package*.json'
2010

2111
jobs:
12+
check-commit:
13+
runs-on: ubuntu-latest
14+
outputs:
15+
should-run: ${{ steps.check.outputs.should-run }}
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Check commit message
23+
id: check
24+
run: |
25+
if [ "${{ github.event_name }}" == "pull_request" ]; then
26+
COMMITS=$(git rev-list --no-merges ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
27+
else
28+
COMMITS="${{ github.sha }}"
29+
fi
30+
31+
should_run=false
32+
for commit in $COMMITS; do
33+
message=$(git log --format=%B -n 1 $commit)
34+
echo "Checking commit: $commit"
35+
echo "Message: $message"
36+
37+
if echo "$message" | grep -E "^[^(]+\((admin|config)\):" > /dev/null; then
38+
should_run=true
39+
break
40+
fi
41+
done
42+
43+
echo "should-run=$should_run" >> $GITHUB_OUTPUT
44+
echo "Admin workflow should run: $should_run"
45+
2246
build-and-deploy:
2347
runs-on: ubuntu-latest
48+
needs: check-commit
49+
if: needs.check-commit.outputs.should-run == 'true'
2450

2551
steps:
2652
- name: Checkout code

.github/workflows/google-cloudrun-docker.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,34 @@ on:
66
- main
77

88
jobs:
9+
check-commit:
10+
runs-on: ubuntu-latest
11+
outputs:
12+
should-run: ${{ steps.check.outputs.should-run }}
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Check commit message
20+
id: check
21+
run: |
22+
message=$(git log --format=%B -n 1 ${{ github.sha }})
23+
echo "Checking commit: ${{ github.sha }}"
24+
echo "Message: $message"
25+
26+
should_run=false
27+
if echo "$message" | grep -E "^[^(]+\((web|config)\):" > /dev/null; then
28+
should_run=true
29+
fi
30+
31+
echo "should-run=$should_run" >> $GITHUB_OUTPUT
32+
echo "Web workflow should run: $should_run"
933
test:
1034
runs-on: ubuntu-latest
35+
needs: check-commit
36+
if: needs.check-commit.outputs.should-run == 'true'
1137
steps:
1238
- uses: actions/checkout@v4
1339

.husky/commit-msg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npx --no -- commitlint --edit $1

.husky/pre-commit

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/bash
2+
3+
# Get list of staged files
4+
staged_files=$(git diff --cached --name-only)
5+
6+
# Define project directories
7+
web_pattern="^projects/web/"
8+
admin_pattern="^projects/admin/"
9+
mfe_pattern="^projects/code-samples-mfe/"
10+
shared_pattern="^projects/shared/"
11+
config_patterns="^(package\.json|package-lock\.json|angular\.json|tsconfig\.json|\.eslint|\.prettier|tailwind\.config|commitlint\.config|\.github/|\.husky/|scripts/|supabase/|e2e/|.*\.md$)"
12+
13+
# Count files in each category
14+
web_count=0
15+
admin_count=0
16+
mfe_count=0
17+
shared_count=0
18+
config_count=0
19+
20+
for file in $staged_files; do
21+
if [[ $file =~ $web_pattern ]]; then
22+
web_count=$((web_count + 1))
23+
elif [[ $file =~ $admin_pattern ]]; then
24+
admin_count=$((admin_count + 1))
25+
elif [[ $file =~ $mfe_pattern ]]; then
26+
mfe_count=$((mfe_count + 1))
27+
elif [[ $file =~ $shared_pattern ]]; then
28+
shared_count=$((shared_count + 1))
29+
elif [[ $file =~ $config_patterns ]]; then
30+
config_count=$((config_count + 1))
31+
fi
32+
done
33+
34+
# Count non-zero project types (exclude shared from count - it can be combined)
35+
project_types=0
36+
if [ $web_count -gt 0 ]; then project_types=$((project_types + 1)); fi
37+
if [ $admin_count -gt 0 ]; then project_types=$((project_types + 1)); fi
38+
if [ $mfe_count -gt 0 ]; then project_types=$((project_types + 1)); fi
39+
40+
# Allow config-only, shared-only, or single project + shared + config
41+
if [ $project_types -gt 1 ]; then
42+
echo "❌ Error: Cannot commit changes to multiple projects in a single commit."
43+
echo ""
44+
echo "Staged files by project:"
45+
if [ $web_count -gt 0 ]; then echo " 📱 Web: $web_count files"; fi
46+
if [ $admin_count -gt 0 ]; then echo " 🔧 Admin: $admin_count files"; fi
47+
if [ $mfe_count -gt 0 ]; then echo " 📦 MFE: $mfe_count files"; fi
48+
if [ $shared_count -gt 0 ]; then echo " 🔄 Shared: $shared_count files"; fi
49+
if [ $config_count -gt 0 ]; then echo " ⚙️ Config: $config_count files"; fi
50+
echo ""
51+
echo "Please commit changes to one project at a time:"
52+
echo " git reset"
53+
echo " git add projects/web/ # or projects/admin/, etc."
54+
echo " git add projects/shared/ # shared can be combined"
55+
echo " git commit -m 'feat(web): your changes'"
56+
exit 1
57+
fi
58+
59+
echo "✅ Single project commit validation passed"

angular.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
},
4343
{
4444
"type": "anyComponentStyle",
45-
"maximumWarning": "4kB",
46-
"maximumError": "8kB"
45+
"maximumWarning": "8kB",
46+
"maximumError": "12kB"
4747
}
4848
],
4949
"outputHashing": "all"

commitlint.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
extends: ['@commitlint/config-conventional'],
3+
rules: {
4+
'scope-enum': [
5+
2,
6+
'always',
7+
['web', 'admin', 'mfe', 'shared', 'config']
8+
],
9+
'scope-empty': [2, 'never'],
10+
'subject-case': [2, 'always', 'lower-case'],
11+
'type-enum': [
12+
2,
13+
'always',
14+
[
15+
'feat',
16+
'fix',
17+
'docs',
18+
'style',
19+
'refactor',
20+
'test',
21+
'build',
22+
'ci',
23+
'chore',
24+
'revert'
25+
]
26+
]
27+
}
28+
};

firebase.json

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,32 @@
22
"hosting": [
33
{
44
"target": "admin",
5-
"public": "dist/apps/admin-spa",
5+
"public": "dist/admin/browser",
66
"ignore": ["**/.*", "**/node_modules/**"],
7-
"rewrites": [{ "source": "**", "destination": "/index.html" }],
7+
"rewrites": [
8+
{ "source": "**", "destination": "/index.html" }
9+
],
10+
"redirects": [
11+
{
12+
"source": "/admin",
13+
"destination": "/",
14+
"type": 301
15+
}
16+
],
817
"headers": [
918
{
10-
"source": "**/*.@(js|css)",
19+
"source": "/",
20+
"headers": [
21+
{ "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" },
22+
{ "key": "X-Frame-Options", "value": "DENY" },
23+
{ "key": "X-Content-Type-Options", "value": "nosniff" },
24+
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
25+
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(), payment=()" },
26+
{ "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co wss://*.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" }
27+
]
28+
},
29+
{
30+
"source": "**/*.@(js|css|woff|woff2|ttf|otf|eot|svg|png|jpg|jpeg|gif|webp|ico)",
1131
"headers": [
1232
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
1333
]
@@ -16,18 +36,29 @@
1636
},
1737
{
1838
"target": "samples",
19-
"public": "dist/apps/code-samples-mfe",
39+
"public": "dist/code-samples-mfe",
2040
"ignore": ["**/.*", "**/node_modules/**"],
21-
"rewrites": [{ "source": "**", "destination": "/index.html" }],
41+
"rewrites": [
42+
{ "source": "**", "destination": "/index.html" }
43+
],
2244
"headers": [
45+
{
46+
"source": "/",
47+
"headers": [
48+
{ "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" },
49+
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
50+
{ "key": "X-Content-Type-Options", "value": "nosniff" }
51+
]
52+
},
2353
{
2454
"source": "**/remoteEntry.@(js|mjs)",
2555
"headers": [
26-
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
56+
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" },
57+
{ "key": "Access-Control-Allow-Origin", "value": "*" }
2758
]
2859
},
2960
{
30-
"source": "**/*.@(js|css)",
61+
"source": "**/*.@(js|css|woff|woff2|ttf|otf|eot|svg|png|jpg|jpeg|gif|webp|ico)",
3162
"headers": [
3263
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
3364
]

0 commit comments

Comments
 (0)