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] 残りのコメントは安定した運用ためにあるいいと思われる項目です。これらのうち、該当コードを再読して必要だと判断・納得されたものに関してはご対応いただけると幸いです
私の理解不足や勘違いなどもあるかもしれませんので、対処不要と判断された項目に関してはご放念ください
| print(f"Created: {url}") | ||
| existing_titles.add(title) | ||
|
|
||
| if fetch_failures == len(contest_ids): |
There was a problem hiding this comment.
create_failures を数えた方がいいかもしれません
理由: token 権限不足・secondary rate limit・API 障害などで、すべての作成に失敗してもワークフローが正常に実行されたと表示される可能性があるため
| """ | ||
|
|
||
|
|
||
| def existing_discussion_titles(token: str) -> set[str]: |
There was a problem hiding this comment.
orderBy: CREATED_AT DESC なので、自動モード(直近 3 コンテスト=最大 21 件)は 1 ページで十分そうです
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def build_title(contest_id: str, task: dict) -> str: |
There was a problem hiding this comment.
例えば、main()もしくはこのメソッドで、re.fullmatch(r"abc\d+", contest_id) のように検証を入れてはいかがでしょうか?
理由: 現時点ではABC のみの想定なので、ARC / AGC などの誤爆を回避するため
| _TASK_HREF_PATTERN = re.compile(r"^/contests/([^/]+)/tasks/([^/]+)$") | ||
|
|
||
|
|
||
| def fetch_tasks(contest_id: str) -> list[dict[str, str]]: |
There was a problem hiding this comment.
# kenkoooo's problems.json lags behind by hours after a contest ends,
# so task data is scraped directly instead.のようなコメントがあると、Problems の API を意図して使っていないことが明確になると思います
|
|
||
| --- | ||
|
|
||
| ### Task 2 (ユーザー確認必須・手動): 実際に1回動かして確認 |
There was a problem hiding this comment.
動作確認済み という理解でいいのでしょうか?
|
|
||
| --- | ||
|
|
||
| ### Task 2: phase-N.md の破棄 |
There was a problem hiding this comment.
コードから読み取るのが難しい内容(意思決定・注意事項など)を集約されてはいかがでしょうか?
理由: planのコードは陳腐化する可能性が高いため
| python-version: "3.13" | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v3 |
There was a problem hiding this comment.
astral-sh/setup-uv@c771a70 # v9.0.0
ようなコミットハッシュを使われてはいかがでしょうか?
(actions などの他の部分も同様です)
理由: セキュリティ対策(改ざん防止、サプライチェーン攻撃対策)のため
|
|
||
| on: | ||
| schedule: | ||
| - cron: '50 13 * * 6' |
There was a problem hiding this comment.
Problems API からコンテスト情報を取得する場合、コンテスト終了後10分では反映されていない可能性が高いかと思います
けんちょんさん、ウルズニャーさんとの議論から、日曜日に投稿できる状態であれば十分そうです
riverさんのお考えはいかがでしょうか?
- cron: '50 8 * * 0'
timezone: 'Asia/Tokyo'
概要
ABC終了後、自動でDiscussionを作成する機能を実装。
Architecture
NoviSteps-Writeups リポジトリ単独で完結する GitHub Actions (cron) から Python スクリプトを実行する。AtCoder の問題ページを直接スクレイピングして問題一覧を取得し、GitHub GraphQL API (
createDiscussion) で Discussion を作成する。