Merge pull request 'fix: Per-agent Forgejo accounts — identity and permissions via authorship (#747)' (#760) from fix/issue-747 into main
This commit is contained in:
commit
ac4eaf93d6
14 changed files with 80 additions and 43 deletions
13
.env.example
13
.env.example
|
|
@ -17,14 +17,23 @@
|
||||||
FORGE_URL=http://localhost:3000 # [CONFIG] local Forgejo instance
|
FORGE_URL=http://localhost:3000 # [CONFIG] local Forgejo instance
|
||||||
|
|
||||||
# ── Auth tokens ───────────────────────────────────────────────────────────
|
# ── Auth tokens ───────────────────────────────────────────────────────────
|
||||||
FORGE_TOKEN= # [SECRET] dev-bot API token
|
# Each agent has its own Forgejo account and API token (#747).
|
||||||
|
# Per-agent tokens fall back to FORGE_TOKEN if not set.
|
||||||
|
FORGE_TOKEN= # [SECRET] dev-bot API token (default for all agents)
|
||||||
FORGE_REVIEW_TOKEN= # [SECRET] review-bot API token
|
FORGE_REVIEW_TOKEN= # [SECRET] review-bot API token
|
||||||
FORGE_BOT_USERNAMES= # [CONFIG] comma-separated bot usernames
|
FORGE_PLANNER_TOKEN= # [SECRET] planner-bot API token
|
||||||
|
FORGE_GARDENER_TOKEN= # [SECRET] gardener-bot API token
|
||||||
|
FORGE_VAULT_TOKEN= # [SECRET] vault-bot API token
|
||||||
|
FORGE_SUPERVISOR_TOKEN= # [SECRET] supervisor-bot API token
|
||||||
|
FORGE_PREDICTOR_TOKEN= # [SECRET] predictor-bot API token
|
||||||
|
FORGE_ACTION_TOKEN= # [SECRET] action-bot API token
|
||||||
|
FORGE_BOT_USERNAMES=dev-bot,review-bot,planner-bot,gardener-bot,vault-bot,supervisor-bot,predictor-bot,action-bot
|
||||||
|
|
||||||
# ── Backwards compatibility ───────────────────────────────────────────────
|
# ── Backwards compatibility ───────────────────────────────────────────────
|
||||||
# If CODEBERG_TOKEN is set but FORGE_TOKEN is not, env.sh falls back to
|
# If CODEBERG_TOKEN is set but FORGE_TOKEN is not, env.sh falls back to
|
||||||
# CODEBERG_TOKEN automatically (same for REVIEW_BOT_TOKEN, CODEBERG_REPO,
|
# CODEBERG_TOKEN automatically (same for REVIEW_BOT_TOKEN, CODEBERG_REPO,
|
||||||
# CODEBERG_BOT_USERNAMES). No action needed for existing deployments.
|
# CODEBERG_BOT_USERNAMES). No action needed for existing deployments.
|
||||||
|
# Per-agent tokens default to FORGE_TOKEN when unset (single-token setups).
|
||||||
|
|
||||||
# ── Woodpecker CI ─────────────────────────────────────────────────────────
|
# ── Woodpecker CI ─────────────────────────────────────────────────────────
|
||||||
WOODPECKER_TOKEN= # [SECRET] Woodpecker API token
|
WOODPECKER_TOKEN= # [SECRET] Woodpecker API token
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ ISSUE="${1:?Usage: action-agent.sh <issue-number> [project.toml]}"
|
||||||
export PROJECT_TOML="${2:-${PROJECT_TOML:-}}"
|
export PROJECT_TOML="${2:-${PROJECT_TOML:-}}"
|
||||||
|
|
||||||
source "$(dirname "$0")/../lib/env.sh"
|
source "$(dirname "$0")/../lib/env.sh"
|
||||||
|
# Use action-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}"
|
||||||
source "$(dirname "$0")/../lib/ci-helpers.sh"
|
source "$(dirname "$0")/../lib/ci-helpers.sh"
|
||||||
source "$(dirname "$0")/../lib/agent-session.sh"
|
source "$(dirname "$0")/../lib/agent-session.sh"
|
||||||
source "$(dirname "$0")/../lib/formula-session.sh"
|
source "$(dirname "$0")/../lib/formula-session.sh"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ set -euo pipefail
|
||||||
|
|
||||||
export PROJECT_TOML="${1:-}"
|
export PROJECT_TOML="${1:-}"
|
||||||
source "$(dirname "$0")/../lib/env.sh"
|
source "$(dirname "$0")/../lib/env.sh"
|
||||||
|
# Use action-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}"
|
||||||
# shellcheck source=../lib/guard.sh
|
# shellcheck source=../lib/guard.sh
|
||||||
source "$(dirname "$0")/../lib/guard.sh"
|
source "$(dirname "$0")/../lib/guard.sh"
|
||||||
check_active action
|
check_active action
|
||||||
|
|
|
||||||
70
bin/disinto
70
bin/disinto
|
|
@ -439,10 +439,25 @@ setup_forge() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create bot users and tokens
|
# Create bot users and tokens
|
||||||
local dev_token="" review_token=""
|
# Each agent gets its own Forgejo account for identity and audit trail (#747).
|
||||||
for bot_user in dev-bot review-bot; do
|
# Map: bot-username -> env-var-name for the token
|
||||||
local bot_pass
|
local -A bot_token_vars=(
|
||||||
|
[dev-bot]="FORGE_TOKEN"
|
||||||
|
[review-bot]="FORGE_REVIEW_TOKEN"
|
||||||
|
[planner-bot]="FORGE_PLANNER_TOKEN"
|
||||||
|
[gardener-bot]="FORGE_GARDENER_TOKEN"
|
||||||
|
[vault-bot]="FORGE_VAULT_TOKEN"
|
||||||
|
[supervisor-bot]="FORGE_SUPERVISOR_TOKEN"
|
||||||
|
[predictor-bot]="FORGE_PREDICTOR_TOKEN"
|
||||||
|
[action-bot]="FORGE_ACTION_TOKEN"
|
||||||
|
)
|
||||||
|
|
||||||
|
local env_file="${FACTORY_ROOT}/.env"
|
||||||
|
local bot_user bot_pass token token_var
|
||||||
|
|
||||||
|
for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot action-bot; do
|
||||||
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
token_var="${bot_token_vars[$bot_user]}"
|
||||||
|
|
||||||
if ! curl -sf --max-time 5 \
|
if ! curl -sf --max-time 5 \
|
||||||
-H "Authorization: token ${admin_token}" \
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
|
@ -476,7 +491,6 @@ setup_forge() {
|
||||||
|
|
||||||
# Generate token via API (basic auth as the bot user — Forgejo requires
|
# Generate token via API (basic auth as the bot user — Forgejo requires
|
||||||
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
||||||
local token
|
|
||||||
token=$(curl -sf -X POST \
|
token=$(curl -sf -X POST \
|
||||||
-u "${bot_user}:${bot_pass}" \
|
-u "${bot_user}:${bot_pass}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|
@ -499,41 +513,23 @@ setup_forge() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$bot_user" = "dev-bot" ]; then
|
# Store token in .env under the per-agent variable name
|
||||||
dev_token="$token"
|
if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^${token_var}=.*|${token_var}=${token}|" "$env_file"
|
||||||
else
|
else
|
||||||
review_token="$token"
|
printf '%s=%s\n' "$token_var" "$token" >> "$env_file"
|
||||||
|
fi
|
||||||
|
export "${token_var}=${token}"
|
||||||
|
echo " ${bot_user} token saved (${token_var})"
|
||||||
|
|
||||||
|
# Backwards-compat aliases for dev-bot and review-bot
|
||||||
|
if [ "$bot_user" = "dev-bot" ]; then
|
||||||
|
export CODEBERG_TOKEN="$token"
|
||||||
|
elif [ "$bot_user" = "review-bot" ]; then
|
||||||
|
export REVIEW_BOT_TOKEN="$token"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Store tokens in .env
|
|
||||||
local env_file="${FACTORY_ROOT}/.env"
|
|
||||||
if [ -n "$dev_token" ]; then
|
|
||||||
if grep -q '^FORGE_TOKEN=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^FORGE_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
|
|
||||||
elif grep -q '^CODEBERG_TOKEN=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^CODEBERG_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file"
|
|
||||||
else
|
|
||||||
printf '\nFORGE_TOKEN=%s\n' "$dev_token" >> "$env_file"
|
|
||||||
fi
|
|
||||||
export FORGE_TOKEN="$dev_token"
|
|
||||||
export CODEBERG_TOKEN="$dev_token"
|
|
||||||
echo " dev-bot token saved"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$review_token" ]; then
|
|
||||||
if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^FORGE_REVIEW_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
|
|
||||||
elif grep -q '^REVIEW_BOT_TOKEN=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^REVIEW_BOT_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file"
|
|
||||||
else
|
|
||||||
printf 'FORGE_REVIEW_TOKEN=%s\n' "$review_token" >> "$env_file"
|
|
||||||
fi
|
|
||||||
export FORGE_REVIEW_TOKEN="$review_token"
|
|
||||||
export REVIEW_BOT_TOKEN="$review_token"
|
|
||||||
echo " review-bot token saved"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Store FORGE_URL in .env if not already present
|
# Store FORGE_URL in .env if not already present
|
||||||
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
|
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
|
||||||
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
|
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
|
||||||
|
|
@ -569,8 +565,8 @@ setup_forge() {
|
||||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true
|
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add bot users as collaborators
|
# Add all bot users as collaborators
|
||||||
for bot_user in dev-bot review-bot; do
|
for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot action-bot; do
|
||||||
curl -sf -X PUT \
|
curl -sf -X PUT \
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
||||||
# shellcheck source=../lib/env.sh
|
# shellcheck source=../lib/env.sh
|
||||||
source "$FACTORY_ROOT/lib/env.sh"
|
source "$FACTORY_ROOT/lib/env.sh"
|
||||||
|
# Use gardener-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_GARDENER_TOKEN:-${FORGE_TOKEN}}"
|
||||||
# shellcheck source=../lib/agent-session.sh
|
# shellcheck source=../lib/agent-session.sh
|
||||||
source "$FACTORY_ROOT/lib/agent-session.sh"
|
source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
# shellcheck source=../lib/formula-session.sh
|
# shellcheck source=../lib/formula-session.sh
|
||||||
|
|
|
||||||
11
lib/env.sh
11
lib/env.sh
|
|
@ -53,8 +53,17 @@ export CODEBERG_TOKEN="${FORGE_TOKEN}" # backwards compat
|
||||||
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
|
export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}"
|
||||||
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
|
export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat
|
||||||
|
|
||||||
|
# Per-agent tokens (#747): each agent gets its own Forgejo identity.
|
||||||
|
# Falls back to FORGE_TOKEN for backwards compat with single-token setups.
|
||||||
|
export FORGE_PLANNER_TOKEN="${FORGE_PLANNER_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
export FORGE_GARDENER_TOKEN="${FORGE_GARDENER_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
export FORGE_VAULT_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
export FORGE_SUPERVISOR_TOKEN="${FORGE_SUPERVISOR_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
export FORGE_PREDICTOR_TOKEN="${FORGE_PREDICTOR_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
export FORGE_ACTION_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
|
||||||
# Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES
|
# Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES
|
||||||
export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-}}"
|
export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-dev-bot,review-bot,planner-bot,gardener-bot,vault-bot,supervisor-bot,predictor-bot,action-bot}}"
|
||||||
export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat
|
export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat
|
||||||
|
|
||||||
# Project config (FORGE_* preferred, CODEBERG_* fallback)
|
# Project config (FORGE_* preferred, CODEBERG_* fallback)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
||||||
# shellcheck source=../lib/env.sh
|
# shellcheck source=../lib/env.sh
|
||||||
source "$FACTORY_ROOT/lib/env.sh"
|
source "$FACTORY_ROOT/lib/env.sh"
|
||||||
|
# Use planner-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_PLANNER_TOKEN:-${FORGE_TOKEN}}"
|
||||||
# shellcheck source=../lib/agent-session.sh
|
# shellcheck source=../lib/agent-session.sh
|
||||||
source "$FACTORY_ROOT/lib/agent-session.sh"
|
source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
# shellcheck source=../lib/formula-session.sh
|
# shellcheck source=../lib/formula-session.sh
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
||||||
# shellcheck source=../lib/env.sh
|
# shellcheck source=../lib/env.sh
|
||||||
source "$FACTORY_ROOT/lib/env.sh"
|
source "$FACTORY_ROOT/lib/env.sh"
|
||||||
|
# Use predictor-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_PREDICTOR_TOKEN:-${FORGE_TOKEN}}"
|
||||||
# shellcheck source=../lib/agent-session.sh
|
# shellcheck source=../lib/agent-session.sh
|
||||||
source "$FACTORY_ROOT/lib/agent-session.sh"
|
source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
# shellcheck source=../lib/formula-session.sh
|
# shellcheck source=../lib/formula-session.sh
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}"
|
||||||
# shellcheck source=../lib/env.sh
|
# shellcheck source=../lib/env.sh
|
||||||
source "$FACTORY_ROOT/lib/env.sh"
|
source "$FACTORY_ROOT/lib/env.sh"
|
||||||
|
# Use supervisor-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_SUPERVISOR_TOKEN:-${FORGE_TOKEN}}"
|
||||||
# shellcheck source=../lib/agent-session.sh
|
# shellcheck source=../lib/agent-session.sh
|
||||||
source "$FACTORY_ROOT/lib/agent-session.sh"
|
source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
# shellcheck source=../lib/formula-session.sh
|
# shellcheck source=../lib/formula-session.sh
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/../lib/env.sh"
|
source "${SCRIPT_DIR}/vault-env.sh"
|
||||||
|
|
||||||
VAULT_DIR="${FACTORY_ROOT}/vault"
|
VAULT_DIR="${FACTORY_ROOT}/vault"
|
||||||
PROMPT_FILE="${VAULT_DIR}/PROMPT.md"
|
PROMPT_FILE="${VAULT_DIR}/PROMPT.md"
|
||||||
|
|
|
||||||
9
vault/vault-env.sh
Normal file
9
vault/vault-env.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# vault-env.sh — Shared vault environment: loads lib/env.sh and activates
|
||||||
|
# vault-bot's Forgejo identity (#747).
|
||||||
|
# Source this instead of lib/env.sh in vault scripts.
|
||||||
|
|
||||||
|
# shellcheck source=../lib/env.sh
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/env.sh"
|
||||||
|
# Use vault-bot's own Forgejo identity
|
||||||
|
FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/../lib/env.sh"
|
source "${SCRIPT_DIR}/vault-env.sh"
|
||||||
|
|
||||||
VAULT_DIR="${FACTORY_ROOT}/vault"
|
VAULT_DIR="${FACTORY_ROOT}/vault"
|
||||||
LOCKS_DIR="${VAULT_DIR}/.locks"
|
LOCKS_DIR="${VAULT_DIR}/.locks"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/../lib/env.sh"
|
source "${SCRIPT_DIR}/../lib/env.sh"
|
||||||
|
# Use vault-bot's own Forgejo identity (#747)
|
||||||
|
FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
|
||||||
LOGFILE="${FACTORY_ROOT}/vault/vault.log"
|
LOGFILE="${FACTORY_ROOT}/vault/vault.log"
|
||||||
STATUSFILE="/tmp/vault-status"
|
STATUSFILE="/tmp/vault-status"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/../lib/env.sh"
|
source "${SCRIPT_DIR}/vault-env.sh"
|
||||||
|
|
||||||
VAULT_DIR="${FACTORY_ROOT}/vault"
|
VAULT_DIR="${FACTORY_ROOT}/vault"
|
||||||
LOGFILE="${VAULT_DIR}/vault.log"
|
LOGFILE="${VAULT_DIR}/vault.log"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue