sync leetcode #124
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: sync leetcode | |
| # Pulls accepted submissions from LeetCode into this repo, then rebuilds the index. | |
| # | |
| # Needs two repository secrets, both read from leetcode.com cookies: | |
| # LEETCODE_SESSION, LEETCODE_CSRF_TOKEN | |
| # Without them every run exits early instead of failing, so an unconfigured repo | |
| # does not mail a failure notice every few hours. | |
| on: | |
| schedule: | |
| # Every 2 hours, plus one at 21:30 KST so the evening's work is in the repo | |
| # before the 22:00 output check looks for it. | |
| # | |
| # Was every 6 hours until 2026-08-31. At that spacing a problem solved in the | |
| # morning stayed missing from the repo until the afternoon, which reads as | |
| # "the sync is broken" when it is only waiting. Two hours keeps the wait short; | |
| # the job is a no-op when there is nothing new, and Actions minutes are free on | |
| # a public repo. | |
| - cron: "0 */2 * * *" | |
| - cron: "30 12 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: sync-leetcode | |
| cancel-in-progress: false | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check whether LeetCode credentials are configured | |
| id: guard | |
| env: | |
| SESSION: ${{ secrets.LEETCODE_SESSION }} | |
| CSRF: ${{ secrets.LEETCODE_CSRF_TOKEN }} | |
| run: | | |
| if [ -z "$SESSION" ] || [ -z "$CSRF" ]; then | |
| echo "ready=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::LEETCODE_SESSION / LEETCODE_CSRF_TOKEN are not set — nothing to sync." | |
| exit 0 | |
| fi | |
| echo "ready=true" >> "$GITHUB_OUTPUT" | |
| # Lengths only, never the values. A csrftoken as long as the session cookie | |
| # means the wrong thing was pasted into it -- a mix-up the length floor below | |
| # cannot catch, and the one that produces an otherwise inexplicable rejection. | |
| echo "::notice::LEETCODE_SESSION is ${#SESSION} chars, LEETCODE_CSRF_TOKEN is ${#CSRF} chars." | |
| if [ "${#CSRF}" -gt 200 ]; then | |
| echo "::warning::LEETCODE_CSRF_TOKEN is ${#CSRF} chars, far longer than the 32-64 that cookie normally is. The session cookie was probably pasted into it by mistake." | |
| fi | |
| # A malformed paste fails downstream as an opaque | |
| # "submissions is not iterable" TypeError, so say something useful here | |
| # instead. Lengths only -- the values themselves are never printed. | |
| # 300 rather than something lenient: the DevTools cookie grid shows a | |
| # shortened value, and copying that cell yields a plausible-looking | |
| # hundred-odd characters that pass any looser floor and then get rejected. | |
| if [ "${#SESSION}" -lt 300 ]; then | |
| echo "::warning::LEETCODE_SESSION is only ${#SESSION} characters. The real cookie is 600+. The DevTools cookie table truncates what it displays — right-click the LEETCODE_SESSION row and use Copy value, or copy from the detail pane below the table, rather than selecting the cell." | |
| fi | |
| if [ "${#CSRF}" -lt 20 ]; then | |
| echo "::warning::LEETCODE_CSRF_TOKEN is only ${#CSRF} characters, which looks truncated. The csrftoken cookie is normally 32-64 characters." | |
| fi | |
| - name: Sync accepted submissions | |
| if: steps.guard.outputs.ready == 'true' | |
| uses: joshcai/leetcode-sync@v1.7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| leetcode-csrf-token: ${{ secrets.LEETCODE_CSRF_TOKEN }} | |
| leetcode-session: ${{ secrets.LEETCODE_SESSION }} | |
| commit-header: "solve" | |
| # The action throws a bare "submissions is not iterable" when LeetCode returns | |
| # a 200 with no submission list, which it does for a rejected session. Upstream | |
| # has this open as joshcai/leetcode-sync#72; until it lands, translate it here. | |
| - name: Explain a credential failure | |
| if: failure() && steps.guard.outputs.ready == 'true' | |
| run: | | |
| echo "::error::Sync failed. If the log shows 'submissions is not iterable', LeetCode" | |
| echo "::error::rejected the session — the cookies are wrong or have expired." | |
| echo "::error::Re-copy them from leetcode.com (DevTools > Application > Cookies) and run:" | |
| echo "::error:: pbpaste | tr -d '[:space:]' | gh secret set LEETCODE_SESSION -R ${{ github.repository }}" | |
| echo "::error:: pbpaste | tr -d '[:space:]' | gh secret set LEETCODE_CSRF_TOKEN -R ${{ github.repository }}" | |
| # The sync commits through the GitHub API, so this checkout is behind once it | |
| # finishes -- rebuilding without pulling would write the index from stale state. | |
| # A push made with GITHUB_TOKEN also does not trigger other workflows, which is | |
| # why the index is rebuilt here rather than left to the `rebuild index` workflow. | |
| - name: Rebuild the index | |
| if: steps.guard.outputs.ready == 'true' | |
| run: | | |
| git pull --ff-only origin main | |
| python3 scripts/build_readme.py | |
| - name: Commit the index | |
| if: steps.guard.outputs.ready == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add README.md stats.json | |
| if git diff --cached --quiet; then | |
| echo "index already current" | |
| else | |
| git commit -m "index: rebuild after sync" | |
| git push | |
| fi |