Skip to content

Add verbose and commit-header functionality #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -64,25 +66,27 @@ 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)_: 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

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`

If you're using Replit, you can also just use the `Run` button, which is already configured to the above command.

#### 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:

Expand Down
13 changes: 11 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: 'LeetCode Sync'
description: 'Sync LeetCode submissions to GitHub'
branding:
branding:
icon: git-commit
color: yellow
inputs:
Expand All @@ -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: '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: 'Sync LeetCode submission'
runs:
using: 'node16'
main: 'index.js'
main: 'index.js'
6 changes: 5 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ async function main() {
leetcodeSession = config.LEETCODE_SESSION;
filterDuplicateSecs = config.FILTER_DUPLICATE_SECS;
destinationFolder = config.DESTINATION_FOLDER;
verbose = config.VERBOSE.toString(); // Convert to string to match core.getInput('verbose') return type
commitHeader = config.COMMIT_HEADER;
} else {
githubToken = core.getInput('github-token');
owner = context.repo.owner;
Expand All @@ -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) => {
Expand Down
101 changes: 85 additions & 16 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,63 @@ function log(message) {
console.log(`[${new Date().toUTCString()}] ${message}`);
}

function pad(n) {
if (n.length > 4) {
return 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) {
Expand All @@ -48,7 +103,8 @@ async function commit(params) {
latestCommitSHA,
submission,
destinationFolder,
question_data
commitHeader,
questionData
} = params;

const name = normalizeName(submission.title);
Expand All @@ -59,21 +115,29 @@ async function commit(params) {
}

const prefix = !!destinationFolder ? `${destinationFolder}/` : '';
const questionPath = `${prefix}problems/${name}/question.md`; // Markdown file for the problem with question data
const solutionPath = `${prefix}problems/${name}/solution.${LANG_TO_EXTENSION[submission.lang]}`; // Separate file for the solution
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 questionPath = `${prefix}${qid}${name}/README.md`; // Markdown file for the problem with question data
const solutionPath = `${prefix}${qid}${name}/solution.${LANG_TO_EXTENSION[submission.lang]}`; // Separate file for the solution

const treeData = [
{
path: questionPath,
mode: '100644',
content: question_data,
content: questionData,
},
{
path: solutionPath,
mode: '100644',
content: submission.code,
},
content: `${submission.code}\n`, // Adds newline at EOF to conform to git recommendations
}
];

const treeResponse = await octokit.git.createTree({
Expand All @@ -87,7 +151,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: {
Expand Down Expand Up @@ -179,10 +243,12 @@ async function sync(inputs) {
githubToken,
owner,
repo,
filterDuplicateSecs,
leetcodeCSRFToken,
leetcodeSession,
destinationFolder
filterDuplicateSecs,
destinationFolder,
verbose,
commitHeader
} = inputs;

const octokit = new Octokit({
Expand All @@ -197,7 +263,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;
Expand Down Expand Up @@ -240,7 +306,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);
}
Expand All @@ -249,7 +315,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);
Expand All @@ -275,10 +341,13 @@ async function sync(inputs) {
let treeSHA = commits.data[0].commit.tree.sha;
for (i = submissions.length - 1; i >= 0; i--) {
submission = submissions[i];

if (verbose != 'false') {
submission = await getInfo(submission, leetcodeSession, leetcodeCSRFToken);
}

// Get the question data for the submission.
const question_data = await getQuestionData(submission.title_slug, leetcodeSession);
[treeSHA, latestCommitSHA] = await commit({ octokit, owner, repo, defaultBranch, commitInfo, treeSHA, latestCommitSHA, submission, destinationFolder, question_data });
const questionData = await getQuestionData(submission.title_slug, leetcodeSession);
[treeSHA, latestCommitSHA] = await commit({ octokit, owner, repo, defaultBranch, commitInfo, treeSHA, latestCommitSHA, submission, destinationFolder, commitHeader, questionData });
}
log('Done syncing all submissions.');
}
Expand Down
11 changes: 6 additions & 5 deletions src/test_config.js
Original file line number Diff line number Diff line change
@@ -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 "<owner>/<repo_name>"
GITHUB_REPO: null,

GITHUB_REPO: null, // Form of '<owner>/<repo_name>'
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,
}
VERBOSE: true,
COMMIT_HEADER: 'Sync LeetCode submission'
}