Compare commits

...

20 Commits

Author SHA1 Message Date
Chris
4fd81ffbf2 chore(directus): rename *_key to key and skip system key autogen 2026-04-04 17:24:54 +08:00
Chris
cf39ea2b0c chore(directus): switch ids to uuid and add key fields only 2026-04-04 16:59:38 +08:00
Chris
560f40ae8a chore(directus): convert ids to uuid and ensure *_key fields 2026-04-04 16:55:09 +08:00
Chris
838c0afc0b chore(directus): add users.user_key to base directus schema 2026-04-04 16:51:23 +08:00
Chris
0666b8683e chore(directus): add import schemas and key auto-generation sql 2026-04-04 16:48:04 +08:00
Chris
428b6292ea chore: bump frontend submodule 2026-04-03 16:02:24 +08:00
Chris
cd7feec38a docs: update env workflow and role_code contracts 2026-04-03 15:50:19 +08:00
Chris
01a4580faf chore(scripts): add helper scripts for submodule push flow 2026-04-03 15:04:32 +08:00
Chris
649af715e2 docs: add workspace guardrails and submodule workflow 2026-04-03 14:51:14 +08:00
Chris
3571cdf2ee docs(env): standardize development and production env workflow 2026-04-03 14:43:40 +08:00
Chris
099ed03be7 chore: bump backend submodule 2026-04-03 06:03:17 +08:00
Chris
f62ed97e2b chore: bump backend submodule for env example 2026-04-03 05:57:13 +08:00
Chris
760902f53c chore: bump frontend submodule to latest 2026-04-03 05:40:12 +08:00
Chris
998771bc11 chore: bump frontend submodule for oidc url guard 2026-04-03 05:27:25 +08:00
Chris
576ba9b2fe chore: bump frontend submodule for dev env 2026-04-03 05:10:26 +08:00
Chris
b7b312e69a fix: remove invalid condition from compose example 2026-04-03 05:04:00 +08:00
Chris
865be73d06 update 2026-04-03 05:02:41 +08:00
Chris
ed4b22a564 chore: bump backend submodule after rebase 2026-04-03 04:59:40 +08:00
Chris
2da43cf027 update 2026-04-03 04:57:16 +08:00
Chris
200c86c924 chore: bump backend submodule for env selection 2026-04-03 04:51:12 +08:00
15 changed files with 11401 additions and 34 deletions

Submodule backend updated: 60608fe199...405000ded5

View File

@@ -3,15 +3,12 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
container_name: memberapi-backend
container_name: memberapi_ose_tw
restart: unless-stopped
env_file:
- ./backend/.env
ports:
- "127.0.0.1:8000:8000"
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/healthz >/dev/null || exit 1"]
interval: 30s
@@ -19,26 +16,14 @@ services:
retries: 5
start_period: 20s
networks:
- member-net
redis:
image: redis:7-alpine
container_name: memberapi-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- member-net
- postgres
- ose-cache
- nginx
networks:
member-net:
driver: bridge
volumes:
redis-data:
postgres:
external: true
ose-cache:
external: true
nginx:
external: true

View File

@@ -34,6 +34,7 @@
## 4) roles
- `id` UUID PK default `gen_random_uuid()`
- `role_key` TEXT NOT NULL UNIQUE
- `role_code` TEXT NOT NULL語意代碼建議格式`<system>:<module>:<action>`,例如 `mkt:marketing_card:edit`
- `system_id` UUID NOT NULL FK -> `systems(id)` ON DELETE CASCADE
- `name` TEXT NOT NULL
- `description` TEXT
@@ -41,6 +42,7 @@
- `created_at` TIMESTAMPTZ NOT NULL default `now()`
- `updated_at` TIMESTAMPTZ NOT NULL default `now()`
- UNIQUE(`system_id`, `name`)
- UNIQUE(`system_id`, `role_code`)
## 5) site_roles
- `id` UUID PK default `gen_random_uuid()`

View File

@@ -36,6 +36,7 @@
"system_key": "SY20260402X0001",
"system_name": "Marketing",
"role_key": "RL20260402X0002",
"role_code": "mkt:marketing_card:edit",
"role_name": "campaign_edit"
}
]
@@ -45,3 +46,4 @@
## 注意事項
- 不提供 user direct role 寫入 API。
- User 最終角色由 `user_sites` + `site_roles` 推導。
- `role_key` 是唯一識別鍵;業務語意解析請使用 `role_code`

View File

@@ -8,13 +8,13 @@ psql "$DATABASE_URL" -f scripts/init_schema.sql
- DB schema 檔案:[backend/scripts/init_schema.sql](../backend/scripts/init_schema.sql)
## 2) 啟動後端
先準備 `.env`
本地開發使用 `.env.development`
```bash
cd backend
cp .env.example .env
# edit .env.development directly
```
本機開發與 VPS 一律使用 `backend/.env`(不再分 `.env.development`
本機開發固定使用 `backend/.env.development`
再啟動:
```bash
@@ -30,9 +30,11 @@ cd frontend
npm install
npm run dev
```
- 本地開發固定使用 `frontend/.env.development`
- production build 讀取 `frontend/.env.production`
- 專案路徑:[frontend](../frontend)
## 4) 必要環境變數([backend/.env](../backend/.env)
## 4) 必要環境變數([backend/.env.development](../backend/.env.development)
- `KEYCLOAK_BASE_URL`
- `KEYCLOAK_REALM`
- `KEYCLOAK_CLIENT_ID`

View File

@@ -11,7 +11,7 @@ git submodule update --init --recursive
## 2) 後端部署Docker
```bash
cd /opt/member-platform/backend
cp .env.example .env
cp .env.production .env
```
編輯 `.env`DB、Keycloak、Realm Roles、Cache
@@ -63,9 +63,8 @@ docker compose down
## 3) 前端部署Nginx
```bash
cd /opt/member-platform/frontend
cp .env.example .env
```
設定:
production build 會自動讀取 `.env.production`,請先確認設定:
```env
VITE_API_BASE_URL=https://memberapi.ose.tw
```

View File

@@ -0,0 +1,81 @@
-- Directus key auto-generation triggers
-- Target tables: companies, sites, users, roles
-- Key format: PREFIX + yyyymmdd + 'X' + 4 digits
-- Example: CP20260404X1234
BEGIN;
CREATE OR REPLACE FUNCTION public.directus_autogen_entity_key()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_column_name text := TG_ARGV[0];
v_prefix text := TG_ARGV[1];
v_current_value text;
v_candidate text;
v_exists boolean;
v_attempt int;
v_day text;
v_suffix text;
BEGIN
v_current_value := to_jsonb(NEW) ->> v_column_name;
IF v_current_value IS NOT NULL AND btrim(v_current_value) <> '' THEN
RETURN NEW;
END IF;
v_day := to_char(clock_timestamp(), 'YYYYMMDD');
FOR v_attempt IN 0..9999 LOOP
v_suffix := lpad((((extract(epoch FROM clock_timestamp()) * 1000)::bigint + v_attempt) % 10000)::text, 4, '0');
v_candidate := v_prefix || v_day || 'X' || v_suffix;
EXECUTE format(
'SELECT EXISTS (SELECT 1 FROM %I.%I WHERE %I = $1)',
TG_TABLE_SCHEMA,
TG_TABLE_NAME,
v_column_name
) INTO v_exists USING v_candidate;
IF NOT v_exists THEN
NEW := jsonb_populate_record(NEW, jsonb_build_object(v_column_name, v_candidate));
RETURN NEW;
END IF;
END LOOP;
RAISE EXCEPTION 'Failed to generate unique key for %.% (column=%)', TG_TABLE_SCHEMA, TG_TABLE_NAME, v_column_name;
END;
$$;
DROP TRIGGER IF EXISTS trg_companies_key_autogen ON public.companies;
CREATE TRIGGER trg_companies_key_autogen
BEFORE INSERT ON public.companies
FOR EACH ROW
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'CP');
DROP TRIGGER IF EXISTS trg_sites_key_autogen ON public.sites;
CREATE TRIGGER trg_sites_key_autogen
BEFORE INSERT ON public.sites
FOR EACH ROW
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'ST');
DROP TRIGGER IF EXISTS trg_users_key_autogen ON public.users;
CREATE TRIGGER trg_users_key_autogen
BEFORE INSERT ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'UE');
DROP TRIGGER IF EXISTS trg_roles_key_autogen ON public.roles;
CREATE TRIGGER trg_roles_key_autogen
BEFORE INSERT ON public.roles
FOR EACH ROW
EXECUTE FUNCTION public.directus_autogen_entity_key('key', 'RL');
-- cleanup old trigger names to avoid duplicates from previous scripts
DROP TRIGGER IF EXISTS trg_companies_company_key_autogen ON public.companies;
DROP TRIGGER IF EXISTS trg_sites_site_key_autogen ON public.sites;
DROP TRIGGER IF EXISTS trg_users_user_key_autogen ON public.users;
DROP TRIGGER IF EXISTS trg_systems_system_key_autogen ON public.systems;
DROP TRIGGER IF EXISTS trg_roles_role_key_autogen ON public.roles;
COMMIT;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,182 @@
{
"version": 1,
"directus": "11.0.0",
"vendor": "postgres",
"collections": [
{ "collection": "users", "meta": null, "schema": { "name": "users" } },
{ "collection": "companies", "meta": null, "schema": { "name": "companies" } },
{ "collection": "sites", "meta": null, "schema": { "name": "sites" } },
{ "collection": "systems", "meta": null, "schema": { "name": "systems" } },
{ "collection": "roles", "meta": null, "schema": { "name": "roles" } },
{ "collection": "site_roles", "meta": null, "schema": { "name": "site_roles" } },
{ "collection": "user_sites", "meta": null, "schema": { "name": "user_sites" } },
{ "collection": "auth_sync_state", "meta": null, "schema": { "name": "auth_sync_state" } },
{ "collection": "api_clients", "meta": null, "schema": { "name": "api_clients" } }
],
"fields": [
{ "collection": "users", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "has_auto_increment": false, "default_value": "gen_random_uuid()" } },
{ "collection": "users", "field": "user_sub", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "users", "field": "provider_user_id", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
{ "collection": "users", "field": "username", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
{ "collection": "users", "field": "email", "type": "string", "meta": null, "schema": { "is_nullable": true, "is_unique": true } },
{ "collection": "users", "field": "display_name", "type": "string", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "users", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "users", "field": "is_active", "type": "boolean", "meta": null, "schema": { "is_nullable": false, "default_value": true } },
{ "collection": "users", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "users", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "companies", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "companies", "field": "company_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "companies", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "companies", "field": "provider_group_id", "type": "string", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "companies", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "companies", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "companies", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "sites", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "sites", "field": "site_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "sites", "field": "company_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "companies", "foreign_key_column": "id" } },
{ "collection": "sites", "field": "display_name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "sites", "field": "domain", "type": "string", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "sites", "field": "provider_group_id", "type": "string", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "sites", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "sites", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "sites", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "systems", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "systems", "field": "system_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "systems", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "systems", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "systems", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "systems", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "roles", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "roles", "field": "role_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "roles", "field": "role_code", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "roles", "field": "system_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "systems", "foreign_key_column": "id" } },
{ "collection": "roles", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "roles", "field": "description", "type": "text", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "roles", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "roles", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "roles", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "site_roles", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "site_roles", "field": "site_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "sites", "foreign_key_column": "id" } },
{ "collection": "site_roles", "field": "role_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "roles", "foreign_key_column": "id" } },
{ "collection": "site_roles", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "user_sites", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "user_sites", "field": "user_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "users", "foreign_key_column": "id" } },
{ "collection": "user_sites", "field": "site_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "foreign_key_table": "sites", "foreign_key_column": "id" } },
{ "collection": "user_sites", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "user_sites", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "auth_sync_state", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "auth_sync_state", "field": "entity_type", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "auth_sync_state", "field": "entity_id", "type": "uuid", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "auth_sync_state", "field": "last_synced_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "auth_sync_state", "field": "source_version", "type": "string", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "auth_sync_state", "field": "last_error", "type": "text", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "auth_sync_state", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "api_clients", "field": "id", "type": "uuid", "meta": null, "schema": { "is_nullable": false, "is_primary_key": true, "default_value": "gen_random_uuid()" } },
{ "collection": "api_clients", "field": "client_key", "type": "string", "meta": null, "schema": { "is_nullable": false, "is_unique": true } },
{ "collection": "api_clients", "field": "name", "type": "string", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "api_clients", "field": "status", "type": "string", "meta": null, "schema": { "is_nullable": false, "default_value": "active" } },
{ "collection": "api_clients", "field": "api_key_hash", "type": "text", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "api_clients", "field": "allowed_origins", "type": "json", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "api_clients", "field": "allowed_ips", "type": "json", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "api_clients", "field": "allowed_paths", "type": "json", "meta": null, "schema": { "is_nullable": false } },
{ "collection": "api_clients", "field": "rate_limit_per_min", "type": "integer", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "api_clients", "field": "expires_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "api_clients", "field": "last_used_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": true } },
{ "collection": "api_clients", "field": "created_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } },
{ "collection": "api_clients", "field": "updated_at", "type": "timestamp", "meta": null, "schema": { "is_nullable": false, "default_value": "now()" } }
],
"relations": [
{
"collection": "sites",
"field": "company_id",
"related_collection": "companies",
"schema": {
"table": "sites",
"column": "company_id",
"foreign_key_table": "companies",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
},
{
"collection": "roles",
"field": "system_id",
"related_collection": "systems",
"schema": {
"table": "roles",
"column": "system_id",
"foreign_key_table": "systems",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
},
{
"collection": "site_roles",
"field": "site_id",
"related_collection": "sites",
"schema": {
"table": "site_roles",
"column": "site_id",
"foreign_key_table": "sites",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
},
{
"collection": "site_roles",
"field": "role_id",
"related_collection": "roles",
"schema": {
"table": "site_roles",
"column": "role_id",
"foreign_key_table": "roles",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
},
{
"collection": "user_sites",
"field": "user_id",
"related_collection": "users",
"schema": {
"table": "user_sites",
"column": "user_id",
"foreign_key_table": "users",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
},
{
"collection": "user_sites",
"field": "site_id",
"related_collection": "sites",
"schema": {
"table": "user_sites",
"column": "site_id",
"foreign_key_table": "sites",
"foreign_key_column": "id",
"on_update": "NO ACTION",
"on_delete": "CASCADE"
},
"meta": null
}
]
}

View File

@@ -35,6 +35,17 @@
- 後端子模組:[backend](../backend)submodule: `../member-backend`
- 前端子模組:[frontend](../frontend)submodule: `../member-frontend`
## 開發工作目錄(防呆)
- 只在 `member-platform` 內開發與提交。
- 後端只改 `member-platform/backend`
- 前端只改 `member-platform/frontend`
- 根目錄外的 `member-backend` / `member-frontend` 若有另一份 clone視為非主要工作副本避免混用。
## 提交順序(固定)
1. 先在 `member-platform/backend``member-platform/frontend` 各自 commit / push。
2. 再回 `member-platform` 根目錄提交子模組版本指標變更submodule pointer並 push。
3. 部署端只需要更新 `member-platform`,再執行 `git submodule update --init --recursive`
## 文件邊界
- 本輪只保留可開發、可交辦、可驗收文件。
- 最終規格白皮書延後到專案完成後再產出。

44
scripts/push-backend.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
if [ $# -lt 1 ]; then
echo "用法: scripts/push-backend.sh \"後端 commit 訊息\" [根目錄 commit 訊息]"
exit 1
fi
SUB_MSG="$1"
ROOT_MSG="${2:-chore: bump backend submodule}"
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SUB_DIR="$ROOT_DIR/backend"
current_branch="$(git -C "$SUB_DIR" branch --show-current)"
if [ -z "$current_branch" ]; then
echo "backend 目前是 detached HEAD請先執行: cd backend && git checkout master"
exit 1
fi
if [ "$current_branch" != "master" ]; then
echo "backend 目前分支是 $current_branch,建議先切到 master 再執行。"
exit 1
fi
if [ -n "$(git -C "$SUB_DIR" status --porcelain)" ]; then
git -C "$SUB_DIR" add -A
git -C "$SUB_DIR" commit -m "$SUB_MSG"
else
echo "backend 無本地變更,略過 backend commit。"
fi
git -C "$SUB_DIR" push origin master
git -C "$ROOT_DIR" add backend
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
git -C "$ROOT_DIR" commit -m "$ROOT_MSG"
else
echo "根目錄無 submodule pointer 變更,略過根目錄 commit。"
fi
git -C "$ROOT_DIR" push origin master
echo "完成: backend 與根目錄已 push。"

44
scripts/push-frontend.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
if [ $# -lt 1 ]; then
echo "用法: scripts/push-frontend.sh \"前端 commit 訊息\" [根目錄 commit 訊息]"
exit 1
fi
SUB_MSG="$1"
ROOT_MSG="${2:-chore: bump frontend submodule}"
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SUB_DIR="$ROOT_DIR/frontend"
current_branch="$(git -C "$SUB_DIR" branch --show-current)"
if [ -z "$current_branch" ]; then
echo "frontend 目前是 detached HEAD請先執行: cd frontend && git checkout master"
exit 1
fi
if [ "$current_branch" != "master" ]; then
echo "frontend 目前分支是 $current_branch,建議先切到 master 再執行。"
exit 1
fi
if [ -n "$(git -C "$SUB_DIR" status --porcelain)" ]; then
git -C "$SUB_DIR" add -A
git -C "$SUB_DIR" commit -m "$SUB_MSG"
else
echo "frontend 無本地變更,略過 frontend commit。"
fi
git -C "$SUB_DIR" push origin master
git -C "$ROOT_DIR" add frontend
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
git -C "$ROOT_DIR" commit -m "$ROOT_MSG"
else
echo "根目錄無 submodule pointer 變更,略過根目錄 commit。"
fi
git -C "$ROOT_DIR" push origin master
echo "完成: frontend 與根目錄已 push。"