├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── contributors.yml │ ├── deploy.yml │ ├── fsync.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── MIGRATION.md ├── Makefile.toml ├── README.md ├── action.yml ├── docker ├── dev.Dockerfile ├── platform.Dockerfile └── prod.Dockerfile ├── fsync.yml ├── rustfmt.toml └── src ├── conf.rs ├── git.rs ├── main.rs └── readme.rs /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug Report' 3 | about: Bug report 4 | title: '' 5 | labels: bug 6 | assignees: '@gleich' 7 | --- 8 | 9 | 12 | 13 | ## Description 14 | 15 | 18 | 19 | ## Steps to reproduce 20 | 21 | 25 | 26 | ## Logs/Screenshots 27 | 28 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🚀 Feature request' 3 | about: Request a feature 4 | title: '' 5 | labels: enhancement 6 | assignees: '@gleich' 7 | --- 8 | 9 | 12 | 13 | ## Description 14 | 15 | 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 6 | 7 | ## Steps 8 | 9 | - [ ] My change requires a change to the documentation 10 | - [ ] I have updated the accessible documentation according 11 | - [ ] There is no duplicate open or closed pull request for this fix/addition/issue resolution. 12 | 13 | ## Original Issue 14 | 15 | This PR resolves #ISSUE_NUMBER_HERE 16 | 17 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | rust: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | filters: | 19 | rust: 20 | - '**.rs' 21 | - 'Makefile.toml' 22 | - 'Cargo.lock' 23 | - '.github/workflows/**' 24 | - if: steps.changes.outputs.rust == 'true' 25 | uses: davidB/rust-cargo-make@v1 26 | - if: steps.changes.outputs.rust == 'true' 27 | run: cargo make build-rust 28 | docker-prod: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: dorny/paths-filter@v2 33 | id: changes 34 | with: 35 | filters: | 36 | docker: 37 | - '**/prod.Dockerfile' 38 | - 'Makefile.toml' 39 | - '.github/workflows/**' 40 | - if: steps.changes.outputs.docker == 'true' 41 | uses: davidB/rust-cargo-make@v1 42 | - if: steps.changes.outputs.docker == 'true' 43 | run: cargo make build-docker-prod 44 | docker-platform: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: dorny/paths-filter@v2 49 | id: changes 50 | with: 51 | filters: | 52 | docker: 53 | - '**/platform.Dockerfile' 54 | - 'Makefile.toml' 55 | - '.github/workflows/**' 56 | - if: steps.changes.outputs.docker == 'true' 57 | uses: davidB/rust-cargo-make@v1 58 | - if: steps.changes.outputs.docker == 'true' 59 | run: cargo make build-docker-prod 60 | docker-dev: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v2 64 | - uses: dorny/paths-filter@v2 65 | id: changes 66 | with: 67 | filters: | 68 | docker: 69 | - '**/dev.Dockerfile' 70 | - 'Makefile.toml' 71 | - '.github/workflows/**' 72 | - if: steps.changes.outputs.docker == 'true' 73 | uses: davidB/rust-cargo-make@v1 74 | - if: steps.changes.outputs.docker == 'true' 75 | run: cargo make build-docker-dev 76 | -------------------------------------------------------------------------------- /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | name: contributors 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | contributor_list: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: cjdenio/contributor_list@master 15 | with: 16 | commit_message: 'docs: update contributor list' 17 | max_contributors: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | packages: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | filters: | 19 | code: 20 | - '**.rs' 21 | - '**.Dockerfile' 22 | - 'Makefile.toml' 23 | - 'Cargo.lock' 24 | - '.github/workflows/**' 25 | - if: steps.changes.outputs.code == 'true' 26 | run: | 27 | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u gleich --password-stdin 28 | docker build -f docker/platform.Dockerfile -t ghcr.io/gleich/profile_stack:platform . 29 | docker push ghcr.io/gleich/profile_stack:platform 30 | -------------------------------------------------------------------------------- /.github/workflows/fsync.yml: -------------------------------------------------------------------------------- 1 | name: fsync 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | schedule: 9 | - cron: '0 */6 * * *' # Run every 6 hours 10 | 11 | jobs: 12 | gh_fsync: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 18 | - uses: gleich/gh_fsync@master 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | rustfmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | filters: | 19 | rust: 20 | - '**.rs' 21 | - 'rustfmt.toml' 22 | - '.github/workflows/**' 23 | - if: steps.changes.outputs.rust == 'true' 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | components: rustfmt 28 | override: true 29 | - if: steps.changes.outputs.rust == 'true' 30 | uses: davidB/rust-cargo-make@v1 31 | - if: steps.changes.outputs.rust == 'true' 32 | run: cargo make lint-rust 33 | hadolint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: dorny/paths-filter@v2 38 | id: changes 39 | with: 40 | filters: | 41 | docker: 42 | - '**.Dockerfile' 43 | - '.github/workflows/**' 44 | - if: steps.changes.outputs.docker == 'true' 45 | name: Linting prod.Dockerfile 46 | uses: brpaz/hadolint-action@master 47 | with: 48 | dockerfile: 'docker/prod.Dockerfile' 49 | - if: steps.changes.outputs.docker == 'true' 50 | name: Linting dev.Dockerfile 51 | uses: brpaz/hadolint-action@master 52 | with: 53 | dockerfile: 'docker/dev.Dockerfile' 54 | - if: steps.changes.outputs.docker == 'true' 55 | name: Linting platform.Dockerfile 56 | uses: brpaz/hadolint-action@master 57 | with: 58 | dockerfile: 'docker/platform.Dockerfile' 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | rust: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | filters: | 19 | rust: 20 | - '**.rs' 21 | - 'Makefile.toml' 22 | - 'Cargo.lock' 23 | - '.github/workflows/**' 24 | - if: steps.changes.outputs.rust == 'true' 25 | uses: davidB/rust-cargo-make@v1 26 | - if: steps.changes.outputs.rust == 'true' 27 | run: cargo make test-rust 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # macOS meta data 13 | .DS_Store 14 | 15 | # sensitive files 16 | .env 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you so much for showing an interest in contributing to profile_stack! I am excited to see what you have to add :) 4 | 5 | ## 🦀 Rust Toolchain 6 | 7 | profile_stack uses the nightly distribution of the rust toolchain. You can get on the nightly edition of rustfmt, the only toolchain component used by profile_stack, by running the following command: 8 | 9 | ```bash 10 | rustup component add rustfmt --toolchain nightly 11 | ``` 12 | 13 | ## 🏗️ Build System 14 | 15 | profile_stack uses [cargo-make](https://github.com/sagiegurari/cargo-make) for all build scripts. Please run the following command to install cargo-make: 16 | 17 | ```bash 18 | cargo install --force cargo-make 19 | ``` 20 | 21 | Once you have cargo-make installed you can run `cargo make TASK_NAME` to run a certain task. All tasks are prefixed with `tasks.` and are stored in the [Makefile.toml](Makefile.toml) file. So if you wanted to build the development binary for example you would run the following command: 22 | 23 | ```bash 24 | cargo make build-rust-dev 25 | ``` 26 | 27 | ## 🧪 Linters 28 | 29 | profile_stack only uses two linters: [hadolint](https://github.com/hadolint/hadolint) and [rustfmt](https://github.com/rust-lang/rustfmt). hadolint will lint the dockerfiles stored in [docker/](docker/) and rustfmt will lint the source code in [src/](src/). Please install hadolint using your system's package manager and rustfmt with the toolchain command provided in the [🦀 Rust Toolchain section](#-rust-toolchain). 30 | 31 | ## 🔄 File Syncing 32 | 33 | profile_stack uses a GitHub action called [gh_fsync](https://github.com/Matt-Gleich/gh_fsync) to automatically sync certain files from other repositories. The configuration for gh_fsync is stored in [fsync.yml](fsync.yml). This all means that if you update a file in this repository before removing it from the configuration then gh_fsync will reset it back. If you are looking to change a file for profile_stack that is listed in the configuration please remove it from the configuration or update the source file. 34 | 35 | --- 36 | 37 | That's all, good luck with your contribution, and please make an issue if anything doesn't make sense. 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "profile_stack" 3 | version = "0.1.0" 4 | authors = ["Matthew Gleich "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | serde = { version = "1.0", features = ["derive"] } 9 | serde_yaml = "0.8" 10 | anyhow = "1.0.40" 11 | tracing = "0.1" 12 | tracing-subscriber = "0.2.0" 13 | percent-encoding = "2.1.0" 14 | lazy_static = "1.4.0" 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | 3 | v2.0.0 of profile_stack is a 100% rewrite in rust. Because it is written in rust the run time has been reduced from ~40+ seconds to ~10 seconds. There are a few things that have changed: 4 | 5 | ## `url` is now required 6 | 7 | ### Before 8 | 9 | ```yaml 10 | - name: Golang 11 | logo: go 12 | url: https://golang.org/ 13 | color: '#7FD6EA' 14 | projects: 15 | - https://github.com/gleich/fgh 16 | - https://github.com/gleich/gh_fsync 17 | ``` 18 | 19 | ### After 20 | 21 | ```yaml 22 | - name: Golang 23 | logo: go 24 | url: https://golang.org/ 25 | color: '#7FD6EA' 26 | projects: 27 | - url: https://github.com/gleich/fgh 28 | - url: https://github.com/gleich/gh_fsync 29 | ``` 30 | 31 | ### Difference 32 | 33 | ```diff 34 | - - https://github.com/gleich/fgh 35 | + - url: https://github.com/gleich/fgh 36 | - - https://github.com/gleich/gh_fsync 37 | + - url: https://github.com/gleich/gh_fsync 38 | ``` 39 | 40 | ## `logoColor` is now `logo_color` 41 | 42 | ### Before 43 | 44 | ```yaml 45 | - name: Golang 46 | logo: go 47 | logoColor: '#000000' 48 | url: https://golang.org/ 49 | color: '#7FD6EA' 50 | projects: 51 | - url: https://github.com/gleich/fgh 52 | - url: https://github.com/gleich/gh_fsync 53 | ``` 54 | 55 | ### After 56 | 57 | ```yaml 58 | - name: Golang 59 | logo: go 60 | logo_color: '#000000' 61 | url: https://golang.org/ 62 | color: '#7FD6EA' 63 | projects: 64 | - url: https://github.com/gleich/fgh 65 | - url: https://github.com/gleich/gh_fsync 66 | ``` 67 | 68 | ### Difference 69 | 70 | ```diff 71 | - logoColor: '#000000' 72 | + logo_color: '#000000' 73 | ``` 74 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | ######### 2 | # Build # 3 | ######### 4 | [tasks.build] 5 | dependencies = ["build-rust", "build-docker"] 6 | 7 | [tasks.build-rust] 8 | dependencies = ["build-rust-prod", "build-rust-dev"] 9 | 10 | [tasks.build-rust-prod] 11 | command = "cargo" 12 | args = ["build", "--release"] 13 | 14 | [tasks.build-rust-dev] 15 | command = "cargo" 16 | args = ["build"] 17 | 18 | [tasks.build-docker] 19 | dependencies = ["build-docker-prod", "build-docker-dev"] 20 | 21 | [tasks.build-docker-prod] 22 | command = "docker" 23 | args = [ 24 | "build", 25 | "-f", 26 | "docker/prod.Dockerfile", 27 | "-t", 28 | "mattgleich/profile_stack:latest", 29 | "." 30 | ] 31 | 32 | [tasks.build-docker-platform] 33 | command = "docker" 34 | args = [ 35 | "build", 36 | "-f", 37 | "docker/platform.Dockerfile", 38 | "-t", 39 | "mattgleich/profile_stack:platform", 40 | "." 41 | ] 42 | 43 | [tasks.build-docker-dev] 44 | command = "docker" 45 | args = [ 46 | "build", 47 | "-f", 48 | "docker/dev.Dockerfile", 49 | "-t", 50 | "mattgleich/profile_stack:dev", 51 | "." 52 | ] 53 | 54 | ######## 55 | # Test # 56 | ######## 57 | [tasks.test] 58 | dependencies = ["test-rust"] 59 | 60 | [tasks.test-rust] 61 | command = "cargo" 62 | args = ["test"] 63 | 64 | ######## 65 | # Lint # 66 | ######## 67 | [tasks.lint] 68 | dependencies = ["lint-rust", "lint-dockerfiles", "lint-docker-platform"] 69 | 70 | [tasks.lint-rust] 71 | command = "cargo" 72 | args = ["fmt", "--all", "--", "--check"] 73 | 74 | [tasks.lint-docker] 75 | dependencies = ["lint-docker-prod", "lint-docker-dev"] 76 | 77 | [tasks.lint-docker-prod] 78 | command = "hadolint" 79 | args = ["docker/prod.Dockerfile"] 80 | 81 | [tasks.lint-docker-platform] 82 | command = "hadolint" 83 | args = ["docker/platform.Dockerfile"] 84 | 85 | [tasks.lint-doocker-dev] 86 | command = "hadolint" 87 | args = ["docker/dev.Dockerfile"] 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # profile_stack ![GitHub release (latest by date)](https://img.shields.io/github/v/release/gleich/profile_stack) 4 | 5 | [![lint](https://github.com/gleich/profile_stack/actions/workflows/lint.yml/badge.svg)](https://github.com/gleich/profile_stack/actions/workflows/lint.yml) 6 | [![build](https://github.com/gleich/profile_stack/actions/workflows/build.yml/badge.svg)](https://github.com/gleich/profile_stack/actions/workflows/build.yml) 7 | [![test](https://github.com/gleich/profile_stack/actions/workflows/test.yml/badge.svg)](https://github.com/gleich/profile_stack/actions/workflows/test.yml) 8 | [![deploy](https://github.com/gleich/profile_stack/actions/workflows/deploy.yml/badge.svg)](https://github.com/gleich/profile_stack/actions/workflows/deploy.yml) 9 | 10 | 🚀 Display your tech stack on your GitHub profile's README 11 | 12 | ## [🆕 v2.0.0 Migration](./MIGRATION.md) 13 | 14 | ## ✨ Example 15 | 16 | Add the following to a file in `.github/workflows`: 17 | 18 | ```yml 19 | name: stack 20 | 21 | on: 22 | push: 23 | branches: 24 | - main 25 | 26 | jobs: 27 | profile_stack: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: gleich/profile_stack@master 32 | ``` 33 | 34 | Based on a [config file](#️-config) this GitHub action will generate a table showing technologies and projects you've used them in (doesn't have to be all, pick any): 35 | 36 | | 💻 **Technology** | 🚀 **Projects** | 37 | | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 38 | | [![Golang](https://img.shields.io/static/v1?label=&message=Golang&color=7FD6EA&logo=go&logoColor=FFFFFF)](https://golang.org/) | ![fgh](https://img.shields.io/static/v1?label=&message=fgh&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) ![gh_fsync](https://img.shields.io/static/v1?label=&message=gh_fsync&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) ![nuke](https://img.shields.io/static/v1?label=&message=nuke&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) ![logoru](https://img.shields.io/static/v1?label=&message=logoru&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) ![statuser](https://img.shields.io/static/v1?label=&message=statuser&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) | 39 | | [![Python](https://img.shields.io/static/v1?label=&message=Python&color=3C78A9&logo=python&logoColor=FFFFFF)](https://www.python.org/) | ![profile_stack](https://img.shields.io/static/v1?label=&message=profile_stack&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) ![Contribution-Hat](https://img.shields.io/static/v1?label=&message=Contribution-Hat&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605) | 40 | 41 | You can see a live example at my repo: [github.com/gleich/gleich](https://github.com/gleich/gleich) 42 | 43 | ## ⚙️ Config 44 | 45 | Configuration for the profile stack. Located by default in `stack.yml` at the root of your repository. Below is an example config: 46 | 47 | ```yml 48 | - name: Golang 49 | logo: go 50 | url: https://golang.org/ 51 | color: '#7FD6EA' 52 | projects: 53 | - url: https://github.com/gleich/fgh 54 | - url: https://github.com/gleich/gh_fsync 55 | - url: https://github.com/gleich/nuke 56 | - url: https://github.com/gleich/logoru 57 | - url: https://github.com/gleich/statuser 58 | 59 | - name: Python 60 | logo: python 61 | url: https://www.python.org/ 62 | color: '#3C78A9' 63 | projects: 64 | - url: https://github.com/gleich/profile_stack 65 | - url: https://github.com/gleich/Contribution-Hat 66 | ``` 67 | 68 | So for each technology, there are the following fields you need to fill in: 69 | 70 | | **Key** | **Example Value** | **Description** | **Default** | 71 | | ------------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------- | 72 | | `name` | Dart | Name of the technology | Required | 73 | | `logo` | dart | [Logo](https://simpleicons.org/) for the technology | Required | 74 | | `url` | https://flutter.dev/ | URL for the technology | Required | 75 | | `logo_color` | FFFFFF | Hex color code for the logo color | `#FFFFFF` | 76 | | `color` | 52C0F2 | Hex color code for the background color | Required | 77 | | `projects` | `- url: https://github.com/gleich/Personal-Site`
`- url: https://github.com/gleich/fgh` | List of GitHub project URLs or [project objects](#project-object) | Required | 78 | 79 | ### Project object 80 | 81 | You pass a list of YAML objects to the `projects` field. 82 | 83 | | **Key** | **Example Value** | **Description** | **Default** | 84 | | ------- | ---------------------------------------------- | ---------------------------------- | ----------- | 85 | | `url` | `https://github.com/gleich/Personal-Site` | URL to a GitHub project | Required | 86 | | `wip` | `true` | Mark a project as work-in-progress | `false` | 87 | 88 | ## 🤖 Action Configuration 89 | 90 | Here is an example config: 91 | 92 | ```yaml 93 | name: stack 94 | 95 | on: 96 | push: 97 | branches: 98 | - main 99 | 100 | jobs: 101 | profile_stack: 102 | runs-on: ubuntu-latest 103 | steps: 104 | - uses: actions/checkout@v2 105 | - uses: gleich/profile_stack@master 106 | with: 107 | path: config/stack.yml 108 | badges: false 109 | technology_emoji: 👨🏻‍💻 110 | project_emoji: ✨ 111 | ``` 112 | 113 | You can also configure the following when declaring your action: 114 | 115 | | **Key** | **Example Value** | **Description** | **Default** | 116 | | ------------------ | ----------------- | ----------------------------------------------------------------- | ----------- | 117 | | `path` | config/stack.yml | The path in your repository where the config file is located | `stack.yml` | 118 | | `badges` | `false` | Don't have badges, just plain old urls | `false` | 119 | | `technology_emoji` | 👨🏻‍💻 | The character to be displayed to the left of the Technology title | `💻` | 120 | | `project_emoji` | ✨ | The character to be displayed to the left of the Project title | `🚀` | 121 | 122 | ## 🙌 Contributing 123 | 124 | We would love to have you contribute! Please read the [contributing guide](CONTRIBUTING.md) before submitting a pull request. Thank you in advance! 125 | 126 | 127 | 128 | ## 👥 Contributors 129 | 130 | 131 | - **[@gleich](https://github.com/gleich)** 132 | 133 | - **[@cjdenio](https://github.com/cjdenio)** 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'profile_stack' 2 | description: "🚀 Display your tech stack on your GitHub profile's README" 3 | branding: 4 | icon: 'user' 5 | color: 'green' 6 | runs: 7 | using: 'docker' 8 | image: 'docker/prod.Dockerfile' 9 | inputs: 10 | path: 11 | required: false 12 | default: 'stack.yml' 13 | description: 'File path for the config file' 14 | badges: 15 | required: false 16 | default: true 17 | description: 'Badges or plain text in the table' 18 | technology_emoji: 19 | required: false 20 | default: '💻' 21 | description: 'The emoji for the technology column' 22 | project_emoji: 23 | required: false 24 | default: '🚀' 25 | description: 'The emoji for the projects column' 26 | output_file: 27 | required: false 28 | default: README.md 29 | description: 'File to output to' 30 | -------------------------------------------------------------------------------- /docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM rust:latest AS builder 3 | 4 | # Meta data 5 | LABEL maintainer="email@mattglei.ch" 6 | LABEL description="🚀 Display your tech stack on your GitHub profile's README" 7 | 8 | # File copy 9 | COPY . /usr/src/app 10 | WORKDIR /usr/src/app 11 | 12 | # Setup nightly 13 | RUN rustup toolchain install nightly && \ 14 | rustup default nightly 15 | 16 | # Install cargo-make 17 | ENV CARGO_MAKE_VERSION 0.33.0 18 | ENV CARGO_MAKE_TMP_DIR /tmp/setup-rust-cargo-make 19 | RUN mkdir ${CARGO_MAKE_TMP_DIR} && \ 20 | wget -qO ${CARGO_MAKE_TMP_DIR}/cargo-make.zip https://github.com/sagiegurari/cargo-make/releases/download/${CARGO_MAKE_VERSION}/cargo-make-v${CARGO_MAKE_VERSION}-x86_64-unknown-linux-musl.zip && \ 21 | unzip -d ${CARGO_MAKE_TMP_DIR} ${CARGO_MAKE_TMP_DIR}/cargo-make.zip && \ 22 | mv ${CARGO_MAKE_TMP_DIR}/cargo-make-v${CARGO_MAKE_VERSION}-x86_64-unknown-linux-musl/cargo-make /usr/local/bin 23 | 24 | # Binary build 25 | RUN cargo make build-rust-dev 26 | 27 | # Copy of binary to smaller image 28 | # hadolint ignore=DL3006,DL3007 29 | FROM debian:stable-slim 30 | WORKDIR / 31 | COPY --from=builder /usr/src/app/target/debug/profile_stack . 32 | 33 | # Install needed deps 34 | # hadolint ignore=DL3008 35 | RUN apt-get update -y \ 36 | && apt-get install -y --no-install-recommends libpq5 ca-certificates libssl-dev \ 37 | && apt-get clean \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | # Setting env vars 41 | ENV RUST_LOG info 42 | ENV RUST_BACKTRACE 1 43 | 44 | CMD ["./profile_stack"] 45 | -------------------------------------------------------------------------------- /docker/platform.Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint ignore=DL3007 2 | FROM rust:latest AS builder 3 | 4 | # Meta data 5 | LABEL maintainer="email@mattglei.ch" 6 | LABEL description="🚀 Display your tech stack on your GitHub profile's README" 7 | 8 | # File copy 9 | COPY . /usr/src/app 10 | WORKDIR /usr/src/app 11 | 12 | # Setup nightly 13 | RUN rustup toolchain install nightly && \ 14 | rustup default nightly 15 | 16 | # Install cargo-make 17 | ENV CARGO_MAKE_VERSION 0.32.16 18 | ENV CARGO_MAKE_TMP_DIR /tmp/setup-rust-cargo-make 19 | RUN mkdir ${CARGO_MAKE_TMP_DIR} && \ 20 | wget -qO ${CARGO_MAKE_TMP_DIR}/cargo-make.zip https://github.com/sagiegurari/cargo-make/releases/download/${CARGO_MAKE_VERSION}/cargo-make-v${CARGO_MAKE_VERSION}-x86_64-unknown-linux-musl.zip && \ 21 | unzip -d ${CARGO_MAKE_TMP_DIR} ${CARGO_MAKE_TMP_DIR}/cargo-make.zip && \ 22 | mv ${CARGO_MAKE_TMP_DIR}/cargo-make-v${CARGO_MAKE_VERSION}-x86_64-unknown-linux-musl/cargo-make /usr/local/bin 23 | 24 | # Binary build 25 | RUN cargo make build-rust-prod 26 | 27 | # Copy of binary to smaller image 28 | # hadolint ignore=DL3006,DL3007 29 | FROM debian:stable-slim 30 | WORKDIR / 31 | COPY --from=builder /usr/src/app/target/release/profile_stack /usr/local/bin 32 | 33 | # Install needed deps 34 | # hadolint ignore=DL3008 35 | RUN apt-get update -y \ 36 | && apt-get install -y --no-install-recommends libpq5 ca-certificates libssl-dev git \ 37 | && apt-get clean \ 38 | && rm -rf /var/lib/apt/lists/* 39 | -------------------------------------------------------------------------------- /docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/gleich/profile_stack:platform 2 | 3 | ENV RUST_LOG info 4 | ENV RUST_BACKTRACE 1 5 | 6 | CMD ["profile_stack"] 7 | -------------------------------------------------------------------------------- /fsync.yml: -------------------------------------------------------------------------------- 1 | commit_message: 'chore(syncing): sync files to latest version' 2 | replace: 3 | - before: project_name 4 | after: profile_stack 5 | - before: project_description 6 | after: 🚀 Display your tech stack on your GitHub profile's README 7 | - before: github_username 8 | after: gleich 9 | - before: project_author_email 10 | after: email@mattglei.ch 11 | - before: docker_username 12 | after: mattgleich 13 | - before: project_author_full_name 14 | after: Matthew Gleich 15 | files: 16 | - path: .gitignore 17 | source: https://github.com/gleich/rust_template/blob/main/.gitignore 18 | # Documentation 19 | - path: CONTRIBUTING.md 20 | source: https://github.com/gleich/rust_template/blob/main/CONTRIBUTING.md 21 | - path: LICENSE 22 | source: https://github.com/gleich/gleich/blob/master/standard_documents/licenses/MPL-2.0.txt 23 | - path: .github/ISSUE_TEMPLATE/bug.md 24 | source: https://github.com/gleich/rust_template/blob/main/.github/ISSUE_TEMPLATE/bug.md 25 | - path: .github/ISSUE_TEMPLATE/feature.md 26 | source: https://github.com/gleich/rust_template/blob/main/.github/ISSUE_TEMPLATE/feature.md 27 | - path: .github/PULL_REQUEST_TEMPLATE.md 28 | source: https://github.com/gleich/rust_template/blob/main/.github/PULL_REQUEST_TEMPLATE.md 29 | # Dockerfiles 30 | - path: docker/dev.Dockerfile 31 | source: https://github.com/gleich/rust_template/blob/main/docker/dev.Dockerfile 32 | # GitHub Action Workflows 33 | - path: .github/workflows/contributors.yml 34 | source: https://github.com/gleich/gleich/blob/master/standard_documents/workflows/contributors.yml 35 | - path: .github/workflows/fsync.yml 36 | source: https://github.com/gleich/gleich/blob/master/standard_documents/workflows/fsync.yml 37 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | indent_style = "Block" 2 | reorder_imports = true 3 | imports_granularity = "Module" 4 | normalize_comments = true 5 | reorder_impl_items = true 6 | hard_tabs = false 7 | format_strings = true 8 | format_code_in_doc_comments = true 9 | force_multiline_blocks = true 10 | fn_single_line = true 11 | -------------------------------------------------------------------------------- /src/conf.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::{env, fs}; 3 | 4 | use anyhow::Context; 5 | use serde::Deserialize; 6 | 7 | #[derive(PartialEq, Debug)] 8 | pub struct Env { 9 | pub badges: bool, 10 | pub technology_emoji: char, 11 | pub project_emoji: char, 12 | pub output_file: PathBuf, 13 | pub config_filename: PathBuf, 14 | } 15 | 16 | fn get_env_var(name: &str, default: &str) -> Result { 17 | let value = env::var(format!("INPUT_{}", name.to_uppercase())); 18 | Ok(value.unwrap_or(default.to_string())) 19 | } 20 | 21 | pub fn env_vars() -> Result { 22 | Ok(Env { 23 | badges: get_env_var("badges", "true")?.parse()?, 24 | technology_emoji: get_env_var("technology_emoji", "💻")?.parse()?, 25 | project_emoji: get_env_var("project_emoji", "🚀")?.parse()?, 26 | config_filename: Path::new(&get_env_var("path", "stack.yml")?).to_owned(), 27 | output_file: Path::new(&get_env_var("output_file", "README.md")?).to_owned(), 28 | }) 29 | } 30 | 31 | #[derive(PartialEq, Deserialize, Debug)] 32 | pub struct Project { 33 | pub url: String, 34 | #[serde(default = "bool::default")] 35 | pub wip: bool, 36 | } 37 | 38 | fn default_color() -> String { String::from("#FFFFFF") } 39 | #[derive(PartialEq, Deserialize, Debug)] 40 | pub struct Technology { 41 | pub name: String, 42 | pub logo: String, 43 | #[serde(default = "default_color")] 44 | pub logo_color: String, 45 | pub url: String, 46 | pub color: String, 47 | pub projects: Vec, 48 | } 49 | 50 | pub fn config_file(env_conf: &Env) -> Result, anyhow::Error> { 51 | let content = fs::read_to_string(&env_conf.config_filename)?; 52 | let deserialized: Vec = 53 | serde_yaml::from_str(&content).context("Deserialize failed")?; 54 | Ok(deserialized) 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use std::fs::File; 60 | use std::io::Write; 61 | 62 | use super::*; 63 | 64 | #[test] 65 | fn test_config_file() -> Result<(), anyhow::Error> { 66 | // Creating a test file 67 | let tmp_dir = "tests"; 68 | fs::create_dir(tmp_dir)?; 69 | let config_path = "tests/tmp.yml"; 70 | let readme_path = "content/STACK.md"; 71 | let mut file = File::create(config_path)?; 72 | file.write_all( 73 | b"- name: Golang 74 | logo: go 75 | url: https://golang.org/ 76 | logo_color: \"#201020\" 77 | color: \"#7FD6EA\" 78 | projects: 79 | - url: https://github.com/gleich/fgh 80 | 81 | - name: Python 82 | logo: python 83 | url: https://www.python.org/ 84 | color: \"#3C78A9\" 85 | projects: 86 | - url: https://github.com/gleich/profile_stack 87 | - url: https://github.com/gleich/test 88 | wip: true", 89 | )?; 90 | 91 | // Getting config data 92 | let file_conf = config_file(&Env { 93 | config_filename: Path::new(config_path).to_owned(), 94 | badges: true, 95 | technology_emoji: ' ', 96 | project_emoji: ' ', 97 | output_file: Path::new(readme_path).to_owned(), 98 | })?; 99 | fs::remove_dir_all(tmp_dir)?; 100 | 101 | assert_eq!( 102 | file_conf, 103 | vec![ 104 | Technology { 105 | name: String::from("Golang"), 106 | logo_color: String::from("#201020"), 107 | logo: String::from("go"), 108 | url: String::from("https://golang.org/"), 109 | color: String::from("#7FD6EA"), 110 | projects: vec![Project { 111 | url: String::from("https://github.com/gleich/fgh"), 112 | wip: false 113 | },] 114 | }, 115 | Technology { 116 | name: String::from("Python"), 117 | logo: String::from("python"), 118 | logo_color: String::from("#FFFFFF"), 119 | url: String::from("https://www.python.org/"), 120 | color: String::from("#3C78A9"), 121 | projects: vec![ 122 | Project { 123 | url: String::from("https://github.com/gleich/profile_stack"), 124 | wip: false 125 | }, 126 | Project { 127 | url: String::from("https://github.com/gleich/test"), 128 | wip: true 129 | } 130 | ] 131 | } 132 | ] 133 | ); 134 | 135 | Ok(()) 136 | } 137 | 138 | #[test] 139 | fn test_env_vars() -> Result<(), anyhow::Error> { 140 | assert_eq!( 141 | env_vars()?, 142 | Env { 143 | config_filename: Path::new("stack.yml").to_owned(), 144 | badges: true, 145 | technology_emoji: '💻', 146 | project_emoji: '🚀', 147 | output_file: Path::new("README.md").to_owned() 148 | } 149 | ); 150 | Ok(()) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Stdio}; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::conf::Env; 6 | 7 | const BINARY: &str = "git"; 8 | 9 | pub fn commit_and_push(env_var_conf: &Env) -> Result<()> { 10 | Command::new(BINARY) 11 | .arg("config") 12 | .arg("--global") 13 | .arg("user.email") 14 | .arg("action@github.com") 15 | .output() 16 | .context("Failed to set commit email")?; 17 | Command::new(BINARY) 18 | .arg("config") 19 | .arg("--global") 20 | .arg("user.name") 21 | .arg("Publishing Bot") 22 | .output() 23 | .context("Failed to set commit name")?; 24 | Command::new(BINARY) 25 | .arg("add") 26 | .arg(&env_var_conf.output_file) 27 | .output() 28 | .context("Failed to stage changes")?; 29 | Command::new(BINARY) 30 | .arg("commit") 31 | .arg("-m") 32 | .arg("Update profile stack") 33 | .output() 34 | .context("Failed to commit staged changes")?; 35 | Command::new(BINARY) 36 | .arg("push") 37 | .output() 38 | .context("Failed to push committed changes")?; 39 | Ok(()) 40 | } 41 | 42 | pub fn repo_owner() -> Result { 43 | let remote = Command::new(BINARY) 44 | .arg("config") 45 | .arg("--get") 46 | .arg("remote.origin.url") 47 | .stdout(Stdio::piped()) 48 | .output() 49 | .context("Failed to get remote URL")?; 50 | Ok(String::from_utf8(remote.stdout)? 51 | .split("/") 52 | .collect::>() 53 | .get(3) 54 | .unwrap() 55 | .to_string()) 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use anyhow::Result; 61 | 62 | #[test] 63 | fn test_repo_owner() -> Result<()> { 64 | assert_eq!(String::from("gleich"), super::repo_owner()?); 65 | Ok(()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::{env, fs}; 3 | 4 | use fs::File; 5 | use tracing::{info, warn}; 6 | 7 | mod conf; 8 | mod git; 9 | mod readme; 10 | 11 | fn main() { 12 | tracing_subscriber::fmt::init(); 13 | 14 | env::set_current_dir("/github/workspace/") 15 | .expect("Failed to change directory to repo location"); 16 | 17 | // Getting configuration 18 | let env_var_conf = conf::env_vars().expect("Failed to get env var config"); 19 | let file_conf = conf::config_file(&env_var_conf) 20 | .expect("Failed to get configuration from file (CHECK FOR NEW UPDATE)"); 21 | info!("Got configuration inputs"); 22 | 23 | // Generating table 24 | let repo_owner = git::repo_owner().expect("Failed gto get repo owner"); 25 | let table = readme::gen_table(&env_var_conf, &file_conf, &repo_owner) 26 | .expect("Failed to generate table"); 27 | info!("Generated table"); 28 | 29 | // Inserting table into README 30 | let readme_content = fs::read_to_string(&env_var_conf.output_file).expect(&format!( 31 | "Failed to read from {}", 32 | &env_var_conf.output_file.display() 33 | )); 34 | let patched_content = readme::insert_table(&readme_content, &table) 35 | .expect("Failed to insert table to README data"); 36 | 37 | // Writing the changes to the README 38 | if readme_content != patched_content { 39 | // Writing changes 40 | let mut readme_file = File::create(&&env_var_conf.output_file) 41 | .expect("Failed to create README.md file struct"); 42 | readme_file 43 | .write_all(patched_content.as_bytes()) 44 | .expect(&format!( 45 | "Failed to write changes to {}", 46 | &env_var_conf.output_file.display() 47 | )); 48 | info!("Wrote changes to {}", &env_var_conf.output_file.display()); 49 | 50 | git::commit_and_push(&env_var_conf).expect("Failed to commit and push changes"); 51 | 52 | info!("Committed changes! Have a good day :)") 53 | } else { 54 | warn!("No changes to README.md") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/readme.rs: -------------------------------------------------------------------------------- 1 | use crate::conf; 2 | 3 | use anyhow::bail; 4 | use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; 5 | 6 | pub fn gen_table( 7 | env_conf: &conf::Env, 8 | file_conf: &Vec, 9 | repo_owner: &str, 10 | ) -> Result { 11 | let mut lines = Vec::new(); 12 | 13 | // Add header 14 | lines.push(format!( 15 | "| {} **Technology** | {} **Projects** |", 16 | &env_conf.technology_emoji, &env_conf.project_emoji 17 | )); 18 | lines.push(String::from("| - | - |")); 19 | 20 | const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); 21 | 22 | for tech in file_conf.iter() { 23 | let mut projects = Vec::new(); 24 | for project in tech.projects.iter() { 25 | // Getting repo name 26 | let url_chunks: Vec<&str> = project.url.split('/').collect(); 27 | let project_repo_owner = url_chunks.get(3).unwrap_or(&"").to_owned(); 28 | let project_repo_name = url_chunks.get(4).unwrap_or(&"").to_owned(); 29 | if project_repo_name.is_empty() { 30 | bail!("Failed to extract repository name from {}", project.url) 31 | } 32 | 33 | // Adding (WIP) to message if wip 34 | let mut message = String::from(project_repo_name); 35 | if project.wip { 36 | message.push_str("%20(WIP)"); 37 | } 38 | 39 | // Add badge/url 40 | let display_text = if repo_owner == project_repo_owner { 41 | String::from(project_repo_name) 42 | } else { 43 | format!("{}/{}", project_repo_owner, project_repo_name) 44 | }; 45 | if env_conf.badges { 46 | projects.push(format!("[![{}](https://img.shields.io/static/v1?label=&message={}&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605)]({})", display_text, utf8_percent_encode(&message, FRAGMENT), project.url)); 47 | } else { 48 | projects.push(format!("[{}]({})", display_text, message)); 49 | } 50 | } 51 | let joined_projects = projects.join(" "); 52 | 53 | if env_conf.badges { 54 | lines.push(format!("| [![{}](https://img.shields.io/static/v1?label=&message={}&color={}&logo={}&logoColor={})]({}) | {} |", 55 | tech.name, 56 | utf8_percent_encode(&tech.name, FRAGMENT), 57 | tech.color.replace("#", ""), 58 | utf8_percent_encode(&tech.logo, FRAGMENT), 59 | tech.logo_color.replace("#", ""), 60 | tech.url, 61 | joined_projects, 62 | )) 63 | } else { 64 | lines.push(format!( 65 | "| [{}]({}) | {} |", 66 | tech.name, tech.url, joined_projects, 67 | )); 68 | } 69 | } 70 | 71 | Ok(lines.join("\n")) 72 | } 73 | 74 | const TABLE_START_MSG: &'static str = ""; 75 | const TABLE_STOP_MSG: &'static str = ""; 76 | 77 | pub fn insert_table(readme: &str, table: &str) -> Result { 78 | let mut new_lines: Vec<&str> = readme.lines().collect(); 79 | 80 | // Finding start and stop headers 81 | let mut found = false; 82 | let mut start = 0; 83 | let mut stop = 0; 84 | for (i, line) in readme.lines().enumerate() { 85 | match line { 86 | TABLE_START_MSG => { 87 | found = true; 88 | start = i + 1; 89 | } 90 | TABLE_STOP_MSG => stop = i, 91 | _ => continue, 92 | } 93 | } 94 | 95 | // Replacing (if found) or inserting table at the end (if not found) 96 | if found { 97 | for _ in start..stop { 98 | new_lines.remove(start); 99 | } 100 | for (i, line) in table.lines().enumerate() { 101 | new_lines.insert(start + i, line); 102 | } 103 | } else { 104 | new_lines.push(TABLE_START_MSG); 105 | for line in table.lines() { 106 | new_lines.push(line); 107 | } 108 | new_lines.push(TABLE_STOP_MSG); 109 | } 110 | 111 | Ok(new_lines.join("\n")) 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use std::path::Path; 117 | 118 | use crate::conf::{Env, Project, Technology}; 119 | 120 | use super::*; 121 | 122 | const TEST_TABLE: &str = "| 💻 **Technology** | 🚀 **Projects** |\n| - | - |\n| [![Go Language](https://img.shields.io/static/v1?label=&message=Go%20Language&color=7FD6EA&logo=go&logoColor=201020)](https://golang.org/) | [![fgh](https://img.shields.io/static/v1?label=&message=fgh&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605)](https://github.com/gleich/fgh) |\n| [![Python](https://img.shields.io/static/v1?label=&message=Python&color=3C78A9&logo=python&logoColor=FFFFFF)](https://www.python.org/) | [![profile_stack](https://img.shields.io/static/v1?label=&message=profile_stack&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605)](https://github.com/gleich/profile_stack) [![github/test](https://img.shields.io/static/v1?label=&message=test%20(WIP)&color=000605&logo=github&logoColor=FFFFFF&labelColor=000605)](https://github.com/github/test) |"; 123 | 124 | #[test] 125 | fn test_gen_table() -> Result<(), anyhow::Error> { 126 | assert_eq!( 127 | gen_table( 128 | &Env { 129 | config_filename: Path::new("stack.yml").to_owned(), 130 | badges: true, 131 | technology_emoji: '💻', 132 | project_emoji: '🚀', 133 | output_file: Path::new("README.md").to_owned() 134 | }, 135 | &vec![ 136 | Technology { 137 | name: String::from("Go Language"), 138 | logo_color: String::from("#201020"), 139 | logo: String::from("go"), 140 | url: String::from("https://golang.org/"), 141 | color: String::from("#7FD6EA"), 142 | projects: vec![Project { 143 | url: String::from("https://github.com/gleich/fgh"), 144 | wip: false 145 | },] 146 | }, 147 | Technology { 148 | name: String::from("Python"), 149 | logo: String::from("python"), 150 | logo_color: String::from("#FFFFFF"), 151 | url: String::from("https://www.python.org/"), 152 | color: String::from("#3C78A9"), 153 | projects: vec![ 154 | Project { 155 | url: String::from("https://github.com/gleich/profile_stack"), 156 | wip: false 157 | }, 158 | Project { 159 | url: String::from("https://github.com/github/test"), 160 | wip: true 161 | } 162 | ] 163 | } 164 | ], 165 | "gleich" 166 | )?, 167 | TEST_TABLE 168 | ); 169 | 170 | Ok(()) 171 | } 172 | 173 | #[test] 174 | fn test_insert_table() -> Result<(), anyhow::Error> { 175 | // No table 176 | assert_eq!( 177 | insert_table("# Hello World!", TEST_TABLE)?, 178 | format!( 179 | "# Hello World!\n{}\n{}\n{}", 180 | TABLE_START_MSG, TEST_TABLE, TABLE_STOP_MSG 181 | ) 182 | ); 183 | 184 | // With same table 185 | assert_eq!( 186 | insert_table( 187 | &format!( 188 | "# Hello World!\n{}\n{}\n{}", 189 | TABLE_START_MSG, TEST_TABLE, TABLE_STOP_MSG 190 | ), 191 | TEST_TABLE 192 | )?, 193 | format!( 194 | "# Hello World!\n{}\n{}\n{}", 195 | TABLE_START_MSG, TEST_TABLE, TABLE_STOP_MSG 196 | ) 197 | ); 198 | 199 | // With different table 200 | assert_eq!( 201 | insert_table( 202 | &format!( 203 | "{}\n{}\n| Testing | Testing |\n{}", 204 | TABLE_START_MSG, TEST_TABLE, TABLE_STOP_MSG 205 | ), 206 | TEST_TABLE 207 | )?, 208 | format!("{}\n{}\n{}", TABLE_START_MSG, TEST_TABLE, TABLE_STOP_MSG) 209 | ); 210 | 211 | Ok(()) 212 | } 213 | } 214 | --------------------------------------------------------------------------------