Skip to content

Commit efc6a7d

Browse files
ci: add ruff format enforcement to CI and pre-commit hooks (#197)
* fix: formatting * ci(lint): add ruff format checks to CI and pre-commit hooks Add ruff formatting enforcement at multiple levels: - Tekton CI pipelines: ruff-format-check task for all 6 pipelines - Pre-commit hooks: auto-format on commit via .pre-commit-config.yaml - Dev setup: auto-install hooks in init.sh Uses UBI9 minimal image for faster CI execution. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: remove merge conflict markers and add missing BOT_SPRINT_PREFIX env var * fix: use trusted task-script bundle instead of inline taskSpec for ruff checks Konflux Enterprise Contract requires all tasks to use trusted bundle references. Replaced inline taskSpec with taskRef pointing to quay.io/konflux-ci/tekton-catalog/task-script:0.1. This resolves the trusted_task.trusted violation while maintaining the same ruff format checking functionality. * fix: use custom Task resource instead of bundle for ruff format checks Konflux doesn't provide a generic task-script bundle. Instead, create a custom Task resource (ruff-format-check-task.yaml) that can be referenced by name in all pipelines. This resolves the bundle authentication error while maintaining ruff format enforcement in CI that cannot be bypassed. * fix: use git resolver for ruff-format-check task instead of namespace task Use Tekton git resolver to fetch the task definition from the same repository and revision being built. This keeps the task definition version-controlled with the code and doesn't require deploying a separate Task resource to the namespace. The task is resolved from .tekton/ruff-format-check-task.yaml at the same revision as the code being tested. * refactor: switch ruff format check from Tekton to GitHub Actions Replace Tekton-based ruff format checks with simpler GitHub Actions workflow. - Remove custom Tekton task and git resolver references from all 6 pipelines - Add .github/workflows/ruff-format.yml for CI formatting checks - Keep pre-commit hooks for local enforcement Tekton approach hit Enterprise Contract policy restrictions. GitHub Actions provides simpler, more maintainable solution. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * chore: trigger proxy pipeline rebuild * chore: touch proxy config to force pipeline rebuild Proxy artifact from commit 2392590 still has untrusted ruff task. Force rebuild with updated pipeline (task removed). * temp: disable proxy path filtering to force rebuild Remove path filtering from proxy pipeline CEL expression. This will trigger proxy rebuild with clean pipeline (no ruff task). Will re-enable path filtering after artifact is clean. * chore: re-enable proxy path filtering Restore original CEL expression now that proxy artifact is clean. Proxy will only rebuild when proxy/, Dockerfile, or pipeline YAML changes. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 19b1f8b commit efc6a7d

33 files changed

Lines changed: 2607 additions & 1720 deletions

.claude/skills/gh-release-upload/upload.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22
"""Upload a file to GitHub Releases via the proxy upload endpoint."""
3+
34
import json
45
import mimetypes
56
import os

.claude/skills/new-work/new_work.py

Lines changed: 126 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,109 @@
1818
from jira_mcp import jira_call
1919
from paths import SLEEP_FILE
2020

21-
PROJECT_REPOS = Path(__file__).resolve().parent.parent.parent.parent / "project-repos.json"
21+
PROJECT_REPOS = (
22+
Path(__file__).resolve().parent.parent.parent.parent / "project-repos.json"
23+
)
2224
BOT_LABEL = os.environ.get("BOT_LABEL", "")
23-
BOT_INCLUDE_BACKLOG = os.environ.get("BOT_INCLUDE_BACKLOG", "").lower() in ("1", "true", "yes")
25+
BOT_BOARD_ID = os.environ.get("BOT_BOARD_ID", "")
26+
BOT_BOARD_NAME = os.environ.get("BOT_BOARD_NAME", "")
27+
BOT_SPRINT_PREFIX = os.environ.get("BOT_SPRINT_PREFIX", "")
28+
BOT_INCLUDE_BACKLOG = os.environ.get("BOT_INCLUDE_BACKLOG", "").lower() in (
29+
"1",
30+
"true",
31+
"yes",
32+
)
2433
BOT_JIRA_EMAIL = os.environ.get("BOT_JIRA_EMAIL", "")
2534
NOT_STARTED_STATUSES = ("New", "Backlog", "Refinement", "To Do")
2635

2736

2837
def jira_search(jql, limit=10):
29-
data = jira_call("jira_search", {
30-
"jql": jql,
31-
"limit": limit,
32-
"fields": "summary,status,labels,assignee,priority,description,comment,issuelinks,issuetype",
33-
})
38+
data = jira_call(
39+
"jira_search",
40+
{
41+
"jql": jql,
42+
"limit": limit,
43+
"fields": "summary,status,labels,assignee,priority,description,comment,issuelinks,issuetype",
44+
},
45+
)
3446
if not data:
3547
print("ERR: Jira search returned no data", file=sys.stderr)
3648
return []
3749
issues = data if isinstance(data, list) else data.get("issues", [])
3850
return issues
3951

4052

53+
def resolve_board_id():
54+
if BOT_BOARD_ID:
55+
return BOT_BOARD_ID
56+
if not BOT_BOARD_NAME:
57+
print(
58+
"WARN: neither BOT_BOARD_ID nor BOT_BOARD_NAME set, skipping sprint query",
59+
file=sys.stderr,
60+
)
61+
return None
62+
data = jira_call(
63+
"jira_get_agile_boards",
64+
{
65+
"board_name": BOT_BOARD_NAME,
66+
"limit": 1,
67+
},
68+
)
69+
if not data:
70+
print(f"ERR: no board found matching name '{BOT_BOARD_NAME}'", file=sys.stderr)
71+
return None
72+
boards = data if isinstance(data, list) else data.get("values", [])
73+
if not boards:
74+
print(f"ERR: no board found matching name '{BOT_BOARD_NAME}'", file=sys.stderr)
75+
return None
76+
board = boards[0]
77+
print(
78+
f"Resolved board: {board.get('name', '?')} (id={board['id']})", file=sys.stderr
79+
)
80+
return str(board["id"])
81+
82+
83+
def get_active_sprint():
84+
board_id = resolve_board_id()
85+
if not board_id:
86+
return None
87+
data = jira_call(
88+
"jira_get_sprints_from_board",
89+
{
90+
"board_id": board_id,
91+
"state": "active",
92+
"limit": 10,
93+
},
94+
)
95+
if not data:
96+
return None
97+
sprints = data if isinstance(data, list) else data.get("values", [])
98+
if not sprints:
99+
return None
100+
if BOT_SPRINT_PREFIX:
101+
matched = [
102+
s for s in sprints if s.get("name", "").startswith(BOT_SPRINT_PREFIX)
103+
]
104+
if matched:
105+
sprint = matched[0]
106+
print(
107+
f"Active sprint (prefix={BOT_SPRINT_PREFIX}): {sprint.get('name', '?')} (id={sprint['id']})",
108+
file=sys.stderr,
109+
)
110+
return sprint
111+
names = [s.get("name", "?") for s in sprints]
112+
print(
113+
f"WARN: no sprint matching prefix '{BOT_SPRINT_PREFIX}', available: {names}",
114+
file=sys.stderr,
115+
)
116+
return None
117+
sprint = sprints[0]
118+
print(
119+
f"Active sprint: {sprint.get('name', '?')} (id={sprint['id']})", file=sys.stderr
120+
)
121+
return sprint
122+
123+
41124
def load_project_repos():
42125
try:
43126
return json.loads(PROJECT_REPOS.read_text())
@@ -59,7 +142,9 @@ def build_repo_lookup(repos_dict):
59142

60143

61144
def match_repo_labels(labels, repo_lookup):
62-
repo_labels = [l.replace("repo:", "") for l in labels if l.startswith("repo:")]
145+
repo_labels = [
146+
label.replace("repo:", "") for label in labels if label.startswith("repo:")
147+
]
63148
if not repo_labels:
64149
return []
65150
matched = [repo_lookup[r] for r in repo_labels if r in repo_lookup]
@@ -107,7 +192,9 @@ def collect(jql, tag):
107192
# Tier 3: backlog (no sprint)
108193
if len(candidates) < 10 and BOT_INCLUDE_BACKLOG:
109194
if BOT_JIRA_EMAIL:
110-
assignee_filter = f'AND (assignee is EMPTY OR assignee = "{BOT_JIRA_EMAIL}") '
195+
assignee_filter = (
196+
f'AND (assignee is EMPTY OR assignee = "{BOT_JIRA_EMAIL}") '
197+
)
111198
else:
112199
assignee_filter = "AND assignee is EMPTY "
113200
collect(
@@ -124,18 +211,20 @@ def collect(jql, tag):
124211
repos = match_repo_labels(labels, repo_lookup)
125212
comments = (fields.get("comment", {}).get("comments") or [])[-5:]
126213

127-
results.append({
128-
"key": issue["key"],
129-
"summary": fields.get("summary", ""),
130-
"status": fields.get("status", {}).get("name", "?"),
131-
"priority": fields.get("priority", {}).get("name", "?"),
132-
"type": fields.get("issuetype", {}).get("name", "?"),
133-
"labels": labels,
134-
"repos": repos,
135-
"description": fields.get("description") or "",
136-
"comments": comments,
137-
"links": fields.get("issuelinks", []),
138-
})
214+
results.append(
215+
{
216+
"key": issue["key"],
217+
"summary": fields.get("summary", ""),
218+
"status": fields.get("status", {}).get("name", "?"),
219+
"priority": fields.get("priority", {}).get("name", "?"),
220+
"type": fields.get("issuetype", {}).get("name", "?"),
221+
"labels": labels,
222+
"repos": repos,
223+
"description": fields.get("description") or "",
224+
"comments": comments,
225+
"links": fields.get("issuelinks", []),
226+
}
227+
)
139228
return results
140229

141230

@@ -145,20 +234,26 @@ def fmt_candidate(c):
145234
if c["repos"]:
146235
lines.append(f" repos: {','.join(c['repos'])}")
147236
else:
148-
repo_labels = [l for l in c["labels"] if l.startswith("repo:")]
237+
repo_labels = [label for label in c["labels"] if label.startswith("repo:")]
149238
if repo_labels:
150-
lines.append(f" repo_labels: {','.join(repo_labels)} (NO MATCH in project-repos.json)")
239+
lines.append(
240+
f" repo_labels: {','.join(repo_labels)} (NO MATCH in project-repos.json)"
241+
)
151242
else:
152243
lines.append(" repos: (no repo: label)")
153-
other_labels = [l for l in c["labels"] if not l.startswith("repo:") and l != BOT_LABEL]
244+
other_labels = [
245+
label
246+
for label in c["labels"]
247+
if not label.startswith("repo:") and label != BOT_LABEL
248+
]
154249
if other_labels:
155250
lines.append(f" labels: {','.join(other_labels)}")
156251
for lk in c["links"][:5]:
157252
lt = lk.get("type", {}).get("name", "?")
158253
linked = lk.get("inwardIssue") or lk.get("outwardIssue", {})
159254
if linked:
160255
lk_status = linked.get("fields", {}).get("status", {}).get("name", "?")
161-
lines.append(f" link: {lt} {linked.get('key','?')} [{lk_status}]")
256+
lines.append(f" link: {lt} {linked.get('key', '?')} [{lk_status}]")
162257
if c["description"]:
163258
lines.append(" description:")
164259
for dl in c["description"].strip().split("\n"):
@@ -180,7 +275,9 @@ def main():
180275
if not candidates:
181276
print("NO CANDIDATES FOUND")
182277
SLEEP_FILE.parent.mkdir(parents=True, exist_ok=True)
183-
SLEEP_FILE.write_text(json.dumps({"recommended_sleep": 3600, "reason": "no_eligible_work"}))
278+
SLEEP_FILE.write_text(
279+
json.dumps({"recommended_sleep": 3600, "reason": "no_eligible_work"})
280+
)
184281
return
185282

186283
print(f"NEW WORK CANDIDATES ({len(candidates)})")
@@ -193,7 +290,9 @@ def main():
193290
without_repos = [c for c in candidates if not c["repos"]]
194291
print(f"-> {len(with_repos)} with matching repos, {len(without_repos)} without")
195292
if with_repos:
196-
print(f"-> Top pick: {with_repos[0]['key']} repos={','.join(with_repos[0]['repos'])}")
293+
print(
294+
f"-> Top pick: {with_repos[0]['key']} repos={','.join(with_repos[0]['repos'])}"
295+
)
197296

198297

199298
if __name__ == "__main__":

.claude/skills/post-pr/post_pr.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@
1717
from scripts.post_pr_operations import execute_post_pr_workflow
1818

1919
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20-
from jira_mcp import jira_call
2120

2221
# Configure logging
23-
logging.basicConfig(
24-
level=logging.INFO,
25-
format="[post-pr] %(levelname)s: %(message)s",
26-
stream=sys.stdout
27-
)
22+
logging.basicConfig(level=logging.INFO, format="[post-pr] %(levelname)s: %(message)s", stream=sys.stdout)
2823
logger = logging.getLogger(__name__)
2924

3025

@@ -163,7 +158,7 @@ def main():
163158
pr_number = task["pr_number"]
164159
summary = task.get("summary", "")
165160

166-
logger.info(f"Task validated successfully")
161+
logger.info("Task validated successfully")
167162
logger.info(f" JIRA: {jira_key}")
168163
logger.info(f" PR: {pr_url} (#{pr_number})")
169164
logger.info(f" Summary: {summary or '(none)'}")
@@ -193,12 +188,14 @@ def main():
193188
print("=" * 80)
194189

195190
for op in result.operations:
196-
status_icon = (
197-
"[OK]" if op.status.value == "success"
198-
else "[FAIL]" if op.status.value == "failed"
199-
else "[SKIP]"
191+
status_icon = "[OK]" if op.status.value == "success" else "[FAIL]" if op.status.value == "failed" else "[SKIP]"
192+
level = (
193+
logging.INFO
194+
if op.status.value == "success"
195+
else logging.WARNING
196+
if op.status.value == "skipped"
197+
else logging.ERROR
200198
)
201-
level = logging.INFO if op.status.value == "success" else logging.WARNING if op.status.value == "skipped" else logging.ERROR
202199
logger.log(level, f"{status_icon} {op.operation}: {op.message}")
203200

204201
print("=" * 80)

0 commit comments

Comments
 (0)