├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── contributors.yml │ └── fsync.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── fsync.yml ├── go.mod ├── go.sum ├── main.go ├── pkg ├── api │ ├── deployments.go │ ├── projects.go │ └── urls.go ├── conf │ ├── model.go │ └── read.go └── lights │ ├── setup.go │ └── update.go ├── setup.py └── verpi.service /.github/ISSUE_TEMPLATE/bug_report.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_request.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 | - [ ] I have read the **CONTRIBUTING.md** file 12 | - [ ] There is no duplicate open or closed pull request for this fix/addition/issue resolution. 13 | 14 | ## Original Issue 15 | 16 | This PR resolves #ISSUE_NUMBER_HERE 17 | 18 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # air 2 | tmp/ 3 | 4 | # goreleaser 5 | dist/ 6 | 7 | # MacOS Meta Data 8 | .DS_Store 9 | 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, built with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | # vendor/ 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at email@mattglei.ch. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👋 Welcome to verpi! Thank you for showing interest in contributing to verpi, we would love to have your contribution. Below are some details on how to contribute to verpi. Please read carefully! 4 | 5 | - [Contributing](#contributing) 6 | - [🐛 Requesting Features/Reporting Bugs](#-requesting-featuresreporting-bugs) 7 | - [➕ Adding/Changing code](#-addingchanging-code) 8 | - [⚠️ Notice](#️-notice) 9 | - [🧾 Process](#-process) 10 | - [✅ Checking Code](#-checking-code) 11 | - [ℹ️ General](#ℹ️-general) 12 | 13 | ## 🐛 Requesting Features/Reporting Bugs 14 | 15 | 1. Click on the "Issues" tab in the repo. 16 | 2. Make sure that the issue doesn't exist already by searching for it. 17 | 3. Pick the issue template. 18 | 4. Fill in the issue template. 19 | 20 | ## ➕ Adding/Changing code 21 | 22 | ### ⚠️ Notice 23 | 24 | This project uses [golangci-lint](https://github.com/golangci/golangci-lint) for code linting, please install it and format your code with `make lint-golangci`. 25 | 26 | ### 🧾 Process 27 | 28 | 1. Make an issue (see above) and check to make sure what you are trying to add/change doesn't already exist. 29 | 2. Create a branch with the name being the issue number. If you don't have contributor access just fork the repo. 30 | 3. Make and commit the changes. 31 | 4. [Validate code](#-checking-code) 32 | 5. Make the pull request! 33 | 6. Now someone on the team will review your PR. Congrats! 34 | 7. **Warning** once your PR gets merged the branch for it will automatically get deleted (only for contributors with contributor access). 35 | 36 | ### ✅ Checking Code 37 | 38 | If both the options stated below don't work for you can just make the changes if the CI jobs fail. 39 | 40 | #### 🐳 Docker Container 41 | 42 | You can check all the code inside of a docker container with all the dependencies installed by running `make docker-lint` and `make test-in-docker`. Both of these commands will build the image for you and run it. No need to install anything! Check the output to make sure you don't have any issues to resolve. 43 | 44 | #### ✍️ Manually 45 | 46 | | **Program** | **Description** | 47 | | ---------------------------------------------------------- | ---------------------------------- | 48 | | [golangci-lint](https://github.com/golangci/golangci-lint) | Linter for all golang files | 49 | | [goreleaser](https://github.com/goreleaser/goreleaser) | Release automation for the program | 50 | | [hadolint](https://github.com/hadolint/hadolint) | Linter for all Dockerfiles | 51 | 52 | Once you have the programs above installed please run `make local-test` and `make local-lint`. If you don't get any errors your code checks out! 53 | 54 | ## ℹ️ General 55 | 56 | - When you take on an issue please set yourself as the assignee or leave a comment. This will let the maintainers and other contributors that know that you are going to work on it. 57 | - **This project syncs files from other repos**. To check where certain files come from check the `fsync.yml` file (sometimes located in `/github/fsync.yml`). This means if you wanna change that file you need to change the file from the source repo. 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Matt Gleich 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ########## 2 | # Building 3 | ########## 4 | 5 | build-go: 6 | GOOS=linux GOARCH=arm go build -v -o dist/verpi 7 | 8 | ######### 9 | # Linting 10 | ######### 11 | 12 | lint-golangci: 13 | golangci-lint run 14 | lint-gomod: 15 | go mod tidy 16 | git diff --exit-code go.mod 17 | git diff --exit-code go.sum 18 | lint-goreleaser: 19 | goreleaser check 20 | 21 | ######### 22 | # Testing 23 | ######### 24 | 25 | test-go: 26 | go get -v -t -d ./... 27 | go test -v ./... 28 | 29 | ########## 30 | # Grouping 31 | ########## 32 | 33 | # Testing 34 | local-test: test-go 35 | # Linting 36 | local-lint: lint-golangci lint-goreleaser lint-hadolint lint-gomod 37 | # Build 38 | local-build: build-docker-prod build-docker-dev build-docker-dev-lint 39 | deploy: build-go 40 | scp dist/verpi $(PI):/home/pi/verpi 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # verpi 4 | 5 | 🚥 See the status of your vercel deployments on the pimoroni blinkt! 6 | 7 | ![build](https://github.com/gleich/verpi/workflows/build/badge.svg) 8 | ![test](https://github.com/gleich/verpi/workflows/test/badge.svg) 9 | ![lint](https://github.com/gleich/verpi/workflows/lint/badge.svg) 10 | ![release](https://github.com/gleich/verpi/workflows/release/badge.svg) 11 | 12 | - [verpi](#verpi) 13 | - [🎥 Demo](#-demo) 14 | - [🚥 Setup your own version](#-setup-your-own-version) 15 | - [💵 Getting the parts](#-getting-the-parts) 16 | - [🚥 Install the pimoroni blinkt](#-install-the-pimoroni-blinkt) 17 | - [🖼️ Flash an image](#️-flash-an-image) 18 | - [🥾 Headless boot](#-headless-boot) 19 | - [🚀 Installing the needed deps](#-installing-the-needed-deps) 20 | - [🔑 Creating a token](#-creating-a-token) 21 | - [🚀 Installing verpi](#-installing-verpi) 22 | - [👋 Uninstalling verpi](#-uninstalling-verpi) 23 | - [⚙️ Configuring verpi](#️-configuring-verpi) 24 | - [🙌 Contributing](#-contributing) 25 | - [👥 Contributors](#-contributors) 26 | 27 | ## 🎥 Demo 28 | 29 | https://user-images.githubusercontent.com/43759105/132117267-ec147769-a7af-4f61-bdc8-269a1fd8a466.mp4 30 | 31 | Note: Normally all 8 LEDs would be lit up but I only have 6 projects on vercel so only 6 are lit up. 32 | 33 | ## 🚥 Setup your own version 34 | 35 | Setting up verpi for yourself is very simple! Just follow the instructions below. If you have any problems please make an issue on this repo. 36 | 37 | ### 💵 Getting the parts 38 | 39 | - [Case, Card, Heat sink, and other tools](https://www.amazon.com/iUniker-Raspberry-Starter-Acrylic-Clear/dp/B075FLGWJL/ref=sr_1_19?dchild=1&keywords=raspberry%2Bpi%2Bzero%2Bw&qid=1630780013&sr=8-19&th=1) - _$8.99_ 40 | - [Raspberry Pi Zero WH (Zero W with Headers)](https://www.amazon.com/Raspberry-Pi-Zero-WH-Headers/dp/B07BHMRTTY/ref=sr_1_9?crid=2VW24AF5F0854&dchild=1&keywords=raspberry+pi+zero+w&qid=1630853169&sprefix=raspberry+pi+zero+%2Caps%2C189&sr=8-9) - _$39.95_ 41 | - [Power supply](https://www.amazon.com/CanaKit-Raspberry-Supply-Adapter-Listed/dp/B00MARDJZ4/ref=sr_1_3?crid=113RLXDYJ9KMZ&dchild=1&keywords=raspberry+pi+charger&qid=1630853230&sprefix=raspberry+pi+charger%2Caps%2C176&sr=8-3) - _$9.95_ 42 | - [Pimoroni blinkt](https://shop.pimoroni.com/products/blinkt) - _~$8.32_ 43 | 44 | ### 🚥 Install the pimoroni blinkt 45 | 46 | To install the pimoroni blinkt simply set it on the GPIO headers. The correct way round is where it has curves on the top that match the corners of your Raspberry Pi. 47 | 48 | ### 🖼️ Flash an image 49 | 50 | To flash an operating system to the micro sd card please use [the Raspberry Pi Imager program](https://www.raspberrypi.org/software/). You only need Raspberry Pi OS Lite for verpi to operate (with some dependencies installed later). 51 | 52 | ### 🥾 Headless boot 53 | 54 | So you can ssh to the pi on boot please follow this tutorial for setting up the raspberry pi headless. It should only take a few seconds to do as you only need to make two small files. 55 | 56 | [Headless Raspberry Pi Tutorial](https://pimylifeup.com/headless-raspberry-pi-setup/). 57 | 58 | ### 🚀 Installing the needed deps 59 | 60 | Before actually installing verpi you need a few deps installed. Please run the following terminal command on your raspberry pi: 61 | 62 | ```sh 63 | sudo apt -yq update && sudo apt -yq upgrade && sudo apt install -yq wiringpi git wget 64 | ``` 65 | 66 | ### 🔑 Creating a token 67 | 68 | 1. Create a token on [vercel's token page](https://vercel.com/account/tokens) with a name of verpi. 69 | 2. Copy the token to your clipboard. 70 | 3. On the raspberry pi add a file to `~/.config/verpi/` called `conf.toml`. 71 | 4. Add the following to that file, replacing `` with your token. 72 | 73 | ```toml 74 | token = "" 75 | ``` 76 | 77 | ### 🚀 Installing verpi 78 | 79 | To install verpi just run the following command on your raspberry pi: 80 | 81 | ```sh 82 | wget -q -O - https://raw.githubusercontent.com/gleich/verpi/master/setup.py | sudo python3 - install 83 | ``` 84 | 85 | This script will install a temporary version of golang to produce a binary and install a systemd service. It will not mess with whatever version of go you might already have installed on the pi. 86 | 87 | ### 👋 Uninstalling verpi 88 | 89 | To uninstall verpi just run the following command on your raspberry pi: 90 | 91 | ```sh 92 | wget -q -O - https://raw.githubusercontent.com/gleich/verpi/master/setup.py | sudo python3 - uninstall 93 | ``` 94 | 95 | ## ⚙️ Configuring verpi 96 | 97 | Using the configuration file located at `~/.config/verpi/conf.toml` you can change the brightness of the lights. By default, the brightness is set to 0.1. To turn the lights off you can set the brightness to 0.0 as seen below: 98 | 99 | ```toml 100 | brightness = 0.0 101 | ``` 102 | 103 | Below is an example configuration file: 104 | 105 | ```toml 106 | token = "" 107 | brightness = 0.0 108 | ``` 109 | 110 | ## 🙌 Contributing 111 | 112 | Before contributing please read the [CONTRIBUTING.md file](https://github.com/gleich/verpi/blob/master/CONTRIBUTING.md). 113 | 114 | 115 | 116 | ## 👥 Contributors 117 | 118 | - **[@gleich](https://github.com/gleich)** 119 | 120 | 121 | -------------------------------------------------------------------------------- /fsync.yml: -------------------------------------------------------------------------------- 1 | commit_message: 'chore(syncing): sync files to latest version' 2 | replace: 3 | - before: project_name 4 | after: verpi 5 | - before: project_description 6 | after: 🚥 See the status of your vercel deployments on the pimoroni blinkt! 7 | - before: github_username 8 | after: gleich 9 | - before: project_author_email 10 | after: email@mattglei.ch 11 | - before: project_author_full_name 12 | after: Matt Gleich 13 | files: 14 | - path: .gitignore 15 | source: https://github.com/gleich/go_template/blob/master/.gitignore 16 | - path: LICENSE.md 17 | source: https://github.com/gleich/gleich/blob/master/standard_documents/licenses/MIT_LICENSE.md 18 | - path: CONTRIBUTING.md 19 | source: https://github.com/gleich/go_template/blob/master/CONTRIBUTING.md 20 | - path: CODE_OF_CONDUCT.md 21 | source: https://github.com/gleich/gleich/blob/master/standard_documents/CODE_OF_CONDUCT.md 22 | - path: .github/PULL_REQUEST_TEMPLATE.md 23 | source: https://github.com/gleich/gleich/blob/master/standard_documents/templates/pull_request.md 24 | - path: .github/ISSUE_TEMPLATE/bug_report.md 25 | source: https://github.com/gleich/gleich/blob/master/standard_documents/templates/issue_bug.md 26 | - path: .github/ISSUE_TEMPLATE/feature_request.md 27 | source: https://github.com/gleich/gleich/blob/master/standard_documents/templates/issue_feature.md 28 | - path: .github/workflows/contributors.yml 29 | source: https://github.com/gleich/gleich/blob/master/standard_documents/workflows/contributors.yml 30 | - path: .github/workflows/fsync.yml 31 | source: https://github.com/gleich/gleich/blob/master/standard_documents/workflows/fsync.yml 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gleich/verpi 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/fatih/color v1.13.0 // indirect 7 | github.com/mattn/go-colorable v0.1.10 // indirect 8 | github.com/mattn/go-isatty v0.0.14 // indirect 9 | github.com/pelletier/go-toml/v2 v2.0.0-beta.3 10 | github.com/pkg/errors v0.9.1 // indirect 11 | github.com/wayneashleyberry/truecolor v1.0.1 // indirect 12 | golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 // indirect 13 | ) 14 | 15 | require ( 16 | github.com/alexellis/blinkt_go v0.0.0-20180120180744-cc0ca163e0bc 17 | github.com/alexellis/rpi v0.0.0-20170116141016-ab6a4e79f0bd // indirect 18 | github.com/gleich/lumber/v2 v2.1.2 19 | github.com/rogpeppe/rog-go v0.0.0-20210831093509-3c886aea54f1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexellis/blinkt_go v0.0.0-20180120180744-cc0ca163e0bc h1:J/jqFQqoBMa8TD5W0FunVxE3kuWaE+ZdiSrZ5L99SVg= 2 | github.com/alexellis/blinkt_go v0.0.0-20180120180744-cc0ca163e0bc/go.mod h1:IeFkr+RPLd7XL6LrhpHfSZerUIwVvVhFMwwrOr/FhYU= 3 | github.com/alexellis/rpi v0.0.0-20170116141016-ab6a4e79f0bd h1:Mx9cUprDyCdGzjWwFRZmrL/G4Cs89/EB8QcZ6j+Mx1Y= 4 | github.com/alexellis/rpi v0.0.0-20170116141016-ab6a4e79f0bd/go.mod h1:/u5IOmNVyGUK5Y/TV6Dk+4BlqWTEhmvbzvpKPSeZXZQ= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 9 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 10 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 11 | github.com/gleich/lumber/v2 v2.1.2 h1:fzfrGXI2isMCtzgC2X/iyoWP7Pikx2939JV3m1IqZnk= 12 | github.com/gleich/lumber/v2 v2.1.2/go.mod h1:pkfSf79z7hol7GyiXlvjOP6SL0vCAtyK2blrluRcc78= 13 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 14 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 15 | github.com/mattn/go-colorable v0.1.10 h1:KWqbp83oZ6YOEgIbNW3BM1Jbe2tz4jgmWA9FOuAF8bw= 16 | github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 17 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 18 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 19 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 20 | github.com/pelletier/go-toml/v2 v2.0.0-beta.3 h1:PNCTU4naEJ8mKal97P3A2qDU74QRQGlv4FXiL1XDqi4= 21 | github.com/pelletier/go-toml/v2 v2.0.0-beta.3/go.mod h1:aNseLYu/uKskg0zpr/kbr2z8yGuWtotWf/0BpGIAL2Y= 22 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 23 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/rog-go v0.0.0-20210831093509-3c886aea54f1 h1:/OCwqErMqAvGqXRIHZUrPQjvLJ757WSqxnlphaw+9Os= 27 | github.com/rogpeppe/rog-go v0.0.0-20210831093509-3c886aea54f1/go.mod h1:k0jrzHKdCmJ6zT6iJj0pE0xqilSXprArlMF7cUBMO/w= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= 32 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 34 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 35 | github.com/wayneashleyberry/truecolor v1.0.1 h1:REnJBycjnvg0AFErbLx2GCmLLar8brlqm62kOKnRsGs= 36 | github.com/wayneashleyberry/truecolor v1.0.1/go.mod h1:fyL3jRES70g94n+Eu+XLhXYvcseza55ph8zlkmUKW7Q= 37 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210414055047-fe65e336abe0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 h1:QOQNt6vCjMpXE7JSK5VvAzJC1byuN3FgTNSBwf+CJgI= 42 | golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 47 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gleich/lumber/v2" 8 | "github.com/gleich/verpi/pkg/api" 9 | "github.com/gleich/verpi/pkg/conf" 10 | "github.com/gleich/verpi/pkg/lights" 11 | ) 12 | 13 | func main() { 14 | log := lumber.NewCustomLogger() 15 | log.Timezone = time.Local 16 | config, err := conf.Read(log) 17 | if err != nil { 18 | log.Fatal(err, "Failed to read from configuration file") 19 | } 20 | client := http.DefaultClient 21 | username, err := api.Username(log, config, client) 22 | if err != nil { 23 | log.Fatal(err, "Failed to get vercel username") 24 | } 25 | display := lights.Setup(log, config) 26 | 27 | for { 28 | // Rereading from configuration file to load any new changes 29 | config, err := conf.Read(log) 30 | if err != nil { 31 | log.Fatal(err, "Failed to read from configuration file") 32 | } 33 | 34 | if *config.Brightness == 0.0 { 35 | log.Info("Not updating lights because brightness set to 0") 36 | display.Clear() 37 | display.Show() 38 | time.Sleep(20 * time.Millisecond) 39 | continue 40 | } 41 | 42 | deployments, err := api.ProjectDeployments(log, username, config, client) 43 | if err != nil { 44 | log.Fatal(err, "Failed to get deployments") 45 | } 46 | 47 | lights.Update(log, config, deployments, display) 48 | 49 | time.Sleep(4 * time.Second) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/api/deployments.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/gleich/lumber/v2" 9 | "github.com/gleich/verpi/pkg/conf" 10 | ) 11 | 12 | // Get a list of the last 10 deployments 13 | func ProjectDeployments( 14 | log lumber.Logger, 15 | username string, 16 | config conf.Conf, 17 | client *http.Client, 18 | ) ([]string, error) { 19 | log.Info("Getting deployments") 20 | // Making request 21 | req, err := http.NewRequest("GET", baseURL+"/v8/projects", nil) 22 | if err != nil { 23 | return []string{}, err 24 | } 25 | req.Header.Add("Authorization", "Bearer "+config.Token) 26 | 27 | resp, err := client.Do(req) 28 | if err != nil { 29 | return []string{}, err 30 | } 31 | defer resp.Body.Close() 32 | 33 | // Reading response 34 | body, err := io.ReadAll(resp.Body) 35 | if err != nil { 36 | return []string{}, err 37 | } 38 | 39 | // Parsing response 40 | var data struct { 41 | Projects []struct { 42 | LatestDeployments []struct { 43 | Creator struct { 44 | Username string 45 | } 46 | ReadyState string 47 | } 48 | } 49 | } 50 | err = json.Unmarshal(body, &data) 51 | if err != nil { 52 | return []string{}, err 53 | } 54 | 55 | // Filtering and cleaning response to only include deployments from the user 56 | deployments := []string{} 57 | for _, project := range data.Projects { 58 | for _, deployment := range project.LatestDeployments { 59 | if deployment.Creator.Username == username && len(deployments) < 8 { 60 | deployments = append(deployments, deployment.ReadyState) 61 | break 62 | } 63 | } 64 | } 65 | 66 | log.Success("Got data for", len(deployments), "deployments") 67 | 68 | return deployments, nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/api/projects.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/gleich/lumber/v2" 10 | "github.com/gleich/verpi/pkg/conf" 11 | ) 12 | 13 | // Get the user's slack ID so we can filter 14 | func Username(log lumber.Logger, config conf.Conf, client *http.Client) (string, error) { 15 | log.Info("Getting the user's vercel username") 16 | // Making request 17 | req, err := http.NewRequest("GET", baseURL+"/www/user", nil) 18 | if err != nil { 19 | return "", err 20 | } 21 | req.Header.Add("Authorization", "Bearer "+config.Token) 22 | 23 | resp, err := client.Do(req) 24 | if err != nil { 25 | return "", err 26 | } 27 | defer resp.Body.Close() 28 | 29 | // Reading response 30 | body, err := io.ReadAll(resp.Body) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | // Parsing response 36 | var data struct { 37 | User struct { 38 | Username string 39 | } 40 | } 41 | err = json.Unmarshal(body, &data) 42 | if err != nil { 43 | return "", err 44 | } 45 | if data.User.Username == "" { 46 | return "", errors.New("No username found with access token") 47 | } 48 | 49 | log.Success("Got", data.User.Username+"'s", "vercel username") 50 | return data.User.Username, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/api/urls.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Base URL for the vercel api 4 | const baseURL = "https://api.vercel.com" 5 | -------------------------------------------------------------------------------- /pkg/conf/model.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | // Outline for the configuration file stored in ~/.config/verpi/conf.toml 4 | type Conf struct { 5 | Brightness *float64 6 | Token string 7 | } 8 | -------------------------------------------------------------------------------- /pkg/conf/read.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/gleich/lumber/v2" 9 | "github.com/pelletier/go-toml/v2" 10 | ) 11 | 12 | // Read from the configuration file and parse it 13 | func Read(log lumber.Logger) (Conf, error) { 14 | log.Info("Loading configuration") 15 | // Configuration location 16 | homeDir, err := os.UserHomeDir() 17 | if err != nil { 18 | return Conf{}, err 19 | } 20 | location := filepath.Join(homeDir, ".config", "verpi", "conf.toml") 21 | 22 | // Reading the binary from the file 23 | b, err := os.ReadFile(location) 24 | if err != nil { 25 | return Conf{}, err 26 | } 27 | 28 | // Parsing toml 29 | var data Conf 30 | err = toml.Unmarshal(b, &data) 31 | if err != nil { 32 | return Conf{}, err 33 | } 34 | 35 | // Validate config 36 | if data.Token == "" { 37 | return Conf{}, errors.New("token value in configuration file is required") 38 | } 39 | if data.Brightness == nil { 40 | defaultBrightness := 0.1 41 | data.Brightness = &defaultBrightness 42 | } 43 | 44 | log.Success("Loaded configuration") 45 | 46 | return data, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/lights/setup.go: -------------------------------------------------------------------------------- 1 | package lights 2 | 3 | import ( 4 | blinkt "github.com/alexellis/blinkt_go" 5 | "github.com/gleich/lumber/v2" 6 | "github.com/gleich/verpi/pkg/conf" 7 | ) 8 | 9 | // Setup the display 10 | func Setup(log lumber.Logger, config conf.Conf) *blinkt.Blinkt { 11 | log.Info("Setting up display") 12 | display := blinkt.NewBlinkt(*config.Brightness) 13 | display.SetClearOnExit(true) 14 | display.Setup() 15 | display.Clear() 16 | log.Success("Setup display") 17 | return &display 18 | } 19 | -------------------------------------------------------------------------------- /pkg/lights/update.go: -------------------------------------------------------------------------------- 1 | package lights 2 | 3 | import ( 4 | "strings" 5 | 6 | blinkt "github.com/alexellis/blinkt_go" 7 | "github.com/gleich/lumber/v2" 8 | "github.com/gleich/verpi/pkg/conf" 9 | ) 10 | 11 | // Update the lights based off the deployment statuses 12 | func Update(log lumber.Logger, config conf.Conf, deployments []string, display *blinkt.Blinkt) { 13 | display.SetBrightness(*config.Brightness) 14 | log.Info("Updating lights") 15 | for i, deployment := range deployments { 16 | switch strings.ToUpper(deployment) { 17 | case "READY": 18 | display.SetPixel(i, 0, 255, 0) // Green 19 | case "QUEUED": 20 | display.SetPixel(i, 255, 128, 0) // Yellow 21 | case "BUILDING": 22 | display.SetPixel(i, 255, 128, 0) // Yellow 23 | default: 24 | display.SetPixel(i, 255, 0, 0) // Red 25 | } 26 | } 27 | display.Show() 28 | log.Success("Updated lights") 29 | } 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import os 3 | import sys 4 | import time 5 | from urllib.request import urlopen 6 | 7 | tmp_dir = "/home/pi/verpi_build" 8 | go_binary_path = os.path.join(tmp_dir, "bin/go") 9 | systemd_service_path = "/etc/systemd/system/verpi.service" 10 | verpi_bin_path = "/usr/local/bin/verpi" 11 | 12 | 13 | def main() -> None: 14 | command = sys.argv[1] 15 | if command == "install": 16 | setup() 17 | install_go() 18 | clone_repo() 19 | compile() 20 | install_verpi() 21 | install_verpi_service() 22 | os.chdir("..") 23 | if os.path.exists(tmp_dir): 24 | shutil.rmtree(tmp_dir) 25 | elif command == "uninstall": 26 | uninstall() 27 | else: 28 | print(command, "isn't a valid command") 29 | exit(1) 30 | reboot() 31 | 32 | 33 | def setup() -> None: 34 | if os.path.exists(tmp_dir): 35 | shutil.rmtree(tmp_dir) 36 | os.mkdir(tmp_dir) 37 | os.chdir(tmp_dir) 38 | 39 | 40 | def install_go() -> None: 41 | go_version = "1.17" 42 | tar_file = f"go{go_version}.linux-armv6l.tar.gz" 43 | print(f"Installing temporaray version of go {go_version}...") 44 | command("wget -c https://golang.org/dl/" + tar_file) 45 | command(f"tar -C {tmp_dir} -xvzf {tar_file}") 46 | print(f"Setup temporaray version of go {go_version}") 47 | 48 | 49 | def clone_repo() -> None: 50 | print("Cloning repo") 51 | command("git clone https://github.com/gleich/verpi.git") 52 | print("Cloned repo") 53 | 54 | 55 | def compile() -> None: 56 | print("Compiling binary from source code:") 57 | original_gopath = os.getenv("GOPATH") 58 | os.environ["GOPATH"] = os.path.join(tmp_dir, "goroot") 59 | os.chdir("verpi") 60 | command("../go/bin/go build -v -o dist/verpi .") 61 | if original_gopath is not None: 62 | os.environ["GOPATH"] = original_gopath 63 | os.chdir("..") 64 | print("Compiled binary") 65 | 66 | 67 | def install_verpi() -> None: 68 | print("Installing verpi at", verpi_bin_path) 69 | if os.path.exists(verpi_bin_path): 70 | os.remove(verpi_bin_path) 71 | os.rename("verpi/dist/verpi", verpi_bin_path) 72 | print("verpi installed at", verpi_bin_path) 73 | 74 | 75 | def install_verpi_service() -> None: 76 | print("Installing systemd service for verpi") 77 | with urlopen( 78 | "https://raw.githubusercontent.com/gleich/verpi/master/verpi.service" 79 | ) as response: 80 | content = response.read().decode("utf-8") 81 | with open(systemd_service_path, "w") as systemd_file: 82 | systemd_file.write(content) 83 | command("systemctl enable verpi") 84 | print("Added and started systemd service") 85 | 86 | 87 | def uninstall() -> None: 88 | os.remove(verpi_bin_path) 89 | print("Deleted binary") 90 | command("systemctl disable verpi") 91 | print("Disabled systemd service") 92 | os.remove(systemd_service_path) 93 | print("Deleted systemd service file") 94 | 95 | 96 | def reboot() -> None: 97 | print( 98 | "\nRebooting pi in 3 seconds. You might need to cut the power to fully turn off the lights." 99 | ) 100 | for i in reversed(range(2)): 101 | print(i + 1) 102 | time.sleep(1) 103 | print("Rebooting now") 104 | command("reboot") 105 | 106 | 107 | def command(cmd: str) -> None: 108 | """Run os.system but exit with a failure if the exit code is not zero 109 | 110 | Args: 111 | cmd (str): Command to run 112 | """ 113 | code = os.system(cmd) 114 | if code != 0: 115 | print(f"Failed to run {cmd} with status code of {code}") 116 | exit(1) 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /verpi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=See the status of your vercel deployments on the pimoroni blinkt! 3 | 4 | Wants=network.target 5 | After=syslog.target network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | ExecStart=/usr/local/bin/verpi 10 | Restart=always 11 | RestartSec=5 12 | KillMode=process 13 | User=pi 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | --------------------------------------------------------------------------------