Skip to content

Commit 2fa4539

Browse files
devakesuCopilot
andauthored
v1.5.3 (#346)
* chore: enhance error handling in AddAttendanceDialog * Improve type safety and fallback logic in isDutyLeaveConstraintError (#347) * Initial plan * fix: improve type safety and error handling in isDutyLeaveConstraintError Co-authored-by: devakesu <61821107+devakesu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devakesu <61821107+devakesu@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent a42096a commit 2fa4539

File tree

10 files changed

+258
-20
lines changed

10 files changed

+258
-20
lines changed

.example.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
NEXT_PUBLIC_APP_NAME=GhostClass
4343

4444
# ⚠️ App Version (displayed in health checks and footer)
45-
NEXT_PUBLIC_APP_VERSION=1.5.2
45+
NEXT_PUBLIC_APP_VERSION=1.5.3
4646

4747
# ⚠️ Your production domain (without https://)
4848
# All URL-based variables are derived from this

.github/workflows/pipeline.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,11 @@ jobs:
146146
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
147147

148148
- name: Sign image (keyless)
149-
env:
150-
COSIGN_EXPERIMENTAL: "true"
151149
run: |
152150
cosign sign --yes \
151+
-a "repo=${{ github.repository }}" \
152+
-a "workflow=${{ github.workflow }}" \
153+
-a "ref=${{ github.ref }}" \
153154
ghcr.io/${{ github.repository_owner }}/${{ secrets.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }}
154155
155156
# Trigger deployment in Coolify

.github/workflows/release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,16 @@ jobs:
218218
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
219219

220220
- name: Sign image (keyless)
221-
env:
222-
COSIGN_EXPERIMENTAL: "true"
223221
run: |
224222
cosign sign --yes \
223+
-a "repo=${{ github.repository }}" \
224+
-a "workflow=${{ github.workflow }}" \
225+
-a "ref=${{ github.ref }}" \
226+
-a "version=${{ needs.calculate-version.outputs.version }}" \
225227
ghcr.io/${{ github.repository_owner }}/${{ steps.prep.outputs.image_name }}@${{ steps.build-push.outputs.digest }}
226228
227229
# Sign SBOM file
228230
- name: Sign SBOM artifact
229-
env:
230-
COSIGN_EXPERIMENTAL: "true"
231231
run: |
232232
cosign sign-blob --yes \
233233
--bundle sbom.json.bundle \

docs/COSIGN_VERIFICATION.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Cosign Signature Verification Guide
2+
3+
This document explains how to verify Docker image signatures created by the CI/CD pipeline using Sigstore Cosign.
4+
5+
## Understanding Keyless Signing
6+
7+
Our pipeline uses **keyless signing** with Sigstore Cosign, which means:
8+
- No private keys to manage or secure
9+
- Signatures are linked to GitHub Actions OIDC tokens
10+
- Certificate identity reflects the exact workflow that signed the image
11+
12+
## Certificate Identity Format
13+
14+
When GitHub Actions signs an image, the certificate identity follows this format:
15+
```
16+
https://github.com/{OWNER}/{REPO}/.github/workflows/{WORKFLOW}.yml@refs/heads/{BRANCH}
17+
```
18+
19+
For our main branch pipeline:
20+
```
21+
https://github.com/devakesu/GhostClass/.github/workflows/pipeline.yml@refs/heads/main
22+
```
23+
24+
For releases:
25+
```
26+
https://github.com/devakesu/GhostClass/.github/workflows/release.yml@refs/tags/{VERSION}
27+
```
28+
29+
## Verification Methods
30+
31+
### Method 1: Regex Pattern (Recommended for Automation)
32+
33+
This method is flexible and works across different workflows and tags:
34+
35+
```bash
36+
cosign verify \
37+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/" \
38+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
39+
ghcr.io/devakesu/ghostclass:main
40+
```
41+
42+
**Advantages:**
43+
- Works for both `pipeline.yml` and `release.yml`
44+
- Works for all branches and tags
45+
- Simpler to maintain
46+
47+
### Method 2: Exact Identity Match (Strict Verification)
48+
49+
For maximum security when you know the exact workflow:
50+
51+
```bash
52+
# For main branch (pipeline.yml)
53+
cosign verify \
54+
--certificate-identity="https://github.com/devakesu/GhostClass/.github/workflows/pipeline.yml@refs/heads/main" \
55+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
56+
ghcr.io/devakesu/ghostclass:main
57+
58+
# For releases (release.yml)
59+
cosign verify \
60+
--certificate-identity="https://github.com/devakesu/GhostClass/.github/workflows/release.yml@refs/tags/v1.3.0" \
61+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
62+
ghcr.io/devakesu/ghostclass:v1.3.0
63+
```
64+
65+
**Advantages:**
66+
- Most restrictive
67+
- Ensures signature came from specific workflow and branch
68+
69+
**Disadvantages:**
70+
- Must update for different workflows or branches
71+
- Harder to automate
72+
73+
## Deployment System Integration
74+
75+
### Coolify
76+
77+
If you're using Coolify or similar deployment systems that run health checks, update your verification script:
78+
79+
```bash
80+
#!/bin/bash
81+
set -e
82+
83+
# Download cosign
84+
wget -qO /tmp/cosign https://github.com/sigstore/cosign/releases/download/v2.2.4/cosign-linux-amd64
85+
chmod +x /tmp/cosign
86+
87+
# Verify signature using regex pattern (more flexible)
88+
/tmp/cosign verify \
89+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/" \
90+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
91+
ghcr.io/devakesu/ghostclass:main
92+
93+
# Verify attestation
94+
/tmp/cosign verify-attestation \
95+
--type cyclonedx \
96+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/" \
97+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
98+
ghcr.io/devakesu/ghostclass:main
99+
100+
echo "✓ Image signature and attestation verified successfully"
101+
```
102+
103+
### Docker Compose / Kubernetes
104+
105+
Add an init container or pre-deployment job:
106+
107+
```yaml
108+
# Example init container for Kubernetes
109+
initContainers:
110+
- name: verify-signature
111+
image: gcr.io/projectsigstore/cosign:v2.2.4
112+
command:
113+
- sh
114+
- -c
115+
- |
116+
cosign verify \
117+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/" \
118+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
119+
ghcr.io/devakesu/ghostclass:main
120+
```
121+
122+
## Troubleshooting
123+
124+
### Error: "no signatures found"
125+
126+
**Possible causes:**
127+
1. Image was built before signing was implemented
128+
2. Signing step failed in CI/CD pipeline
129+
3. Using wrong certificate identity or OIDC issuer
130+
4. Image digest doesn't match (use `@sha256:...` instead of tags when possible)
131+
132+
**Solutions:**
133+
```bash
134+
# Check if signature exists
135+
cosign tree ghcr.io/devakesu/ghostclass:main
136+
137+
# Verify with more verbose output
138+
cosign verify \
139+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass" \
140+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
141+
ghcr.io/devakesu/ghostclass:main \
142+
--verbose
143+
```
144+
145+
### Error: "certificate identity mismatch"
146+
147+
Your certificate identity doesn't match what was used during signing.
148+
149+
**Solution:** Use regex pattern instead of exact match:
150+
```bash
151+
# ❌ Too specific
152+
--certificate-identity="https://github.com/devakesu/GhostClass"
153+
154+
# ✅ Flexible regex
155+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/"
156+
```
157+
158+
### Verifying Specific Image Digest
159+
160+
For maximum security, verify using the image digest instead of tags:
161+
162+
```bash
163+
# Get the digest
164+
docker pull ghcr.io/devakesu/ghostclass:main
165+
docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/devakesu/ghostclass:main
166+
167+
# Verify the digest
168+
cosign verify \
169+
--certificate-identity-regexp="^https://github.com/devakesu/GhostClass/.github/workflows/" \
170+
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
171+
ghcr.io/devakesu/ghostclass@sha256:abc123...
172+
```
173+
174+
## Reference
175+
176+
- [Sigstore Cosign Documentation](https://docs.sigstore.dev/cosign/overview/)
177+
- [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
178+
- [Keyless Signing Explained](https://docs.sigstore.dev/cosign/keyless/)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ghostclass",
3-
"version": "1.5.2",
3+
"version": "1.5.3",
44
"private": true,
55
"engines": {
66
"node": "^20.19.0 || >=22.12.0",

public/api-docs/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ openapi: 3.1.0
55

66
info:
77
title: GhostClass API
8-
version: 1.5.2
8+
version: 1.5.3
99
description: |
1010
**GhostClass API** provides endpoints for managing attendance synchronization with EzyGo.
1111

src/components/attendance/AddAttendanceDialog.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,15 @@ export function AddAttendanceDialog({
356356
onOpenChange(false);
357357

358358
} catch (error: any) {
359-
logger.error("Add Record Failed:", error);
360-
361359
// Check for duty leave constraint violation in catch block as well
362360
if (isDutyLeaveConstraintError(error)) {
363361
toast.error(getDutyLeaveErrorMessage(courseId, coursesData));
364-
// Expected business constraint violation; do not report to Sentry
362+
// Expected business constraint violation; do not report to Sentry or log as error
365363
return;
366-
}
364+
}
365+
366+
// Only log and report unexpected errors
367+
logger.error("Add Record Failed:", error);
367368
toast.error("Failed to add record");
368369

369370
Sentry.captureException(error, {

src/lib/__tests__/duty-leave-error-handling.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,37 @@ describe('Duty Leave Constraint Error Handling', () => {
5252
it('should handle undefined error gracefully', () => {
5353
expect(isDutyLeaveConstraintError(undefined)).toBe(false);
5454
});
55+
56+
it('should match P0001 error using message fallback when hint is missing', () => {
57+
const error = {
58+
code: 'P0001',
59+
message: 'Maximum 5 Duty Leaves exceeded for course: 12345'
60+
};
61+
62+
expect(isDutyLeaveConstraintError(error)).toBe(true);
63+
});
64+
65+
it('should match P0001 error when nested in details property', () => {
66+
const error = {
67+
details: {
68+
code: 'P0001',
69+
hint: 'Only 5 duty leaves allowed per semester per course',
70+
message: 'Maximum 5 Duty Leaves exceeded for course: 72329'
71+
}
72+
};
73+
74+
expect(isDutyLeaveConstraintError(error)).toBe(true);
75+
});
76+
77+
it('should still use message fallback when details is a non-object value', () => {
78+
const error = {
79+
code: 'P0001',
80+
message: 'Maximum 5 Duty Leaves exceeded for course: 72329',
81+
details: 'Additional error context'
82+
};
83+
84+
expect(isDutyLeaveConstraintError(error)).toBe(true);
85+
});
5586
});
5687

5788
describe('getDutyLeaveErrorMessage', () => {

src/lib/error-handling.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,39 @@ export interface DatabaseError {
2727
* }
2828
* ```
2929
*/
30-
export function isDutyLeaveConstraintError(error: DatabaseError | null | undefined): boolean {
31-
if (!error) return false;
32-
return (
33-
error.code === "P0001" &&
34-
error.hint === "Only 5 duty leaves allowed per semester per course"
30+
export function isDutyLeaveConstraintError(error: unknown): boolean {
31+
if (!error || typeof error !== "object") return false;
32+
33+
const errorObj = error as Record<string, unknown>;
34+
35+
// Check direct error properties
36+
const isDirectMatch = (
37+
errorObj.code === "P0001" &&
38+
errorObj.hint === "Only 5 duty leaves allowed per semester per course"
3539
);
40+
41+
if (isDirectMatch) return true;
42+
43+
// Check if error is wrapped in a details property or other nested structure
44+
if (errorObj.details && typeof errorObj.details === "object") {
45+
const details = errorObj.details as Record<string, unknown>;
46+
const isNestedMatch =
47+
details.code === "P0001" &&
48+
details.hint === "Only 5 duty leaves allowed per semester per course";
49+
50+
if (isNestedMatch) {
51+
return true;
52+
}
53+
}
54+
55+
// Check error message as fallback
56+
if (errorObj.message && typeof errorObj.message === 'string') {
57+
return errorObj.message.includes('Maximum') &&
58+
errorObj.message.includes('Duty Leaves exceeded') &&
59+
errorObj.code === "P0001";
60+
}
61+
62+
return false;
3663
}
3764

3865
/**

0 commit comments

Comments
 (0)