├── .env.example ├── .github ├── csm-logo.png ├── csm-logo-256.png ├── csm-logo-560.png ├── csm-logo-1280-640.png ├── csm-logo-560-transparent.png ├── dependabot.yml └── workflows │ ├── test.yml │ └── release.yml ├── src ├── utils │ ├── logger.ts │ ├── fileUtils.ts │ ├── dateUtils.ts │ └── gitUtils.ts ├── github │ ├── githubTypes.ts │ └── githubApi.ts └── main.ts ├── tsconfig.json ├── __tests__ ├── utils │ ├── logger.test.ts │ └── fileUtils.test.ts └── github │ └── githubApi.test.ts ├── LICENSE ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── LEARN.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | DEVFUL_GITHUB_REPO=/ 2 | DEVFUL_GITHUB_TOKEN= 3 | -------------------------------------------------------------------------------- /.github/csm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devful/contribution-streak-maintainer/HEAD/.github/csm-logo.png -------------------------------------------------------------------------------- /.github/csm-logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devful/contribution-streak-maintainer/HEAD/.github/csm-logo-256.png -------------------------------------------------------------------------------- /.github/csm-logo-560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devful/contribution-streak-maintainer/HEAD/.github/csm-logo-560.png -------------------------------------------------------------------------------- /.github/csm-logo-1280-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devful/contribution-streak-maintainer/HEAD/.github/csm-logo-1280-640.png -------------------------------------------------------------------------------- /.github/csm-logo-560-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devful/contribution-streak-maintainer/HEAD/.github/csm-logo-560-transparent.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export function logContributionsToday(count: number): void { 2 | if (count > 0) { 3 | console.log(`Current contributions today: ${count}`); 4 | } else { 5 | console.log("No contributions today."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from "fs"; 2 | 3 | export function createFileIfNotExists(filename: string): void { 4 | if (!existsSync(filename)) { 5 | const header = "# Automated Contributions\n\n"; 6 | writeFileSync(filename, header); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/github/githubTypes.ts: -------------------------------------------------------------------------------- 1 | export interface ContributionResponse { 2 | user: { 3 | contributionsCollection: { 4 | contributionCalendar: { 5 | totalContributions: number; 6 | weeks: { 7 | contributionDays: { 8 | date: string; 9 | contributionCount: number; 10 | }[]; 11 | }[]; 12 | }; 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | export function getFormattedDate(date: Date): string { 2 | // US English uses month-day-year order 3 | return new Intl.DateTimeFormat("en-US", { 4 | weekday: "long", 5 | month: "long", 6 | day: "numeric", 7 | hour: "numeric", 8 | minute: "numeric", 9 | second: "numeric", 10 | timeZoneName: "long", 11 | year: "numeric", 12 | }).format(date); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "baseUrl": "./", 11 | "paths": { 12 | "*": ["src/*"] 13 | }, 14 | "outDir": "./dist", 15 | "rootDir": "./src", 16 | "sourceMap": true, 17 | "types": ["node", "jest"] 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/utils/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { logContributionsToday } from "../../src/utils/logger"; 2 | 3 | describe("Logger Tests", () => { 4 | it("should log contributions today", () => { 5 | // Mock the console.log method to check if it is called 6 | const consoleLogSpy = jest 7 | .spyOn(console, "log") 8 | .mockImplementation(() => {}); 9 | 10 | logContributionsToday(5); 11 | 12 | expect(consoleLogSpy).toHaveBeenCalledWith( 13 | "Current contributions today: 5" 14 | ); 15 | 16 | consoleLogSpy.mockRestore(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/utils/fileUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { createFileIfNotExists } from "../../src/utils/fileUtils"; 2 | import { existsSync, readFileSync, unlinkSync } from "fs"; 3 | 4 | describe("File Utils Tests", () => { 5 | const filename = "testFile.txt"; 6 | 7 | afterEach(() => { 8 | // Clean up the created file after each test 9 | if (existsSync(filename)) { 10 | unlinkSync(filename); 11 | } 12 | }); 13 | 14 | it("should create a file if it does not exist", () => { 15 | createFileIfNotExists(filename); 16 | 17 | expect(existsSync(filename)).toBe(true); 18 | 19 | const fileContent = readFileSync(filename, "utf-8"); 20 | 21 | expect(fileContent).toBe("# Automated Contributions\n\n"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20.x 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Run unit tests 32 | run: npm run test 33 | env: 34 | DEVFUL_GITHUB_TOKEN: ${{ secrets.DEVFUL_GITHUB_TOKEN }} 35 | 36 | - name: Upload coverage reports to Codecov 37 | uses: codecov/codecov-action@v3 38 | env: 39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 40 | -------------------------------------------------------------------------------- /__tests__/github/githubApi.test.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | import * as githubApi from "../../src/github/githubApi"; 3 | import { retry } from "@octokit/plugin-retry"; 4 | import { throttling } from "@octokit/plugin-throttling"; 5 | import "dotenv/config"; 6 | 7 | const githubToken = process.env.DEVFUL_GITHUB_TOKEN || ""; 8 | 9 | const MyOctokit = Octokit.plugin(retry, throttling); 10 | 11 | const octokit = new MyOctokit({ 12 | auth: githubToken, 13 | }); 14 | 15 | describe("GitHub API Tests", () => { 16 | it("should get contributions", async () => { 17 | // Call the actual getContributions function 18 | const contributions = await githubApi.getContributions( 19 | "eliasafara", 20 | octokit 21 | ); 22 | 23 | // Assert that contributions is a number and greater than or equal to 0 24 | expect(typeof contributions).toBe("number"); 25 | expect(contributions).toBeGreaterThanOrEqual(0); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | 12 | concurrency: release 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: main 22 | 23 | - name: Configure git 24 | run: | 25 | git config user.name "${GITHUB_ACTOR}" 26 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 27 | 28 | - name: Bump version 29 | run: | 30 | npm version patch 31 | git push 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Build 37 | run: npm run build 38 | 39 | - name: Release 40 | run: | 41 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 42 | npm publish 43 | env: 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | DEVFUL_GITHUB_TOKEN: ${{ secrets.DEVFUL_GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Elias Afara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/github/githubApi.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | import type { ContributionResponse } from "./githubTypes"; 3 | 4 | export async function getContributions( 5 | username: string, 6 | octokit: Octokit 7 | ): Promise { 8 | validateUsername(username); 9 | 10 | const contributionsQuery = buildContributionsQuery(username); 11 | 12 | const response = (await octokit.graphql( 13 | contributionsQuery 14 | )) as ContributionResponse; 15 | 16 | const todayContributions = findTodayContributions(response); 17 | 18 | return todayContributions?.contributionCount || 0; 19 | } 20 | 21 | function validateUsername(username: string): void { 22 | if (!username) { 23 | throw new Error("Specify username"); 24 | } 25 | } 26 | 27 | function buildContributionsQuery(username: string): string { 28 | return `query { 29 | user(login: "${username}") { 30 | contributionsCollection { 31 | contributionCalendar { 32 | totalContributions 33 | weeks { 34 | contributionDays { 35 | date 36 | contributionCount 37 | } 38 | } 39 | } 40 | } 41 | } 42 | }`; 43 | } 44 | 45 | function findTodayContributions(response: ContributionResponse): 46 | | { 47 | date: string; 48 | contributionCount: number; 49 | } 50 | | undefined { 51 | const today = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format 52 | 53 | // Find today's contributions 54 | return response.user.contributionsCollection.contributionCalendar.weeks 55 | .flatMap((week) => week.contributionDays) 56 | .find((day) => day.date.split("T")[0] === today); 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | 3 | # Dependency directory 4 | node_modules 5 | 6 | # Ignore repositories ending with "-clone" 7 | *-clone/ 8 | 9 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | jspm_packages/ 51 | 52 | # TypeScript v1 declaration files 53 | typings/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # next.js build output 81 | .next 82 | 83 | # nuxt.js build output 84 | .nuxt 85 | 86 | # vuepress build output 87 | .vuepress/dist 88 | 89 | # Serverless directories 90 | .serverless/ 91 | 92 | # FuseBox cache 93 | .fusebox/ 94 | 95 | # DynamoDB Local files 96 | .dynamodb/ 97 | 98 | # OS metadata 99 | .DS_Store 100 | Thumbs.db 101 | 102 | # Ignore built ts files 103 | __tests__/runner/* 104 | 105 | # IDE files 106 | .idea 107 | .vscode 108 | *.code-workspace 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contribution-streak-maintainer", 3 | "description": "Automatically maintain your GitHub contribution streak", 4 | "version": "1.0.22", 5 | "author": "Elias Afara ", 6 | "type": "commonjs", 7 | "bin": { 8 | "contribution-streak-maintainer": "dist/main.js" 9 | }, 10 | "scripts": { 11 | "start": "tsc --watch", 12 | "tsc": "tsc", 13 | "build": "npm run tsc", 14 | "ci-test": "jest", 15 | "test": "jest" 16 | }, 17 | "keywords": [ 18 | "GitHub Actions", 19 | "GitHub Workflow", 20 | "GitHub Contributions", 21 | "Contribution Streak", 22 | "Automation", 23 | "Node.js", 24 | "NPM Package", 25 | "Continuous Integration", 26 | "Developer Tools", 27 | "Daily Contributions", 28 | "Daily Commits", 29 | "Open Source", 30 | "TypeScript", 31 | "Jest" 32 | ], 33 | "files": [ 34 | "dist" 35 | ], 36 | "homepage": "https://github.com/devful/contribution-streak-maintainer", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/devful/contribution-streak-maintainer.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/devful/contribution-streak-maintainer/issues" 43 | }, 44 | "license": "MIT", 45 | "jest": { 46 | "preset": "ts-jest", 47 | "verbose": true, 48 | "clearMocks": true, 49 | "testEnvironment": "node", 50 | "testMatch": [ 51 | "**/*.test.ts" 52 | ], 53 | "testPathIgnorePatterns": [ 54 | "/node_modules/", 55 | "/dist/" 56 | ], 57 | "transform": { 58 | "^.+\\.ts$": "ts-jest" 59 | }, 60 | "coverageReporters": [ 61 | "json-summary", 62 | "text", 63 | "lcov" 64 | ], 65 | "collectCoverage": true, 66 | "collectCoverageFrom": [ 67 | "./src/**" 68 | ] 69 | }, 70 | "dependencies": { 71 | "@octokit/plugin-retry": "^6.0.1", 72 | "@octokit/plugin-throttling": "^8.1.2", 73 | "@types/fs-extra": "^11.0.4", 74 | "dotenv": "^16.3.1", 75 | "fs-extra": "^11.1.1", 76 | "minimist": "^1.2.8", 77 | "octokit": "^3.1.1" 78 | }, 79 | "devDependencies": { 80 | "@octokit/graphql-schema": "^14.41.0", 81 | "@types/jest": "^29.5.8", 82 | "@types/minimist": "^1.2.5", 83 | "@types/node": "^20.9.0", 84 | "jest": "^29.7.0", 85 | "ts-jest": "^29.1.1", 86 | "ts-node": "^10.9.1", 87 | "typescript": "^5.2.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

Contributing

2 | 3 | Thank you for considering and taking the time to contribute! 4 | The following are the guidelines for contributing to this project: 5 | 6 | ## How to Report Bugs 7 | 8 | Please open a new issue with steps to reproduce the problem you're experiencing. 9 | Provide as much information as possible, including screenshots, text output, and both your expected and actual results. 10 | 11 | ## How to Request Enhancements 12 | 13 | First, please refer to the [issues](https://github.com/devful/contribution-streak-maintainer/issues) section of this project and search [the repository's GitHub issues](https://github.com/devful/contribution-streak-maintainer/issues) to make sure that your idea has not been (or is not still) considered. 14 | Then, please [create a new issue in the GitHub repository](https://github.com/devful/contribution-streak-maintainer/issues/new/choose) describing the enhancement you would like to have. 15 | Be sure to include as much detail as possible including step-by-step descriptions, specific examples, screenshots or mockups, and providing a reason why the enhancement might be worthwhile. 16 | 17 | ## Quick guide of some common ways to contribute 18 | 19 | ### Issues 20 | 21 | [**Issues**](https://github.com/devful/contribution-streak-maintainer/issues) are very valuable to this project. Please check if any similar issues are already present. If not, then feel free to open that issue. 22 | - **Feature 💡 Requests** are valuable sources of enhancements which the project can make. 23 | - **Bug 🐞 Reports** show where this project is lacking and errors come up. 24 | - With a **question**, you show where contributors can improve the user experience. 25 | 26 | 30 | 31 | ### Pull Requests 32 | 33 | [**Pull requests**](https://github.com/devful/contribution-streak-maintainer/pulls) are a great way to get your ideas (through code changes) into this project. Please open an issue at first, describing the changes you want to make, then feel free to open a PR (Pull Request). 34 | 35 | #### Does it state intent? 36 | 37 | You should be _clear_ which problem you're trying to _solve or the feature you want to include_ with your contribution. 38 | 39 | For example: 40 | 41 | > Added some files 42 | 43 | Does not tell me anything about why you're doing that 44 | 45 | > Add files to make the app more user-friendly 46 | 47 | Tells me the problem that you have found, and the pull request shows me the action you have taken to solve it. 48 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import minimist from "minimist"; 4 | import { Octokit } from "octokit"; 5 | import { retry } from "@octokit/plugin-retry"; 6 | import { throttling } from "@octokit/plugin-throttling"; 7 | import * as gitUtils from "./utils/gitUtils"; 8 | import { getContributions } from "./github/githubApi"; 9 | import { logContributionsToday } from "./utils/logger"; 10 | import "dotenv/config"; 11 | 12 | export async function main() { 13 | try { 14 | const argv = minimist(process.argv.slice(2), { 15 | string: ["email", "token", "condition"], 16 | }); 17 | 18 | const [username, repository] = getUsername(argv); 19 | const email = argv.email; 20 | const token = getToken(argv); 21 | const condition = getCondition(argv); 22 | 23 | // Initialize Octokit 24 | const MyOctokit = Octokit.plugin(retry, throttling); 25 | const octokit = new MyOctokit({ 26 | auth: token, 27 | log: console, 28 | userAgent: "contribution-streak-maintainer", 29 | throttle: { 30 | onRateLimit: (retryAfter, options: any, octokit, retryCount) => { 31 | octokit.log.warn( 32 | `Request quota exhausted for request ${options.method} ${options.url}` 33 | ); 34 | if (retryCount <= 3) { 35 | octokit.log.info(`Retrying after ${retryAfter} seconds!`); 36 | return true; 37 | } 38 | }, 39 | onSecondaryRateLimit: (retryAfter, options: any, octokit) => { 40 | octokit.log.warn( 41 | `SecondaryRateLimit detected for request ${options.method} ${options.url}` 42 | ); 43 | }, 44 | }, 45 | retry: { doNotRetry: ["429"] }, 46 | }); 47 | 48 | const cloneDirectoryName = `${repository}-clone`; 49 | 50 | if (username && repository) { 51 | gitUtils.checkGitInstalled(); 52 | 53 | if (!email) { 54 | throw new Error( 55 | "Specify your primary email address so that GitHub can associate commits with your account and the commits can be counted towards your GitHub contribution graph. You can specify your email address using the --email= option" 56 | ); 57 | } 58 | 59 | gitUtils.gitClone(username, email, repository, token, cloneDirectoryName); 60 | 61 | const contributions = await getContributions(username, octokit); 62 | 63 | logContributionsToday(contributions); 64 | 65 | // Check if contributions meet the specified condition 66 | if (contributions <= condition) { 67 | await gitUtils.generateAndPushCommits(cloneDirectoryName); 68 | } 69 | } 70 | } catch (e) { 71 | console.error(e); 72 | process.exit(1); 73 | } 74 | } 75 | 76 | function getUsername( 77 | argv: minimist.ParsedArgs 78 | ): [string | undefined, string | undefined] { 79 | const repoString = argv._[0] || process.env.DEVFUL_GITHUB_REPO || ""; 80 | const [username, repo] = repoString.split("/", 2); 81 | return [username || undefined, repo || undefined]; 82 | } 83 | 84 | function getToken(argv: minimist.ParsedArgs): string { 85 | return argv.token || process.env.DEVFUL_GITHUB_TOKEN || ""; 86 | } 87 | 88 | function getCondition(argv: minimist.ParsedArgs): number { 89 | return +argv.condition || 0; 90 | } 91 | 92 | void main(); 93 | -------------------------------------------------------------------------------- /src/utils/gitUtils.ts: -------------------------------------------------------------------------------- 1 | import { chdir } from "node:process"; 2 | import { execSync, spawnSync } from "node:child_process"; 3 | import fse from "fs-extra"; 4 | 5 | import { getFormattedDate } from "./dateUtils"; 6 | import { createFileIfNotExists } from "./fileUtils"; 7 | 8 | /** 9 | * Executes a shell command and logs the details. Throws an error if the command fails. 10 | * @param command - The command to execute. 11 | * @param args - Arguments to pass to the command. 12 | */ 13 | export function exec(command: string, args: string[]): void { 14 | console.log(`Executing: ${command} ${args.join(" ")}`); 15 | const p = spawnSync(command, args, { stdio: "inherit" }); 16 | if (p.status !== 0) { 17 | console.error( 18 | `Command failed: ${command} ${args.join(" ")}\n${p.stderr.toString()}` 19 | ); 20 | throw new Error(`Command failed: ${command} ${args.join(" ")}`); 21 | } 22 | } 23 | 24 | /** 25 | * Checks if Git is installed by attempting to run "git --version". 26 | * @returns true if Git is installed, false otherwise. 27 | */ 28 | export function isGitInstalled(): boolean { 29 | try { 30 | exec("git", ["--version"]); 31 | return true; 32 | } catch (e) { 33 | return false; 34 | } 35 | } 36 | 37 | /** 38 | * Checks if Git is installed and exits the process with an error message if not. 39 | */ 40 | export function checkGitInstalled(): void { 41 | if (!isGitInstalled()) { 42 | console.error("Git is not installed. Please install Git."); 43 | throw new Error("Git is not installed. Please install Git."); 44 | } 45 | } 46 | 47 | /** 48 | * Clones a GitHub repository using the provided credentials and directory. 49 | * @param owner - GitHub repository owner. 50 | * @param repo - GitHub repository name. 51 | * @param token - GitHub personal access token. 52 | * @param directory - Directory to clone the repository into. 53 | */ 54 | export function gitClone( 55 | owner: string, 56 | email: string, 57 | repo: string, 58 | token: string, 59 | directory: string 60 | ): void { 61 | try { 62 | // Delete the existing directory if it exists 63 | if (fse.existsSync(directory)) { 64 | fse.removeSync(directory); 65 | console.log(`Deleted existing directory: ${directory}`); 66 | } 67 | 68 | console.log(`Cloning repository: ${owner}/${repo}`); 69 | // Clone the repository with depth 1 to minimize download size 70 | exec("git", [ 71 | "clone", 72 | "--depth=1", 73 | `https://${owner}:${token}@github.com/${owner}/${repo}.git`, 74 | directory, 75 | ]); 76 | 77 | chdir(directory); 78 | 79 | // Configure Git user information for the cloned repository 80 | exec("git", ["config", "user.name", `${owner}`]); 81 | exec("git", ["config", "user.email", `${email}`]); 82 | } finally { 83 | chdir(".."); 84 | } 85 | } 86 | 87 | export async function generateAndPushCommits(directory: string) { 88 | try { 89 | chdir(directory); 90 | const filename = "AUTOMATED_CONTRIBUTIONS.md"; 91 | createFileIfNotExists(filename); 92 | 93 | const randomCommitCount = Math.floor(Math.random() * 3) + 1; 94 | 95 | for (let i = 1; i <= randomCommitCount; i++) { 96 | const commitMessage = getCommitMessage(); 97 | 98 | // Append the commit message to AUTOMATED_CONTRIBUTIONS.md 99 | execSync(`echo "${commitMessage}\n" >> ${filename}`); 100 | 101 | console.log("Committing changes..."); 102 | // Stage, commit, and push changes to GitHub 103 | exec("git", ["add", "."]); 104 | exec("git", ["status"]); 105 | exec("git", ["commit", "-m", `"${commitMessage}"`]); 106 | } 107 | console.log("Pushing changes..."); 108 | exec("git", ["push"]); 109 | } finally { 110 | chdir(".."); 111 | } 112 | } 113 | 114 | function getCommitMessage(): string { 115 | const currentDate = new Date(); 116 | const formattedDate = getFormattedDate(currentDate); 117 | return `Commit: ${formattedDate}`; 118 | } 119 | -------------------------------------------------------------------------------- /LEARN.md: -------------------------------------------------------------------------------- 1 | # 📅 Contribution Streak Maintainer 2 | 3 | 4 | Contribution Streak Maintainer Logo 5 | 6 | 7 | [![npm](https://img.shields.io/npm/v/contribution-streak-maintainer)](https://www.npmjs.com/package/contribution-streak-maintainer) [![npm](https://img.shields.io/npm/dm/contribution-streak-maintainer)](https://www.npmjs.com/package/contribution-streak-maintainer) [![GitHub License](https://img.shields.io/github/license/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/blob/main/LICENSE) [![GitHub Repo stars](https://img.shields.io/github/stars/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/stargazers) 8 | 9 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/devful/contribution-streak-maintainer/test.yml)](https://github.com/devful/contribution-streak-maintainer/actions) [![GitHub issues](https://img.shields.io/github/issues/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/issues) [![GitHub pull requests](https://img.shields.io/github/issues-pr/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/pulls) [![GitHub contributors](https://img.shields.io/github/contributors/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/graphs/contributors) 10 | 11 | 12 | ![Test](https://github.com/devful/contribution-streak-maintainer/workflows/Test/badge.svg) [![GitHub last commit](https://img.shields.io/github/last-commit/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/commits/main) [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer) ![Codecov](https://img.shields.io/codecov/c/github/devful/contribution-streak-maintainer) 13 | 14 | - _Have you ever wanted to keep your contribution streak going but never been able to?_ 15 | - _Do you find it challenging to make manual contributions every single day?_ 16 | - _Worry no more!_ 17 | 18 | **Contribution Streak Maintainer** is a tool, available as an [npm package](https://www.npmjs.com/package/contribution-streak-maintainer), designed to be used inside a GitHub Actions workflow. This npm package is utilized within your workflow, automatically maintaining your GitHub contribution streak. The GitHub Actions workflow, configured through YAML files, generates and pushes commits to your repository, ensuring your activity stays active even on days when you're not able to make manual contributions. 19 | 20 | Now you can focus on your work, knowing that your GitHub contributions are being taken care of automatically. Keep your streak alive effortlessly by integrating the Contribution Streak Maintainer npm package into your GitHub Actions workflow! 21 | 22 | ## Installation 23 | 24 | Here is how to add my badges to your profile: 25 | 26 | 1. Star this repository. 27 | 2. Create a private repository `your-username/name-of-your-choice` 28 | 3. Add the following workflow to your repository at `.github/workflows/contribution-streak-maintainer.yml` 29 | 30 | ```yaml 31 | name: contribution-streak-maintainer 32 | 33 | on: 34 | workflow_dispatch: 35 | schedule: 36 | # For more information on the cron scheduler, 37 | # see https://crontab.guru/ or https://crontab.cronhub.io/. 38 | # This cron schedule means the action runs every day at midnight UTC. 39 | - cron: "0 22 * * *" 40 | 41 | permissions: 42 | contents: write 43 | 44 | jobs: 45 | contribution-streak-maintainer: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Make a contribution 49 | run: npx contribution-streak-maintainer ${{github.repository}} --email=primary-github-email@email.com 50 | env: 51 | DEVFUL_GITHUB_TOKEN: ${{ secrets.CSM_GITHUB_TOKEN }} 52 | ``` 53 | 54 | 4. Create your own personal access tokens (classic) 55 | 56 | 1. [Verify your email address](https://docs.github.com/en/get-started/signing-up-for-github/verifying-your-email-address), if it hasn't been verified yet. 57 | 2. In the upper-right corner of any page, click your profile photo, then click **Settings**. 58 | 3. In the left sidebar, click **Developer settings**. 59 | 4. In the left sidebar, under **Personal access tokens**, click **Tokens (classic)**. 60 | 5. Click **Generate new token** button then **Generate new token (classic)**. 61 | 6. Under **Note**, enter a name for the token. 62 | 7. Under **Expiration**, select **No expiration** for the token. 63 | 8. Under **Select scopes**, check **repo**, **read:user**, **user:email**. 64 | repo scope 65 | read scope 66 | 9. Click **Generate token**. 67 | 10. Note down the generated token. 68 | 69 | 5. Create a secret for your private repository 70 | - Check out the [docs](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) 71 | - In the **Name** field, type `CSM_GITHUB_TOKEN`. 72 | - In the **Secret** field, enter the newly generated token. 73 | 74 | > **_NOTE:_** Ensure that the GitHub token (`CSM_GITHUB_TOKEN`) is kept private and not shared publicly. 75 | 76 | ## Run Manually 77 | 78 | You can start **contribution-streak-maintainer** workflow manually, or wait for it to run automatically. 79 | 80 | Alternatively, you can perform these steps manually: 81 | 82 | - Go to your newly created local repo. 83 | - Run `npx contribution-streak-maintainer / --email= --token= --condition=` 84 | - Example: `npx contribution-streak-maintainer my-username --email=my-primary-github-email@email.com --token=gph_nJkKQKJKFb7YxqkLtFf3wvXyU6X --condition=6` 85 | - Verify changes in `AUTOMATED_CONTRIBUTIONS.md`. 86 | 87 | ## Configuration 88 | 89 | | Param | ENV alias | Type | Description | Required | Default | 90 | | ------------ | --------------------- | ------ | ----------------------------------------------------------------- | -------- | ------- | 91 | | `repository` | `DEVFUL_GITHUB_REPO` | String | The owner and repository name. For example, `octocat/Hello-World` | Yes | | 92 | | `email` | | String | Primary email address associated with your GitHub account | Yes | | 93 | | `token` | `DEVFUL_GITHUB_TOKEN` | String | Github Auth token | Yes | | 94 | | `condition` | | String | Condition for contribution to be made | No | 0 | 95 | 96 | By default, this action runs daily, checks if the user has made any contribution, and generates a random number of commits between 1-3. If the user sets the condition to, for example, 5, the action will only make commits if the user has made 0-5 commits that day. 97 | 98 | ## Authors 99 | 100 | - [@EliasAfara](https://www.github.com/eliasafara) 101 | 102 | ## License 103 | 104 | [MIT](LICENSE) 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📅 Contribution Streak Maintainer 2 | 3 | 4 | Contribution Streak Maintainer Logo 5 | 6 | 7 | [![npm](https://img.shields.io/npm/v/contribution-streak-maintainer)](https://www.npmjs.com/package/contribution-streak-maintainer) [![npm](https://img.shields.io/npm/dm/contribution-streak-maintainer)](https://www.npmjs.com/package/contribution-streak-maintainer) [![GitHub License](https://img.shields.io/github/license/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/blob/main/LICENSE) [![GitHub Repo stars](https://img.shields.io/github/stars/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/stargazers) 8 | 9 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/devful/contribution-streak-maintainer/test.yml)](https://github.com/devful/contribution-streak-maintainer/actions) [![GitHub issues](https://img.shields.io/github/issues/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/issues) [![GitHub pull requests](https://img.shields.io/github/issues-pr/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/pulls) [![GitHub contributors](https://img.shields.io/github/contributors/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/graphs/contributors) 10 | 11 | 12 | ![Test](https://github.com/devful/contribution-streak-maintainer/workflows/Test/badge.svg) [![GitHub last commit](https://img.shields.io/github/last-commit/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer/commits/main) [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/devful/contribution-streak-maintainer)](https://github.com/devful/contribution-streak-maintainer) ![Codecov](https://img.shields.io/codecov/c/github/devful/contribution-streak-maintainer) 13 | 14 | - _Have you ever wanted to keep your contribution streak going but never been able to?_ 15 | - _Do you find it challenging to make manual contributions every single day?_ 16 | - _Worry no more!_ 17 | 18 | **Contribution Streak Maintainer** is a tool, available as an [npm package](https://www.npmjs.com/package/contribution-streak-maintainer), designed to be used inside a GitHub Actions workflow. This npm package is utilized within your workflow, automatically maintaining your GitHub contribution streak. The GitHub Actions workflow, configured through YAML files, generates and pushes commits to your repository, ensuring your activity stays active even on days when you're not able to make manual contributions. 19 | 20 | Now you can focus on your work, knowing that your GitHub contributions are being taken care of automatically. Keep your streak alive effortlessly by integrating the Contribution Streak Maintainer npm package into your GitHub Actions workflow! 21 | 22 | ## Installation 23 | 24 | Here is how to add my badges to your profile: 25 | 26 | 1. Star this repository. 27 | 2. Create a private repository `your-username/name-of-your-choice` 28 | 3. Add the following workflow to your repository at `.github/workflows/contribution-streak-maintainer.yml` 29 | 30 | ```yaml 31 | name: contribution-streak-maintainer 32 | 33 | on: 34 | workflow_dispatch: 35 | schedule: 36 | # For more information on the cron scheduler, 37 | # see https://crontab.guru/ or https://crontab.cronhub.io/. 38 | # This cron schedule means the action runs every day at midnight UTC. 39 | - cron: "0 22 * * *" 40 | 41 | permissions: 42 | contents: write 43 | 44 | jobs: 45 | contribution-streak-maintainer: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Make a contribution 49 | run: npx contribution-streak-maintainer ${{github.repository}} --email=primary-github-email@email.com 50 | env: 51 | DEVFUL_GITHUB_TOKEN: ${{ secrets.CSM_GITHUB_TOKEN }} 52 | ``` 53 | 54 | 4. Create your own personal access tokens (classic) 55 | 56 | 1. [Verify your email address](https://docs.github.com/en/get-started/signing-up-for-github/verifying-your-email-address), if it hasn't been verified yet. 57 | 2. In the upper-right corner of any page, click your profile photo, then click **Settings**. 58 | 3. In the left sidebar, click **Developer settings**. 59 | 4. In the left sidebar, under **Personal access tokens**, click **Tokens (classic)**. 60 | 5. Click **Generate new token** button then **Generate new token (classic)**. 61 | 6. Under **Note**, enter a name for the token. 62 | 7. Under **Expiration**, select **No expiration** for the token. 63 | 8. Under **Select scopes**, check **repo**, **read:user**, **user:email**. 64 | repo scope 65 | read scope 66 | 9. Click **Generate token**. 67 | 10. Note down the generated token. 68 | 69 | 5. Create a secret for your private repository 70 | - Check out the [docs](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) 71 | - In the **Name** field, type `CSM_GITHUB_TOKEN`. 72 | - In the **Secret** field, enter the newly generated token. 73 | 74 | > **_NOTE:_** Ensure that the GitHub token (`CSM_GITHUB_TOKEN`) is kept private and not shared publicly. 75 | 76 | ## Run Manually 77 | 78 | You can start **contribution-streak-maintainer** workflow manually, or wait for it to run automatically. 79 | 80 | Alternatively, you can perform these steps manually: 81 | 82 | - Go to your newly created local repo. 83 | - Run `npx contribution-streak-maintainer / --email= --token= --condition=` 84 | - Example: `npx contribution-streak-maintainer my-username --email=my-primary-github-email@email.com --token=gph_nJkKQKJKFb7YxqkLtFf3wvXyU6X --condition=6` 85 | - Verify changes in `AUTOMATED_CONTRIBUTIONS.md`. 86 | 87 | ## Configuration 88 | 89 | | Param | ENV alias | Type | Description | Required | Default | 90 | | ------------ | --------------------- | ------ | ----------------------------------------------------------------- | -------- | ------- | 91 | | `repository` | `DEVFUL_GITHUB_REPO` | String | The owner and repository name. For example, `octocat/Hello-World` | Yes | | 92 | | `email` | | String | Primary email address associated with your GitHub account | Yes | | 93 | | `token` | `DEVFUL_GITHUB_TOKEN` | String | Github Auth token | Yes | | 94 | | `condition` | | String | Condition for contribution to be made | No | 0 | 95 | 96 | By default, this action runs daily, checks if the user has made any contribution, and generates a random number of commits between 1-3. If the user sets the condition to, for example, 5, the action will only make commits if the user has made 0-5 commits that day. 97 | 98 | ## Authors 99 | 100 | - [@EliasAfara](https://www.github.com/eliasafara) 101 | 102 | ## License 103 | 104 | [MIT](LICENSE) 105 | --------------------------------------------------------------------------------