Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,62 @@ jobs:
- run: pnpm --filter @project-template/docs check
- run: pnpm --filter @project-template/docs build:site

mobile-android:
name: Mobile Android
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: gradle/actions/setup-gradle@v6
- uses: android-actions/setup-android@v4
- run: sdkmanager "platforms;android-36" "build-tools;36.0.0"
- run: mobile/gradlew -p mobile :composeApp:testDebugUnitTest :composeApp:lintDebug :composeApp:assembleDebug
- name: Upload Android APK
id: android-artifact
uses: actions/upload-artifact@v7
with:
name: sitlab-android-debug
path: mobile/composeApp/build/outputs/apk/debug/composeApp-debug.apk
if-no-files-found: error
retention-days: 14
- name: Add Android download to summary
run: |
echo "### Android build" >> "$GITHUB_STEP_SUMMARY"
echo "[Download debug APK](${{ steps.android-artifact.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY"

mobile-ios:
name: Mobile iOS framework
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: gradle/actions/setup-gradle@v6
- run: mobile/gradlew -p mobile :composeApp:linkDebugFrameworkIosSimulatorArm64
- name: Upload iOS simulator framework
id: ios-artifact
uses: actions/upload-artifact@v7
with:
name: sitlab-ios-simulator-framework
path: mobile/composeApp/build/bin/iosSimulatorArm64/debugFramework/SitLabShared.framework
if-no-files-found: error
retention-days: 14
- name: Add iOS download to summary
run: |
echo "### iOS build" >> "$GITHUB_STEP_SUMMARY"
echo "[Download simulator framework](${{ steps.ios-artifact.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY"

complete:
name: CI complete
if: always()
needs: [format, contract, backend, web, ui, docs]
needs: [format, contract, backend, web, ui, docs, mobile-android, mobile-ios]
runs-on: ubuntu-latest
steps:
- name: Require all jobs
Expand All @@ -176,7 +228,9 @@ jobs:
WEB_RESULT: ${{ needs.web.result }}
UI_RESULT: ${{ needs.ui.result }}
DOCS_RESULT: ${{ needs.docs.result }}
MOBILE_ANDROID_RESULT: ${{ needs.mobile-android.result }}
MOBILE_IOS_RESULT: ${{ needs.mobile-ios.result }}
run: |
for result in "$FORMAT_RESULT" "$CONTRACT_RESULT" "$BACKEND_RESULT" "$WEB_RESULT" "$UI_RESULT" "$DOCS_RESULT"; do
for result in "$FORMAT_RESULT" "$CONTRACT_RESULT" "$BACKEND_RESULT" "$WEB_RESULT" "$UI_RESULT" "$DOCS_RESULT" "$MOBILE_ANDROID_RESULT" "$MOBILE_IOS_RESULT"; do
test "$result" = success
done
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
**/node_modules/**
**/dist/**
**/build/**
**/.gradle/**
**/.astro/**
**/storybook-static/**
**/playwright-report/**
Expand Down
30 changes: 27 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ api_filter := "@project-template/api"
docs_filter := "@project-template/docs"
ui_filter := "@project-template/ui"
web_filter := "@project-template/web"
mobile_dir := "mobile"

alias fmt := format
alias generate := contract-generate
Expand All @@ -22,13 +23,13 @@ install:
dev: backend-dev

# Build every production surface.
build: api-build backend-build ui-build web-build storybook-build docs-build
build: api-build backend-build ui-build web-build storybook-build docs-build mobile-android-build

# Run static analysis for every language surface.
lint: api-lint backend-lint frontend-style-check web-lint ui-lint docs-check
lint: api-lint backend-lint frontend-style-check web-lint ui-lint docs-check mobile-lint

# Run all unit and component tests.
test: backend-test web-test ui-test template-test
test: backend-test web-test ui-test template-test mobile-test

# Run all type checkers.
typecheck: api-check web-typecheck ui-typecheck docs-check
Expand Down Expand Up @@ -135,6 +136,29 @@ web-test:
frontend-style-check:
pnpm check:frontend-style

# Check Kotlin source whitespace and Android lint policy.
mobile-lint: mobile-format-check
cd {{ mobile_dir }} && ./gradlew :composeApp:lintDebug

# Apply Kotlin formatting through the IDE/ktfmt before committing; this gate rejects whitespace damage.
mobile-format:
@echo "Format Kotlin sources with IntelliJ's Kotlin formatter (official style)."

mobile-format-check:
git diff --check -- {{ mobile_dir }}

# Run shared domain tests on the Android JVM host.
mobile-test:
cd {{ mobile_dir }} && ./gradlew :composeApp:testDebugUnitTest

# Build the Android application package.
mobile-android-build:
cd {{ mobile_dir }} && ./gradlew :composeApp:assembleDebug

# Link the simulator framework; requires macOS/Xcode.
mobile-ios-framework:
cd {{ mobile_dir }} && ./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64

# Verify initializer rollback, retry, and one-time semantics.
template-test:
pnpm test:template
Expand Down
53 changes: 53 additions & 0 deletions api/models/auth.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,56 @@ model GitLabOAuthCallbackQuery {
@minLength(1)
state: string;
}

model MobileOAuthStartQuery {
@query
@minLength(43)
@maxLength(128)
codeChallenge: string;
}

model MobileOAuthCallbackQuery {
@query
code?: string;

@query
state?: string;

@query
error?: string;
}

model MobileOAuthFallbackResponse {
...OkResponse;

@header
contentType: "text/html";

@body
body: string;
}

model MobileOAuthExchangeRequest {
@minLength(1)
code: string;

@minLength(1)
state: string;

@minLength(43)
@maxLength(128)
codeVerifier: string;
}

model MobileOAuthExchangeResult {
authenticated: true;
}

model MobileOAuthSessionResponse {
...OkResponse;

@header("Set-Cookie")
setCookie: string;

...Body<MobileOAuthExchangeResult>;
}
24 changes: 24 additions & 0 deletions api/services/auth.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ interface AuthService {
@route("/gitlab/callback")
completeGitLabOAuth(...GitLabOAuthCallbackQuery): OAuthRedirect | BadRequestProblem | UnauthorizedProblem | ServiceUnavailableProblem | InternalServerProblem;

@operationId("startMobileGitLabOAuth")
@summary("Start GitLab OAuth for a mobile PKCE client")
@get
@route("/gitlab/mobile")
startMobileGitLabOAuth(...MobileOAuthStartQuery): OAuthRedirect | BadRequestProblem | InternalServerProblem;

@operationId("completeMobileGitLabOAuthCallback")
@summary("Open the mobile GitLab callback or show a safe fallback page")
@get
@route("/gitlab/mobile/callback")
completeMobileGitLabOAuthCallback(...MobileOAuthCallbackQuery): MobileOAuthFallbackResponse;

@operationId("exchangeMobileGitLabOAuth")
@summary("Exchange a mobile GitLab callback for a SITCON session")
@post
@route("/gitlab/mobile/exchange")
exchangeMobileGitLabOAuth(@body body: MobileOAuthExchangeRequest):
| MobileOAuthSessionResponse
| BadRequestProblem
| UnauthorizedProblem
| ForbiddenProblem
| ServiceUnavailableProblem
| InternalServerProblem;

@operationId("logoutUser")
@summary("Log out user")
@useAuth(SessionCookieAuth)
Expand Down
1 change: 1 addition & 0 deletions deployments/docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ x-sitcon-board-environment: &sitcon-board-environment
SITCON_BOARD_GITLAB_CLIENT_ID: ${SITCON_BOARD_GITLAB_CLIENT_ID:?set SITCON_BOARD_GITLAB_CLIENT_ID}
SITCON_BOARD_GITLAB_CLIENT_SECRET: ${SITCON_BOARD_GITLAB_CLIENT_SECRET:?set SITCON_BOARD_GITLAB_CLIENT_SECRET}
SITCON_BOARD_GITLAB_OAUTH_REDIRECT_URL: ${SITCON_BOARD_GITLAB_OAUTH_REDIRECT_URL:-http://localhost:3000/api/v1/auth/gitlab/callback}
SITCON_BOARD_GITLAB_MOBILE_OAUTH_REDIRECT_URL: ${SITCON_BOARD_GITLAB_MOBILE_OAUTH_REDIRECT_URL:-http://localhost:3000/api/v1/auth/gitlab/mobile/callback}
SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN: ${SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN:?set SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN}
SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN: ${SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN:-}
SITCON_BOARD_GITLAB_GROUP_WEBHOOK_SIGNING_TOKEN: ${SITCON_BOARD_GITLAB_GROUP_WEBHOOK_SIGNING_TOKEN:-}
Expand Down
1 change: 1 addition & 0 deletions deployments/docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ SITCON_BOARD_CSRF_ALLOWED_ORIGINS=http://localhost:3000
SITCON_BOARD_GITLAB_CLIENT_ID=change-me
SITCON_BOARD_GITLAB_CLIENT_SECRET=change-me
SITCON_BOARD_GITLAB_OAUTH_REDIRECT_URL=http://localhost:3000/api/v1/auth/gitlab/callback
SITCON_BOARD_GITLAB_MOBILE_OAUTH_REDIRECT_URL=http://localhost:3000/api/v1/auth/gitlab/mobile/callback
SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN=change-me
# Optional for local development. Copy the whsec_ values generated by GitLab to exercise webhooks.
SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN=
Expand Down
8 changes: 5 additions & 3 deletions deployments/dokploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ Choose the production origin, for example `https://board.example.com`.
Create a GitLab OAuth application:

- Name: `SITCON Board`
- Redirect URI: `https://board.example.com/api/v1/auth/gitlab/callback`
- Redirect URIs (register both):
- `https://board.example.com/api/v1/auth/gitlab/callback`
- `https://board.example.com/api/v1/auth/gitlab/mobile/callback`
- Confidential: enabled
- Scope: `api`

The redirect URI must exactly match the public URL used in Dokploy.
Both redirect URIs must exactly match the public URL used in Dokploy.

Create a project access token in `sitcon-tw/2027`:

Expand Down Expand Up @@ -127,7 +129,7 @@ Back up the `sitcon-board-postgres` volume or configure Dokploy database backups
- `production session cookie must be Secure`: confirm `SITCON_BOARD_ENV=production` and use this Dokploy Compose file, which forces `SITCON_BOARD_SESSION_COOKIE_SECURE=true`.
- `initial source sync` with a directory file error: verify the image was rebuilt from a revision containing `.sitcon/board-directory.yml`.
- `initial source sync` with a GitLab error: verify the project access token and its role/scope in `sitcon-tw/2027`.
- OAuth callback error: compare the GitLab Redirect URI and the generated `${SITCON_BOARD_PUBLIC_URL}/api/v1/auth/gitlab/callback` character for character.
- OAuth callback error: compare both GitLab redirect URIs with the generated `${SITCON_BOARD_PUBLIC_URL}/api/v1/auth/gitlab/callback` and `${SITCON_BOARD_PUBLIC_URL}/api/v1/auth/gitlab/mobile/callback` values character for character.
- Webhook `401`: confirm the complete generated `whsec_...` value is stored under the matching project or group environment key, and that GitLab and the app clocks are synchronized.
- Webhook `400`: confirm the project is exactly `sitcon-tw/2027`, the group is exactly `sitcon-tw`, and no custom webhook template is configured.
- Webhook succeeds but the board is stale: inspect `gitlab_webhook_deliveries_total`, `gitlab_webhook_processing_duration_seconds`, and GitLab Recent events; the 5-second Board poll remains the issue catch-up fallback.
Expand Down
1 change: 1 addition & 0 deletions deployments/dokploy/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ x-sitcon-board-environment: &sitcon-board-environment
SITCON_BOARD_GITLAB_CLIENT_ID: ${SITCON_BOARD_GITLAB_CLIENT_ID:?set SITCON_BOARD_GITLAB_CLIENT_ID}
SITCON_BOARD_GITLAB_CLIENT_SECRET: ${SITCON_BOARD_GITLAB_CLIENT_SECRET:?set SITCON_BOARD_GITLAB_CLIENT_SECRET}
SITCON_BOARD_GITLAB_OAUTH_REDIRECT_URL: ${SITCON_BOARD_PUBLIC_URL:?set SITCON_BOARD_PUBLIC_URL}/api/v1/auth/gitlab/callback
SITCON_BOARD_GITLAB_MOBILE_OAUTH_REDIRECT_URL: ${SITCON_BOARD_PUBLIC_URL:?set SITCON_BOARD_PUBLIC_URL}/api/v1/auth/gitlab/mobile/callback
SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN: ${SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN:?set SITCON_BOARD_GITLAB_PROJECT_ACCESS_TOKEN}
SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN: ${SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN:?set SITCON_BOARD_GITLAB_PROJECT_WEBHOOK_SIGNING_TOKEN}
SITCON_BOARD_GITLAB_GROUP_WEBHOOK_SIGNING_TOKEN: ${SITCON_BOARD_GITLAB_GROUP_WEBHOOK_SIGNING_TOKEN:?set SITCON_BOARD_GITLAB_GROUP_WEBHOOK_SIGNING_TOKEN}
Expand Down
Loading
Loading