Compare commits
6 Commits
t-refactor
...
obs-groups
| Author | SHA256 | Date | |
|---|---|---|---|
|
2ff8784e40
|
|||
|
|
e1ed2e78e0 | ||
|
|
e494d545e7 | ||
|
|
e1ce27250b | ||
|
|
f87b3345fc | ||
|
|
5440476d10 |
@@ -34,7 +34,9 @@ jobs:
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images
|
||||
run: make build
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
run: make down
|
||||
@@ -43,12 +45,16 @@ jobs:
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests
|
||||
run: make pytest
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
if: always()
|
||||
run: make down
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
working-directory: ./autogits/integration
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -1,4 +1,4 @@
|
||||
MODULES := devel-importer utils/hujson utils/maintainer-update gitea-events-rabbitmq-publisher gitea_status_proxy group-review obs-forward-bot obs-staging-bot obs-status-service workflow-direct workflow-pr
|
||||
MODULES := devel-importer utils/hujson utils/maintainer-update gitea-events-rabbitmq-publisher gitea_status_proxy group-review obs-forward-bot obs-groups-bot obs-staging-bot obs-status-service workflow-direct workflow-pr
|
||||
|
||||
build:
|
||||
for m in $(MODULES); do go build -C $$m -buildmode=pie || exit 1 ; done
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -204,21 +205,16 @@ func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string
|
||||
|
||||
func ResolveWorkflowConfigs(gitea GiteaFileContentAndRepoFetcher, config *ConfigFile) (AutogitConfigs, error) {
|
||||
configs := make([]*AutogitConfig, 0, len(config.GitProjectNames))
|
||||
var errs []error
|
||||
for _, git_project := range config.GitProjectNames {
|
||||
c, err := ReadWorkflowConfig(gitea, git_project)
|
||||
if err != nil {
|
||||
// can't sync, so ignore for now
|
||||
errs = append(errs, err)
|
||||
log.Println(err)
|
||||
} else {
|
||||
configs = append(configs, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return configs, errors.Join(errs...)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,6 @@ func main() {
|
||||
interval := flag.Int64("interval", 10, "Notification polling interval in minutes (min 1 min)")
|
||||
configFile := flag.String("config", "", "PrjGit listing config file")
|
||||
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
|
||||
exitOnConfigError := flag.Bool("exit-on-config-error", false, "Exit if any repository in configuration cannot be resolved")
|
||||
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
|
||||
flag.Parse()
|
||||
|
||||
@@ -383,10 +382,8 @@ func main() {
|
||||
giteaTransport := common.AllocateGiteaTransport(*giteaUrl)
|
||||
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData)
|
||||
if err != nil {
|
||||
common.LogError("Failed to resolve some configuration repositories:", err)
|
||||
if *exitOnConfigError {
|
||||
return
|
||||
}
|
||||
common.LogError("Cannot parse workflow configs:", err)
|
||||
return
|
||||
}
|
||||
|
||||
reviewer, err := giteaTransport.GetCurrentUser()
|
||||
|
||||
@@ -70,7 +70,7 @@ wait_healthy:
|
||||
@echo "All services are healthy!"
|
||||
|
||||
pytest:
|
||||
podman-compose exec tester pytest -v tests/*
|
||||
podman-compose exec tester pytest -v tests
|
||||
|
||||
build:
|
||||
podman pull docker.io/library/rabbitmq:3.13.7-management
|
||||
|
||||
@@ -81,6 +81,11 @@ services:
|
||||
init: true
|
||||
networks:
|
||||
- gitea-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pgrep workflow-pr && [ $$(awk '{print $22}' /proc/$$(pgrep workflow-pr)/stat) -lt $$(($$(awk '{print $1}' /proc/uptime | cut -d. -f1)*100 - 1000)) ]"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
gitea:
|
||||
condition: service_started
|
||||
@@ -96,7 +101,6 @@ services:
|
||||
- ./workflow-pr-repos:/var/lib/workflow-pr/repos:Z
|
||||
command: [
|
||||
"-check-on-start",
|
||||
"-exit-on-config-error",
|
||||
"-debug",
|
||||
"-gitea-url", "http://gitea-test:3000",
|
||||
"-url", "amqps://rabbitmq-test:5671",
|
||||
@@ -130,9 +134,7 @@ services:
|
||||
networks:
|
||||
- gitea-network
|
||||
depends_on:
|
||||
gitea:
|
||||
condition: service_started
|
||||
tester:
|
||||
workflow-pr:
|
||||
condition: service_started
|
||||
environment:
|
||||
- OBS_USER=mock
|
||||
|
||||
@@ -162,6 +162,10 @@ BRANCH_CONFIG_CUSTOM = {
|
||||
"workflow.config": {
|
||||
"MergeMode": "devel"
|
||||
}
|
||||
},
|
||||
"zz-ready-to-start": {
|
||||
"workflow.config": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,8 @@ class GiteaAPIClient:
|
||||
"gitignores": "Go",
|
||||
"license": "MIT",
|
||||
"private": False,
|
||||
"readme": "Default"
|
||||
"readme": "Default",
|
||||
"object_format_name": "sha256"
|
||||
}
|
||||
response, duration = self._request("POST", f"orgs/{org_name}/repos", json=data)
|
||||
vprint(f"[{duration:.3f}s] Repository '{org_name}/{repo_name}' created with a README.")
|
||||
@@ -179,7 +180,7 @@ class GiteaAPIClient:
|
||||
|
||||
diff_content = f"""diff --git a/.gitmodules b/.gitmodules
|
||||
new file mode 100644
|
||||
index 0000000..f1838bd
|
||||
index 00000000..f1838bd9
|
||||
--- /dev/null
|
||||
+++ b/.gitmodules
|
||||
@@ -0,0 +1,6 @@
|
||||
@@ -191,14 +192,14 @@ index 0000000..f1838bd
|
||||
+ url = ../../mypool/pkgB.git
|
||||
diff --git a/pkgA b/pkgA
|
||||
new file mode 160000
|
||||
index 0000000..{pkg_a_sha}
|
||||
index 00000000..{pkg_a_sha}
|
||||
--- /dev/null
|
||||
+++ b/pkgA
|
||||
@@ -0,0 +1 @@
|
||||
+Subproject commit {pkg_a_sha}
|
||||
diff --git a/pkgB b/pkgB
|
||||
new file mode 160000
|
||||
index 0000000..{pkg_b_sha}
|
||||
index 00000000..{pkg_b_sha}
|
||||
--- /dev/null
|
||||
+++ b/pkgB
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -14,7 +14,7 @@ from tests.lib.common_test_utils import (
|
||||
def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||
"""End-to-end test for a successful PR workflow."""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
diff = "diff --git a/test.txt b/test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should succeed", False, base_branch=merge_branch_name)
|
||||
initial_pr_number = pr["number"]
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_pr_workflow_succeeded(staging_main_env, mock_build_result):
|
||||
def test_pr_workflow_failed(staging_main_env, mock_build_result):
|
||||
"""End-to-end test for a failed PR workflow."""
|
||||
gitea_env, test_full_repo_name, merge_branch_name = staging_main_env
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR - should fail", False, base_branch=merge_branch_name)
|
||||
initial_pr_number = pr["number"]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_001_project_pr_labels(label_env, staging_bot_client):
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/label_test_fixture.txt b/label_test_fixture.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} ---")
|
||||
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Labels Fixture", False, base_branch=branch_name)
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_001_automerge(automerge_env, test_user_client):
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/automerge_test.txt b/automerge_test.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Automerge Fixture", False, base_branch=merge_branch_name)
|
||||
@@ -49,7 +49,7 @@ def test_002_manual_merge(manual_merge_env, test_user_client, usera_client, stag
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||
@@ -149,7 +149,7 @@ def test_003_refuse_manual_merge(manual_merge_env, test_user_client, ownerB_clie
|
||||
# 1. Create a package PR
|
||||
diff = """diff --git a/manual_merge_test.txt b/manual_merge_test.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {merge_branch_name} ---")
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test Manual Merge Fixture", False, base_branch=merge_branch_name)
|
||||
@@ -245,7 +245,7 @@ def test_008_merge_mode_ff_only_success(merge_ff_env, test_user_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = """diff --git a/ff_test.txt b/ff_test.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
package_pr = test_user_client.create_gitea_pr("mypool/pkgA", diff, "Test FF Merge", False, base_branch=merge_branch_name)
|
||||
package_pr_number = package_pr["number"]
|
||||
@@ -269,7 +269,7 @@ def test_009_merge_mode_ff_only_failure(merge_ff_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -291,7 +291,7 @@ index 0000000..e69de29
|
||||
|
||||
print("Pushing another change to PR branch to trigger sync...")
|
||||
gitea_env.modify_gitea_pr("mypool/pkgA", package_pr_number,
|
||||
"diff --git a/sync_test.txt b/sync_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n",
|
||||
"diff --git a/sync_test.txt b/sync_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n",
|
||||
"Trigger Sync")
|
||||
|
||||
# The bot should detect it's not FF and NOT merge, and re-request reviews because of the new commit
|
||||
@@ -323,7 +323,7 @@ def test_010_merge_mode_devel_success(merge_devel_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -365,7 +365,7 @@ def test_011_merge_mode_replace_success(merge_replace_env, ownerA_client):
|
||||
# 1. Create a package PR that adds a file
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -419,7 +419,7 @@ def test_012_merge_mode_devel_ff_success(merge_devel_env, ownerA_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
@@ -461,7 +461,7 @@ def test_013_merge_mode_replace_ff_success(merge_replace_env, ownerA_client):
|
||||
# 1. Create a package PR (this will be FF-mergeable by default)
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/{filename}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_001_review_requests_matching_config(automerge_env, ownerA_client):
|
||||
filename = f"pkgB_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Requests Config", True, base_branch=branch_name)
|
||||
@@ -86,7 +86,7 @@ def test_004_maintainer(maintainer_env, ownerA_client):
|
||||
filename = f"maintainer_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgA on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgA", diff, "Test Maintainer Merge", True, base_branch=branch_name)
|
||||
@@ -155,7 +155,7 @@ def test_005_any_maintainer_approval_sufficient(maintainer_env, ownerA_client, o
|
||||
filename = f"pkgB_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Single Maintainer Merge", True, base_branch=branch_name)
|
||||
@@ -217,7 +217,7 @@ def test_006_maintainer_rejection_removes_other_requests(maintainer_env, ownerA_
|
||||
filename = f"pkgB_rejection_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Maintainer Rejection", True, base_branch=branch_name)
|
||||
@@ -277,7 +277,7 @@ def test_007_review_required_needs_all_approvals(review_required_env, ownerA_cli
|
||||
filename = f"pkgB_review_required_test_{ts}.txt"
|
||||
diff = f"""diff --git a/{filename} b/{filename}
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
"""
|
||||
print(f"--- Creating package PR in mypool/pkgB on branch {branch_name} as ownerA ---")
|
||||
package_pr = ownerA_client.create_gitea_pr("mypool/pkgB", diff, "Test Review Required", True, base_branch=branch_name)
|
||||
|
||||
@@ -22,7 +22,7 @@ pytest.forwarded_pr_number = None
|
||||
@pytest.mark.dependency()
|
||||
def test_001_project_pr(gitea_env):
|
||||
"""Forwarded PR correct title"""
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 0000000..e69de29\n"
|
||||
diff = "diff --git a/another_test.txt b/another_test.txt\nnew file mode 100644\nindex 00000000..473a0f4c\n"
|
||||
pytest.pr = gitea_env.create_gitea_pr("mypool/pkgA", diff, "Test PR", False)
|
||||
pytest.initial_pr_number = pytest.pr["number"]
|
||||
time.sleep(5) # Give Gitea some time to process the PR and make the timeline available
|
||||
@@ -114,7 +114,7 @@ def test_005_NoProjectGitPR_edits_disabled(no_project_git_pr_env, test_user_clie
|
||||
# 1. Create a Package PR (without "Allow edits from maintainers" enabled)
|
||||
initial_diff = """diff --git a/first_file.txt b/first_file.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/first_file.txt
|
||||
@@ -0,0 +1 @@
|
||||
@@ -160,7 +160,7 @@ index {pkgA_main_sha[:7]}..{pkgA_pr_head_sha[:7]} 160000
|
||||
# 4. Trigger an update on the Package PR to prompt the bot to react to the manual Project PR
|
||||
new_diff_content = """diff --git a/trigger_bot.txt b/trigger_bot.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/trigger_bot.txt
|
||||
@@ -0,0 +1 @@
|
||||
@@ -200,7 +200,7 @@ def test_006_NoProjectGitPR_edits_enabled(no_project_git_pr_env, test_user_clien
|
||||
# 2. Create a Package PR with "Allow edits from maintainers" enabled
|
||||
diff = """diff --git a/new_feature.txt b/new_feature.txt
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
index 00000000..473a0f4c
|
||||
--- /dev/null
|
||||
+++ b/new_feature.txt
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -19,22 +19,18 @@ export GITEA_TOKEN
|
||||
echo "GITEA_TOKEN exported (length: ${#GITEA_TOKEN})"
|
||||
|
||||
# Wait for the dummy data to be created by the gitea setup script
|
||||
echo "Waiting for workflow.config in myproducts/mySLFO..."
|
||||
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config"
|
||||
echo "Waiting for workflow.config in myproducts/mySLFO (branch zz-ready-to-start)..."
|
||||
API_URL="http://gitea-test:3000/api/v1/repos/myproducts/mySLFO/contents/workflow.config?ref=zz-ready-to-start"
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||
|
||||
WAITED=false
|
||||
while [ "$HTTP_STATUS" != "200" ]; do
|
||||
WAITED=true
|
||||
echo "workflow.config not found yet (HTTP Status: $HTTP_STATUS). Retrying in 5s..."
|
||||
sleep 5
|
||||
echo "workflow.config on zz-ready-to-start not found yet (HTTP Status: $HTTP_STATUS). Retrying in 1s..."
|
||||
sleep 1
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$API_URL")
|
||||
done
|
||||
|
||||
if [ "$WAITED" = true ]; then
|
||||
echo "workflow.config found. Sleeping 15s to let other configurations settle..."
|
||||
sleep 15
|
||||
fi
|
||||
echo "workflow.config found on zz-ready-to-start."
|
||||
|
||||
# Wait for the shared SSH key to be generated by the gitea setup script
|
||||
echo "Waiting for /var/lib/gitea/ssh-keys/id_ed25519..."
|
||||
|
||||
1
obs-groups-bot/.gitignore
vendored
Normal file
1
obs-groups-bot/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
obs-groups-bot
|
||||
242
obs-groups-bot/main.go
Normal file
242
obs-groups-bot/main.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Connect to the Open Build Service (OBS) API, retrieves a list of all groups,
|
||||
// and exports their metadata (specifically member lists) into individual JSON files.
|
||||
//
|
||||
// The tool supports both command-line flags and environment variables for configuration
|
||||
// (not for authentication, which is only via env vars), and includes a debug mode for verbose output.
|
||||
// It handles different XML response formats from the OBS API and ensures that
|
||||
// the output JSON files are properly sanitized and formatted.
|
||||
//
|
||||
// The accepted command-line flags are:
|
||||
//
|
||||
// -debug: Enable debug output showing API URLs and responses.
|
||||
// -instance: Name of the OBS instance (used in metadata, default "openSUSE").
|
||||
// -host: Base URL of the OBS API (default "http://localhost:3000").
|
||||
// -output: Directory to save the JSON files (default "groups").
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// # Using environment variables (OBS_USER, OBS_PASSWORD)
|
||||
// go run main.go
|
||||
//
|
||||
// # Targeting a specific OBS instance and output directory
|
||||
// go run main.go -host "https://api.opensuse.org" -output "./obs_groups"
|
||||
//
|
||||
// # Full command with debug mode
|
||||
// go run main.go -host http://localhost:8000 -output "./obs_groups" -instance "OBS" -debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
type groupsList struct {
|
||||
XMLName xml.Name `xml:"groups"`
|
||||
Groups []groupItem `xml:"group"`
|
||||
}
|
||||
|
||||
type groupsListAlt struct {
|
||||
XMLName xml.Name `xml:"directory"`
|
||||
Entries []groupEntry `xml:"entry"`
|
||||
}
|
||||
|
||||
type groupEntry struct {
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Inner string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (e *groupEntry) getName() string {
|
||||
if e.Name != "" {
|
||||
return e.Name
|
||||
}
|
||||
return e.Inner
|
||||
}
|
||||
|
||||
type groupItem struct {
|
||||
GroupID string `xml:"groupid,attr"`
|
||||
}
|
||||
|
||||
func getAllGroups(client *common.ObsClient) ([]string, error) {
|
||||
res, err := client.ObsRequest("GET", []string{"group"}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Response status: %d, body length: %d", res.StatusCode, len(data))
|
||||
if res.StatusCode != 200 {
|
||||
bodyStr := string(data)
|
||||
if len(bodyStr) > 500 {
|
||||
bodyStr = bodyStr[:500]
|
||||
}
|
||||
return nil, fmt.Errorf("Unexpected return code: %d, body: %s", res.StatusCode, bodyStr)
|
||||
}
|
||||
|
||||
// Try parsing as <groups> format
|
||||
var groupsList groupsList
|
||||
err = xml.Unmarshal(data, &groupsList)
|
||||
if err == nil && len(groupsList.Groups) > 0 {
|
||||
groupIDs := make([]string, len(groupsList.Groups))
|
||||
for i, g := range groupsList.Groups {
|
||||
groupIDs[i] = g.GroupID
|
||||
}
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
// Try parsing as <directory> format
|
||||
var groupsAlt groupsListAlt
|
||||
err = xml.Unmarshal(data, &groupsAlt)
|
||||
if err == nil && len(groupsAlt.Entries) > 0 {
|
||||
groupIDs := make([]string, len(groupsAlt.Entries))
|
||||
for i, e := range groupsAlt.Entries {
|
||||
groupIDs[i] = e.getName()
|
||||
}
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
// Log what we got
|
||||
bodyStr := string(data)
|
||||
if len(bodyStr) > 1000 {
|
||||
bodyStr = bodyStr[:1000]
|
||||
}
|
||||
log.Printf("Failed to parse XML, got: %s", bodyStr)
|
||||
return nil, fmt.Errorf("Could not parse groups response")
|
||||
}
|
||||
|
||||
type GroupOutput struct {
|
||||
Meta ImportMeta `json:"_meta,omitempty"`
|
||||
Name string `json:"Name"`
|
||||
Reviewers []string `json:"Reviewers"`
|
||||
Silent bool `json:"Silent,omitempty"`
|
||||
}
|
||||
|
||||
type ImportMeta struct {
|
||||
ImportedFrom string `json:"imported_from"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ImportTime time.Time `json:"import_time"`
|
||||
}
|
||||
|
||||
func sanitizeFilename(name string) string {
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
return name
|
||||
}
|
||||
|
||||
func processGroup(client *common.ObsClient, groupID, outputDir, instanceName string, importTime time.Time) error {
|
||||
meta, err := client.GetGroupMeta(groupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching group meta: %w", err)
|
||||
}
|
||||
|
||||
if meta == nil {
|
||||
return fmt.Errorf("group not found")
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("Group meta for %s: Title: %s, Persons: %d", groupID, meta.Title, len(meta.Persons.Persons)))
|
||||
|
||||
reviewers := make([]string, 0, len(meta.Persons.Persons))
|
||||
for _, p := range meta.Persons.Persons {
|
||||
reviewers = append(reviewers, p.UserID)
|
||||
}
|
||||
|
||||
output := GroupOutput{
|
||||
Meta: ImportMeta{
|
||||
ImportedFrom: instanceName,
|
||||
ReadOnly: true,
|
||||
ImportTime: importTime,
|
||||
},
|
||||
Name: groupID,
|
||||
Reviewers: reviewers,
|
||||
}
|
||||
|
||||
filename := sanitizeFilename(groupID) + ".json"
|
||||
filePath := filepath.Join(outputDir, filename)
|
||||
|
||||
data, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling json: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("Saved group %s to %s", groupID, filePath))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
debugModePtr := flag.Bool("debug", false, "Enable debug output showing API URLs")
|
||||
obsInstance := flag.String("instance", "openSUSE", "OBS instance name (used in metadata)")
|
||||
obsHost := flag.String("host", "http://localhost:3000", "OBS API host URL")
|
||||
outputDir := flag.String("output", "groups", "Output directory for JSON files")
|
||||
flag.Parse()
|
||||
|
||||
if *debugModePtr {
|
||||
common.SetLoggingLevel(common.LogLevelDebug)
|
||||
}
|
||||
|
||||
if err := common.RequireObsSecretToken(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Connecting to OBS at %s (instance: %s)", *obsHost, *obsInstance)
|
||||
|
||||
client, err := common.NewObsClient(*obsHost)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create OBS client: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Fetching list of all groups...")
|
||||
groupIDs, err := getAllGroups(client)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get groups list: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Found %d groups: %v", len(groupIDs), groupIDs)
|
||||
log.Printf("Found %s ", groupIDs)
|
||||
|
||||
err = os.MkdirAll(*outputDir, 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
importTime := time.Now()
|
||||
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for i, groupID := range groupIDs {
|
||||
log.Printf("[%d/%d] Fetching group: %s", i+1, len(groupIDs), groupID)
|
||||
|
||||
if err := processGroup(client, groupID, *outputDir, *obsInstance, importTime); err != nil {
|
||||
log.Printf("Error processing group %s: %v", groupID, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Printf("Done! Success: %d, Errors: %d", successCount, errorCount)
|
||||
log.Printf("JSON files saved to: %s", *outputDir)
|
||||
}
|
||||
211
obs-groups-bot/main_test.go
Normal file
211
obs-groups-bot/main_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
func TestGroupsListParsing(t *testing.T) {
|
||||
// Test <groups> format
|
||||
groupsXML := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<groups>
|
||||
<group groupid="group1"/>
|
||||
<group groupid="group2"/>
|
||||
<group groupid="group3"/>
|
||||
</groups>`
|
||||
|
||||
var groupsList groupsList
|
||||
err := xml.Unmarshal([]byte(groupsXML), &groupsList)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal groups XML: %v", err)
|
||||
}
|
||||
|
||||
if len(groupsList.Groups) != 3 {
|
||||
t.Errorf("Expected 3 groups, got %d", len(groupsList.Groups))
|
||||
}
|
||||
|
||||
expected := []string{"group1", "group2", "group3"}
|
||||
for i, g := range groupsList.Groups {
|
||||
if g.GroupID != expected[i] {
|
||||
t.Errorf("Expected group %s, got %s", expected[i], g.GroupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessGroup(t *testing.T) {
|
||||
// 1. Mock the OBS API server for GetGroupMeta
|
||||
groupID := "test:group"
|
||||
mockGroupMetaResponse := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<group>
|
||||
<title>Test Group Title</title>
|
||||
<person>
|
||||
<person userid="user1" role="maintainer"/>
|
||||
<person userid="user2" role="reviewer"/>
|
||||
</person>
|
||||
</group>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
expectedPath := "/group/" + groupID
|
||||
if r.URL.Path != expectedPath {
|
||||
t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(mockGroupMetaResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// 2. Create a temporary directory for output
|
||||
outputDir := t.TempDir()
|
||||
|
||||
// 3. Initialize client pointing to mock server
|
||||
client, err := common.NewObsClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// 4. Call processGroup
|
||||
instanceName := "test-instance"
|
||||
importTime := time.Now().UTC().Truncate(time.Second) // Truncate for stable comparison
|
||||
err = processGroup(client, groupID, outputDir, instanceName, importTime)
|
||||
if err != nil {
|
||||
t.Fatalf("processGroup failed: %v", err)
|
||||
}
|
||||
|
||||
// 5. Verify the output file
|
||||
expectedFilename := sanitizeFilename(groupID) + ".json"
|
||||
filePath := filepath.Join(outputDir, expectedFilename)
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
t.Fatalf("Expected output file was not created: %s", filePath)
|
||||
}
|
||||
|
||||
// Read and verify file content
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
}
|
||||
|
||||
var result GroupOutput
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("Failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
// Assertions
|
||||
expectedReviewers := []string{"user1", "user2"}
|
||||
expectedOutput := GroupOutput{
|
||||
Meta: ImportMeta{
|
||||
ImportedFrom: instanceName,
|
||||
ReadOnly: true,
|
||||
ImportTime: importTime,
|
||||
},
|
||||
Name: groupID,
|
||||
Reviewers: expectedReviewers,
|
||||
}
|
||||
|
||||
// Use reflect.DeepEqual for a robust comparison of the structs
|
||||
if !reflect.DeepEqual(result, expectedOutput) {
|
||||
t.Errorf("Output JSON does not match expected.\nGot: %+v\nWant: %+v", result, expectedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllGroups(t *testing.T) {
|
||||
// Mock the OBS API server
|
||||
mockResponse := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<groups>
|
||||
<group groupid="mock-group-1"/>
|
||||
<group groupid="mock-group-2"/>
|
||||
</groups>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify the request path
|
||||
if r.URL.Path != "/group" {
|
||||
t.Errorf("Expected path /group, got %s", r.URL.Path)
|
||||
}
|
||||
// Verify method
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("Expected method GET, got %s", r.Method)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Initialize client pointing to mock server
|
||||
client, err := common.NewObsClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
groups, err := getAllGroups(client)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllGroups failed: %v", err)
|
||||
}
|
||||
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("Expected 2 groups, got %d", len(groups))
|
||||
}
|
||||
if groups[0] != "mock-group-1" {
|
||||
t.Errorf("Expected first group to be mock-group-1, got %s", groups[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupsListDirectoryFormat(t *testing.T) {
|
||||
// Test <directory> format with name attribute
|
||||
dirXML := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<directory>
|
||||
<entry name="group-a"/>
|
||||
<entry name="group-b"/>
|
||||
<entry name="group-c"/>
|
||||
</directory>`
|
||||
|
||||
var groupsAlt groupsListAlt
|
||||
err := xml.Unmarshal([]byte(dirXML), &groupsAlt)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal directory XML: %v", err)
|
||||
}
|
||||
|
||||
if len(groupsAlt.Entries) != 3 {
|
||||
t.Errorf("Expected 3 entries, got %d", len(groupsAlt.Entries))
|
||||
}
|
||||
|
||||
expected := []string{"group-a", "group-b", "group-c"}
|
||||
for i, e := range groupsAlt.Entries {
|
||||
if e.getName() != expected[i] {
|
||||
t.Errorf("Expected entry %s, got %s", expected[i], e.getName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"group/name", "group_name"},
|
||||
{"project:group", "project_group"},
|
||||
{"group with spaces", "group_with_spaces"},
|
||||
{"group/name:space", "group_name_space"},
|
||||
{"", ""},
|
||||
{"multiple///slashes", "multiple___slashes"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := sanitizeFilename(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,10 +503,7 @@ func updateConfiguration(configFilename string, orgs *[]string) {
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
configs, err := common.ResolveWorkflowConfigs(gitea, configFile)
|
||||
if err != nil {
|
||||
common.LogError("Failed to resolve some configuration repositories:", err)
|
||||
}
|
||||
configs, _ := common.ResolveWorkflowConfigs(gitea, configFile)
|
||||
configuredRepos = make(map[string][]*common.AutogitConfig)
|
||||
*orgs = make([]string, 0, 1)
|
||||
for _, c := range configs {
|
||||
|
||||
@@ -58,7 +58,6 @@ func main() {
|
||||
checkOnStart := flag.Bool("check-on-start", common.GetEnvOverrideBool(os.Getenv("AUTOGITS_CHECK_ON_START"), false), "Check all repositories for consistency on start, without delays")
|
||||
checkIntervalHours := flag.Float64("check-interval", 5, "Check interval (+-random delay) for repositories for consitency, in hours")
|
||||
flag.BoolVar(&ListPROnly, "list-prs-only", false, "Only lists PRs without acting on them")
|
||||
exitOnConfigError := flag.Bool("exit-on-config-error", false, "Exit if any repository in configuration cannot be resolved")
|
||||
flag.Int64Var(&PRID, "id", -1, "Process only the specific ID and ignore the rest. Use for debugging")
|
||||
basePath := flag.String("repo-path", common.GetEnvOverrideString(os.Getenv("AUTOGITS_REPO_PATH"), ""), "Repository path. Default is temporary directory")
|
||||
pr := flag.String("only-pr", "", "Only specific PR to process. For debugging")
|
||||
@@ -98,10 +97,8 @@ func main() {
|
||||
|
||||
configs, err := common.ResolveWorkflowConfigs(Gitea, config)
|
||||
if err != nil {
|
||||
common.LogError("Failed to resolve some configuration repositories:", err)
|
||||
if *exitOnConfigError {
|
||||
return
|
||||
}
|
||||
common.LogError("Cannot resolve config files:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range configs {
|
||||
|
||||
Reference in New Issue
Block a user