Feature/contest discussion automation - #30
Conversation
Add pyproject.toml with requests/selectolax runtime deps and pytest/responses dev deps, plus the generated uv.lock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verifies (manually, via workflow_dispatch) that the default GITHUB_TOKEN combined with permissions.discussions:write is sufficient to create Discussions via the GraphQL API, so no separate PAT/repo secret is needed for the contest-discussions automation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cument exception surface)
Ties together atcoder.py and github_client.py: for each recently finished ABC, fetch tasks, skip titles that already have a Discussion, and create the rest. Per-contest and per-task failures are caught and logged so one bad contest/task doesn't stop the run. --contest-id lets main() bypass auto-detection for manual backfill.
Runs weekly via cron (Sat 13:50 UTC, 10 min after the ABC 100-min window ends at 22:40 JST) and supports manual backfill via workflow_dispatch with an optional contest_id input. Uses the ambient GITHUB_TOKEN with discussions:write permission, no PAT required (verified in the Phase 1 smoke test). astral-sh/setup-uv@v3 has no python-version input (confirmed via its action.yml), so Python 3.13 is installed explicitly with actions/setup-python@v5 beforehand.
Superseded by post_discussions.yml, which now covers the real discussion-posting job; the smoke test already confirmed that the default GITHUB_TOKEN can create Discussions via GraphQL.
There was a problem hiding this comment.
Pull request overview
This PR adds an automated pipeline (Python script + scheduled GitHub Actions workflow) that detects recently finished AtCoder Beginner Contests (ABCs), scrapes the tasks list from AtCoder, and creates per-problem GitHub Discussions in this repository when missing.
Changes:
- Introduces a new
contest_discussionsPython package to fetch contest/task data and post Discussions via the GitHub GraphQL API. - Adds a scheduled + manual-dispatch GitHub Actions workflow to run the posting script using
uv. - Adds pytest coverage (with fixtures + HTTP mocking) for the AtCoder scraping, GitHub client, and orchestration logic.
Reviewed changes
Copilot reviewed 19 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Locks Python dependencies for the new automation tooling. |
| pyproject.toml | Defines the new Python project, dependencies, and pytest config. |
| .gitignore | Ignores uv virtualenv/cache and Python bytecode artifacts. |
| .github/workflows/post_discussions.yml | Schedules and dispatches the script execution with proper permissions. |
| src/contest_discussions/init.py | Introduces the new Python package namespace. |
| src/contest_discussions/constants.py | Stores repository/category IDs and shared constants for posting. |
| src/contest_discussions/atcoder.py | Implements contest discovery and task scraping from AtCoder endpoints/pages. |
| src/contest_discussions/github_client.py | Implements GitHub GraphQL calls to read/create Discussions. |
| src/contest_discussions/main.py | Orchestrates contest selection, de-duping, and discussion creation with CLI entrypoint. |
| tests/test_atcoder.py | Tests scraping + contest selection logic with mocked HTTP responses. |
| tests/test_github_client.py | Tests GraphQL client behavior using mocked responses. |
| tests/test_main.py | Tests orchestration behavior (skips, partial failures, backfill mode). |
| tests/fixtures/contests_sample.json | Fixture for contests.json filtering tests. |
| tests/fixtures/abc_tasks_sample.html | Fixture for AtCoder tasks-page parsing tests. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/plan.md | Design/implementation plan documentation. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-1.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-2.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-3.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-4.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-5.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-6.md | Implementation phase notes for the feature. |
| docs/dev-notes/2026-07-21/contest-discussion-automation/phase-7.md | Implementation phase notes for the feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def existing_discussion_titles(token: str) -> set[str]: | ||
| """Returns the titles of the most recent Discussions in the General category. | ||
|
|
||
| Only the first 100 (no pagination) — accepted limitation given weekly ABC | ||
| volume; revisit if the General category ever accumulates enough non-contest | ||
| discussions to push older unposted contest titles out of this window. | ||
| Network/HTTP failures propagate as requests.RequestException (e.g. | ||
| requests.HTTPError, requests.Timeout) — callers should catch that. | ||
| """ | ||
| data = _post_graphql( | ||
| token, | ||
| _EXISTING_TITLES_QUERY, | ||
| {"repositoryId": REPOSITORY_ID, "categoryId": GENERAL_CATEGORY_ID}, | ||
| ) | ||
| nodes = data["node"]["discussions"]["nodes"] | ||
|
|
||
| return {node["title"] for node in nodes} |
There was a problem hiding this comment.
Fixed in 0903188. existing_discussion_titles() now requests 100 discussions per page and follows pageInfo.endCursor until hasNextPage is false, so older Discussions are included during normal runs and backfills. Added a pagination regression test.
| if now is None: | ||
| now = datetime.now(timezone.utc) | ||
|
|
||
| response = requests.get(CONTESTS_JSON_URL, timeout=REQUEST_TIMEOUT_SECONDS) |
There was a problem hiding this comment.
Fixed in 0903188. find_recently_finished_abc_ids() now rejects a supplied naive datetime with ValueError("now must be timezone-aware"), preventing the cutoff from depending on the runner's local timezone. Added a regression test.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 22 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/contest_discussions/main.py:16
build_title()assumescontest_idis always in the formabcNNN. When--contest-idis provided via CLI/workflow and is malformed (e.g. whitespace, uppercase, wrong prefix),int(contest_id[3:])raises a bareValueErrorthat is hard to diagnose. Add explicit validation with a clear error message before parsing.
def build_title(contest_id: str, task: dict) -> str:
number = int(contest_id[3:])
return f"ABC {number} {task['problem_index']} - {task['name']}"
src/contest_discussions/github_client.py:106
_post_graphql()unconditionally returnspayload["data"]. If GitHub returns a non-standard/partial payload without adatafield (while still being HTTP 200 and without anerrorskey), this becomes aKeyErrorthat obscures the real response. Prefer an explicitdata is Nonecheck and raise a clearerRuntimeErrorincluding the payload.
if "errors" in payload:
raise RuntimeError(f"GitHub GraphQL API returned errors: {payload['errors']}")
return payload["data"]
.github/workflows/post_discussions.yml:36
- CI should fail fast if
uv.lockandpyproject.tomlare out of sync. Usinguv syncwithout--frozencan allow implicit resolution drift in automation. Useuv sync --frozento guarantee the workflow installs exactly what is locked.
- name: Install dependencies
run: uv sync
There was a problem hiding this comment.
PR 作成ありがとうございます
とても助かります
遅くなりましたが、拝見いたしました
[high]
- .github/workflows/post_discussions.yml で cron の実行時間の設定で、現状だと コンテスト情報が Problems API へ反映される前に実行する可能性があるかもしれません
- 動作確認済みという理解でいいでしょうか?
[low] 残りのコメントは安定した運用ためにあるいいと思われる項目です。これらのうち、該当コードを再読して必要だと判断・納得されたものに関してはご対応いただけると幸いです
私の理解不足や勘違いなどもあるかもしれませんので、対処不要と判断された項目に関してはご放念ください
|
|
||
| --- | ||
|
|
||
| ### Task 2: phase-N.md の破棄 |
There was a problem hiding this comment.
コードから読み取るのが難しい内容(意思決定・注意事項など)を集約されてはいかがでしょうか?
理由: planのコードは陳腐化する可能性が高いため
There was a problem hiding this comment.
Fixed in f6c1c8a. コードから読み取りにくい運用上の意思決定・注意事項を docs/contest-discussion-automation.md に集約し、README からリンクしました。
|
@KATO-Hiro |
概要
ABC終了後、自動でDiscussionを作成する機能を実装。
Architecture
NoviSteps-Writeups リポジトリ単独で完結する GitHub Actions (cron) から Python スクリプトを実行する。AtCoder の問題ページを直接スクレイピングして問題一覧を取得し、GitHub GraphQL API (
createDiscussion) で Discussion を作成する。