├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── images ├── result.png └── update-readme-image_post.png ├── main.py ├── requirements.txt └── tests ├── __init__.py └── test_update_readme_image.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | install: 5 | - pip install -r requirements.txt 6 | script: 7 | - python -m unittest discover -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | # Install dependencies. 4 | ADD requirements.txt /requirements.txt 5 | RUN pip install -r requirements.txt 6 | 7 | # Copy code. 8 | ADD main.py /main.py 9 | 10 | CMD ["python", "/main.py"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Siddharth Chandra 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Random Image on README [![Travis CI Build Status](https://api.travis-ci.org/siddharth2016/update-readme-image.svg?branch=main)](https://travis-ci.org/github/siddharth2016/update-readme-image) 2 | 3 | ![Github Socialify](https://socialify.git.ci/siddharth2016/update-readme-image/image?font=Source%20Code%20Pro&forks=1&issues=1&pattern=Brick%20Wall&stargazers=1&theme=Dark) 4 | 5 | --- 6 | 7 | ## GitHub Action to update an image present on README 8 | 9 | ![Random Image On Readme Result](images/result.png) 10 | 11 | --- 12 | 13 | ## Random Image (from a collection of given images) on your (Profile) Readme 14 | 15 | ### How To Use This Action 16 | 17 | There is a short tutorial with enough steps for you to get started with this not-so-useful GitHub Action. Check that out [here](https://chandraji.dev/update-image-readme-see-random-image-on-your-github-profile-readme). 18 | 19 | If you want to get an in-depth idea on the usage of this action, please follow the below points. 20 | 21 | ### Prepare Your Repository 22 | 23 | 1. You need to update the markdown file(.md) with 2 comments. You can refer [here](#update-your-readme) for updating it. 24 | 2. Make sure to have a `.github/images` folder on your profile repository or any other repository. Jump [here](#images-repository) to know more. 25 | 3. **Optional** You'll need a GitHub API Token with `repo` scope from [here](https://github.com/settings/tokens) if you're running the action for a non profile repository. 26 | - You can use [this](#other-repository-not-profile) example to work it out. 27 | 4. You can follow any one example below according to your needs to get started ! 28 | - Use [this](#profile-repository) on Profile Repository. 29 | - Use [this](#other-repository-not-profile) on any other Repository. 30 | 5. It is better to run the Action on your Profile Repository, since you won't be needing a GitHub Access Token ! 31 | 6. Check [this](#examples) to see available options while creating a workflow for this action. 32 | 33 | --- 34 | 35 | ### Update Your Readme 36 | 37 | Add a comment to your `README.md` like this: 38 | 39 | ```md 40 | 41 | 42 | ``` 43 | 44 | You can place these 2 lines anywhere you want your images to be displayed. 45 | 46 | --- 47 | 48 | ### Images Repository 49 | 50 | You need to have your own collection of images that you want to be displayed on your README. 51 | 52 | You can do so in 2 ways: 53 | 54 | 1. Add `.github/images` folder on the same repository as README or on a different repository, this folder should contain the images. 55 | 56 | You can add `.github/images` on a different repository as well, then you would need to mention that repository explicitly by passing a parameter `IMG_REPOSITORY` on your workflow yaml. 57 | 58 | ```yml 59 | with: 60 | IMG_REPOSITORY: / 61 | ``` 62 | 63 | 2. Add a folder (name it whatever you like) on the same repository as README or on a different repository, this folder should contain the images. 64 | 65 | You need to specify `IMG_PATH` on your workflow yaml, with this if you are creating this folder on a different repository, then you would need to specify `IMG_REPOSITORY` as well. 66 | 67 | If on same repo but image folder is not `.github/images`: 68 | ```yml 69 | with: 70 | IMG_PATH: 71 | ``` 72 | 73 | If on a different repo and image folder is not `.github/images`: 74 | ```yml 75 | with: 76 | IMG_PATH: 77 | IMG_REPOSITORY: / 78 | ``` 79 | 80 | --- 81 | 82 | ### Profile Repository 83 | 84 | _If you're executing the workflow on your Profile Repository (`/`)_ 85 | 86 | You wouldn't need a GitHub Access Token since GitHub Actions already makes one for you. 87 | 88 | Please follow the steps below: 89 | 90 | 1. Go to your `//actions`, hit `New workflow`, then `set up a workflow yourself`, delete all the default content github made for you. 91 | 2. Copy the following code and paste it to your new workflow you created at step 1: 92 | 93 | ```yml 94 | name: Update Image Readme 95 | 96 | on: 97 | workflow_dispatch: 98 | schedule: 99 | # Runs at 1 UTC everyday 100 | - cron: "0 1 * * *" 101 | 102 | jobs: 103 | update-readme: 104 | name: Update Image README 105 | runs-on: ubuntu-latest 106 | steps: 107 | - uses: siddharth2016/update-readme-image@main 108 | with: 109 | IMG_ALT: Image ALT 110 | ``` 111 | 112 | 3. Make sure you have `.github/images` folder in `/` repo. If you have created a different folder, then add `IMG_PATH` after `IMG_ALT` under `with`. Check [this](#images-repository). 113 | 4. There are other options as well, height/width you want for image and it's alignment within README. Check [examples](#examples) for more. 114 | 5. Add a comment to your `README.md` like this: 115 | 116 | ```md 117 | 118 | 119 | ``` 120 | 121 | 6. Go to Workflows menu (mentioned in step 1), click `Update Image Readme`, click `Run workflow`. 122 | 7. Go to your profile page, you will be able to see a random image from your collection of images. 123 | 124 | --- 125 | 126 | ### Other Repository (not Profile) 127 | 128 | _If you're executing the workflow on another repo other than (`/`)_ 129 | 130 | You'll need to get a [GitHub Access Token](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) with a `repo` scope. 131 | 132 | You need to save the GitHub API Token in the repository secrets. You can find that in the Settings of your Repository. 133 | 134 | 1. Go to your `//actions`, hit `New workflow`, then `set up a workflow yourself`, delete all the default content github made for you. 135 | 2. Copy the following code and paste it to your new workflow you created at step 1: 136 | 137 | ```yml 138 | name: Update Image Readme 139 | 140 | on: 141 | workflow_dispatch: 142 | schedule: 143 | # Runs at 1 UTC everyday 144 | - cron: "0 1 * * *" 145 | 146 | jobs: 147 | update-readme: 148 | name: Update Image README 149 | runs-on: ubuntu-latest 150 | steps: 151 | - uses: siddharth2016/update-readme-image@main 152 | with: 153 | IMG_ALT: Image ALT 154 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 155 | README_REPOSITORY: / # No need to mention this if workflow present in current non profile repo. 156 | ``` 157 | 158 | Add `IMG_REPOSITORY` or `IMG_PATH` as required, check [this](#images-repository) for more info. 159 | 160 | 3. There are other options as well, height/width you want for image and it's alignment within README. Check [examples](#examples) for more. 161 | 4. Add a comment to your `README.md` like this: 162 | 163 | ```md 164 | 165 | 166 | ``` 167 | 168 | 5. Go to Workflows menu (mentioned in step 1), click `Update Image Readme`, click `Run workflow`. 169 | 6. Go to your non-profile readme page, you will be able to see a random image from your collection of images. 170 | 171 | --- 172 | 173 | ### Examples 174 | 175 | 1. If you want to use this action for a README that is not present in current workflow repository. 176 | 177 | ```yml 178 | - uses: siddharth2016/update-readme-image@main 179 | with: 180 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # Needed if README repository is not profile repo 181 | README_REPOSITORY: / 182 | ``` 183 | 184 | Using `README_REPOSITORY` will change README present in that repository head. 185 | 186 | For example, if your workflow is present in `/repo1` and you want to update README present in `/repo2`, then assign `README_REPOSITORY` to `/repo2` in workflow at `/repo1`. 187 | 188 | 2. If your images are present in a different repository than current workflow repository. 189 | 190 | ```yml 191 | - uses: siddharth2016/update-readme-image@main 192 | with: 193 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # Needed if README repository is not profile repo 194 | README_REPOSITORY: / # Needed if README repository is not current repo 195 | IMG_PATH: # Needed if images are not present in .github/images of image repository 196 | IMG_REPOSITORY: / 197 | ``` 198 | 199 | It's better to have all images in `.github/images` to avoid any confusion and keep the images in same repository as of README and workflow repository. 200 | 201 | 3. If you want to set image alt, height, width and alignment, then use these flags: 202 | 203 | ```yml 204 | - uses: siddharth2016/update-readme-image@main 205 | with: 206 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # Needed if README repository is not profile repo 207 | README_REPOSITORY: / # Needed if README repository is not current repo 208 | IMG_PATH: # Needed if images are not present in .github/images of image repository 209 | IMG_REPOSITORY: / # Needed if images are not present in current repo 210 | HEIGHT: 180px # default 211 | WIDTH: 180px # default 212 | ALIGN: right # default 213 | IMG_ALT: Profile Image # default 214 | ``` 215 | 216 | 4. You can specify a commit message to override the default _"Update Readme Image"_. 217 | 218 | ```yml 219 | - uses: siddharth2016/update-readme-image@main 220 | with: 221 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # Needed if README repository is not profile repo 222 | README_REPOSITORY: / # Needed if README repository is not current repo 223 | IMG_PATH: # Needed if images are not present in .github/images of image repository 224 | IMG_REPOSITORY: / # Needed if images are not present in current repo 225 | HEIGHT: 180px # default 226 | WIDTH: 180px # default 227 | ALIGN: right # default 228 | IMG_ALT: Profile Image # default 229 | COMMIT_MESSAGE: # default - Update Readme Image 230 | ``` 231 | 232 | --- 233 | 234 | ### Tests 235 | 236 | To run tests simply execute the following in the directory containing `main.py`: 237 | 238 | ```bash 239 | python -m unittest discover 240 | ``` 241 | 242 | --- 243 | 244 | #### Another intriguing action you would want to use - [quote-readme](https://github.com/marketplace/actions/quote-readme) 245 | 246 | #### If you liked this Action and want to contribute to upgrade this utility, please create an issue or throw a PR ! 247 | 248 | --- 249 | 250 | #### Inspired From 251 | 252 | [athul/waka-readme](https://github.com/athul/waka-readme) 253 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Update Image - README" 2 | author: Siddharth Chandra 3 | description: "Update a section with given images randomly in your Readme" 4 | 5 | inputs: 6 | GH_TOKEN: 7 | description: "GitHub access token with Repo scope" 8 | required: true 9 | default: ${{ github.token }} 10 | 11 | README_REPOSITORY: 12 | description: "Your GitHub repository" 13 | default: ${{ github.repository }} 14 | required: false 15 | 16 | IMG_REPOSITORY: 17 | description: "Your Repo which contains images" 18 | default: ${{ github.repository }} 19 | required: false 20 | 21 | IMG_PATH: 22 | description: "Your path to images in IMG_REPO" 23 | default: ".github/images" 24 | required: false 25 | 26 | WIDTH: 27 | description: "Give width of image" 28 | default: "180px" 29 | required: false 30 | 31 | HEIGHT: 32 | description: "Give height of image" 33 | default: "180px" 34 | required: false 35 | 36 | ALIGN: 37 | description: "Alignment of image" 38 | default: "right" 39 | required: false 40 | 41 | IMG_ALT: 42 | description: "Give alternative title to image" 43 | default: "Profile Image" 44 | required: false 45 | 46 | COMMIT_MESSAGE: 47 | description: "Add a commit message of your choice" 48 | default: "Update Readme Image" 49 | required: false 50 | 51 | runs: 52 | using: "docker" 53 | image: "Dockerfile" 54 | 55 | branding: 56 | icon: "image" 57 | color: "orange" 58 | -------------------------------------------------------------------------------- /images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharth2016/update-readme-image/8be55180ff3108c7d636ccfff81059f5b59a589b/images/result.png -------------------------------------------------------------------------------- /images/update-readme-image_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharth2016/update-readme-image/8be55180ff3108c7d636ccfff81059f5b59a589b/images/update-readme-image_post.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #main.py 2 | """ 3 | GitHub Action Code to update README file with provided images randomly. 4 | """ 5 | 6 | import os 7 | import re 8 | import sys 9 | import base64 10 | import requests 11 | import random 12 | from github import Github, GithubException 13 | 14 | START_COMMENT = '' 15 | END_COMMENT = '' 16 | IMAGE_REPL = f"{START_COMMENT}[\\s\\S]+{END_COMMENT}" 17 | 18 | REPO = os.getenv("INPUT_README_REPOSITORY") 19 | IMG_REPO = os.getenv("INPUT_IMG_REPOSITORY") 20 | IMG_PATH = os.getenv("INPUT_IMG_PATH") 21 | GHTOKEN = os.getenv("INPUT_GH_TOKEN") 22 | COMMIT_MSG = os.getenv("INPUT_COMMIT_MESSAGE") 23 | WIDTH = os.getenv("INPUT_WIDTH") 24 | HEIGHT = os.getenv("INPUT_HEIGHT") 25 | ALIGN = os.getenv("INPUT_ALIGN") 26 | IMG_ALT = os.getenv("INPUT_IMG_ALT") 27 | 28 | VALID_IMAGES_EXT = ['png', 'jpg', 'jpeg', 'gif', 'svg'] 29 | 30 | 31 | def verify_image_ext(image): 32 | ''' Validate image obtained ''' 33 | global VALID_IMAGES_EXT 34 | if image.path.split('/')[-1].split('.')[-1].lower() not in VALID_IMAGES_EXT: 35 | print(f"Please make sure image is one of following type {VALID_IMAGES_EXT}, error caused by image - {image.path}") 36 | return False 37 | return True 38 | 39 | def get_image_tag(repo): 40 | ''' Get new image tag to place in README ''' 41 | global IMG_PATH 42 | images = repo.get_contents(IMG_PATH) 43 | image = random.choice(images) 44 | is_image = verify_image_ext(image) 45 | if not is_image: 46 | sys.exit(1) 47 | img_src = image.download_url 48 | img_tag = f"{IMG_ALT}" 49 | return img_tag 50 | 51 | def decode_readme(data: str) -> str: 52 | '''Decode the contents of old readme''' 53 | decoded_bytes = base64.b64decode(data) 54 | return str(decoded_bytes, 'utf-8') 55 | 56 | def generate_new_readme(readme: str, image_tag: str) -> str: 57 | '''Generate a new Readme.md''' 58 | update_readme_with = f"{START_COMMENT}\n{image_tag}\n{END_COMMENT}" 59 | return re.sub(IMAGE_REPL, update_readme_with, readme) 60 | 61 | if __name__ == "__main__": 62 | g = Github(GHTOKEN) 63 | try: 64 | readme_repo = g.get_repo(REPO) 65 | img_repo = g.get_repo(IMG_REPO) 66 | except GithubException: 67 | print("Authentication Error. Try saving a GitHub Token in your Repo Secrets or Use the GitHub Actions Token, which is automatically used by the action.") 68 | sys.exit(1) 69 | image_tag = get_image_tag(img_repo) 70 | readme_obj = readme_repo.get_readme() 71 | readme_content = readme_obj.content 72 | readme_content_decoded = decode_readme(readme_content) 73 | new_readme = generate_new_readme(readme=readme_content_decoded, image_tag=image_tag) 74 | if readme_content_decoded != new_readme: 75 | readme_repo.update_file(path=readme_obj.path, message=COMMIT_MSG, 76 | content=new_readme, sha=readme_obj.sha) 77 | print("Success") 78 | else: 79 | print("No change") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyGithub==1.51 2 | requests==2.31.0 3 | urllib3==2.0.6 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharth2016/update-readme-image/8be55180ff3108c7d636ccfff81059f5b59a589b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_update_readme_image.py: -------------------------------------------------------------------------------- 1 | #tests/update-readme-image-tests.py 2 | """ 3 | Test file to support continuous integration. 4 | """ 5 | 6 | from main import get_image_tag, verify_image_ext 7 | import unittest 8 | from unittest.mock import patch, Mock, call 9 | 10 | 11 | class TestUpdateReadmeImage(unittest.TestCase): 12 | 13 | def test_get_image_tag(self): 14 | repo = Mock() 15 | repo.get_contents.return_value = Mock() 16 | with patch('main.random.choice') as mockChoice: 17 | with patch('main.verify_image_ext') as mockVerifyImg: 18 | newMock = Mock() 19 | mockChoice.return_value = newMock 20 | mockVerifyImg.return_value = True 21 | newMock.download_url = 'some_raw_github_content.png' 22 | actual_img_tag = get_image_tag(repo) 23 | expected_img_tag = "None" 24 | self.assertEqual(actual_img_tag, expected_img_tag) 25 | 26 | def test_get_image_tag_sys_exit(self): 27 | repo = Mock() 28 | repo.get_contents.return_value = Mock() 29 | with patch('main.random.choice') as mockChoice: 30 | with patch('main.verify_image_ext') as mockVerifyImg: 31 | newMock = Mock() 32 | mockChoice.return_value = newMock 33 | mockVerifyImg.return_value = False 34 | with self.assertRaises(SystemExit): 35 | get_image_tag(repo) 36 | 37 | def test_verify_image_ext_true(self): 38 | image = Mock() 39 | image.path = 'images/image1.png' 40 | actual = verify_image_ext(image) 41 | self.assertTrue(actual) 42 | 43 | def test_verify_image_ext_false(self): 44 | image = Mock() 45 | image.path = 'images/image1.xyz' 46 | with patch('builtins.print') as mockPrint: 47 | actual = verify_image_ext(image) 48 | assert mockPrint.mock_calls == [call("Please make sure image is one of following type ['png', 'jpg', 'jpeg', 'gif', 'svg'], error caused by image - images/image1.xyz")] 49 | self.assertFalse(actual) 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() --------------------------------------------------------------------------------