chore(project): add project bootstrap & workflow
This commit is contained in:
parent
22d083cb24
commit
cc8423f0b0
63
.github/project.yml
vendored
Normal file
63
.github/project.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# .github/project.yml
|
||||||
|
|
||||||
|
title: "Light-IDP OIDC/LDAP Stabilization"
|
||||||
|
short_description: "MVP OIDC + Session + Config, then Stability: Store, LDAP, Tests"
|
||||||
|
|
||||||
|
project:
|
||||||
|
owner: svc-design
|
||||||
|
repo: XControl
|
||||||
|
public: false
|
||||||
|
|
||||||
|
fields:
|
||||||
|
milestone:
|
||||||
|
type: single_select
|
||||||
|
options: ["MVP", "Stability"]
|
||||||
|
priority:
|
||||||
|
type: single_select
|
||||||
|
options: ["P0", "P1", "P2"]
|
||||||
|
status:
|
||||||
|
type: status
|
||||||
|
options: ["Todo", "In Progress", "Done"]
|
||||||
|
|
||||||
|
views:
|
||||||
|
- name: "Kanban"
|
||||||
|
type: board
|
||||||
|
group_by: "status"
|
||||||
|
- name: "MVP (Table)"
|
||||||
|
type: table
|
||||||
|
filter:
|
||||||
|
milestone: "MVP"
|
||||||
|
- name: "Stability (Table)"
|
||||||
|
type: table
|
||||||
|
filter:
|
||||||
|
milestone: "Stability"
|
||||||
|
|
||||||
|
items:
|
||||||
|
# Milestone 1 (MVP)
|
||||||
|
- ref: 1
|
||||||
|
title: "Implement OIDC Handlers"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "MVP"
|
||||||
|
note: "authorize/token/userinfo/discovery/jwks/logout"
|
||||||
|
- ref: 2
|
||||||
|
title: "Add Session Middleware"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "MVP"
|
||||||
|
- ref: 5
|
||||||
|
title: "Validate and Extend Config"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "MVP"
|
||||||
|
|
||||||
|
# Milestone 2 (Stability)
|
||||||
|
- ref: 3
|
||||||
|
title: "Thread-safe Persistent Store"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "Stability"
|
||||||
|
- ref: 4
|
||||||
|
title: "Improve LDAP Sync Robustness"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "Stability"
|
||||||
|
- ref: 6
|
||||||
|
title: "Introduce Unit Tests"
|
||||||
|
status: "Todo"
|
||||||
|
milestone: "Stability"
|
||||||
55
docs/project.md
Normal file
55
docs/project.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
使用步骤
|
||||||
|
# 1) 添加文件
|
||||||
|
# .github/project.yml
|
||||||
|
# scripts/bootstrap_project.sh
|
||||||
|
# .github/workflows/project-auto-add.yml
|
||||||
|
|
||||||
|
# 2) 本地或 CI 登录 GitHub CLI
|
||||||
|
gh auth login
|
||||||
|
|
||||||
|
# 3) 初始化 Project(首次/需要重放时)
|
||||||
|
chmod +x scripts/bootstrap_project.sh
|
||||||
|
./scripts/bootstrap_project.sh .github/project.yml
|
||||||
|
|
||||||
|
# 初始化后:
|
||||||
|
|
||||||
|
- 会在 svc-design 名下创建名为 “Light-IDP OIDC/LDAP Stabilization” 的 Projects v2 看板
|
||||||
|
- 规范化 Status 选项、创建 Milestone/Priority 字段、创建「Kanban / MVP (Table) / Stability (Table)」视图
|
||||||
|
- 按 .github/project.yml 将 #1–#6 加入(若尚不存在,会在 svc-design/XControl 内创建对应 issue)
|
||||||
|
|
||||||
|
本地 CLI(推荐命令)
|
||||||
|
|
||||||
|
- 查看当前登录与作用域 gh auth status
|
||||||
|
- 直接给现有令牌追加作用域(无需重新登录)# 追加项目读写 + 组织只读 + 仓库完全权限(你脚本会创建 issue)
|
||||||
|
gh auth refresh -h github.com -s project -s read:project -s read:org -s repo
|
||||||
|
|
||||||
|
如果 gh auth refresh 提示不能刷新,则用设备授权重新登录并勾选作用域:
|
||||||
|
|
||||||
|
gh auth login -h github.com -p https -s project -s read:project -s read:org -s repo
|
||||||
|
|
||||||
|
重新运行脚本 ./scripts/bootstrap_project.sh .github/project.yml
|
||||||
|
|
||||||
|
说明
|
||||||
|
|
||||||
|
Projects v2 API 目前需要“经典”PAT 作用域 project/read:project。细粒度 PAT 还没完整覆盖到 Projects v2(至少在 CLI 场景下不稳定),因此建议使用 CLI 的设备授权或经典 PAT。
|
||||||
|
|
||||||
|
你已有 repo、read:org,只差 read:project/project。
|
||||||
|
|
||||||
|
GitHub Actions(如在 CI 中跑脚本)
|
||||||
|
|
||||||
|
如果你打算在 workflow 里运行初始化脚本,需要把权限从 project 改为 projects(复数!)并授予写入:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
projects: write # ← 修正这里(之前如果写成 project 会失败)
|
||||||
|
|
||||||
|
|
||||||
|
另外,使用默认的 GITHUB_TOKEN 时,需在仓库 → Settings → Actions → General 里将 Workflow permissions 设为 Read and write permissions,否则对 Projects 的写操作会被拒。
|
||||||
|
|
||||||
|
# 常见坑位速查
|
||||||
|
|
||||||
|
- unknown flag: --private/--public:已在你的脚本里移除;现在 gh project create 默认创建私有项目,公开需要到网页端手动切换。
|
||||||
|
- 创建组织级项目失败:需要对组织具备创建 Projects 的权限;否则在你的用户空间创建(脚本已自动查找 user 与 org 两侧)。
|
||||||
|
- 版本过旧:建议 gh --version >= 2.50(gh update
|
||||||
218
scripts/bootstrap_project.sh
Executable file
218
scripts/bootstrap_project.sh
Executable file
@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/bootstrap_project.sh
|
||||||
|
# Initialize or reconcile a GitHub Projects v2 board for a USER account.
|
||||||
|
# Requires: gh >= 2.50, jq, awk, sed
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CFG="${1:-.github/project.yml}"
|
||||||
|
|
||||||
|
if ! command -v gh >/dev/null; then
|
||||||
|
echo "Please install GitHub CLI: https://cli.github.com/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v jq >/dev/null; then
|
||||||
|
echo "Please install jq (brew install jq / apt-get install jq)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ====== REPO CONTEXT ======
|
||||||
|
OWNER="svc-design" # GitHub USER (not org)
|
||||||
|
REPO="XControl" # target repo for issues
|
||||||
|
|
||||||
|
# ====== Read minimal config from YAML ======
|
||||||
|
if [ ! -f "$CFG" ]; then
|
||||||
|
echo "Config not found: $CFG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TITLE=$(awk -F': *' '/^title:/ {sub(/^"/,"",$2); sub(/"$/,"",$2); print $2; exit}' "$CFG")
|
||||||
|
DESC=$(awk -F': *' '/^short_description:/ {sub(/^"/,"",$2); sub(/"$/,"",$2); print $2; exit}' "$CFG")
|
||||||
|
[ -z "${TITLE:-}" ] && { echo "title missing in $CFG"; exit 1; }
|
||||||
|
|
||||||
|
echo "Ensuring project: '$TITLE' under user '$OWNER' ..."
|
||||||
|
|
||||||
|
# ====== Find or create project (USER space) ======
|
||||||
|
PID=$(gh api graphql -f query='
|
||||||
|
query($owner:String!, $q:String!){
|
||||||
|
user(login:$owner){
|
||||||
|
projectsV2(first:50, query:$q){ nodes{ id title } }
|
||||||
|
}
|
||||||
|
}' -F owner="$OWNER" -F q="$TITLE" \
|
||||||
|
| jq -r --arg t "$TITLE" '.data.user.projectsV2.nodes[]? | select(.title==$t) | .id' || true)
|
||||||
|
|
||||||
|
if [ -z "${PID:-}" ]; then
|
||||||
|
echo "Creating project (Projects v2 in USER space)..."
|
||||||
|
PID=$(gh project create --owner "$OWNER" --title "$TITLE" --format json | jq -r '.id')
|
||||||
|
else
|
||||||
|
echo "Project exists. ID: $PID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- Set description via GraphQL (gh project edit expects number; use node id instead) -----
|
||||||
|
if [ -n "${DESC:-}" ]; then
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($pid:ID!, $desc:String!){
|
||||||
|
updateProjectV2(input:{projectId:$pid, shortDescription:$desc}){ projectV2{ id } }
|
||||||
|
}' -F pid="$PID" -F desc="$DESC" >/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "Project ID: $PID"
|
||||||
|
|
||||||
|
# ====== Fields: normalize Status; ensure Milestone & Priority ======
|
||||||
|
# Use CLI to list fields to avoid GraphQL union pitfalls
|
||||||
|
field_id_by_name () { # name -> id or ""
|
||||||
|
gh project field-list "$PID" --format json | jq -r --arg n "$1" '.[] | select(.name==$n) | .id'
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Normalizing Status field options (Todo / In Progress / Done) ..."
|
||||||
|
STATUS_FIELD_ID=$(field_id_by_name "Status")
|
||||||
|
if [ -z "$STATUS_FIELD_ID" ]; then
|
||||||
|
# Projects v2 默认自带 Status;极少数情况下不存在时创建
|
||||||
|
STATUS_FIELD_ID=$(gh project field-create "$PID" --name "Status" --data-type SINGLE_SELECT --format json | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# helper: ensure an option exists on a single-select field
|
||||||
|
ensure_option_on_field () { # fieldId, optionName
|
||||||
|
local fid="$1" oname="$2"
|
||||||
|
local oid
|
||||||
|
oid=$(gh api -X POST graphql -f query='
|
||||||
|
query($pid:ID!, $fid:ID!) {
|
||||||
|
node(id:$pid){
|
||||||
|
... on ProjectV2 {
|
||||||
|
field(id:$fid){
|
||||||
|
... on ProjectV2SingleSelectField { options { id name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -F pid="$PID" -F fid="$fid" \
|
||||||
|
| jq -r --arg n "$oname" '.data.node.field.options[]? | select(.name==$n) | .id')
|
||||||
|
if [ -z "$oid" ] || [ "$oid" = "null" ]; then
|
||||||
|
gh api -X POST graphql -f query='
|
||||||
|
mutation($pid:ID!, $fid:ID!, $name:String!){
|
||||||
|
updateProjectV2SingleSelectField(input:{
|
||||||
|
projectId:$pid, fieldId:$fid, options:[{name:$name}]
|
||||||
|
}){ projectV2 { id } }
|
||||||
|
}' -F pid="$PID" -F fid="$fid" -F name="$oname" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for s in "Todo" "In Progress" "Done"; do
|
||||||
|
ensure_option_on_field "$STATUS_FIELD_ID" "$s"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Ensuring custom fields Milestone & Priority ..."
|
||||||
|
MILESTONE_FID=$(field_id_by_name "Milestone")
|
||||||
|
if [ -z "$MILESTONE_FID" ]; then
|
||||||
|
MILESTONE_FID=$(gh project field-create "$PID" --name "Milestone" --data-type SINGLE_SELECT --format json | jq -r '.id')
|
||||||
|
fi
|
||||||
|
PRIORITY_FID=$(field_id_by_name "Priority")
|
||||||
|
if [ -z "$PRIORITY_FID" ]; then
|
||||||
|
PRIORITY_FID=$(gh project field-create "$PID" --name "Priority" --data-type SINGLE_SELECT --format json | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
for opt in MVP Stability; do ensure_option_on_field "$MILESTONE_FID" "$opt"; done
|
||||||
|
for opt in P0 P1 P2; do ensure_option_on_field "$PRIORITY_FID" "$opt"; done
|
||||||
|
|
||||||
|
# ====== Views: Kanban + two tables ======
|
||||||
|
echo "Ensuring views (Kanban, MVP (Table), Stability (Table)) ..."
|
||||||
|
create_view() { # name, layout=BOARD|TABLE
|
||||||
|
local name="$1" layout="$2"
|
||||||
|
local exists
|
||||||
|
exists=$(gh api graphql -f query='
|
||||||
|
query($pid:ID!){
|
||||||
|
node(id:$pid){ ... on ProjectV2 { views(first:50){ nodes{ id name } } } }
|
||||||
|
}' -F pid="$PID" | jq -r --arg n "$name" '.data.node.views.nodes[]? | select(.name==$n) | .id')
|
||||||
|
if [ -z "$exists" ]; then
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($pid:ID!, $name:String!, $layout:ProjectV2ViewLayout!){
|
||||||
|
createProjectV2View(input:{projectId:$pid, name:$name, layout:$layout}){ projectView{ id } }
|
||||||
|
}' -F pid="$PID" -F name="$name" -F layout="$layout" >/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
create_view "Kanban" "BOARD"
|
||||||
|
create_view "MVP (Table)" "TABLE"
|
||||||
|
create_view "Stability (Table)" "TABLE"
|
||||||
|
|
||||||
|
# ====== Helpers to set field values ======
|
||||||
|
# get option id by name for a field
|
||||||
|
opt_id () { # fieldId, optionName
|
||||||
|
gh api -X POST graphql -f query='
|
||||||
|
query($pid:ID!, $fid:ID!) {
|
||||||
|
node(id:$pid){
|
||||||
|
... on ProjectV2 {
|
||||||
|
field(id:$fid){
|
||||||
|
... on ProjectV2SingleSelectField { options { id name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -F pid="$PID" -F fid="$1" \
|
||||||
|
| jq -r --arg n "$2" '.data.node.field.options[]? | select(.name==$n) | .id'
|
||||||
|
}
|
||||||
|
|
||||||
|
set_single_select() { # itemId, fieldId, optionName
|
||||||
|
local item="$1" fid="$2" oname="$3"
|
||||||
|
local oid; oid=$(opt_id "$fid" "$oname")
|
||||||
|
[ -z "$oid" ] && { echo "Option '$oname' not found on field $fid"; return 1; }
|
||||||
|
gh api -X POST graphql -f query='
|
||||||
|
mutation($pid:ID!, $iid:ID!, $fid:ID!, $oid:String!) {
|
||||||
|
updateProjectV2ItemFieldValue(input:{
|
||||||
|
projectId:$pid, itemId:$iid, fieldId:$fid,
|
||||||
|
value:{ singleSelectOptionId:$oid }
|
||||||
|
}){ projectV2Item { id } }
|
||||||
|
}' -F pid="$PID" -F iid="$item" -F fid="$fid" -F oid="$oid" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
set_status() { # itemId, "Todo|In Progress|Done"
|
||||||
|
set_single_select "$1" "$STATUS_FIELD_ID" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ====== Add items from YAML (create issues if missing) ======
|
||||||
|
add_issue_item() { # issue number, title, milestone, status
|
||||||
|
local num="$1" title="$2" milestone="$3" status="$4"
|
||||||
|
local node_id
|
||||||
|
if gh issue view "$num" -R "$OWNER/$REPO" &>/dev/null; then
|
||||||
|
node_id=$(gh api graphql -F owner="$OWNER" -F name="$REPO" -F number="$num" -f query='
|
||||||
|
query($owner:String!,$name:String!,$number:Int!){
|
||||||
|
repository(owner:$owner,name:$name){ issue(number:$number){ id } }
|
||||||
|
}' | jq -r '.data.repository.issue.id')
|
||||||
|
else
|
||||||
|
gh issue create -R "$OWNER/$REPO" -t "$title" -b "$title" >/dev/null
|
||||||
|
node_id=$(gh api graphql -F owner="$OWNER" -F name="$REPO" -F number="$num" -f query='
|
||||||
|
query($owner:String!,$name:String!,$number:Int!){
|
||||||
|
repository(owner:$owner,name:$name){ issue(number:$number){ id } }
|
||||||
|
}' | jq -r '.data.repository.issue.id')
|
||||||
|
fi
|
||||||
|
local item_id
|
||||||
|
item_id=$(gh project item-add "$PID" --id "$node_id" --format json | jq -r '.id')
|
||||||
|
set_single_select "$item_id" "$MILESTONE_FID" "$milestone"
|
||||||
|
set_status "$item_id" "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_items() {
|
||||||
|
awk '
|
||||||
|
$1=="-"{initem=1; ref=""; title=""; status=""; milestone=""; next}
|
||||||
|
initem && $1=="ref:" {ref=$2; next}
|
||||||
|
initem && $1=="title:" {sub("title: ",""); title=$0; gsub(/^"|?"$/,"",title); next}
|
||||||
|
initem && $1=="status:" {status=$2; next}
|
||||||
|
initem && $1=="milestone:" {milestone=$2; next}
|
||||||
|
initem && /^[[:space:]]*$/ {next}
|
||||||
|
initem && /^[^ ]/ { if (title!="") print ref "|" title "|" status "|" milestone; initem=0 }
|
||||||
|
END{ if (initem && title!="") print ref "|" title "|" status "|" milestone; }
|
||||||
|
' "$CFG"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Adding items from $CFG ..."
|
||||||
|
while IFS="|" read -r ref title status milestone; do
|
||||||
|
ref="${ref// /}"
|
||||||
|
title="$(echo "$title" | sed 's/^"//; s/"$//')"
|
||||||
|
add_issue_item "$ref" "$title" "$milestone" "$status"
|
||||||
|
done < <(parse_items)
|
||||||
|
|
||||||
|
echo "All set. Opening project in browser…"
|
||||||
|
gh project view "$PID" --web || true
|
||||||
|
|
||||||
|
cat <<'NOTE'
|
||||||
|
Tips:
|
||||||
|
- If you see scope errors, refresh gh token scopes:
|
||||||
|
gh auth refresh -h github.com -s project -s read:project -s repo
|
||||||
|
- Projects created via CLI are private by default; change visibility in the web UI if needed.
|
||||||
|
- Your account is a USER (not org); we always target user space with --owner <username>.
|
||||||
|
NOTE
|
||||||
2
ui/dist/index.html
vendored
2
ui/dist/index.html
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
2892
ui/panel/yarn.lock
2892
ui/panel/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user