From 2a205fd10af2adddfaba7e83bc585674f9048320 Mon Sep 17 00:00:00 2001 From: ravibrock <66334356+ravibrock@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:39:39 -0400 Subject: [PATCH 1/3] Add verbose and commit-header functionality --- README.md | 12 ++++-- action.yml | 13 ++++++- index.js | 6 ++- src/action.js | 94 +++++++++++++++++++++++++++++++++++++++------- src/test_config.js | 11 +++--- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b43b539d..1f57027f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ GitHub Action for automatically syncing LeetCode submissions to a GitHub reposit leetcode-csrf-token: ${{ secrets.LEETCODE_CSRF_TOKEN }} leetcode-session: ${{ secrets.LEETCODE_SESSION }} destination-folder: my-folder + verbose: true + commit-header: '[LeetCode Sync]' ``` 6. After you've submitted a LeetCode solution, run the workflow by going to the `Actions` tab, clicking the action name, e.g. `Sync Leetcode`, and then clicking `Run workflow`. The workflow will also automatically run once a week by default (can be configured via the `cron` parameter). @@ -64,17 +66,19 @@ GitHub Action for automatically syncing LeetCode submissions to a GitHub reposit - `leetcode-csrf-token` _(required)_: The LeetCode CSRF token for retrieving submissions from LeetCode - `leetcode-session` _(required)_: The LeetCode session value for retrieving submissions from LeetCode - `filter-duplicate-secs`: Number of seconds after an accepted solution to ignore other accepted solutions for the same problem, default: 86400 (1 day) -- `destination-folder` _(optional)_: The folder in your repo to save the submissions to (necessary for shared repos) +- `destination-folder` _(optional)_: The folder in your repo to save the submissions to (necessary for shared repos), default: _none_ +- `verbose` _(optional)_: Requires an additional API call but adds submission percentiles and question numbers to the repo, default: true +- `commit-header` _(optional)_: How the automated commits should be prefixed, default: '[LeetCode Sync]' ## Shared Repos -A single repo can be shared by multiple users by using the `destination-folder` input field to sync each user's files to a separate folder. This is useful for users who want to add a more social, collaborative, or competitive aspect to their LeetCode sync repo. +Problems can be routed to a specific folder within a single repo using the `destination-folder` input field. This is useful for users who want to share a repo to add a more social, collaborative, or competitive aspect to their LeetCode sync repo. ## Contributing #### Testing locally -If you want to test changes to the action locally without having to commit and run the workflow on GitHub, you can edit `src/test_config.js` to have the required config values and then run: +If you want to test changes to the action locally without having to commit and run the workflow on GitHub, you can edit `src/test_config.js` to have the required config values and then run: `$ node index.js test` @@ -82,7 +86,7 @@ If you're using Replit, you can also just use the `Run` button, which is already #### Adding a new workflow parameter -If you add a workflow parameter, please make sure to also add it in `src/test_config.js`, so that it can be tested locally. +If you add a workflow parameter, please make sure to also add it in `src/test_config.js`, so that it can be tested locally. You will need to manually run: diff --git a/action.yml b/action.yml index 01625c86..18d47200 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ name: 'LeetCode Sync' description: 'Sync LeetCode submissions to GitHub' -branding: +branding: icon: git-commit color: yellow inputs: @@ -20,6 +20,15 @@ inputs: destination-folder: description: 'The folder to save the synced files in (relative to the top level of your repo)' required: false + default: null + verbose: + description: 'Requires an additional API call but adds submission percentiles and question numbers to the repo' + required: false + default: true + commit-header: + description: 'How the automated commits should be prefixed' + required: false + default: '[LeetCode Sync]' runs: using: 'node16' - main: 'index.js' \ No newline at end of file + main: 'index.js' diff --git a/index.js b/index.js index b9d334fe..3b53f6af 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,8 @@ async function main() { leetcodeSession = config.LEETCODE_SESSION; filterDuplicateSecs = config.FILTER_DUPLICATE_SECS; destinationFolder = config.DESTINATION_FOLDER; + verbose = config.VERBOSE; + commitHeader = config.COMMIT_HEADER; } else { githubToken = core.getInput('github-token'); owner = context.repo.owner; @@ -26,9 +28,11 @@ async function main() { leetcodeSession = core.getInput('leetcode-session'); filterDuplicateSecs = core.getInput('filter-duplicate-secs'); destinationFolder = core.getInput('destination-folder'); + verbose = core.getInput('verbose'); + commitHeader = core.getInput('commit-header'); } - await action.sync({ githubToken, owner, repo, filterDuplicateSecs, leetcodeCSRFToken, leetcodeSession, destinationFolder }); + await action.sync({ githubToken, owner, repo, leetcodeCSRFToken, leetcodeSession, filterDuplicateSecs, destinationFolder, verbose, commitHeader }); } main().catch((error) => { diff --git a/src/action.js b/src/action.js index 51823355..0ef24bd8 100644 --- a/src/action.js +++ b/src/action.js @@ -1,7 +1,7 @@ const axios = require('axios'); const { Octokit } = require('@octokit/rest'); -const COMMIT_MESSAGE = 'Sync LeetCode submission'; +const COMMIT_MESSAGE = '[LeetCode Sync]'; const LANG_TO_EXTENSION = { 'bash': 'sh', 'c': 'c', @@ -33,8 +33,60 @@ function log(message) { console.log(`[${new Date().toUTCString()}] ${message}`); } +function pad(n) { + var s = '000' + n; + return s.substring(s.length-4); +} + function normalizeName(problemName) { - return problemName.toLowerCase().replace(/\s/g, '_'); + return problemName.toLowerCase().replace(/\s/g, '-'); +} + +async function getInfo(submission, session, csrf) { + let data = JSON.stringify({ + query: `query submissionDetails($submissionId: Int!) { + submissionDetails(submissionId: $submissionId) { + runtimePercentile + memoryPercentile + question { + questionId + } + } + }`, + variables: {'submissionId':submission.id} + }); + + let config = { + maxBodyLength: Infinity, + headers: { + 'Content-Type': 'application/json', + 'Cookie': `LEETCODE_SESSION=${session};csrftoken=${csrf};`, + }, + data : data + }; + + // No need to break on first request error since that would be done when getting submissions + const getInfo = async (maxRetries = 5, retryCount = 0) => { + try { + const response = await axios.post('https://leetcode.com/graphql/', data, config); + const runtimePercentile = `${response.data.data.submissionDetails.runtimePercentile.toFixed(2)}%`; + const memoryPercentile = `${response.data.data.submissionDetails.memoryPercentile.toFixed(2)}%`; + const questionId = pad(response.data.data.submissionDetails.question.questionId.toString()); + + log(`Got info for submission #${submission.id}`); + return { runtimeperc: runtimePercentile, memoryperc: memoryPercentile, qid: questionId }; + } catch (exception) { + if (retryCount >= maxRetries) { + throw exception; + } + log('Error fetching submission info, retrying in ' + 3 ** retryCount + ' seconds...'); + await delay(3 ** retryCount * 1000); + return getInfo(maxRetries, retryCount + 1); + } + }; + + info = await getInfo(); + return {...submission, ...info}; } async function commit(params) { @@ -47,7 +99,8 @@ async function commit(params) { treeSHA, latestCommitSHA, submission, - destinationFolder + destinationFolder, + commitHeader } = params; const name = normalizeName(submission.title); @@ -58,13 +111,23 @@ async function commit(params) { } const prefix = !!destinationFolder ? `${destinationFolder}/` : ''; - const path = `${prefix}problems/${name}/solution.${LANG_TO_EXTENSION[submission.lang]}` + const commitName = !!commitHeader ? commitHeader : COMMIT_MESSAGE; + + if ('runtimeperc' in submission) { + message = `${commitName} Runtime - ${submission.runtime} (${submission.runtimeperc}), Memory - ${submission.memory} (${submission.memoryperc})`; + qid = `${submission.qid}-`; + } else { + message = `${commitName} Runtime - ${submission.runtime}, Memory - ${submission.memory}`; + qid = ''; + } + + const path = `${prefix}${qid}${name}/solution.${LANG_TO_EXTENSION[submission.lang]}` const treeData = [ { path, mode: '100644', - content: submission.code, + content: `${submission.code}\n`, // Adds newline at EOF to conform to git recommendations } ]; @@ -79,7 +142,7 @@ async function commit(params) { const commitResponse = await octokit.git.createCommit({ owner: owner, repo: repo, - message: `${COMMIT_MESSAGE} - ${submission.title} (${submission.lang})`, + message: message, tree: treeResponse.data.sha, parents: [latestCommitSHA], author: { @@ -144,10 +207,12 @@ async function sync(inputs) { githubToken, owner, repo, - filterDuplicateSecs, leetcodeCSRFToken, leetcodeSession, - destinationFolder + filterDuplicateSecs, + destinationFolder, + verbose, + commitHeader } = inputs; const octokit = new Octokit({ @@ -162,7 +227,7 @@ async function sync(inputs) { }); let lastTimestamp = 0; - // commitInfo is used to get the original name / email to use for the author / committer. + // commitInfo is used to get the original name / email to use for the author / committer. // Since we need to modify the commit time, we can't use the default settings for the // authenticated user. let commitInfo = commits.data[commits.data.length - 1].commit.author; @@ -205,7 +270,7 @@ async function sync(inputs) { throw exception; } log('Error fetching submissions, retrying in ' + 3 ** retryCount + ' seconds...'); - // There's a rate limit on LeetCode API, so wait with backoff before retrying. + // There's a rate limit on LeetCode API, so wait with backoff before retrying. await delay(3 ** retryCount * 1000); return getSubmissions(maxRetries, retryCount + 1); } @@ -214,7 +279,7 @@ async function sync(inputs) { // the tokens are configured incorrectly. const maxRetries = (response === null) ? 0 : 5; if (response !== null) { - // Add a 1 second delay before all requests after the initial request. + // Add a 1 second delay before all requests after the initial request. await delay(1000); } response = await getSubmissions(maxRetries); @@ -239,8 +304,11 @@ async function sync(inputs) { let latestCommitSHA = commits.data[0].sha; let treeSHA = commits.data[0].commit.tree.sha; for (i = submissions.length - 1; i >= 0; i--) { - submission = submissions[i]; - [treeSHA, latestCommitSHA] = await commit({ octokit, owner, repo, defaultBranch, commitInfo, treeSHA, latestCommitSHA, submission, destinationFolder }); + let submission = submissions[i]; + if (verbose != 'false') { + submission = await getInfo(submission, leetcodeSession, leetcodeCSRFToken); + } + [treeSHA, latestCommitSHA] = await commit({ octokit, owner, repo, defaultBranch, commitInfo, treeSHA, latestCommitSHA, submission, destinationFolder, commitHeader }) } log('Done syncing all submissions.'); } diff --git a/src/test_config.js b/src/test_config.js index e507d73b..b9ad8709 100644 --- a/src/test_config.js +++ b/src/test_config.js @@ -1,14 +1,15 @@ // Modify this file to run index.js locally and not as a GitHub Action. module.exports = { + // These parameters are required. GITHUB_TOKEN: null, - // Form of "/" - GITHUB_REPO: null, - + GITHUB_REPO: null, // Form of '/' LEETCODE_CSRF_TOKEN: null, LEETCODE_SESSION: null, - // These parameters are optional and have default values. + // These parameters are optional and have default values if needed. FILTER_DUPLICATE_SECS: 86400, DESTINATION_FOLDER: null, -} \ No newline at end of file + VERBOSE: true, + COMMIT_HEADER: '[LeetCode Sync]' +} From 263775964568d614c57ffcdba6fc22313c4d9f74 Mon Sep 17 00:00:00 2001 From: Ravi Brock <66334356+ravibrock@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:28:08 -0400 Subject: [PATCH 2/3] Update test values to match live values --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 3b53f6af..c9f47a3e 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ async function main() { leetcodeSession = config.LEETCODE_SESSION; filterDuplicateSecs = config.FILTER_DUPLICATE_SECS; destinationFolder = config.DESTINATION_FOLDER; - verbose = config.VERBOSE; + verbose = config.VERBOSE.toString(); // Convert to string to match core.getInput('verbose') return type commitHeader = config.COMMIT_HEADER; } else { githubToken = core.getInput('github-token'); From 23719bcf3749ccdb3946d722a8981e8961087268 Mon Sep 17 00:00:00 2001 From: ravibrock <66334356+ravibrock@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:38:31 -0400 Subject: [PATCH 3/3] Address nits --- README.md | 4 ++-- action.yml | 4 ++-- src/action.js | 11 +++++++---- src/test_config.js | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1f57027f..9588bf0c 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ GitHub Action for automatically syncing LeetCode submissions to a GitHub reposit - `leetcode-session` _(required)_: The LeetCode session value for retrieving submissions from LeetCode - `filter-duplicate-secs`: Number of seconds after an accepted solution to ignore other accepted solutions for the same problem, default: 86400 (1 day) - `destination-folder` _(optional)_: The folder in your repo to save the submissions to (necessary for shared repos), default: _none_ -- `verbose` _(optional)_: Requires an additional API call but adds submission percentiles and question numbers to the repo, default: true -- `commit-header` _(optional)_: How the automated commits should be prefixed, default: '[LeetCode Sync]' +- `verbose` _(optional)_: Adds submission percentiles and question numbers to the repo (requires an additional API call), default: true +- `commit-header` _(optional)_: How the automated commits should be prefixed, default: 'Sync LeetCode submission' ## Shared Repos diff --git a/action.yml b/action.yml index 18d47200..ffece640 100644 --- a/action.yml +++ b/action.yml @@ -22,13 +22,13 @@ inputs: required: false default: null verbose: - description: 'Requires an additional API call but adds submission percentiles and question numbers to the repo' + description: 'Adds submission percentiles and question numbers to the repo (requires an additional API call)' required: false default: true commit-header: description: 'How the automated commits should be prefixed' required: false - default: '[LeetCode Sync]' + default: 'Sync LeetCode submission' runs: using: 'node16' main: 'index.js' diff --git a/src/action.js b/src/action.js index fc183c5b..523aff9f 100644 --- a/src/action.js +++ b/src/action.js @@ -1,7 +1,7 @@ const axios = require('axios'); const { Octokit } = require('@octokit/rest'); -const COMMIT_MESSAGE = '[LeetCode Sync]'; +const COMMIT_MESSAGE = 'Sync LeetCode submission'; const LANG_TO_EXTENSION = { 'bash': 'sh', 'c': 'c', @@ -34,6 +34,9 @@ function log(message) { } function pad(n) { + if (n.length > 4) { + return n; + } var s = '000' + n; return s.substring(s.length-4); } @@ -74,7 +77,7 @@ async function getInfo(submission, session, csrf) { const questionId = pad(response.data.data.submissionDetails.question.questionId.toString()); log(`Got info for submission #${submission.id}`); - return { runtimeperc: runtimePercentile, memoryperc: memoryPercentile, qid: questionId }; + return { runtimePerc: runtimePercentile, memoryPerc: memoryPercentile, qid: questionId }; } catch (exception) { if (retryCount >= maxRetries) { throw exception; @@ -114,8 +117,8 @@ async function commit(params) { const prefix = !!destinationFolder ? `${destinationFolder}/` : ''; const commitName = !!commitHeader ? commitHeader : COMMIT_MESSAGE; - if ('runtimeperc' in submission) { - message = `${commitName} Runtime - ${submission.runtime} (${submission.runtimeperc}), Memory - ${submission.memory} (${submission.memoryperc})`; + if ('runtimePerc' in submission) { + message = `${commitName} Runtime - ${submission.runtime} (${submission.runtimePerc}), Memory - ${submission.memory} (${submission.memoryPerc})`; qid = `${submission.qid}-`; } else { message = `${commitName} Runtime - ${submission.runtime}, Memory - ${submission.memory}`; diff --git a/src/test_config.js b/src/test_config.js index b9ad8709..5d9180dd 100644 --- a/src/test_config.js +++ b/src/test_config.js @@ -11,5 +11,5 @@ module.exports = { FILTER_DUPLICATE_SECS: 86400, DESTINATION_FOLDER: null, VERBOSE: true, - COMMIT_HEADER: '[LeetCode Sync]' + COMMIT_HEADER: 'Sync LeetCode submission' }