fix deploy: Gitea login_name, WP token from DB, pin infra images
Some checks are pending
ci/woodpecker/push/deploy Pipeline is running
ci/woodpecker/push/infra-ci Pipeline was successful
coverage 99% coverage
ci/woodpecker/push/ci Pipeline was successful

- GiteaClient.change_admin_password: add required login_name and
  source_id fields (was returning 422 Unprocessable Entity)
- provision_woodpecker: read WP API token from woodpecker DB hash
  instead of using Gitea token (Woodpecker uses Gitea OAuth, its
  API requires the user hash not a Gitea API token)
- Pin fhirworx/rustfs and fhirworx/postgresql to :latest — these
  aren't built by the pipeline so COMMIT_SHA tag doesn't exist
This commit is contained in:
kert
2026-03-23 10:35:48 -04:00
parent 9022effa57
commit 8f1e7c8d13
4 changed files with 59 additions and 13 deletions

View File

@@ -58,7 +58,7 @@ services:
restart: unless-stopped restart: unless-stopped
rustfs: rustfs:
image: fhirworx/rustfs:${COMMIT_SHA:-latest} image: fhirworx/rustfs:latest
container_name: rustfs container_name: rustfs
user: "10001:10001" user: "10001:10001"
networks: networks:
@@ -98,7 +98,7 @@ services:
restart: unless-stopped restart: unless-stopped
postgres: postgres:
image: fhirworx/postgresql:${COMMIT_SHA:-latest} image: fhirworx/postgresql:latest
container_name: postgres container_name: postgres
networks: networks:
- storage - storage

View File

@@ -264,6 +264,41 @@ WOODPECKER_SECRET_MAP = {
} }
def _get_woodpecker_token(
values: dict[str, str], *, container: str = "postgres"
) -> str:
"""Read Woodpecker user API token from its database.
Woodpecker authenticates via Gitea OAuth. Its REST API uses the
user's `hash` field from the woodpecker DB, not a Gitea API token.
"""
wp_db_pw = values.get("WOODPECKER_DB_PASSWORD", "")
result = subprocess.run(
[
"docker",
"exec",
"-e",
f"PGPASSWORD={wp_db_pw}",
container,
"psql",
"-U",
"woodpecker",
"-d",
"woodpecker",
"-t",
"-A",
"-c",
"SELECT hash FROM users WHERE login='kert' LIMIT 1",
],
capture_output=True,
text=True,
)
token = result.stdout.strip()
if not token:
raise ValueError("Could not read Woodpecker token from DB")
return token
def provision_woodpecker( def provision_woodpecker(
values: dict[str, str], values: dict[str, str],
*, *,
@@ -271,9 +306,7 @@ def provision_woodpecker(
base_url: str = "http://woodpecker-server:8000/api", base_url: str = "http://woodpecker-server:8000/api",
) -> None: ) -> None:
"""Sync rotated credentials to Woodpecker repo secrets.""" """Sync rotated credentials to Woodpecker repo secrets."""
token = values.get("GITEA_TOKEN", "") token = _get_woodpecker_token(values)
if not token:
raise ValueError("GITEA_TOKEN not available — cannot sync Woodpecker secrets")
wp = WoodpeckerClient(token, base_url=base_url) wp = WoodpeckerClient(token, base_url=base_url)
for secret_name, env_var in WOODPECKER_SECRET_MAP.items(): for secret_name, env_var in WOODPECKER_SECRET_MAP.items():

View File

@@ -66,7 +66,12 @@ class GiteaClient(Client):
def change_admin_password(self, username: str, password: str) -> dict: def change_admin_password(self, username: str, password: str) -> dict:
return self.patch( return self.patch(
f"/admin/users/{username}", f"/admin/users/{username}",
json={"password": password, "must_change_password": False}, json={
"login_name": username,
"source_id": 0,
"password": password,
"must_change_password": False,
},
).json() ).json()
# ── Tokens ───────────────────────────────────────────── # ── Tokens ─────────────────────────────────────────────

View File

@@ -225,28 +225,36 @@ class TestProvisionGitea:
class TestProvisionWoodpecker: class TestProvisionWoodpecker:
def test_updates_secrets(self): def test_updates_secrets(self):
values = derive_all(ROOT, COMMIT) values = derive_all(ROOT, COMMIT)
values["GITEA_TOKEN"] = "tok"
mock_wp = MagicMock() mock_wp = MagicMock()
with patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp): with (
patch("api.auth.provision._get_woodpecker_token", return_value="wp-hash"),
patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp),
):
provision_woodpecker(values, repo_id=1) provision_woodpecker(values, repo_id=1)
assert mock_wp.update_secret.call_count == len(WOODPECKER_SECRET_MAP) assert mock_wp.update_secret.call_count == len(WOODPECKER_SECRET_MAP)
def test_creates_on_update_failure(self): def test_creates_on_update_failure(self):
values = derive_all(ROOT, COMMIT) values = derive_all(ROOT, COMMIT)
values["GITEA_TOKEN"] = "tok"
mock_wp = MagicMock() mock_wp = MagicMock()
mock_wp.update_secret.side_effect = Exception("not found") mock_wp.update_secret.side_effect = Exception("not found")
with patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp): with (
patch("api.auth.provision._get_woodpecker_token", return_value="wp-hash"),
patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp),
):
provision_woodpecker(values, repo_id=1) provision_woodpecker(values, repo_id=1)
assert mock_wp.create_secret.call_count == len(WOODPECKER_SECRET_MAP) assert mock_wp.create_secret.call_count == len(WOODPECKER_SECRET_MAP)
def test_raises_without_token(self): def test_raises_without_db_token(self):
with pytest.raises(ValueError, match="GITEA_TOKEN"): with patch(
provision_woodpecker({}, repo_id=1) "api.auth.provision._get_woodpecker_token",
side_effect=ValueError("Could not read"),
):
with pytest.raises(ValueError, match="Could not read"):
provision_woodpecker({}, repo_id=1)
class TestProvisionEndToEnd: class TestProvisionEndToEnd: