Compare commits

...

22 Commits

Author SHA1 Message Date
71414ef9c9
chore: better logs
All checks were successful
build / trigger-build-image (push) Successful in 14s
2025-11-30 00:25:30 -08:00
128142e965
feat: link to joj-mon from run ID
All checks were successful
build / trigger-build-image (push) Successful in 15s
2025-11-29 23:55:08 -08:00
743331c81b
feat: failed stage in scoreboard commit
All checks were successful
build / trigger-build-image (push) Successful in 23s
2025-11-29 00:11:17 -08:00
3a69be006d
chore: less redundant log
All checks were successful
build / trigger-build-image (push) Successful in 12s
2025-11-27 20:42:13 -08:00
a901c2bbde
fix: use penalized total score
All checks were successful
build / trigger-build-image (push) Successful in 24s
2025-11-07 16:36:06 -08:00
77064ac37c
chore: better help
All checks were successful
build / trigger-build-image (push) Successful in 15s
2025-10-29 20:38:22 -07:00
332e522051
feat: joj3-check-env ignore submitter
All checks were successful
build / trigger-build-image (push) Successful in 24s
2025-10-29 20:36:11 -07:00
69e097e04b
fix: remove git lock
All checks were successful
build / trigger-build-image (push) Successful in 28s
2025-10-28 19:37:08 -07:00
f4fb5eae05
feat: add filelock back
All checks were successful
build / trigger-build-image (push) Successful in 47s
2025-10-25 09:05:23 -07:00
3a511660bb
feat: remove all .lock files
All checks were successful
build / trigger-build-image (push) Successful in 14s
2025-10-09 00:14:34 -07:00
9fc7649696
feat: remove more locks
All checks were successful
build / trigger-build-image (push) Successful in 12s
2025-10-08 20:29:43 -07:00
e160023cbf
chore: log more commits length
All checks were successful
build / trigger-build-image (push) Successful in 16s
2025-10-08 20:21:27 -07:00
99d889ee12
feat: check stderr isatty for colorize
All checks were successful
build / trigger-build-image (push) Successful in 15s
2025-10-03 00:00:44 -07:00
f755fb44f6
chore: log unwatch
All checks were successful
build / trigger-build-image (push) Successful in 16s
2025-09-28 18:07:11 -07:00
94d3f993b2
feat: remove output repos in joj3-check-gitea-token
All checks were successful
build / trigger-build-image (push) Successful in 15s
2025-09-21 06:15:10 -07:00
aa33dcc2f1
feat: list orgs in joj3-check-gitea-token
All checks were successful
build / trigger-build-image (push) Successful in 8s
2025-09-21 06:02:58 -07:00
e24545324d
revert: "fix: disable file log in joj3-check-gitea-token"
This reverts commit 3dc6667716.
2025-09-21 06:01:04 -07:00
3dc6667716
fix: disable file log in joj3-check-gitea-token
All checks were successful
build / trigger-build-image (push) Successful in 8s
2025-09-21 05:26:08 -07:00
5b6c61af6d
fix: echo user
All checks were successful
build / trigger-build-image (push) Successful in 10s
2025-09-21 05:03:13 -07:00
8264152022
feat: joj3-check-gitea-token
All checks were successful
build / trigger-build-image (push) Successful in 9s
2025-09-21 05:01:03 -07:00
992f450004
feat: check current gitea user for joj3
All checks were successful
build / trigger-build-image (push) Successful in 24s
2025-09-21 04:56:14 -07:00
5478052c23
chore: updgrade to latest hooks 2025-09-21 04:53:13 -07:00
8 changed files with 82 additions and 70 deletions

View File

@ -1,41 +1,41 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v6.0.0
hooks: hooks:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.4.1" rev: "v1.18.2"
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: additional_dependencies:
- pydantic - pydantic
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.9.0 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
- repo: https://github.com/hadialqattan/pycln - repo: https://github.com/hadialqattan/pycln
rev: v2.4.0 rev: v2.5.0
hooks: hooks:
- id: pycln - id: pycln
args: [-a] args: [-a]
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: '1.7.5' rev: '1.8.6'
hooks: hooks:
- id: bandit - id: bandit
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.12.0 rev: 6.0.1
hooks: hooks:
- id: isort - id: isort
args: ["--profile", "black", "--filter-files"] args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.7.0 rev: 25.9.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.1 rev: v1.5.5
hooks: hooks:
- id: remove-crlf - id: remove-crlf
- id: remove-tabs - id: remove-tabs

View File

@ -6,7 +6,7 @@ from pathlib import Path
from time import sleep from time import sleep
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
# from filelock import FileLock from filelock import FileLock
from git import Repo from git import Repo
from typer import Argument, Exit, Option, Typer, echo from typer import Argument, Exit, Option, Typer, echo
@ -349,6 +349,7 @@ def joj3_all_env(
submitter_repo_name = env.github_repository.split("/")[-1] submitter_repo_name = env.github_repository.split("/")[-1]
penalty_factor = joj3.get_penalty_factor(end_time, penalty_config) penalty_factor = joj3.get_penalty_factor(end_time, penalty_config)
total_score = joj3.get_total_score(env.joj3_output_path) total_score = joj3.get_total_score(env.joj3_output_path)
total_score = round(total_score - abs(total_score) * (1 - penalty_factor))
res = { res = {
"totalScore": total_score, "totalScore": total_score,
"cappedTotalScore": ( "cappedTotalScore": (
@ -388,13 +389,12 @@ def joj3_all_env(
lock_file_path = os.path.join( lock_file_path = os.path.join(
settings.repos_dir, grading_repo_name, settings.joj3_lock_file_path settings.repos_dir, grading_repo_name, settings.joj3_lock_file_path
) )
logger.info( logger.debug(
f"try to acquire lock, file path: {lock_file_path}, " f"try to acquire lock, file path: {lock_file_path}, "
+ f"timeout: {settings.joj3_lock_file_timeout}" + f"timeout: {settings.joj3_lock_file_timeout}"
) )
if True: # disable the file lock temporarily with FileLock(lock_file_path, timeout=settings.joj3_lock_file_timeout).acquire():
# with FileLock(lock_file_path, timeout=settings.joj3_lock_file_timeout).acquire(): logger.debug("file lock acquired")
logger.info("file lock acquired")
retry_interval = 1 retry_interval = 1
git_push_ok = False git_push_ok = False
while not git_push_ok: while not git_push_ok:
@ -424,7 +424,9 @@ def joj3_all_env(
os.path.join(repo_path, scoreboard_filename), os.path.join(repo_path, scoreboard_filename),
exercise_name, exercise_name,
submitter_repo_name, submitter_repo_name,
total_score,
) )
failed_stage = joj3.get_failed_stage_from_file(env.joj3_output_path)
tea.pot.git.add_commit( tea.pot.git.add_commit(
grading_repo_name, grading_repo_name,
[scoreboard_filename], [scoreboard_filename],
@ -434,6 +436,7 @@ def joj3_all_env(
f"gitea actions link: {gitea_actions_url}\n" f"gitea actions link: {gitea_actions_url}\n"
f"gitea issue link: {gitea_issue_url}\n" f"gitea issue link: {gitea_issue_url}\n"
f"groups: {env.joj3_groups}\n" f"groups: {env.joj3_groups}\n"
f"failed stage: {failed_stage}\n"
), ),
) )
if not skip_failed_table: if not skip_failed_table:
@ -502,6 +505,9 @@ def joj3_check_env(
"Example: --penalty-config 24=0.75,48=0.5" "Example: --penalty-config 24=0.75,48=0.5"
), ),
), ),
ignore_submitter: bool = Option(
False, help="ignore submitter when checking submission count"
),
) -> None: ) -> None:
app.pretty_exceptions_enable = False app.pretty_exceptions_enable = False
set_settings(Settings(_env_file=env_path)) set_settings(Settings(_env_file=env_path))
@ -520,7 +526,7 @@ def joj3_check_env(
penalty_config, penalty_config,
) )
count_msg, count_failed = tea.pot.joj3_check_submission_count( count_msg, count_failed = tea.pot.joj3_check_submission_count(
env, grading_repo_name, group_config, scoreboard_filename env, grading_repo_name, group_config, scoreboard_filename, ignore_submitter
) )
echo( echo(
json.dumps( json.dumps(
@ -533,6 +539,16 @@ def joj3_check_env(
logger.info("joj3-check-env done") logger.info("joj3-check-env done")
@app.command("joj3-check-gitea-token")
def joj3_check_gitea_token(
env_path: str = Argument("", help="path to .env file")
) -> None:
app.pretty_exceptions_enable = False
set_settings(Settings(_env_file=env_path))
set_logger(settings.stderr_log_level)
tea.pot.gitea.organization_api.org_list_repos(settings.gitea_org_name)
if __name__ == "__main__": if __name__ == "__main__":
try: try:
app() app()

View File

@ -40,7 +40,7 @@ class Settings(BaseSettings):
joj_sid: str = "" joj_sid: str = ""
# joj3 # joj3
joj3_lock_file_path: str = ".git/teapot.lock" joj3_lock_file_path: str = ".git/teapot-joj3-all-env.lock"
joj3_lock_file_timeout: int = 30 joj3_lock_file_timeout: int = 30
# moss # moss

View File

@ -266,7 +266,6 @@ class Teapot:
): ):
if issue.title.startswith(title_prefix): if issue.title.startswith(title_prefix):
joj3_issue = issue joj3_issue = issue
logger.info(f"found joj3 issue: #{joj3_issue.number}")
break break
else: else:
new_issue = True new_issue = True
@ -293,9 +292,7 @@ class Teapot:
submitter_repo_name, submitter_repo_name,
body={"title": title, "body": comment, "labels": [label_id]}, body={"title": title, "body": comment, "labels": [label_id]},
) )
logger.info(f"created joj3 issue: #{joj3_issue.number}")
gitea_issue_url = joj3_issue.html_url gitea_issue_url = joj3_issue.html_url
logger.info(f"gitea issue url: {gitea_issue_url}")
if not new_issue: if not new_issue:
self.gitea.issue_api.issue_edit_issue( self.gitea.issue_api.issue_edit_issue(
self.gitea.org_name, self.gitea.org_name,
@ -359,7 +356,9 @@ class Teapot:
grading_repo_name: str, grading_repo_name: str,
group_config: str, group_config: str,
scoreboard_filename: str, scoreboard_filename: str,
ignore_submitter: bool,
) -> Tuple[str, bool]: ) -> Tuple[str, bool]:
submitter = env.github_actor
submitter_repo_name = env.github_repository.split("/")[-1] submitter_repo_name = env.github_repository.split("/")[-1]
repo: Repo = self.git.get_repo(grading_repo_name) repo: Repo = self.git.get_repo(grading_repo_name)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
@ -386,11 +385,13 @@ class Teapot:
time_windows.append(since) time_windows.append(since)
valid_items.append((name, max_count, time_period, since)) valid_items.append((name, max_count, time_period, since))
logger.info(f"valid items: {valid_items}, time windows: {time_windows}") logger.info(f"valid items: {valid_items}, time windows: {time_windows}")
all_commits = [] matched_commits = []
all_commits_length = 0
if time_windows: if time_windows:
earliest_since = min(time_windows).strftime("%Y-%m-%dT%H:%M:%S") earliest_since = min(time_windows).strftime("%Y-%m-%dT%H:%M:%S")
commits = repo.iter_commits(paths=scoreboard_filename, since=earliest_since) commits = repo.iter_commits(paths=scoreboard_filename, since=earliest_since)
for commit in commits: for commit in commits:
all_commits_length += 1
lines = commit.message.strip().splitlines() lines = commit.message.strip().splitlines()
if not lines: if not lines:
continue continue
@ -400,25 +401,28 @@ class Teapot:
d = match.groupdict() d = match.groupdict()
if ( if (
env.joj3_conf_name != d["exercise_name"] env.joj3_conf_name != d["exercise_name"]
or env.github_actor != d["submitter"]
or submitter_repo_name != d["submitter_repo_name"] or submitter_repo_name != d["submitter_repo_name"]
): ):
continue continue
if not ignore_submitter and submitter != d["submitter"]:
continue
groups_line = next((l for l in lines if l.startswith("groups: ")), None) groups_line = next((l for l in lines if l.startswith("groups: ")), None)
commit_groups = ( commit_groups = (
groups_line[len("groups: ") :].split(",") if groups_line else [] groups_line[len("groups: ") :].split(",") if groups_line else []
) )
all_commits.append( matched_commits.append(
{ {
"time": commit.committed_datetime, "time": commit.committed_datetime,
"groups": [g.strip() for g in commit_groups], "groups": [g.strip() for g in commit_groups],
} }
) )
logger.info(f"all commits length: {len(all_commits)}") logger.info(
f"matched commits length: {len(matched_commits)}, all commits length: {all_commits_length}"
)
for name, max_count, time_period, since in valid_items: for name, max_count, time_period, since in valid_items:
submit_count = 0 submit_count = 0
time_limit = now - timedelta(hours=time_period) time_limit = now - timedelta(hours=time_period)
for commit in all_commits: for commit in matched_commits:
if commit["time"] < time_limit: if commit["time"] < time_limit:
continue continue
if name: if name:
@ -428,7 +432,7 @@ class Teapot:
continue continue
submit_count += 1 submit_count += 1
logger.info( logger.info(
f"submitter {env.github_actor} is submitting for the {submit_count + 1} time, " f"submitter {submitter} is submitting for the {submit_count + 1} time, "
f"{min(0, max_count - submit_count - 1)} time(s) remaining, " f"{min(0, max_count - submit_count - 1)} time(s) remaining, "
f"group={name}, " f"group={name}, "
f"time period={time_period} hour(s), " f"time period={time_period} hour(s), "

View File

@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from joint_teapot.config import settings
from joint_teapot.utils.logger import logger from joint_teapot.utils.logger import logger
@ -41,6 +42,7 @@ def generate_scoreboard(
scoreboard_file_path: str, scoreboard_file_path: str,
exercise_name: str, exercise_name: str,
submitter_repo_name: str, submitter_repo_name: str,
exercise_total_score: int,
) -> None: ) -> None:
if not scoreboard_file_path.endswith(".csv"): if not scoreboard_file_path.endswith(".csv"):
logger.error( logger.error(
@ -99,11 +101,6 @@ def generate_scoreboard(
for row in data: for row in data:
row.insert(index, "") row.insert(index, "")
exercise_total_score = 0
for stage in stages:
for result in stage["results"]:
exercise_total_score += result["score"]
exercise_total_score = exercise_total_score
submitter_row[columns.index(exercise_name)] = str(exercise_total_score) submitter_row[columns.index(exercise_name)] = str(exercise_total_score)
total = 0 total = 0
@ -146,6 +143,18 @@ def get_failed_table_from_file(table_file_path: str) -> List[List[str]]:
return data return data
def get_failed_stage_from_file(score_file_path: str) -> str:
with open(score_file_path) as json_file:
stages: List[Dict[str, Any]] = json.load(json_file)
failed_stage = ""
for stage in stages:
if stage["force_quit"] == True:
failed_stage = stage["name"]
break
return failed_stage
def update_failed_table_from_score_file( def update_failed_table_from_score_file(
data: List[List[str]], data: List[List[str]],
score_file_path: str, score_file_path: str,
@ -153,31 +162,23 @@ def update_failed_table_from_score_file(
repo_link: str, repo_link: str,
action_link: str, action_link: str,
) -> None: ) -> None:
# get info from score file failed_stage = get_failed_stage_from_file(score_file_path)
with open(score_file_path) as json_file:
stages: List[Dict[str, Any]] = json.load(json_file)
failed_name = ""
for stage in stages:
if stage["force_quit"] == True:
failed_name = stage["name"]
break
# append to failed table # append to failed table
now = datetime.now().strftime("%Y-%m-%d %H:%M") now = datetime.now().strftime("%Y-%m-%d %H:%M")
repo = f"[{repo_name}]({repo_link})" repo = f"[{repo_name}]({repo_link})"
failure = f"[{failed_name}]({action_link})" failure = f"[{failed_stage}]({action_link})"
row_found = False row_found = False
for i, row in enumerate(data[:]): for i, row in enumerate(data[:]):
if row[1] == repo: if row[1] == repo:
row_found = True row_found = True
if failed_name == "": if failed_stage == "":
data.remove(row) data.remove(row)
else: else:
data[i][0] = now data[i][0] = now
data[i][2] = failure data[i][2] = failure
break break
if not row_found and failed_name != "": if not row_found and failed_stage != "":
data.append([now, repo, failure]) data.append([now, repo, failure])
@ -243,7 +244,7 @@ def generate_title_and_comment(
f"Generated at {now} from [Gitea Actions #{run_number}]({action_link}), " f"Generated at {now} from [Gitea Actions #{run_number}]({action_link}), "
f"commit {commit_hash}, " f"commit {commit_hash}, "
f"triggered by @{submitter}, " f"triggered by @{submitter}, "
f"run ID `{run_id}`.\n" f"run ID [`{run_id}`](https://focs.ji.sjtu.edu.cn/joj-mon/d/{settings.gitea_org_name}?var-Filters=RunID%7C%3D%7C{run_id}).\n"
"Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and " "Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and "
"[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n" "[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n"
) )

View File

@ -38,10 +38,7 @@ def set_logger(
) -> None: ) -> None:
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
logger.remove() logger.remove()
logger.add( logger.add(stderr, level=stderr_log_level, colorize=stderr.isatty())
stderr,
level=stderr_log_level,
)
logger.add(settings.log_file_path, level="DEBUG") logger.add(settings.log_file_path, level="DEBUG")

View File

@ -90,28 +90,23 @@ class Git:
retry_interval = 2 retry_interval = 2
while retry_interval and auto_retry: while retry_interval and auto_retry:
try: try:
current_branch = ""
if repo.head.is_detached:
current_branch = repo.head.commit.hexsha
else:
current_branch = repo.active_branch.name
if clean_git_lock: if clean_git_lock:
lock_files = [ locks_removed_count = 0
"index.lock", for root, _, files in os.walk(os.path.join(repo_dir, ".git")):
"HEAD.lock", for filename in files:
"fetch-pack.lock", if filename.endswith(".lock"):
"logs/HEAD.lock", lock_file_path = os.path.join(root, filename)
"packed-refs.lock", if (
"config.lock", os.path.relpath(lock_file_path, repo_dir)
f"refs/heads/{current_branch}.lock", == settings.joj3_lock_file_path
f"refs/remotes/origin/{current_branch}.lock", ):
f"refs/heads/{checkout_dest}.lock", continue
f"refs/remotes/origin/{checkout_dest}.lock", try:
] os.remove(lock_file_path)
for lock_file in lock_files: locks_removed_count += 1
lock_path = os.path.join(repo_dir, ".git", lock_file) except OSError as e:
if os.path.exists(lock_path): logger.warning(f"error removing lock file: {e}")
os.remove(lock_path) logger.info(f"removed {locks_removed_count} lock files")
repo.git.fetch("--tags", "--all", "-f") repo.git.fetch("--tags", "--all", "-f")
repo.git.reset("--hard", reset_target) repo.git.reset("--hard", reset_target)
repo.git.clean("-d", "-f", "-x") repo.git.clean("-d", "-f", "-x")
@ -148,9 +143,7 @@ class Git:
try: try:
repo.index.add(file) repo.index.add(file)
except OSError: except OSError:
logger.warning( logger.warning(f'file path "{file}" does not exist, skipped')
f'File path "{file}" does not exist. Skipping this file.'
)
continue continue
if repo.is_dirty(untracked_files=True) or repo.index.diff(None): if repo.is_dirty(untracked_files=True) or repo.index.diff(None):
repo.index.commit(commit_message) repo.index.commit(commit_message)

View File

@ -494,6 +494,7 @@ class Gitea:
self.repository_api.user_current_delete_subscription( self.repository_api.user_current_delete_subscription(
self.org_name, repo.name self.org_name, repo.name
) )
logger.info(f"Unwatched {repo.name}")
def get_all_teams(self) -> Dict[str, List[str]]: def get_all_teams(self) -> Dict[str, List[str]]:
res: Dict[str, List[str]] = {} res: Dict[str, List[str]] = {}