[add] 設定settings/actions/secrets

This commit is contained in:
建喵 2025-08-15 11:39:08 +08:00
parent 3f363c9a3c
commit 91f314d297
10 changed files with 1740 additions and 276 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor

29
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
// 使 IntelliSense
//
// : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
// "hostname": "jianmiau.tk",
"port": 9000,
"pathMappings": {
"web/MyWeb/GiteaRepoManager": "${workspaceRoot}",
},
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9003,
"pathMappings": {
"web/MyWeb/GiteaRepoManager": "${workspaceRoot}",
},
}
]
}

44
actions_settings.json Normal file
View File

@ -0,0 +1,44 @@
{
"project_settings": {
"secrets": {
"PROJECT_NAME": "excel:1082812249:$B",
"PLATFORM": "excel:1082812249:$C"
}
},
"repos": [
{
"name": "Resource-B2B",
"secrets": {
"ENV": "excel:1659610869:$C",
"ACTION": "excel:1082812249:$D",
"POS": "B4",
"REPO": "eI4Z6Iuhuf"
}
},
{
"name": "Resource-Out",
"secrets": {
"ENV": "excel:1866603134:$C",
"POS": "D4",
"REPO": "eI4Z6Iuhuf"
}
},
{
"name": "Official-B2B",
"secrets": {
"ENV": "excel:1659610869:$C",
"ACTION": "excel:1082812249:$D",
"POS": "D4",
"REPO": "xZcvkubN1o"
}
},
{
"name": "Official-Out",
"secrets": {
"ENV": "excel:1866603134:$C",
"POS": "D4",
"REPO": "xZcvkubN1o"
}
}
]
}

17
composer.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "user/gitearepomanager",
"autoload": {
"psr-4": {
"User\\GiteaRepoManager\\": "src/"
}
},
"authors": [
{
"name": "JianMiau",
"email": "bir840124@gmail.com"
}
],
"require": {
"google/apiclient": "^2.14"
}
}

1281
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

1
favicon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

14
google-service-key.json Normal file
View File

@ -0,0 +1,14 @@
{
"type": "service_account",
"project_id": "fresh-strategy-369406",
"private_key_id": "a816afe3911c78b8618ca1846f703aefba3519b2",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDXzAYyDqjMNbSM\noGMuFKxagDhJH9TKoV2yyj4AAJZ2wGYOTKar5pJ78KW7GCwPr+w3Oom2YJX6QDXe\nxnrguikmku9bPUmHkoVb7ZK5R8aWqR6tjn+czRHU5wlU5FVv2Bn/uxI94vnfRb48\nZmKP6BG6RcLdoBBE7TFb8Kny+D0O7w3Q63r7hbJFssnfm/YBXyPpCOnUuB30GJmQ\n0rPNT37+BfbuLDH6mX9KCAfz4VTKPNQbje3r8b+7Il/woPs9DtLmJtkiHDMbxkjt\nFJCqwyUwjjAFE5Youepjv5Tpm5Q1IWSCKhE9rQ/XeuD0pezeF8wzaQMHfUPM9WxS\nYrVYP0odAgMBAAECggEAFNdqE8SRreNT9C77VTR+7uCqTvmpigZqr71Tnplv7rkn\nQiNKB5kltZ2oy/iKLNuvQyg+q6QJaBlyenkN3g1sswKG5nd1VggjJB0+SfGyLtPX\nmCiGj6TIn5jOsGm7DKnA3Q96tAprWpJ4TIoQ49gkeiqJpvDyEU4dMcV9DG/IKdxk\nKs99yw5dIcMOAq8o1C6ZM7eEJGcG9iyshlB86Bg0hPY8EfEIuP1dF/kTGELhBNKz\nbku7UQsBJdeBGb31Qa361Qx9tbifPK2zl1gpjG6dZmgTHgxYb2xEEAcUdzyKsWyR\n18D3PRmGHKLsCxYFmj9jzwYaxaEarYx9ap2VHPS+wQKBgQD6EWrN56oGUiujbwQC\nCM0a+NchGQ0LWF+09anDHohIKEA+Nto54k3FxqAkRfTC0flBfe0AzYmPNxbOSb8Q\ncb/FIaqbLRdIfNjnbpse8bNFh27UVMQDr18jm0ab2t7BKSyoKN4eVok6soxCRqlZ\nNSUPxspsC+zO9S69KkGP0SqO4QKBgQDc6n1lU0U06jz2R52IshOq47+nC5TvK3r7\nqRfIRjjSMyabhwtTCSU8xALAqQLS+lFauFtGRRSFLK07aXvaWPWwW37xADq7ICc6\nLdsvaJv2rFvx0Uuwp+EJOSXl5NDEU27PTf/44tzC4Gp/SyfD1wQVu0TzVKFe1ZXT\n0P1X0fOOvQKBgGSGJeIZ035w/7vWP801joXeLFTQxi6eWvLaomCeYHhpPdIEqNsF\n/u+XNf7+5DKAx+ss3N4qwbaBlbhdauIIZ+et7fAtQyPPlD4Md20MCl3T4JiYbqdw\nkxU0MUErzcnmbF4493lInieraLinwSHsPDbIWczvSkWzyBMg7nQKyEnhAoGBAI82\nAAZQnfu4ob5yHKDB+Ff+/n4W1vzY7gf4zS8KvskdWbjXKbMxqY8j7jjhF7CXj2fF\nPX5nR+8xUDfEoQKiStuB5N/s6yXlqShhE8c/BGQ7xfsUWAH0QsEM6BGJbQDoqVwA\nT6ETyFMY0lEk8mlVmRNRbFhmE5p70X4X7DQjKcXtAoGBAMlyvK404Ne9hc/cvoLp\n25zliltf0s5599xdX4tHINbKupGqAIr8KlMxEKZWr49Fys9D1U4SkMck7OKvWSSX\n8kQWwJZVfkJWcdbdNZWcO3QPvF7UQ2w4D+an2t/DngjecMlNEPfS9AO6UhN5LCft\nJ0ldwDas8FggZEoFqmG+VGbC\n-----END PRIVATE KEY-----\n",
"client_email": "google-sheet@fresh-strategy-369406.iam.gserviceaccount.com",
"client_id": "110536180295168446962",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-sheet%40fresh-strategy-369406.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -2,294 +2,44 @@
<html lang="zh-TW"> <html lang="zh-TW">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<title>手動輸入組織,批次建立 Gitea 儲存庫與團隊</title> <title>手動輸入組織,批次建立 Gitea 儲存庫與團隊</title>
<style> <link rel="stylesheet" href="style.css">
body { <link rel="icon" href="favicon.svg" type="image/svg+xml">
font-family: sans-serif;
}
#log {
white-space: pre-wrap;
background: #f0f0f0;
padding: 1em;
border-radius: 8px;
height: 100%;
overflow-y: auto;
margin-top: 1em;
}
label,
input {
font-size: 1rem;
}
input {
margin-left: 0.5em;
}
button {
margin-left: 1em;
}
</style>
</head> </head>
<body> <body>
<h2> <h2>輸入組織名稱並從 JSON 建立儲存庫+設定團隊<br>PS.請先建好團隊(或使用下方按鈕批次建立)</h2>
輸入組織名稱並從 JSON 建立儲存庫+設定團隊
<br />
PS.請先建好團隊(或使用下方按鈕批次建立)
</h2>
<label for="orgInput">組織名稱:</label> <label>組織名稱:<input type="text" id="orgInput"></label>
<input type="text" id="orgInput" placeholder="請輸入組織名稱" /> <button data-action="createTeams">創建團隊</button>
<button onclick="loadAndCreateTeams()">創建團隊</button> <button data-action="createRepos">建立儲存庫</button>
<button onclick="loadAndCreateRepos()">開始建立儲存庫</button> <button data-action="setActions">設定 Actions</button>
<div id="log"></div> <div id="log"></div>
<script> <script>
const accessToken = "96ed6b6d33931b122c7f12f94153594be0d75b32"; const logDiv = document.getElementById('log');
const proxy = "https://cors-anywhere.bir840124.workers.dev/?url=";
const giteaAPIBase = "https://git.catan.com.tw/api/v1";
const logBox = document.getElementById("log"); document.querySelectorAll('button').forEach(btn => {
function log(message) { btn.addEventListener('click', () => {
logBox.textContent += message + "\n"; logDiv.innerHTML = ""; // ← 清空舊訊息
logBox.scrollTop = logBox.scrollHeight; const org = document.getElementById('orgInput').value.trim();
} if (!org) {
logDiv.innerHTML = "❌ 請輸入組織名稱<br>";
// 建立儲存庫功能 return;
async function loadAndCreateRepos() {
logBox.textContent = "";
const orgInput = document.getElementById("orgInput").value.trim();
if (!orgInput) {
alert("請輸入組織名稱");
return;
}
log(`✅ 開始建立儲存庫:${orgInput}`);
try {
const res = await fetch("repos.json");
const repoList = await res.json();
for (const repo of repoList) {
repo.org = orgInput;
await createRepoWithSettings(repo);
} }
const evt = new EventSource(`run.php?org=${encodeURIComponent(org)}&action=${btn.dataset.action}`);
log("✅ 所有儲存庫處理完成!"); evt.onmessage = e => {
} catch (err) { logDiv.innerHTML += e.data + "<br>";
log("❌ 讀取 repos.json 失敗:" + err.message); logDiv.scrollTop = logDiv.scrollHeight;
} };
} evt.onerror = () => {
// logDiv.innerHTML += "執行完成<br>";
async function repoExists(org, repoName) { evt.close();
const url = `${giteaAPIBase}/orgs/${org}/repos`; };
const res = await fetch(proxy + url, {
headers: { "Authorization": `token ${accessToken}` }
}); });
if (!res.ok) { });
throw new Error(`查詢儲存庫失敗,狀態碼:${res.status}`);
}
const repos = await res.json();
return repos.some(r => r.name === repoName);
}
async function createRepoWithSettings(repo) {
if (await repoExists(repo.org, repo.name)) {
log(`⚠️ 儲存庫已存在,略過建立:${repo.org}/${repo.name}`);
return;
}
const createUrl = `${giteaAPIBase}/orgs/${repo.org}/repos`;
const payload = {
name: repo.name,
description: repo.description || "",
default_branch: repo.default_branch || "master",
private: false,
auto_init: true
};
try {
const response = await fetch(proxy + createUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `token ${accessToken}`
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
log(`✅ 建立儲存庫:${repo.org}/${repo.name}`);
if (repo.teams?.length > 0) {
for (const team of repo.teams) {
await addTeamToRepo(repo.org, repo.name, team);
}
}
// 新增:建立 Issue
if (repo.issue) {
await createIssue(repo.org, repo.name, repo.issue);
}
} else {
log(`⚠️ 建立失敗:${repo.org}/${repo.name} - ${data.message}`);
}
} catch (err) {
log(`❌ 建立錯誤:${repo.org}/${repo.name} - ${err.message}`);
}
}
async function createIssue(org, repoName, issue) {
const url = `${giteaAPIBase}/repos/${org}/${repoName}/issues`;
const payload = {
title: issue.title || "",
body: issue.content || ""
};
try {
const res = await fetch(proxy + url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `token ${accessToken}`
},
body: JSON.stringify(payload)
});
if (res.ok) {
log(`📌 已建立 Issue${issue.title}`);
} else {
const data = await res.json();
log(`⚠️ 建立 Issue 失敗:${data.message || res.statusText}`);
}
} catch (err) {
log(`❌ 建立 Issue 錯誤:${err.message}`);
}
}
async function addTeamToRepo(org, repo, teamName) {
const teamsURL = `${giteaAPIBase}/orgs/${org}/teams`;
const teamRes = await fetch(proxy + teamsURL, {
headers: { "Authorization": `token ${accessToken}` }
});
const teams = await teamRes.json();
const targetTeam = teams.find(t => t.name === teamName);
if (!targetTeam) {
log(`⚠️ 找不到團隊:${teamName}`);
return;
}
const url = `${giteaAPIBase}/teams/${targetTeam.id}/repos/${org}/${repo}`;
const res = await fetch(proxy + url, {
method: "PUT",
headers: { "Authorization": `token ${accessToken}` }
});
if (res.ok) {
log(`👥 加入團隊:${teamName}`);
} else {
log(`⚠️ 加團隊失敗:${teamName}`);
}
}
// 創建團隊功能
async function loadAndCreateTeams() {
logBox.textContent = "";
const orgInput = document.getElementById("orgInput").value.trim();
if (!orgInput) {
alert("請輸入組織名稱");
return;
}
log(`✅ 開始建立團隊:${orgInput}`);
try {
const res = await fetch("teams.json");
const teamList = await res.json();
for (const team of teamList) {
await createTeam(orgInput, team);
}
log("✅ 所有團隊建立完成!");
} catch (err) {
log("❌ 讀取 teams.json 失敗:" + err.message);
}
}
async function createTeam(org, team) {
const url = `${giteaAPIBase}/orgs/${org}/teams`;
const payload = {
name: team.name,
description: team.description || "",
permission: "none",
units: team.units || [],
units_map: team.units_map || {},
can_create_org_repo: false,
includes_all_repositories: false
};
try {
const response = await fetch(proxy + url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `token ${accessToken}`
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
log(`✅ 團隊建立成功:${team.name}`);
if (team.members?.length > 0) {
for (const username of team.members) {
await addMemberToTeam(data.id, username);
}
}
} else if (data.message?.includes("team already exists")) {
log(`⚠️ 團隊已存在,略過:${team.name}`);
} else {
log(`⚠️ 建立團隊失敗:${team.name} - ${data.message}`);
}
} catch (err) {
log(`❌ 團隊建立錯誤:${team.name} - ${err.message}`);
}
}
async function addMemberToTeam(teamId, username) {
const url = `${giteaAPIBase}/teams/${teamId}/members/${username}`;
try {
const res = await fetch(proxy + url, {
method: "PUT",
headers: { "Authorization": `token ${accessToken}` }
});
if (res.ok) {
log(`👤 新增成員:${username}`);
} else {
const data = await res.json();
log(`⚠️ 新增成員失敗:${username} - ${data.message || res.statusText}`);
}
} catch (err) {
log(`❌ 新增成員錯誤:${username} - ${err.message}`);
}
}
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
</script> </script>
</body> </body>

301
run.php Normal file
View File

@ -0,0 +1,301 @@
<?php
// run.php
require 'vendor/autoload.php';
use Google\Client;
use Google\Service\Sheets;
$GITEA_URL = 'https://git.catan.com.tw/api/v1';
$GITEA_TOKEN = '96ed6b6d33931b122c7f12f94153594be0d75b32';
$SERVICE_KEY = __DIR__ . '/google-service-key.json';
$SHEET_ID = '1e-8Cj3Szkb-P0lTKQTeeaS_wI4S7KLS4wyzf5PjNyHQ';
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
function logMsg($msg) {
echo "data: $msg\n\n";
@ob_flush();
flush();
}
function getExcelValue($excelRef, $orgInput) {
global $SERVICE_KEY, $SHEET_ID;
if (!preg_match('/^excel:(\d+):\$(\w)$/', $excelRef, $m)) return null;
$gid = $m[1];
$targetCol = strtoupper($m[2]);
$colIndex = ord($targetCol) - 65;
try {
$client = new Client();
$client->setAuthConfig($SERVICE_KEY);
$client->addScope(\Google\Service\Sheets::SPREADSHEETS_READONLY);
$service = new \Google\Service\Sheets($client);
$spreadsheet = $service->spreadsheets->get($SHEET_ID);
$sheetName = null;
foreach ($spreadsheet->getSheets() as $sheet) {
if ($sheet->getProperties()->getSheetId() === intval($gid)) {
$sheetName = $sheet->getProperties()->getTitle();
break;
}
}
if (!$sheetName) { logMsg("⚠️ 找不到 GID: $gid"); return null; }
$response = $service->spreadsheets_values->get($SHEET_ID, $sheetName);
$values = $response->getValues() ?? [];
$lastNonEmptyCol = []; // 保存每個欄位最後非空值
foreach ($values as $r => &$row) {
// 保證列長度至少到目標欄位
for ($c = 0; $c <= $colIndex; $c++) {
$cell = $row[$c] ?? '';
$cell = trim($cell);
if ($cell !== '') {
$lastNonEmptyCol[$c] = $cell;
} else if (isset($lastNonEmptyCol[$c])) {
$row[$c] = $lastNonEmptyCol[$c]; // 更新列
} else {
$row[$c] = ''; // 避免未設定
}
// logMsg("DEBUG row={$r} col={$c} value='{$row[$c]}'");
}
$cellOrg = $row[0] ?? '';
if ($cellOrg === $orgInput) {
$value = $row[$colIndex] ?? '';
// logMsg("DEBUG 找到 org='{$orgInput}',回傳 col={$colIndex} 值='{$value}'");
return $value;
}
}
unset($row);
logMsg("⚠️ Excel 找不到 org={$orgInput} 的資料 (sheet='{$sheetName}')");
return '';
} catch (\Exception $e) {
logMsg("⚠️ Excel 讀取錯誤: " . $e->getMessage());
return '';
}
}
function fetchJSON($url, $method='GET', $data=null) {
global $GITEA_TOKEN;
$ch = curl_init($url);
$headers = ["Content-Type: application/json"];
if ($GITEA_TOKEN) $headers[] = "Authorization: token $GITEA_TOKEN";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($method === 'POST') curl_setopt($ch, CURLOPT_POST, true);
if ($method === 'PUT') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
if ($method === 'PATCH') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$code, json_decode($res, true)];
}
function createTeams($org) {
global $GITEA_URL;
$teams = json_decode(file_get_contents('teams.json'), true) ?? [];
logMsg("🟢 開始執行 createTeams...");
// 先抓全部團隊
[$codeAll, $allTeams] = fetchJSON("$GITEA_URL/orgs/$org/teams", 'GET');
$allTeams = $allTeams ?? [];
foreach ($teams as $team) {
$foundTeam = null;
foreach ($allTeams as $t) {
if (strcasecmp($t['name'], $team['name']) === 0) {
$foundTeam = $t;
break;
}
}
if ($foundTeam) {
logMsg("🔄 團隊已存在,更新權限: {$team['name']}");
// 更新權限
fetchJSON("$GITEA_URL/teams/{$foundTeam['id']}", 'PATCH', [
'units_map' => $team['units_map'] ?? [],
'permission' => $team['permission'] ?? 'write'
]);
// 補齊成員
[$codeMembers, $resMembers] = fetchJSON("$GITEA_URL/teams/{$foundTeam['id']}/members", 'GET');
$existingMembers = array_column($resMembers ?? [], 'username');
foreach ($team['members'] ?? [] as $member) {
if (!in_array($member, $existingMembers)) {
[$codeAdd, $resAdd] = fetchJSON("$GITEA_URL/teams/{$foundTeam['id']}/members/$member", 'PUT');
if ($codeAdd === 204) {
logMsg("  ✅ 新增成員: $member");
} else {
logMsg("  ⚠️ 新增成員失敗: $member" . json_encode($resAdd, JSON_UNESCAPED_UNICODE));
}
}
}
} else {
logMsg(" 建立團隊: {$team['name']}");
[$codeCreate, $resCreate] = fetchJSON("$GITEA_URL/orgs/$org/teams", 'POST', [
'name' => $team['name'],
'units_map' => $team['units_map'] ?? [],
'permission' => $team['permission'] ?? 'write'
]);
if ($codeCreate === 201) {
$teamId = $resCreate['id'];
foreach ($team['members'] ?? [] as $member) {
[$codeAdd, $resAdd] = fetchJSON("$GITEA_URL/teams/$teamId/members/$member", 'PUT');
if ($codeAdd === 204) {
logMsg("  ✅ 新增成員: $member");
} else {
logMsg("  ⚠️ 新增成員失敗: $member" . json_encode($resAdd, JSON_UNESCAPED_UNICODE));
}
}
} else {
logMsg("⚠️ 建立團隊失敗: {$team['name']}" . json_encode($resCreate, JSON_UNESCAPED_UNICODE));
}
}
}
logMsg("✅ 所有團隊設定完成!");
}
function createRepos($org) {
global $GITEA_URL;
$repos = json_decode(file_get_contents('repos.json'), true) ?? [];
logMsg("🟢 開始執行 createRepos...");
// 先抓全部 repo避免重複建立
[$codeAll, $allRepos] = fetchJSON("$GITEA_URL/orgs/$org/repos");
$allRepos = $allRepos ?? [];
foreach ($repos as $repo) {
$repoExists = false;
foreach ($allRepos as $r) {
if ($r['name'] === $repo['name']) {
$repoExists = true;
break;
}
}
if ($repoExists) {
logMsg("⚠️ 儲存庫已存在,略過建立:{$repo['name']}");
continue;
}
$payload = [
'name' => $repo['name'],
'description' => $repo['description'] ?? '',
'default_branch' => $repo['default_branch'] ?? 'master',
'private' => false,
'auto_init' => true
];
[$codeCreate, $resCreate] = fetchJSON("$GITEA_URL/orgs/$org/repos", 'POST', $payload);
if ($codeCreate === 201) {
$repoName = $resCreate['name'];
logMsg("✅ 建立儲存庫:{$repoName}");
// 加入團隊
foreach ($repo['teams'] ?? [] as $teamName) {
[$codeTeams, $teamsList] = fetchJSON("$GITEA_URL/orgs/$org/teams");
$targetTeam = null;
foreach ($teamsList ?? [] as $t) {
if ($t['name'] === $teamName) {
$targetTeam = $t;
break;
}
}
if ($targetTeam) {
[$codeAdd, $resAdd] = fetchJSON("$GITEA_URL/teams/{$targetTeam['id']}/repos/$org/$repoName", 'PUT');
if (in_array($codeAdd, [200,204])) {
logMsg("  👥 已加入團隊:{$teamName}");
} else {
logMsg("  ⚠️ 加入團隊失敗:{$teamName}" . json_encode($resAdd, JSON_UNESCAPED_UNICODE));
}
} else {
logMsg("  ⚠️ 找不到團隊:{$teamName}");
}
}
// 建立 Issue
if (!empty($repo['issue'])) {
$issuePayload = [
'title' => $repo['issue']['title'] ?? '',
'body' => $repo['issue']['content'] ?? ''
];
[$codeIssue, $resIssue] = fetchJSON("$GITEA_URL/repos/$org/$repoName/issues", 'POST', $issuePayload);
if ($codeIssue === 201) logMsg("  📌 已建立 Issue{$issuePayload['title']}");
else logMsg("  ⚠️ 建立 Issue 失敗:{$issuePayload['title']}" . json_encode($resIssue, JSON_UNESCAPED_UNICODE));
}
} else {
logMsg("⚠️ 建立儲存庫失敗:{$repo['name']}" . json_encode($resCreate, JSON_UNESCAPED_UNICODE));
}
}
logMsg("✅ 所有儲存庫處理完成!");
}
function setActions($org) {
global $GITEA_URL;
$actions = json_decode(file_get_contents('actions_settings.json'), true) ?? [];
logMsg("🟢 開始執行 setActions...");
$projectSecrets = $actions['project_settings']['secrets'] ?? [];
logMsg("🔧 設定 project");
foreach($projectSecrets as $key=>$value) {
$finalValue = (is_string($value) && str_starts_with($value,'excel:'))
? getExcelValue($value,$org)
: $value;
if(trim($finalValue)==='') { logMsg("  ⚠️ 略過空值 Project Secret: $key"); continue; }
[$code,$res] = fetchJSON("$GITEA_URL/orgs/$org/actions/secrets/$key",'PUT',['data'=>$finalValue]);
if (in_array($code, [200,204,201])) logMsg("  ✅ Project Secret 設定成功: $key");
else logMsg("  ⚠️ Project Secret 設定失敗: $key".json_encode($res,JSON_UNESCAPED_UNICODE));
}
$reposArr = $actions['repos'] ?? [];
foreach($reposArr as $repo) {
$repoName = $repo['name'];
logMsg("🔧 設定 repo: $repoName");
$secrets = $repo['secrets'] ?? [];
foreach($secrets as $key=>$value) {
$finalValue = (is_string($value) && str_starts_with($value,'excel:'))
? getExcelValue($value,$org)
: $value;
if(trim($finalValue)==='') { logMsg("  ⚠️ 略過空值 Repo Secret: $key"); continue; }
[$code,$res] = fetchJSON("$GITEA_URL/repos/$org/$repoName/actions/secrets/$key",'PUT',['data'=>$finalValue]);
if (in_array($code, [200,204,201])) logMsg("  ✅ Repo Secret 設定成功: $key");
else logMsg("  ⚠️ Repo Secret 設定失敗: $key".json_encode($res,JSON_UNESCAPED_UNICODE));
}
}
logMsg("✅ 所有 Actions 設定完成!");
}
$org = $_GET['org'] ?? '';
$action = $_GET['action'] ?? '';
if (!$org) { logMsg("❌ 請輸入組織名稱"); exit; }
switch($action) {
case 'createTeams': createTeams($org); break;
case 'createRepos': createRepos($org); break;
case 'setActions': setActions($org); break;
default: logMsg("⚠️ 未知操作: $action"); break;
}
logMsg("✅ 任務完成");

26
style.css Normal file
View File

@ -0,0 +1,26 @@
body {
font-family: sans-serif;
}
#log {
white-space: pre-wrap;
background: #f0f0f0;
padding: 1em;
border-radius: 8px;
height: 600px;
overflow-y: auto;
margin-top: 1em;
}
label,
input {
font-size: 1rem;
}
input {
margin-left: 0.5em;
}
button {
margin-left: 1em;
}