├── .dockerignore ├── .github ├── dependabot.yaml └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.arm ├── LICENCE ├── README.md ├── docs ├── config.toml ├── content │ ├── _index.md │ ├── guides │ │ ├── _index.md │ │ ├── compiled.md │ │ ├── configuration.md │ │ ├── development.md │ │ ├── docker-compose.md │ │ ├── dynamic.md │ │ ├── netlify.md │ │ └── php.md │ ├── reference │ │ ├── _index.md │ │ ├── authentication.md │ │ ├── commandline.md │ │ ├── comparison.md │ │ └── environment-variables.md │ └── usage │ │ ├── _index.md │ │ ├── actions.md │ │ ├── crontab.md │ │ ├── docker.md │ │ ├── installation.md │ │ ├── start.md │ │ ├── systemd.md │ │ └── webhook.md ├── static │ ├── custom.css │ ├── install.sh │ ├── sakura-dark.css │ ├── sakura.css │ ├── webhook-github-deliveries.png │ ├── webhook-github.png │ └── webhook-gitlab.png └── templates │ ├── anchor-link.html │ ├── index.html │ ├── page.html │ ├── partials │ └── toc.html │ └── section.html ├── src ├── actions │ ├── mod.rs │ ├── process.rs │ ├── script.rs │ └── utils │ │ ├── command.rs │ │ └── mod.rs ├── args.rs ├── checks │ ├── git.rs │ ├── git │ │ ├── config.rs │ │ ├── credentials.rs │ │ ├── known_hosts.rs │ │ └── repository.rs │ ├── mod.rs │ └── watch.rs ├── context.rs ├── lib.rs ├── logger.rs ├── main.rs ├── start.rs └── triggers │ ├── http.rs │ ├── mod.rs │ ├── once.rs │ ├── schedule.rs │ └── signal.rs └── test_directories └── .keep /.dockerignore: -------------------------------------------------------------------------------- 1 | target/* 2 | !target/arm-unknown-linux-gnueabihf 3 | target/arm-unknown-linux-gnueabihf/* 4 | !target/arm-unknown-linux-gnueabihf/release 5 | target/arm-unknown-linux-gnueabihf/release/* 6 | !target/arm-unknown-linux-gnueabihf/release/gw 7 | docs 8 | test_directories -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | isRcRelease: ${{ contains(github.ref, 'rc') }} 13 | isLiveRelease: ${{ ! contains(github.ref, 'rc') }} 14 | 15 | jobs: 16 | release: 17 | name: Create GitHub release 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout the tag 21 | uses: actions/checkout@v3 22 | - name: Add toolchain for Rust 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | - name: Restore cached dependencies 27 | uses: Swatinem/rust-cache@v2 28 | - name: Release for tags 29 | uses: taiki-e/create-gh-release-action@v1 30 | if: ${{ env.isLiveRelease == 'true' }} 31 | with: 32 | changelog: CHANGELOG.md 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | - name: Publish to Crates.io 35 | uses: katyo/publish-crates@v2 36 | if: ${{ env.isLiveRelease == 'true' }} 37 | with: 38 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 39 | ignore-unpublished-changes: true 40 | 41 | release_linux: 42 | name: Release for Linux 43 | runs-on: ubuntu-latest 44 | needs: release 45 | steps: 46 | - name: Checkout the tag 47 | uses: actions/checkout@v3 48 | - name: Add toolchain for Rust 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: stable 52 | - name: Restore cached dependencies 53 | uses: Swatinem/rust-cache@v2 54 | - name: Build for Linux 55 | uses: actions-rs/cargo@v1 56 | with: 57 | command: build 58 | args: --release --target x86_64-unknown-linux-gnu 59 | - name: Convert binaries into compressed files 60 | run: | 61 | cd target/x86_64-unknown-linux-gnu/release && tar czf gw-bin_x86_64-unknown-linux-gnu.tar.gz gw && cd - 62 | cd target/x86_64-unknown-linux-gnu/release && zip gw-bin_x86_64-unknown-linux-gnu.zip gw && cd - 63 | - name: Upload zip to release 64 | uses: svenstaro/upload-release-action@v2 65 | if: ${{ env.isLiveRelease == 'true' }} 66 | with: 67 | file: target/x86_64-unknown-linux-gnu/release/gw-bin_x86_64-unknown-linux-gnu.zip 68 | asset_name: gw-bin_x86_64-unknown-linux-gnu.zip 69 | - name: Upload tar.gz to release 70 | uses: svenstaro/upload-release-action@v2 71 | if: ${{ env.isLiveRelease == 'true' }} 72 | with: 73 | file: target/x86_64-unknown-linux-gnu/release/gw-bin_x86_64-unknown-linux-gnu.tar.gz 74 | asset_name: gw-bin_x86_64-unknown-linux-gnu.tar.gz 75 | - name: Archive production artifacts 76 | uses: actions/upload-artifact@v4 77 | if: ${{ env.isRcRelease == 'true' }} 78 | with: 79 | name: gw-bin_x86_64-unknown-linux-gnu 80 | path: | 81 | target/x86_64-unknown-linux-gnu/release/gw 82 | 83 | release_musl: 84 | name: Release for Musl 85 | runs-on: ubuntu-latest 86 | needs: release 87 | steps: 88 | - name: Checkout the tag 89 | uses: actions/checkout@v3 90 | - name: Add toolchain for Rust 91 | uses: actions-rs/toolchain@v1 92 | with: 93 | toolchain: stable 94 | target: x86_64-unknown-linux-musl 95 | - name: Restore cached dependencies 96 | uses: Swatinem/rust-cache@v2 97 | - name: Install MUSL dependencies 98 | run: sudo apt-get install musl-tools --no-install-recommends -y 99 | - name: Build for Musl 100 | uses: actions-rs/cargo@v1 101 | with: 102 | command: build 103 | args: --release --target x86_64-unknown-linux-musl 104 | - name: Convert binaries into compressed files 105 | run: | 106 | cd target/x86_64-unknown-linux-musl/release && tar czf gw-bin_x86_64-unknown-linux-musl.tar.gz gw && cd - 107 | cd target/x86_64-unknown-linux-musl/release && zip gw-bin_x86_64-unknown-linux-musl.zip gw && cd - 108 | - name: Upload zip to release 109 | uses: svenstaro/upload-release-action@v2 110 | if: ${{ env.isLiveRelease == 'true' }} 111 | with: 112 | file: target/x86_64-unknown-linux-musl/release/gw-bin_x86_64-unknown-linux-musl.zip 113 | asset_name: gw-bin_x86_64-unknown-linux-musl.zip 114 | - name: Upload tar.gz to release 115 | uses: svenstaro/upload-release-action@v2 116 | if: ${{ env.isLiveRelease == 'true' }} 117 | with: 118 | file: target/x86_64-unknown-linux-musl/release/gw-bin_x86_64-unknown-linux-musl.tar.gz 119 | asset_name: gw-bin_x86_64-unknown-linux-musl.tar.gz 120 | - name: Archive production artifacts 121 | uses: actions/upload-artifact@v4 122 | if: ${{ env.isRcRelease == 'true' }} 123 | with: 124 | name: gw-bin_x86_64-unknown-linux-musl 125 | path: | 126 | target/x86_64-unknown-linux-musl/release/gw 127 | 128 | release_windows: 129 | name: Release for Windows 130 | runs-on: ubuntu-latest 131 | needs: release 132 | steps: 133 | - name: Checkout the tag 134 | uses: actions/checkout@v3 135 | - name: Add toolchain for Rust 136 | uses: actions-rs/toolchain@v1 137 | with: 138 | toolchain: stable 139 | - name: Restore cached dependencies 140 | uses: Swatinem/rust-cache@v2 141 | - name: Build for Windows 142 | uses: actions-rs/cargo@v1 143 | with: 144 | command: build 145 | args: --release --target x86_64-pc-windows-gnu 146 | use-cross: true 147 | - name: Convert binaries into compressed files 148 | run: | 149 | cd target/x86_64-pc-windows-gnu/release && zip gw-bin_x86_64-pc-windows-gnu.zip gw.exe && cd - 150 | - name: Upload zip to release 151 | uses: svenstaro/upload-release-action@v2 152 | if: ${{ env.isLiveRelease == 'true' }} 153 | with: 154 | file: target/x86_64-pc-windows-gnu/release/gw-bin_x86_64-pc-windows-gnu.zip 155 | asset_name: gw-bin_x86_64-pc-windows-gnu.zip 156 | - name: Archive production artifacts 157 | uses: actions/upload-artifact@v4 158 | if: ${{ env.isRcRelease == 'true' }} 159 | with: 160 | name: gw-bin_x86_64-pc-windows-gnu 161 | path: | 162 | target/x86_64-pc-windows-gnu/release/gw.exe 163 | 164 | 165 | release_arm: 166 | name: Release for ARM 167 | runs-on: ubuntu-latest 168 | needs: release 169 | steps: 170 | - name: Checkout the tag 171 | uses: actions/checkout@v3 172 | - name: Add toolchain for Rust 173 | uses: actions-rs/toolchain@v1 174 | with: 175 | toolchain: stable 176 | target: arm-unknown-linux-gnueabihf 177 | - name: Restore cached dependencies 178 | uses: Swatinem/rust-cache@v2 179 | - name: Add new apt sources that support armhf 180 | run: | 181 | sudo tee /etc/apt/sources.list.d/ubuntu.sources << EOF 182 | Types: deb 183 | URIs: http://archive.ubuntu.com/ubuntu/ 184 | Suites: noble noble-updates noble-backports 185 | Components: main universe restricted multiverse 186 | Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 187 | Architectures: amd64 188 | 189 | Types: deb 190 | URIs: http://security.ubuntu.com/ubuntu/ 191 | Suites: noble-security 192 | Components: main universe restricted multiverse 193 | Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 194 | Architectures: amd64 195 | 196 | Types: deb 197 | URIs: http://ports.ubuntu.com/ 198 | Suites: noble noble-updates noble-backports 199 | Components: main universe restricted multiverse 200 | Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 201 | Architectures: armhf 202 | EOF 203 | - name: Enable ARM cross-build capabilities 204 | run: sudo dpkg --add-architecture armhf 205 | - name: Update apt repositories 206 | run: sudo apt-get update 207 | - name: Install cross-platform installation dependencies 208 | run: sudo apt-get install libc6-dev gcc-arm-linux-gnueabihf libc6-dev:armhf libssl-dev:armhf --no-install-recommends -y 209 | - name: Add setup for ARM cross-building 210 | run: | 211 | mkdir -p ~/.cargo 212 | cat > ~/.cargo/config.toml << EOF 213 | [target.arm-unknown-linux-gnueabihf] 214 | linker = "arm-linux-gnueabihf-gcc" 215 | rustflags = ["-L/usr/lib/arm-linux-gnueabihf", "-C", "target-feature=+crt-static"] 216 | EOF 217 | - name: Build for ARM 218 | run: | 219 | export OPENSSL_STATIC=1 220 | export OPENSSL_DIR=/usr/arm-linux-gnueabihf 221 | export OPENSSL_LIB_DIR=/usr/lib/arm-linux-gnueabihf 222 | export OPENSSL_INCLUDE_DIR=/usr/include/arm-linux-gnueabihf 223 | cargo build --release --target arm-unknown-linux-gnueabihf 224 | - name: Convert binaries into compressed files 225 | run: | 226 | cd target/arm-unknown-linux-gnueabihf/release && tar czf gw-bin_arm-unknown-linux-gnueabihf.tar.gz gw && cd - 227 | cd target/arm-unknown-linux-gnueabihf/release && zip gw-bin_arm-unknown-linux-gnueabihf.zip gw && cd - 228 | - name: Upload zip to release 229 | uses: svenstaro/upload-release-action@v2 230 | if: ${{ env.isLiveRelease == 'true' }} 231 | with: 232 | file: target/arm-unknown-linux-gnueabihf/release/gw-bin_arm-unknown-linux-gnueabihf.zip 233 | asset_name: gw-bin_arm-unknown-linux-gnueabihf.zip 234 | - name: Upload tar.gz to release 235 | uses: svenstaro/upload-release-action@v2 236 | if: ${{ env.isLiveRelease == 'true' }} 237 | with: 238 | file: target/arm-unknown-linux-gnueabihf/release/gw-bin_arm-unknown-linux-gnueabihf.tar.gz 239 | asset_name: gw-bin_arm-unknown-linux-gnueabihf.tar.gz 240 | - name: Archive production artifacts 241 | uses: actions/upload-artifact@v4 242 | with: 243 | name: gw-bin_arm-unknown-linux-gnueabihf 244 | path: | 245 | target/arm-unknown-linux-gnueabihf/release/gw 246 | 247 | release_mac: 248 | name: Release for MacOS 249 | needs: release 250 | runs-on: macos-latest 251 | steps: 252 | - name: Checkout the tag 253 | uses: actions/checkout@v3 254 | - name: Add toolchain for Rust 255 | uses: actions-rs/toolchain@v1 256 | with: 257 | toolchain: stable 258 | - name: Restore cached dependencies 259 | uses: Swatinem/rust-cache@v2 260 | - name: Build for MacOS 261 | uses: actions-rs/cargo@v1 262 | with: 263 | command: build 264 | args: --release --target aarch64-apple-darwin 265 | - name: Convert binaries into compressed files 266 | run: | 267 | cd target/aarch64-apple-darwin/release && zip gw-bin_aarch64-apple-darwin.zip gw && cd - 268 | - name: Upload zip to live release 269 | uses: svenstaro/upload-release-action@v2 270 | if: ${{ env.isLiveRelease == 'true' }} 271 | with: 272 | file: target/aarch64-apple-darwin/release/gw-bin_aarch64-apple-darwin.zip 273 | asset_name: gw-bin_aarch64-apple-darwin.zip 274 | - name: Store artifacts for the release candidates 275 | uses: actions/upload-artifact@v4 276 | if: ${{ env.isRcRelease == 'true' }} 277 | with: 278 | name: gw-bin_aarch64-apple-darwin 279 | path: | 280 | target/aarch64-apple-darwin/release/gw 281 | 282 | docker: 283 | name: Docker build if we are on a tag 284 | needs: release_arm 285 | runs-on: ubuntu-latest 286 | steps: 287 | - name: Checkout the tag 288 | uses: actions/checkout@v3 289 | - name: Set up QEMU 290 | uses: docker/setup-qemu-action@v3 291 | - name: Set up Docker Buildx 292 | uses: docker/setup-buildx-action@v3 293 | - name: Log in to Docker Hub 294 | uses: docker/login-action@v3 295 | with: 296 | username: ${{ secrets.DOCKERHUB_USERNAME }} 297 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 298 | - name: Download artifacts 299 | uses: actions/download-artifact@v4 300 | with: 301 | name: gw-bin_arm-unknown-linux-gnueabihf 302 | path: target/arm-unknown-linux-gnueabihf/release 303 | - name: Make artifact executable 304 | run: chmod +x target/arm-unknown-linux-gnueabihf/release/gw 305 | - name: Docker meta for Debian 306 | id: meta 307 | uses: docker/metadata-action@v5 308 | with: 309 | images: | 310 | danielgrant/gw 311 | tags: | 312 | type=raw,value=latest 313 | type=semver,pattern={{version}} 314 | type=semver,pattern={{major}}.{{minor}} 315 | - name: Build and push Docker image for x86_64 316 | id: build_amd64 317 | uses: docker/build-push-action@v5 318 | with: 319 | context: . 320 | file: ./Dockerfile 321 | push: true 322 | tags: "danielgrant/gw:amd64" 323 | cache-from: type=registry,ref=danielgrant/gw:latest 324 | cache-to: type=inline 325 | - name: Build and push Docker image for ARMv7 326 | uses: docker/build-push-action@v5 327 | with: 328 | context: . 329 | file: ./Dockerfile.arm 330 | push: true 331 | tags: "danielgrant/gw:armv7" 332 | platforms: linux/arm/v7 333 | cache-from: type=registry,ref=danielgrant/gw:latest 334 | cache-to: type=inline 335 | - name: Build and push Docker image for ARM64 336 | uses: docker/build-push-action@v5 337 | with: 338 | context: . 339 | file: ./Dockerfile.arm 340 | push: true 341 | tags: "danielgrant/gw:arm64" 342 | platforms: linux/arm64 343 | cache-from: type=registry,ref=danielgrant/gw:latest 344 | cache-to: type=inline 345 | - name: Merge tags with docker manifest 346 | run: | 347 | TAGS=$(echo "${{ steps.meta.outputs.tags }}" | sed 's/^/--tag /' | xargs) 348 | docker buildx imagetools create $TAGS \ 349 | danielgrant/gw:amd64 \ 350 | danielgrant/gw:arm64 \ 351 | danielgrant/gw:armv7 352 | - name: Update repo description 353 | uses: peter-evans/dockerhub-description@v4 354 | if: ${{ env.isLiveRelease == 'true' }} 355 | with: 356 | username: ${{ secrets.DOCKERHUB_USERNAME }} 357 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 358 | repository: danielgrant/gw 359 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | name: Check and test on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout the tag 16 | uses: actions/checkout@v3 17 | - name: Add toolchain for Rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | - name: Install nextest 22 | uses: taiki-e/install-action@nextest 23 | - name: Cache Rust dependencies 24 | uses: Swatinem/rust-cache@v2 25 | - name: Test for linting issues 26 | run: cargo clippy -- -D warnings 27 | - name: Setup git configuration 28 | run: | 29 | git config --global user.email "test@example.com" 30 | git config --global user.name "Test Thomas" 31 | git config --global init.defaultBranch master 32 | - name: Run tests 33 | run: cargo nextest run --no-fail-fast 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | docs/public 3 | test_directories/* 4 | !test_directories/.keep -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Changed 6 | - Updated test dependencies 7 | - Updated ureq to avoid ring vulnerability 8 | 9 | ## [0.4.1] - 2025-01-26 10 | 11 | ### Changed 12 | 13 | - **New feature**: Trigger on git tags 14 | - Use the `--on tag` to only pull to the latest tag on the branch 15 | - Use `--on tag:v*` to match the tags with the given glob 16 | - Git repository will stay on the branch, but will do a partial pull to the tag and run actions 17 | - Updated dependencies, with libgit2 updated to 1.9.0 18 | 19 | ## [0.4.0] - 2024-10-23 20 | 21 | ### Added 22 | 23 | - **New feature**: Subprocess handling 24 | - Use `-p` to start a process directly and restart on change 25 | - Configure the retries to restart the process in case of a failure 26 | - Set the stop signal and stop timeout args to configure graceful shutdown before restart 27 | - Add `-P` to start the process in the shell instead 28 | - The order of script and process flags now matter, scripts are run in order before and after the process 29 | - Add testing for Windows and MacOS machines 30 | 31 | ### Changed 32 | 33 | - **Breaking change**: Scripts are now running directly, you can run it in a shell using `-S` 34 | - Only change gitconfig (safe.directory) if there isn't one 35 | - Don't overwrite script environment, use already set variables 36 | - If the user presses Ctrl+C a second time, the program exits immediately 37 | 38 | ## [0.3.2] - 2024-08-26 39 | 40 | ### Added 41 | 42 | - Add Docker image support for arm/v7 43 | 44 | ### Changed 45 | 46 | - Make ARM binaries statically linked 32-bit to maintain compatibility with older devices 47 | 48 | ## [0.3.1] - 2024-08-21 49 | 50 | ### Added 51 | 52 | - Support cross-compilation for Linux ARM machines 53 | - Support compilation for MacOS ARM machines 54 | - Support multi-platform Docker images 55 | - Add Changelog to releases automatically 56 | 57 | ### Changed 58 | 59 | - Fix accidentally dropped Windows support 60 | 61 | ## [0.3.0] - 2024-08-19 62 | 63 | ### Added 64 | 65 | - Add context to share data between different steps 66 | - Expose the context through environmental variables for the scripts 67 | - Add documentation for the environmental variables 68 | - Add `--version` flag to print current version 69 | - Add `--quiet` flag to improve logging 70 | - Add signal handling to handle SIGINT and SIGTERM 71 | - Add `--ssh-key` flag to change the ssh-key path 72 | - Add `--git-username` and `--git-token` flags to change the https authentication 73 | - Generate `.ssh/known_hosts` file if there is none found on the system 74 | - Add `--git-known-host` to add an entry to the `.ssh/known_hosts` file 75 | - Add installation script 76 | 77 | ### Changed 78 | 79 | - Change musl release to build everything statically 80 | - Ignore different owner repository warnings 81 | - Improve error messages, print original error in fetch 82 | 83 | ### Removed 84 | 85 | - Remove `-o` short flag for `--once` 86 | - Remove debian-based image to streamline usage 87 | - Remove on tag argument for now 88 | 89 | ## [0.2.2] - 2024-02-17 90 | 91 | ### Added 92 | 93 | - Add `-v` flag to increase verbosity, default log level changed to INFO 94 | - Add check to avoid pulling, when the repository is dirty 95 | - Add more tracing to git repository 96 | - Add safe directory inside Docker image 97 | 98 | ### Changed 99 | 100 | - Fix bug with tag fetching 101 | 102 | ## [0.2.1] - 2024-02-12 103 | 104 | ### Added 105 | 106 | - Add Docker image 107 | - Add image building to GitHub Actions 108 | 109 | ### Changed 110 | 111 | - Improve documentation 112 | 113 | ## [0.2.0] - 2024-02-09 114 | 115 | ### Changed 116 | 117 | - Rewrite code to be more modular 118 | - Introduce tests to every part of the codebase 119 | - Add documentation to every module 120 | - Refactor error handling to use thiserror 121 | - Add testing to GitHub Actions 122 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gw-bin" 3 | description = "Watch git repositories, pull changes and run commands on the new files" 4 | version = "0.4.1" 5 | license = "MIT" 6 | edition = "2021" 7 | documentation = "https://github.com/daniel7grant/gw" 8 | repository = "https://github.com/daniel7grant/gw" 9 | 10 | [package.metadata.binstall] 11 | pkg-url = "{ repo }/releases/download/v{ version }/{ name }_{ target }{ archive-suffix }" 12 | bin-dir = "{ bin }{ binary-ext }" 13 | pkg-fmt = "zip" 14 | 15 | [dependencies] 16 | dirs = "6" 17 | duct = "0.13.7" 18 | duct_sh = "0.13.7" 19 | duration-string = "0.5.2" 20 | git2 = "0.20.0" 21 | gumdrop = "0.8.1" 22 | log = "0.4.20" 23 | mockall = "0.13.0" 24 | nix = { version = "0.29.0", features = ["signal"] } 25 | shlex = "1.3.0" 26 | signal-hook = "0.3.17" 27 | simplelog = "0.12.2" 28 | thiserror = "2.0.3" 29 | time = "0.3.36" 30 | tiny_http = "0.12.0" 31 | 32 | [target.'cfg(any(target_env = "musl", target_arch = "arm", target_arch = "aarch64"))'.dependencies] 33 | git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] } 34 | 35 | [dev-dependencies] 36 | duct = "0.13.7" 37 | rand = "0.9.0" 38 | testing_logger = "0.1.1" 39 | ureq = { version = "3.0.5", default-features = false } 40 | 41 | [profile.release] 42 | strip = true 43 | 44 | [[bin]] 45 | name = "gw" 46 | path = "src/main.rs" 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.80-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | ARG OPENSSL_STATIC=1 6 | 7 | RUN apk add --no-cache \ 8 | make \ 9 | musl-dev \ 10 | perl 11 | 12 | COPY ./Cargo.lock ./Cargo.toml /app 13 | COPY ./src /app/src 14 | 15 | RUN cargo build --release 16 | 17 | 18 | FROM alpine:3.20 19 | 20 | RUN apk add --no-cache \ 21 | ca-certificates 22 | 23 | COPY --from=builder /app/target/release/gw /usr/bin/gw 24 | 25 | ENTRYPOINT ["/usr/bin/gw"] 26 | -------------------------------------------------------------------------------- /Dockerfile.arm: -------------------------------------------------------------------------------- 1 | # This image is only for CI to avoid the very slow QEMU compilation 2 | FROM alpine:3.20 3 | 4 | RUN apk add --no-cache \ 5 | ca-certificates 6 | 7 | # Use the previously built binary artifact 8 | COPY target/arm-unknown-linux-gnueabihf/release/gw /usr/bin/gw 9 | 10 | ENTRYPOINT ["/usr/bin/gw"] 11 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Grant 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gw 2 | 3 | Watch local git repositories, keep in sync with remote and run commands. 4 | 5 | ## Motivation 6 | 7 | `gw` is a lightweight binary that manages a simple pull-based continuous deployment for you. It watches a local git repository, fetches if the remote changes, and builds or deploys your code. Current CD solutions either lock you into proprietary software (e.g. Netlify or Vercel) or complicated to run and manage (e.g. ArgoCD). `gw` is a service that can run everywhere (even behind NAT or VPN), synchronizes code with your remote and deploys immediately, saving your developers time and energy. 8 | 9 | Features of `gw`: 10 | - **lightweight**: it is only a 1.5MB binary (~7MB with git and ssh statically built-in) 11 | - **runs anywhere**: use it on baremetal or [systemd](https://gw.danielgrants.com/usage/systemd.md) on Linux (x86_64 and ARM) or in [Docker](https://gw.danielgrants.com/usage/docker.md) (Windows and MacOS is supported on a best-effort basis) 12 | - **open source**: written entirely in Rust, you can build it from source in a few minutes 13 | - **pull-based**: works on any network, even behind a NAT or VPN 14 | - **flexible**: build, deploy, restart or anything you can imagine 15 | 16 | If you want to see how `gw` compare to other products: look at the [comparisons](https://gw.danielgrants.com/reference/comparison). 17 | 18 | ## Installation 19 | 20 | To get started with `gw`, you can use the install script: 21 | 22 | ```sh 23 | curl https://gw.danielgrants.com/install.sh | sh 24 | ``` 25 | 26 | For more installation methods, see the [documentation](https://gw.danielgrants.com/usage/installation/). 27 | 28 | ## Get started 29 | 30 | `gw` is a simple program, that you can use to pull changes from a remote repository and run scripts on the change. 31 | 32 | ### Prerequisites 33 | 34 | First, make sure, that `gw` is installed successfully and is in your PATH: 35 | 36 | ```sh 37 | $ gw --version 38 | 0.4.1 39 | ``` 40 | 41 | The other necessary part is a git repository to which you have pull access. It is recommended to use a repository that you know, but if you don't have one at hand, you can use the [daniel7grant/time](https://github.com/daniel7grant/time) repository. This is an example repository that is updated in every minute, so it is useful to test the auto update of `gw`. First clone this repository (if you are using your own, clone again), and enter the cloned directory: 42 | 43 | ```sh 44 | git clone https://github.com/daniel7grant/time.git 45 | cd time 46 | ``` 47 | 48 | ### Pull files automatically 49 | 50 | To get started, point `gw` to this local repository. By default it pulls the changes every minute. We can add the `--verbose` or `-v` flag to see when the changes occur: 51 | 52 | ```sh 53 | gw /path/to/repo -v 54 | ``` 55 | 56 | If you are using your own repository, create a commit in a different place, and see how it gets automatically pulled (in the case of the `time` repo, there is a commit every minute). The verbose logs should print that a git pull happened: 57 | 58 | ```sh 59 | $ gw /path/to/repo -v 60 | # ... 61 | 2024-03-10T14:48:13.447Z [DEBUG] Checked out fc23d21 on branch main. 62 | 2024-03-10T14:48:13.447Z [INFO ] There are updates, pulling. 63 | ``` 64 | 65 | Also check the files or the `git log` to see that it the repository has been updated: 66 | 67 | ```sh 68 | cat DATETIME # it should contain the latest time 69 | git log -1 # it should be a commit in the last minute 70 | ``` 71 | 72 | ### Run scripts on pull 73 | 74 | Pulling files automatically is useful but the `--script` or `-s` flag unlocks `gw`'s potential: it can run any kind of custom script if there are any changes. For a simple example, we can print the content of a file to the log with `cat`: 75 | 76 | ```sh 77 | gw /path/to/repo -v --script 'cat DATETIME' 78 | ``` 79 | 80 | This will run every time there is a new commit, and after the pull it will print the file contents. You can see that the results are printed in the log: 81 | 82 | ```sh 83 | $ gw /path/to/repo -v --script 'cat DATETIME' 84 | # ... 85 | 2024-10-18T16:28:53.907Z [INFO ] There are updates, running actions. 86 | 2024-10-18T16:28:53.907Z [INFO ] Running script "cat" in /path/to/repo. 87 | 2024-10-18T16:28:53.913Z [DEBUG] [cat] 2024-10-18T16:28:00+0000 88 | 2024-10-18T16:28:53.913Z [INFO ] Script "cat" finished successfully. 89 | ``` 90 | 91 | You can add multiple scripts, which will run one after another. Use these scripts to build source files, restarts deployments and anything else that you can imagine. 92 | 93 | ### Run subprocess, restart on pull 94 | 95 | It is often enough to run scripts, but many times you also want to maintain a long-running process e.g. for web services. `gw` can help you with this, using the `-p` flag. This will start a process in the background and restart it on pull. 96 | 97 | For example starting a python web server: 98 | 99 | ```sh 100 | $ gw /path/to/repo -v --process "python -m http.server" 101 | # ... 102 | 2024-10-06T21:58:21.306Z [DEBUG] Setting up ProcessAction "python -m http.server" on change. 103 | 2024-10-06T21:58:21.306Z [DEBUG] Starting process: "python" in directory /path/to/repo. 104 | 2024-10-06T21:58:56.211Z [DEBUG] [python] Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 105 | ``` 106 | 107 | This will run a python process in the background and stop and start it again if a git pull happened. Just wrap your deployment script with `gw` and see it gets updated every time you push to git. 108 | 109 | ## Run actions on tags 110 | 111 | Pulling on every commit might not be the fit for every product, especially ones that needs to maintains compatibility or strictly versioned. For these, you can instead trigger on tags. Use the `--on tag` flag to only pull changes if there is a tag on the current branch. 112 | 113 | ```sh 114 | $ gw /path/to/repo -v --on tag -S 'echo $GIT_TAG_NAME' 115 | # ... 116 | 2024-10-18T16:28:53.907Z [INFO ] There are updates, running actions. 117 | 2024-10-18T16:28:53.907Z [INFO ] Running script "echo" in /path/to/repo. 118 | 2024-10-18T16:28:53.913Z [DEBUG] [echo] v0.1.0 119 | 2024-10-18T16:28:53.913Z [INFO ] Script "echo" finished successfully. 120 | ``` 121 | 122 | This will always fetch the current branch, check for the latest tag on it and pull only the commits up to that tag. To match some kind of commit, you can use the `--on tag:v*` which will only pull if the tag is matching the passed glob (in this case starting with `v`). 123 | 124 | ```sh 125 | gw /path/to/repo -v --on 'tag:v*' -S 'echo "new version: $GIT_TAG_NAME"' 126 | ``` 127 | 128 | ## Next steps 129 | 130 | If you like `gw`, there are multiple ways to use it for real-life use-cases. 131 | 132 | If you want to put the `gw` script in the background, you can: 133 | 134 | - wrap into a [systemd unit](https://gw.danielgrants.com/usage/systemd), if you want to manage it with a single file; 135 | - start in a [docker container](https://gw.danielgrants.com/usage/docker), if you already use Docker in your workflow; 136 | - or run periodically with [cron](https://gw.danielgrants.com/usage/crontab), if you don't have shell access to the server. 137 | 138 | If you are interested in some ideas on how to use `gw`: 139 | 140 | - if you only need to pull files, see [PHP guide](https://gw.danielgrants.com/guides/php); 141 | - if you are using a dynamic language (e.g. JavaScript, Python, Ruby), see [Guide for dynamic languages](https://gw.danielgrants.com/guides/dynamic) for example on running your process; 142 | - if you are using a compiled language (e.g. TypeScript, Go, Rust), see [Guide for compiled languages](https://gw.danielgrants.com/guides/compiled) for example on compiling your program; 143 | - if you use a `docker-compose.yaml`, see [Guide for docker-compose](guides/docker-compose); 144 | - if you want to easily manage configuration files as GitOps, see [Configuration guide](https://gw.danielgrants.com/guides/configuration); 145 | - for a full-blown example, check out [Netlify](https://gw.danielgrants.com/guides/netlify); 146 | - and many other things, for the incomplete list [guides page](https://gw.danielgrants.com/guides). 147 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | # The URL the site will be built for 2 | base_url = "http://localhost" 3 | 4 | # Whether to automatically compile all Sass files in the sass directory 5 | compile_sass = false 6 | 7 | # Whether to build a search index to be used later on by a JavaScript library 8 | build_search_index = false 9 | 10 | [markdown] 11 | # Whether to do syntax highlighting 12 | # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola 13 | highlight_code = true 14 | 15 | [extra] 16 | # Put all your custom variables here 17 | -------------------------------------------------------------------------------- /docs/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Get started" 3 | sort_by = "weight" 4 | 5 | insert_anchor_links = "right" 6 | +++ 7 | 8 | # gw 9 | 10 | Watch local git repositories, keep in sync with remote and run commands. 11 | 12 | ## Motivation 13 | 14 | `gw` is a lightweight binary that manages a simple pull-based continuous deployment for you. It watches a local git repository, fetches if the remote changes, and builds or deploys your code. Current CD solutions either lock you into proprietary software (e.g. Netlify or Vercel) or complicated to run and manage (e.g. ArgoCD). `gw` is a service that can run everywhere (even behind NAT or VPN), synchronizes code with your remote and deploys immediately, saving your developers time and energy. 15 | 16 | Features of `gw`: 17 | - **lightweight**: it is only a 1.5MB binary (~7MB with git and ssh statically built-in) 18 | - **runs anywhere**: use it on baremetal or [systemd](https://gw.danielgrants.com/usage/systemd.md) on Linux (x86_64 and ARM) or in [Docker](https://gw.danielgrants.com/usage/docker.md) (Windows and MacOS is supported on a best-effort basis) 19 | - **open source**: written entirely in Rust, you can build it from source in a few minutes 20 | - **pull-based**: works on any network, even behind a NAT or VPN 21 | - **flexible**: build, deploy, restart or anything you can imagine 22 | 23 | If you want to see how `gw` compare to other products: look at the [comparisons](/reference/comparison). 24 | 25 | ## Installation 26 | 27 | To get started with `gw`, you can use the install script: 28 | 29 | ```sh 30 | curl https://gw.danielgrants.com/install.sh | sh 31 | ``` 32 | 33 | For more installation methods, see [Installation](/usage/installation). 34 | 35 | ## Get started 36 | 37 | To use `gw`, you have to point it to your local repository and it will pull changes automatically. You can run scripts on every pull to build with the `--script` (or `-s`) flag or run your deployments with the `--process` (or `-p`) flag. 38 | 39 | ```sh 40 | gw /path/to/repo --script 'run build process' --process 'run deployment' 41 | ``` 42 | 43 | For your first steps with `gw`, see [Get started](/usage/start). 44 | 45 | ## Next steps 46 | 47 | But this is not all `gw` can do. With a little creativity you can create a lot of things, for example: 48 | 49 | - pull changes for [development](/guides/development) and get a notification; 50 | - rollout a [docker-compose](/guides/docker-compose) deployment continously; 51 | - build on all commits for a minimal [Netlify](/guides/netlify) alternative, 52 | 53 | ...and many thing else. For a complete list, check out the [guides page](/guides). 54 | -------------------------------------------------------------------------------- /docs/content/guides/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Guides" 3 | sort_by = "weight" 4 | 5 | weight = 100 6 | paginate_by = 1000 # display all posts 7 | insert_anchor_links = "right" 8 | +++ 9 | 10 | `gw` is useful for any setup that requires reacting to changes in a git repository. Here are a few ideas, but the limit is your creativity: -------------------------------------------------------------------------------- /docs/content/guides/compiled.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Compiled languages" 3 | weight = 3 4 | +++ 5 | 6 | # Compiled languages 7 | 8 | For compiled (Go, Rust) or transpiled (TypeScript) you can use `gw` to build new assets then restart the running binary to restart the server. 9 | 10 | ## Configuration 11 | 12 | Simply add the scripts to build the binary then another one to restart it and watch the repository. 13 | 14 | ### TypeScript 15 | 16 | For example for TypeScript, transpile to JS and run with Node.js: 17 | 18 | ```sh 19 | gw /path/to/repo -s 'npx tsc -p tsconfig.json' -p 'node dist/index.js' 20 | ``` 21 | 22 | If you want to ensure that your code is correct, you can run the unit tests first: 23 | 24 | ```sh 25 | gw /path/to/repo -s 'npm run test' -s 'npx tsc -p tsconfig.json' -p 'node dist/index.js' 26 | ``` 27 | 28 | For Next.js and other frameworks that require a build step before starting, you can use: 29 | 30 | ```sh 31 | gw /path/to/repo -s 'npm run build' -p 'npm run start' 32 | ``` 33 | 34 | ### Go 35 | 36 | For Go, you can either run it directly or build it first and run it as a subprocess: 37 | 38 | ```sh 39 | gw /path/to/repo -p 'go run main.go' 40 | # or 41 | gw /path/to/repo -s 'go build main.go' -p './main' 42 | ``` 43 | 44 | You can add testing as a script, if you want to run the unit tests before the code is deployed: 45 | 46 | ```sh 47 | gw /path/to/repo -s 'go test' -s 'go build main.go' -p './main' 48 | ``` 49 | 50 | ### Rust 51 | 52 | For Rust, you can either run it directly or build it first and run it as a subprocess: 53 | 54 | ```sh 55 | gw /path/to/repo -p 'cargo run --release' 56 | # or 57 | gw /path/to/repo -s 'cargo build --release' -p './target/release/repo' 58 | ``` 59 | 60 | Add the tests here as well to make sure that the code is correct: 61 | 62 | ```sh 63 | gw /path/to/repo -s 'cargo test' -s 'cargo build --release' -p './target/release/repo' 64 | ``` 65 | 66 | Also checkout [Docker configuration](/guides/docker-compose). 67 | -------------------------------------------------------------------------------- /docs/content/guides/configuration.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Configuration files" 3 | weight = 5 4 | +++ 5 | 6 | # Configuration files 7 | 8 | Configuration files for services are rarely commited, which means that in case of a fatal issue, you can't restore files, audit changes or rollback. With `gw` you can commit your configurations, and restart the service on change, without ever having to use a VPN or SSH. 9 | 10 | ## Project configuration 11 | 12 | Simply create a new git repository with your specific config files, and push it to a remote server. 13 | 14 | ## gw configuration 15 | 16 | All you have to do is point `gw` to the config directory and reload or restart the service if there are any changes. 17 | 18 | ```sh 19 | gw /etc/repo -s 'systemctl restart service' 20 | ``` 21 | 22 | ## Examples 23 | 24 | ### nginx configuration 25 | 26 | An example configuration could be codifying the reverse proxy. This could be used as a backup for worst-case scenario, but also would help to be able to modify files on your local computer and have those reflected on your production environment. 27 | 28 | To start off, create a git repository in your `/etc/nginx` directory, and push it to a remote: 29 | 30 | ```sh 31 | cd /etc/nginx 32 | git init 33 | git add -A 34 | git commit -m 'Initial commit' 35 | # set a remote and push to it 36 | ``` 37 | 38 | Then you can setup `gw` to reload `nginx` on config change. You can either use `nginx` command or reload using `systemctl`: 39 | 40 | ```sh 41 | gw /etc/nginx -s 'nginx -s reload' 42 | # or 43 | gw /etc/nginx -s 'systemctl reload nginx' 44 | ``` 45 | 46 | You can also run `nginx` as a subprocess if you don't want to manage it separately, but this will stop and restart it every time a pull occurs: 47 | 48 | ```sh 49 | gw /etc/nginx -p "nginx -g 'daemon off;'" 50 | ``` 51 | 52 | If you want to avoid getting your system into a bad state by mistake, you can test the config files first with `nginx -t`: 53 | 54 | ```sh 55 | gw /etc/nginx -s 'nginx -t' -s 'systemctl reload nginx' 56 | ``` 57 | 58 | ### DNS configuration with bind 59 | 60 | Another great experiment is codifying the most popular DNS service: bind (Berkeley Internet Name Domain). A nice feature of bind is that it can be configured entirely from plaintext files. This means that we can commit our DNS records and modify it with a simple text editor locally. It can also be rolled out to multiple hosts at the same time, thus avoiding any kind of zone transfer. We are actually using a setup like this in production! 61 | 62 | To start off, just initialize a git repository in the `/etc/bind` directory and push it to a remote: 63 | 64 | ```sh 65 | cd /etc/bind 66 | git init 67 | git add -A 68 | git commit -m 'Initial commit' 69 | # set a remote and push to it 70 | ``` 71 | 72 | Then if there are any changes, restart bind with `rdnc` or `systemctl`: 73 | 74 | ```sh 75 | gw /etc/bind -s 'rdnc reload' 76 | # or 77 | gw /etc/bind -s 'systemctl restart named' 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/content/guides/development.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Local development" 3 | weight = 6 4 | +++ 5 | 6 | # Local development 7 | 8 | You can use `gw` to help in local development. You can pull your repository continuously, so in case somebody commits (and there is no conflicts) you can get the newest version. You can also set a notification to see immediately if somebody modified anything. 9 | 10 | > **Note**: I don't recommend working on the same branch with multiple people, but sometimes it happens. 11 | 12 | ## Configuration 13 | 14 | Simply set the path to your repository: 15 | 16 | ```sh 17 | gw /path/to/repo 18 | ``` 19 | 20 | You can use the `notify-send` command to popup notifications on Linux, let's use it to show if somebody commited to our branch. 21 | 22 | ```sh 23 | gw /path/to/repo -s 'notify-send "There are new commits on your branch!"' 24 | ``` -------------------------------------------------------------------------------- /docs/content/guides/docker-compose.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Docker Compose" 3 | weight = 4 4 | +++ 5 | 6 | # Docker Compose 7 | 8 | `gw` plays very well with `docker`, especially `docker compose`. You can use `gw` to build the Docker image and then stop and recreate the container. It can be done with `docker`, but it is recommended to do it with `docker compose` as it will handle the whole lifecycle of build, stop and restart. 9 | 10 | ## Project configuration 11 | 12 | Make sure to have a `docker-compose.yaml` file in the root directory. It can start existing containers or build images from the local files. 13 | 14 | > **Note**: if you build docker images locally, you save the storage and transfer costs of the docker image repository. 15 | 16 | Since it is also a file in your git repository, it basically doubles as an infrastructure-as-a-code. You can modify the `docker compose` setup (e.g. add another dependency, for example cache), commit and have the changes reflected in your infrastructure immediately. 17 | 18 | ## gw configuration 19 | 20 | Just simply point to your repository and run `docker compose up`. It will restart your containers and apply any new changes. 21 | 22 | ```sh 23 | gw /path/to/repo -s 'docker compose up -d' 24 | ``` 25 | 26 | If you are building your containers, add `--build`: 27 | 28 | ```sh 29 | gw /path/to/repo -s 'docker compose up -d --build' 30 | ``` 31 | 32 | ## Systemd unit 33 | 34 | One neat way to use `docker-compose` is to use it together with [systemd units](/usage/systemd). They play very nicely together 35 | because `docker-compose` is hard to be containerized, but this way it can run in the background on the host and update automatically. 36 | 37 | To create a systemd unit, you can use the [systemd usage guide](/usage/systemd#usage), but add this to the unit file (e.g. `/etc/systemd/system/gw.service`): 38 | 39 | ```ini 40 | # /etc/systemd/system/gw.service 41 | [Unit] 42 | Description = Autorestartable docker-compose for application 43 | After = NetworkManager-wait-online.service network.target docker.service 44 | PartOf = docker.service 45 | 46 | [Service] 47 | Type = simple 48 | ExecStart = /usr/local/bin/gw /path/to/repo -v -p '/usr/bin/docker compose -f /path/to/repo/docker-compose.yml up --build' 49 | 50 | [Install] 51 | WantedBy = default.target 52 | ``` 53 | 54 | This will rebuild your containers in the directory, every time there is a change. 55 | 56 | ### Template systemd unit 57 | 58 | If you have many applications that you want to autorestart with `docker-compose`, it might make sense to use a [template systemd unit](https://fedoramagazine.org/systemd-template-unit-files/). 59 | These are units that have a pattern (`%I`) in their unit files, which you can call multiple times with multiple configurations. 60 | 61 | For example if you have a `/path/to/repos/app1` and `/path/to/repos/app2`, you can create a generic systemd unit such as this 62 | (make sure that the filename ends with `@`): 63 | 64 | ```ini 65 | # /etc/systemd/system/gw@.service 66 | [Unit] 67 | Description = Autorestartable docker-compose for %I 68 | After = NetworkManager-wait-online.service network.target docker.service 69 | PartOf = docker.service 70 | 71 | [Service] 72 | Type = simple 73 | WorkingDirectory = /path/to/repos/%I 74 | ExecStart = /usr/local/bin/gw /path/to/repos/%I -vv -p '/usr/bin/docker compose -f /path/to/repos/%I/docker-compose.yml -p %I up --build' 75 | 76 | [Install] 77 | WantedBy = default.target 78 | ``` 79 | 80 | You can call it using the app name after a `@`, like `gw@app1` and `gw@app2`, and the `%I` will be automatically replaced with `app1` and `app2`. 81 | So in this case `systemctl start gw@app1` will start a `docker-compose` in `/path/to/repos/app1` using the `/path/to/repos/app1/docker-compose.yml` file 82 | with the project name `app1`. 83 | 84 | You can extend these further by simply adding new directories and starting an automaticly deploying process with one line of code. 85 | -------------------------------------------------------------------------------- /docs/content/guides/dynamic.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Dynamic languages" 3 | weight = 2 4 | +++ 5 | 6 | # Dynamic languages 7 | 8 | For dynamic languages you can use `gw` very simply with [processes](/usage/actions#processes). 9 | 10 | ## Configuration 11 | 12 | Wrap the way you normally start your program with the `--process` flag. 13 | 14 | ### Node.js 15 | 16 | For example for Node.js programs, use the usual `npm run start` script with a process: 17 | 18 | ```sh 19 | gw /path/to/repo -p 'npm run start' 20 | ``` 21 | 22 | You can also run the unit tests first, if you want to make sure to restart if the code is in a correct state: 23 | 24 | ```sh 25 | gw /path/to/repo -s 'npm run test' -p 'npm run start' 26 | ``` 27 | 28 | If you want to use a build step, for example for TypeScript or Next.js, look at the [TypeScript guide](/guides/compiled#typescript). 29 | 30 | ### Python 31 | 32 | Use the same idea with Python, wrap your program's entrypoint in a process: 33 | 34 | ```sh 35 | gw /path/to/repo -p 'python manage.py runserver' 36 | ``` 37 | 38 | ### Ruby 39 | 40 | Same thing with Ruby, add process to your program's entrypoint: 41 | 42 | ```sh 43 | gw /path/to/repo -p 'bin/rails server' 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/content/guides/netlify.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Netlify alternative" 3 | weight = 7 4 | +++ 5 | 6 | # Netlify alternative 7 | 8 | If you want to generate a version for every commit of your code, and expose it as hashes, you can `gw` and environment variables. 9 | 10 | > **Note**: This solution **won't** be exactly like Netlify, it won't have the web UI, on-click rollbacks, etc. As this already requires a generous amount of ducktape, please be vary of using this on production. If you need all the features of Netlify, just use Netlify. 11 | 12 | ## Project configuration 13 | 14 | The main idea behind this solution is that most static site generators allow changing output directories. We can wire this together with the git short hash set in the environment by `gw`, and build the different versions side-by-side. 15 | 16 | For this you have to find the output directory flag in your static site generator and set it to the `GW_GIT_COMMIT_SHORT_SHA` variable. For example for Jekyll and Hugo this is the `--destination` flag, in 11ty this is the `--output` flag. 17 | 18 | ```sh 19 | jekyll build --destination=output/$GW_GIT_COMMIT_SHORT_SHA 20 | hugo --destination=output/$GW_GIT_COMMIT_SHORT_SHA 21 | npx @11ty/eleventy --input=. --output=output/$GW_GIT_COMMIT_SHORT_SHA 22 | ``` 23 | 24 | ## gw configuration 25 | 26 | You can use this command to configure your `gw`: 27 | 28 | ```sh 29 | gw /path/to/repo -S 'jekyll build --destination=output/$GW_GIT_COMMIT_SHORT_SHA' 30 | ``` 31 | 32 | To build another version for the latest you can copy the files to another folder: 33 | 34 | ```sh 35 | gw /path/to/repo -S 'jekyll build --destination=output/$GW_GIT_COMMIT_SHORT_SHA' -S 'cp -r output/$GW_GIT_COMMIT_SHORT_SHA output/latest' 36 | ``` 37 | 38 | ## Web server configuration 39 | 40 | One extra setup that you have to do is point your web server to this directory. By default you can use it as path prefixes, but it can be configured to sub domains to these directories. That way you could reach the commit `0c431ff1` on the url `0c431ff1.example.net`. 41 | 42 | > **Note**: Make sure to setup wildcard domains in your DNS server so it redirects all domains to your server! 43 | 44 | ## Nginx 45 | 46 | You can use regexes in [server_name](https://nginx.org/en/docs/http/server_names.html) to rewrite subdomains into different folders. For example this configuration will resolve `0c431ff1.example.net` to `/path/to/repo/0c431ff1`: 47 | 48 | ```sh 49 | http { 50 | server { 51 | # It will capture the subdomain (e.g. 0c431ff1.example.net) 52 | server_name ~^([0-9a-f])\.example\.net$; 53 | 54 | location / { 55 | # And resolve to /path/to/repo/0c431ff1 56 | root /path/to/repo/$1; 57 | } 58 | } 59 | 60 | # You can add another to reach the latest 61 | server { 62 | server_name example.net; 63 | 64 | location / { 65 | root /path/to/repo/latest; 66 | } 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /docs/content/guides/php.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "PHP" 3 | weight = 1 4 | +++ 5 | 6 | # PHP 7 | 8 | The simplest configuration is for PHP, because you don't have to build or restart anything. 9 | 10 | ## Configuration 11 | 12 | Just simply set `gw` to watch the directory and it will pull the changes: 13 | 14 | ```sh 15 | gw /path/to/directory 16 | ``` 17 | 18 | In case, you don't have access in your shared hosting to start long-running tasks, but you can run cronjobs (e.g. CPanel), you can still use `gw`. Just download the binary and add to the crontab with the `--once` flag: 19 | 20 | ```sh 21 | * * * * * gw /path/to/directory --once 22 | ``` 23 | 24 | This will pull changes every minute. -------------------------------------------------------------------------------- /docs/content/reference/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Reference" 3 | sort_by = "weight" 4 | 5 | weight = 200 6 | paginate_by = 1000 7 | insert_anchor_links = "right" 8 | +++ 9 | 10 | The reference contains detailed information about the inner workings of `gw`. 11 | -------------------------------------------------------------------------------- /docs/content/reference/authentication.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Authentication" 3 | weight = 2 4 | +++ 5 | 6 | # Authentication 7 | 8 | By default `gw` supports the same authentication methods that `git` does: you can use username-password authentication for HTTPS and ssh-keys with SSH. It uses your platform's credential helpers to find saved authentications. If you want to change these settings, you can use command-line flags. 9 | 10 | To debug authentication issues, it is recommended to run `gw` with `-vvv` (tracing mode) to log every credential attempt. 11 | 12 | ## SSH 13 | 14 | For SSH it is recommended to use the ssh-keys that you are already using on your system. If you cloned the repository with a user, you can use the same user to run `gw`. If you are running `gw` in a container, you can mount the whole `.ssh` folder into `/root/.ssh` (`.ssh/known_hosts` is usually needed as well). 15 | 16 | > **Note**: If you only running `gw` for a single repository, for improved security use read-only [Deploy keys](#deploy-keys). 17 | 18 | By default SSH authentication checks these files for credentials: 19 | 20 | - `.ssh/id_dsa` 21 | - `.ssh/id_ecdsa` 22 | - `.ssh/id_ecdsa_sk` 23 | - `.ssh/id_ed25519` 24 | - `.ssh/id_ed25519_sk` 25 | - `.ssh/id_rsa` 26 | 27 | If you want to use another file, you can use the `--ssh-key` (or `-i`) option: 28 | 29 | ```sh 30 | gw /path/to/repo --ssh-key ~/.ssh/id_deploy 31 | ``` 32 | 33 | ### SSH known hosts 34 | 35 | It is recommended to use the same `.ssh` directory that you used for cloning the repository, because git also requires that the remote's host key appears in `known_hosts`. However, if `gw` doesn't find a `.ssh/known_hosts` file (e.g. in a container), it will create a new one using some common host keys for GitHub, GitLab and Bitbucket. If you are using any of these services, `gw` should work out of the box. 36 | 37 | In case you are using another service or self-host your git server, host key checking might fail. Use `--git-known-host` to add a custom host key instead of the default contents to the `.ssh/known_hosts`. 38 | 39 | ```sh 40 | gw /path/to/repo --git-known-host "codeberg.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIVIC02vnjFyL+I4RHfvIGNtOgJMe769VTF1VR4EB3ZB" 41 | ``` 42 | 43 | This is designed for simple operations, if you want to add multiple entries you are better off creating your own `.ssh/known_hosts` file. 44 | 45 | ### Deploy keys 46 | 47 | If you want to use ssh keys with only one repository it is usually better to create a Deploy Key. These are the same as regular ssh keys, but only have pull access to one repository, reducing the attack surface. To get started generate an ssh key: 48 | 49 | ``` 50 | ssh-keygen 51 | ``` 52 | 53 | Both [GitHub](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys) and [GitLab](https://docs.gitlab.com/ee/user/project/deploy_keys) supports Deploy keys by uploading it in the repository settings. Go to **Settings** (or **Settings** > **Repository** on GitLab) and enter **Deploy keys** and press **Add new deploy key**. Copy the content of the public key into the **Key** textarea and save it. `gw` never writes, so pull access is enough. 54 | 55 | If you used a non-default path, set it with `--ssh-key`: 56 | 57 | ```sh 58 | gw /path/to/repo --ssh-key ~/.ssh/id_deploy 59 | ``` 60 | 61 | ## Https 62 | 63 | Even though it is less common on servers, you can also use HTTPS for pulling repositories. By default `gw` will check credential helpers to extract username and passwords. If you want to set a username and password manually you can use the `--git-username` and `--git-token` fields. 64 | 65 | > **Note**: **Never** use your password as the `--git-token`, always use read-only [repository-level tokens](#repository-level-tokens) instead. 66 | 67 | ```sh 68 | gw /path/to/repo --git-username username --git-token f7818t23fb1amsc 69 | ``` 70 | 71 | If you are going this route, be careful to never leak your credentials and only use tokens with minimal privileges! 72 | 73 | ### Repository-level tokens 74 | 75 | Similarly to Deploy keys, it is recommended to only allow tokens access to a single repository. For this you can use [GitHub's Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) or [GitLab's Deploy tokens](https://docs.gitlab.com/ee/user/project/deploy_tokens/). 76 | 77 | #### Set up fine-grained access tokens in GitHub 78 | 79 | In GitHub, there are no repository scoped tokens, but you can emulate one with [Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token). 80 | 81 | Click on your avatar and go to **Settings** > **Developer Settings** > **Personal access tokens** > **Fine-grained tokens**. Click on **Generate new token**, enter a name and set an expiration longer into the future. Select the repositories that this token should have access under **Only select repositories**. Under **Repository Permissions** select **Access: read-only** for **Contents**. Copy this token and use it with your GitHub username: 82 | 83 | ```sh 84 | gw /path/to/repo --git-username octocat --git-token github_pat_11AD... 85 | ``` 86 | 87 | #### Set up deploy tokens in GitLab 88 | 89 | In GitLab there are [Deploy tokens](https://docs.gitlab.com/ee/user/project/deploy_tokens/), that are access tokens scoped to a repository. 90 | 91 | Go to the project's **Settings** > **Repository** > **Deploy tokens** and click on **Add token**. Fill out the username and check `read_repository` to be able to pull commits. Copy the username and the token to use it: 92 | 93 | ```sh 94 | gw /path/to/repo --git-username git_token --git-token gldt-... 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/content/reference/commandline.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "CLI arguments" 3 | weight = 3 4 | +++ 5 | 6 | # Command-line arguments 7 | 8 | This details the arguments that the `gw` binary takes. 9 | 10 | ## Positional arguments 11 | 12 | Every `gw` execution should specify a directory to a git repository. This will be the repository which the `gw` checks to see if there are any changes and run actions. 13 | 14 | ## Flag arguments 15 | 16 | `gw` follows GNU argument conventions, so every short arguments start with `-` and long arguments start with `--`. 17 | 18 | ### Basic flags 19 | 20 | To get information about `gw` or change the output settings (more verbose with `-v` or quieter with `-q`), you can use these flags: 21 | 22 | | Argument name | Example | Notes | 23 | | ----------------- | ----------- | ---------------------------------------------------------------------- | 24 | | `-v`, `--verbose` | `-v`, `-vv` | Increase verbosity, can be set multiple times (-v debug, -vv tracing). | 25 | | `-q`, `--quiet` | `-q` | Only print error messages. | 26 | | `-V`, `--version` | `--version` | Print the current version. | 27 | | `-h`, `--help` | `--help` | Print this help. | 28 | 29 | ### Trigger flags 30 | 31 | Use these flags, to set the different modes to check for changes: 32 | 33 | - Scheduled triggers (`-d`, default every 1 minute): check with a specified interval using [duration-string](https://github.com/Ronniskansing/duration-string) settings. Pass `0s` for disabling scheduled triggers. 34 | - Trigger once (`--once`): check if there are changes and then exit immediately. 35 | - Http trigger (`--http`): run an HTTP server on an interface and port (e. g. `0.0.0.0:8000`), which trigger on any incoming request. For more information, see [Webhook](/usage/webhook). 36 | 37 | | Argument name | Example | Notes | 38 | | --------------- | ------------------------------------------------ | ---------------------------------------------------------------------- | 39 | | `-d`, `--every` | `-d 5m`, `-d 1h`, `-d 0s` | Refreshes the repo with this interval. (default: 1m) | 40 | | `--once` | `--once` | Try to pull only once. Useful for cronjobs. | 41 | | `--http` | `--http localhost:1234`, `--http 127.0.0.1:4321` | Runs an HTTP server on the URL, which allows to trigger by calling it. | 42 | 43 | ### Check flags 44 | 45 | These flags change the way `gw` checks the git repository for changes: 46 | 47 | - On every push (`--on push`, default): pull the commits on the current branch and run actions if there are any new commits. 48 | - On every tag (`--on tag` or `--on tag:v*`): fetch the commits on the current branch and only pull to the first tag. You can pass a glob, in which case the first tag matching the glob. If there are no matching tags, no pull happens. 49 | 50 | You can also configure the authentication for the git repository: 51 | 52 | - SSH authentication (`-i`, `--ssh-key`): specify the path to the SSH key that will. 53 | - HTTP authentication (`--git-username`, `--git-token`): specify a username-token pair. 54 | 55 | For more information see [Authentication](/reference/authentication). 56 | 57 | | Argument name | Example | Notes | 58 | | ------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | 59 | | `--on` | `--on push`, `--on tag`, `--on tag:v*` | The trigger on which to run (can be `push`, `tag` or `tag:pattern`). (default: push) | 60 | | `-i`, `--ssh-key` | `-i ~/.ssh/test.id_rsa` | Set the path for an ssh-key to be used when pulling. | 61 | | `--git-username` | `--git-username daniel7grant` | Set the username for git to be used when pulling with HTTPS. | 62 | | `--git-token` | `--git-token 'ghp_jB3c5...'` | Set the token for git to be used when pulling with HTTPS. | 63 | | `--git-known-host` | `--git-known-host 'example.com ssh-rsa AAAAB3NzaC...'` | Add this line to the known_hosts file to be created (e.g. "example.com ssh-ed25519 AAAAC3..."). | 64 | 65 | ### Action flags 66 | 67 | These flags configure the actions that should be run when the changes occur. These come in two flavours lowercase letters indicate that it is run directly, while uppercase letters will run in a subshell (e.g. `/bin/sh` on Linux). This is useful if you want to expand variables, pipe commands etc. It is recommended to always use single-quotes for the argument values to avoid accidental shell issues (e.g. expanding variables at start time). 68 | 69 | - Run scripts (`-s`, `-S`): execute a script on every change, that will be waited until it ends. 70 | - Start process (`-p`, `-P`): start a process, when starting `gw`, that will be restarted on every change. 71 | 72 | You can also configure the process running: 73 | 74 | - Retries (`--process-retries`): in case of a failed process, how many time should it be restarted, before marking it failed. 75 | - Stop settings (`--stop-signal`, `--stop-timeout`): how to stop the process in case of a restart, by default sending `SIGINT` and after 10s a `SIGKILL` (supported only on `*NIX`). 76 | 77 | For more information see [Actions on pull](/usage/actions). 78 | 79 | | Argument name | Example | Notes | 80 | | ------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | 81 | | `-s`, `--script` | `-s 'cat FILENAME'` | A script to run on changes, you can define multiple times. | 82 | | `-S` | | Run a script in a shell. | 83 | | `-p`, `--process` | | A background process that will be restarted on change. | 84 | | `-P` | | Run a background process in a shell. | 85 | | `--process-retries` | | The number of times to retry the background process in case it fails. By default 0 for no retries. | 86 | | `--stop-signal` | | The stop signal to give the background process. Useful for graceful shutdowns. By default SIGINT. (Only supported on \*NIX) | 87 | | `--stop-timeout` | | The timeout to wait before killing for the background process to shutdown gracefully. By default 10s. | 88 | -------------------------------------------------------------------------------- /docs/content/reference/comparison.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Comparison" 3 | weight = 1 4 | +++ 5 | 6 | # Comparison 7 | 8 | There are a lot of tools that can help you deploy code only by pushing to a git repository. `gw` offers a completely unique proposition without locking yourself into expensive cloud services or complicated setups like Kubernetes. 9 | 10 | ## ArgoCD 11 | 12 | **Similar products**: [FluxCD](https://fluxcd.io/) 13 | 14 | The main inspiration for `gw` was undeniably [ArgoCD](https://argo-cd.readthedocs.io/en/stable/). It is a great tool that can access git repositories and sync your application to Kubernetes with the latest changes. Leveraging the Kubernetes platform it can reconcile changes (you don't need to figure out imperatively how to update) and provides autohealing, no-downtime rollouts and simple rollbacks. There is a web interface which allows to visualise, redeploy or rollback the applications with one click. 15 | 16 | The main disadvantage of ArgoCD (and similar tools) is the tight integration with Kubernetes. For smaller applications it's not worth to maintain a Kubernetes cluster, but you might want to still use GitOps. If you don't need scalability, it is less complex to setup a `gw` script on a cheap VPS. 17 | 18 | ## Netlify 19 | 20 | **Similar products**: [Vercel](https://vercel.com/), [Cloudflare Workers/Pages](https://workers.cloudflare.com/) 21 | 22 | Cloud tools like [Netlify](https://www.netlify.com/) were the first ones that really moved automatic deployments to the mainstream. You can connect a git repository to Netlify, which then builds and deploys a separate version of your application on every commit. You can preview these and promote them to production or rollback if an issue arises. Netlify also takes care of DNS and certificate management. 23 | 24 | However with Netlify you can only deploy some compatible stacks (static application with serverless functions). If you want to deploy full-stack applications or need advanced features (e.g. task management or notifications), you might need to pay for separate services... if you can do it at all. These cloud-based vendors also lock you into their services, which makes it harder to move between the platforms. `gw` is entirely platform-independent and can build and deploy your application even if it is completely full-stack. By deploying to a single VPS you can avoid suprising bills that you can't get out of. 25 | 26 | ## GitHub Actions 27 | 28 | **Similar products**: [GitLab CI](https://docs.gitlab.com/ee/ci/), [Jenkins](https://www.jenkins.io/) 29 | 30 | A common way to deploy applications automatically is push-based CD. It means using CI (for example [GitHub Actions](https://docs.github.com/en/actions)) to build and push the code to the server. It can be useful because it can integrate with your already existing solutions. You check the code by unit and integration testing before pulling the trigger on a deployment. It also provides pre-built actions which can handle complex use-cases (Docker building, deploying IaaC). 31 | 32 | The biggest drawback of push-based deployments is that it needs access to your server. If you just want to copy some code to a server it might be a security risk to allow SSH access from an untrusted CI worker. It can get even more complicated when your servers are in a secure network (behind NAT or VPN). On the other hand, `gw` can run on your server and can pull the code, avoiding the security problems altogether. 33 | 34 | ## Watchtower 35 | 36 | [Watchtower](https://containrrr.dev/watchtower/) provides a half-push, half-pull approach using the common element between the CI and the server: the image registry. You can use your existing CI infrastructure to build a Docker image and push it to an image registry, while the server can listen to the registry and update the running containers on demand. This is a very good solution that can use existing CI code, while also providing instant feedback from the server. 37 | 38 | The main issue with this solution is that it puts the image registry under a lot of pressure. If you are deploying often, the storage and network costs might climb very quickly. If you are building large Docker images, it can also get considerable slow: CI-s rarely cache effectively and pushing and pulling from a registry can also be a slow operation. With `gw` you can save this roundtrip, building the Docker image right on the server. It also improves caching because the previous version of the image is right there. You have to be careful not to overload your server but it might be worth to avoid slow and expensive registries. 39 | 40 | ## Coolify 41 | 42 | **Similar products**: [Dokploy](https://dokploy.com/) 43 | 44 | [Coolify](https://coolify.io/) is very definitely the closest product to `gw`: an open-source, self-hostable, pull-based deployment platform that runs everywhere. You can install Coolify and all of their dependencies with a single command. It has a neat web interface, can manage databases with external backups and provides a reverse proxy with automatically renewing certificates. It is a great solution if you want to have everything set up automatically! 45 | 46 | The main problem with Coolify is that it takes over your server entirely. Instead of working with your existing deployments, it handles Docker, reverse proxying and certificates. It might be an excellent way of running things, but if you have some specific feature or you already have a running server, it might be a serious investment to transfer. Compared to this, `gw` draws from the UNIX philosophy: it is a modular piece of software that you can integrate with other parts to achieve what you want. You can slot in `gw` to almost any existing deployment, but expect to handle databases, certificates and other problems with different tools. 47 | 48 | ## while true; do git pull; done 49 | 50 | > I can do all of this with shell scripts... 51 | 52 | While it is true that in the core, `gw` is just a loop running git pull and actions, but it is also much more. With shell scripts you have to handle git polling, process management and logging, while also handling errors without crashing the script. Not to mention more advanced features like graceful shutdowns, multiple scripts and processes, git authentication or webhooks. `gw` is a very lightweight binary that provides all of these, while being configurable, portable and reliable. If you prefer to write the shell script you can do it, but if you don't, just drop `gw` in and let it handle the boring parts for you! 53 | -------------------------------------------------------------------------------- /docs/content/reference/environment-variables.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Env variables" 3 | weight = 4 4 | +++ 5 | 6 | # Environment variables 7 | 8 | The different steps can add variables to the context, which are exposed to the scripts as environment variables. 9 | All of these are prefixed with `GW_` to avoid collisions. The second part usually identifies the specific trigger, 10 | check or action. 11 | 12 | If you want to use the environment variables in [command-line arguments](/reference/commandline), make sure to use the subshell variants (`-S`, `-P`), 13 | because only these can expand variables. It is recommended to use single-quotes to avoid expanding at start time. A good way 14 | to debug environment variables is to print them with `-S 'printenv'`. 15 | 16 | ## Trigger variables 17 | 18 | These are the variables that are exposed from the trigger, which can be scheduled trigger or an HTTP endpoint. 19 | 20 | | Variable name | Example | Notes | 21 | | ------------------- | ------------------ | --------------------------------------- | 22 | | `GW_TRIGGER_NAME` | `SCHEDULE`, `HTTP` | The identifier of the trigger. | 23 | | `GW_HTTP_METHOD` | `GET`, `POST` | The HTTP method that was called. | 24 | | `GW_HTTP_URL` | `/`, `/trigger` | The HTTP URL that was called. | 25 | | `GW_SCHEDULE_DELAY` | `1m`, `1d`, `1w` | The delay between two scheduled checks. | 26 | 27 | ## Check variables 28 | 29 | These are the variables that are exposed from the check, which currently is always git. 30 | 31 | | Variable name | Example | Notes | 32 | | -------------------------------- | ------------------------------------ | --------------------------------------------- | 33 | | `GW_CHECK_NAME` | `GIT` | The identifier of the check. | 34 | | `GW_GIT_BEFORE_COMMIT_SHA` | `acfd4f88da199...` | The SHA of the commit before the pull. | 35 | | `GW_GIT_BEFORE_COMMIT_SHORT_SHA` | `acfd4f8` | The 7-character short hash of the commit. | 36 | | `GW_GIT_BRANCH_NAME` | `main` | The name of the branch, that the repo is on. | 37 | | `GW_GIT_COMMIT_SHA` | `acfd4f88da199...` | The SHA of the commit after the pull. | 38 | | `GW_GIT_COMMIT_SHORT_SHA` | `acfd4f8` | The 7-character short hash of the commit. | 39 | | `GW_GIT_REF_NAME` | `refs/heads/main`, `refs/tags/v1.0` | The full name of the current git ref. | 40 | | `GW_GIT_REF_TYPE` | `branch`, `tag` | The type of the ref we are currently on. | 41 | | `GW_GIT_REMOTE_NAME` | `origin` | The name of the remote used. | 42 | | `GW_GIT_REMOTE_URL` | `git@github.com:daniel7grant/gw.git` | The URL to the git remote. | 43 | | `GW_GIT_TAG_NAME` | `v1.0` | The tag of the pulled commit if there is one. | 44 | 45 | ## Action variables 46 | 47 | These are the variables added by the action, which is script or process. 48 | 49 | | Variable name | Example | Notes | 50 | | ---------------- | ------------------- | ------------------------------------------- | 51 | | `GW_ACTION_NAME` | `SCRIPT`, `PROCESS` | The identifier of the action. | 52 | | `GW_DIRECTORY` | `/src/http/gw` | The absolute path to the current directory. | 53 | -------------------------------------------------------------------------------- /docs/content/usage/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Usage" 3 | sort_by = "weight" 4 | 5 | weight = 0 6 | paginate_by = 1000 7 | 8 | insert_anchor_links = "right" 9 | +++ 10 | 11 | Here are your first steps with `gw`: -------------------------------------------------------------------------------- /docs/content/usage/actions.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Actions on pull" 3 | weight = 3 4 | +++ 5 | 6 | # Actions on pull 7 | 8 | The main point of `gw` is to do actions every time there code is pulled. There are multiple actions: running a script or restarting a background process. The order of the actions matter, and they will be executed sequentially based on the order of the command-line arguments. 9 | 10 | ## Scripts 11 | 12 | The simplest action is to run a command on pull with `--script` or `-s`: 13 | 14 | ```sh 15 | gw /path/to/repo -s 'echo "updated"' 16 | ``` 17 | 18 | You can define multiple scripts, these will run one after another (there is currently no way to parallelise these). If one of the scripts fail, the other scripts won't run at all. You can use scripts to run tests before updating your code. 19 | 20 | ```sh 21 | gw /path/to/repo -s 'echo "testing"' -s 'echo "updating"' 22 | ``` 23 | 24 | > **Note**: If you have more than 2-3 scripts on every pull it might be worth it to refactor it into an `update.sh` shell script. It can contain logic and be commited which helps if you want to change it without updating the gw process. 25 | 26 | The output of the script is not printed by default, you can increase verbosity (`-v`) to get output from the script: 27 | 28 | ```sh 29 | $ gw /path/to/repo -v -s 'echo "updated"' 30 | 2024-10-18T16:28:53.907Z [INFO ] There are updates, running actions. 31 | 2024-10-18T16:28:53.907Z [INFO ] Running script "echo" in /path/to/repo. 32 | 2024-10-18T16:28:53.913Z [DEBUG] [echo] updated 33 | 2024-10-18T16:28:53.913Z [INFO ] Script "echo" finished successfully. 34 | ``` 35 | 36 | By default, scripts are executed directly to avoid common issues with shells (e.g. shell injection and unexpected globbing). If you instead want to run in a shell to expand variables or use shell specific functionality (e.g. pipes or multiple commands), use the `-S` flag. These scripts will run in a shell: `/bin/sh` on Linux and `cmd.exe` on Windows. 37 | 38 | ```sh 39 | gw /path/to/repo -S 'ls -l . | wc -l' 40 | ``` 41 | 42 | The full enviroment is passed to scripts with a number of [gw-specific environment variables](/reference/environment-variables). If you want to use variables make sure to use singlequotes so they aren't expanded beforehand. 43 | 44 | ```sh 45 | gw /path/to/repo -S 'ls -l $BUILD_DIRECTORY | wc -l' 46 | ``` 47 | 48 | Best use-cases for scripts: 49 | 50 | - [compile](/guides/compiled) or transpile your code, 51 | - rebuild some assets, 52 | - restart or reload a separately running program. 53 | 54 | ## Processes 55 | 56 | If you have some long-running program, you can use `--process` or `-p` to start as a background process and `gw` will restart it on every pull: 57 | 58 | ```sh 59 | gw /path/to/repo -p 'ping 1.1.1.1' 60 | ``` 61 | 62 | Processes are started when `gw` is started and they are kept in the background. If there is a change the process is stopped and a new process is started. If you want to look at the output of process, you have to increase verbosity (`-v`): 63 | 64 | ```sh 65 | $ gw /path/to/repo -v -s 'ping 1.1.1.1' 66 | 2024-03-10T15:04:37.740Z [INFO ] There are updates, running actions. 67 | 2024-10-16T18:04:25.888Z [INFO ] Starting process "ping" in /path/to/repo. 68 | 2024-10-16T18:04:25.906Z [DEBUG] [ping] PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. 69 | 2024-10-16T18:04:25.906Z [DEBUG] [ping] 64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=16.8 ms 70 | ``` 71 | 72 | Similarly to scripts, processes are executed directly. If you want to use the native shell for variable expansion or shell-specific functionality, you can use `-P`. 73 | 74 | ```sh 75 | gw /path/to/repo -P 'ping $TARGET_IP' 76 | ``` 77 | 78 | Unlike scripts, you can only define one process. Processes also can't access gw-specific environment variables. Scripts defined before process will be run before the restart and if defined after they will run after. If any of the scripts before the process fails the process will not be restarted. You can add tests and other checks to only restart the process if the code is 100% correct. 79 | 80 | ```sh 81 | gw /path/to/repo -s 'echo this runs before' -p 'ping 1.1.1.1' -s 'echo this runs after' 82 | ``` 83 | 84 | If a process fails, by default it marked failed and an error printed. If you want to retry the process you can set the `--process-retries` flag: 85 | 86 | ```sh 87 | gw /path/to/repo -v -s 'ping 1.1.1.1' --process-retries 5 88 | ``` 89 | 90 | You can also change the stopping behaviour. By default processes are first tried to be gracefully stopped with SIGINT and after some timeout (default: 10s) they are killed. If you want to influence these values you can set `--stop-signal` and `--stop-timeout` respectively. On non-Unix systems these options do nothing and the process is always killed. 91 | 92 | ```sh 93 | gw /path/to/repo -v -s 'ping 1.1.1.1' --stop-signal SIGTERM --stop-timeout 10s 94 | ``` 95 | 96 | Best use-cases for processes: 97 | 98 | - run [interpreted programs](/guides/interpreted) e.g. web frameworks, 99 | - run binaries after [compiling](/guides/compiled), 100 | - run external programs to restart [on config change](/guides/configuration). 101 | -------------------------------------------------------------------------------- /docs/content/usage/crontab.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Crontab" 3 | weight = 6 4 | +++ 5 | 6 | # Crontab 7 | 8 | If you don't have shell access to the server, you can still run `gw` with the crontab. 9 | 10 | > **Note:** this will disable some advanced functions like [webhooks](/usage/webhook). Only use this if you cannot use any other solution. 11 | 12 | ## Usage 13 | 14 | There is a `--once` flag in `gw`, that checks the repository for updates and then exits. You can use this to pair with your own scheduled runner to pull for changes manually. Simply open your crontab: 15 | 16 | ```sh 17 | crontab -e 18 | ``` 19 | 20 | ...and add a new line with your `gw` script. You can use `* * * * *` to run it every minute, but you can use more advanced patterns as well (see [crontab.guru](https://crontab.guru/)). For the command, make sure to specify `--once` to avoid running continuously and add `--quiet` so it will only print on a failure: 21 | 22 | ```sh 23 | * * * * * gw /path/to/repo --once --quiet 24 | ``` 25 | 26 | > **Warning**: Cronjobs are known to be error-prone and hard to debug, so make sure to test this solution extensively before relying on this in the real world. 27 | -------------------------------------------------------------------------------- /docs/content/usage/docker.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Docker container" 3 | weight = 5 4 | +++ 5 | 6 | # Docker container 7 | 8 | If you don't want to install or run `gw` on your server, you can also use the prebuilt Docker images at [danielgrant/gw](https://hub.docker.com/r/danielgrant/gw). 9 | 10 | ## Usage 11 | 12 | If you just want to pull a repository or run simple scripts, you can run the container with [docker](https://docs.docker.com/engine/install/). You can mount a repository to a directory and watch it. For example: 13 | 14 | ```sh 15 | docker run -d --name gw -v /path/to/repo:/app danielgrant/gw /app 16 | ``` 17 | 18 | You can also run scripts, but these images are very small and only have a few programs set up: 19 | 20 | ```sh 21 | docker run -d --name gw -v /path/to/repo:/app danielgrant/gw /app -s "cp -r build/ html/" 22 | ``` 23 | 24 | If you prefer to use `docker-compose`, you can copy this file to a `docker-compose.yaml` and run `docker compose up -d`: 25 | 26 | ```yaml 27 | # docker-compose.yaml 28 | version: "3" 29 | 30 | services: 31 | gw: 32 | container_name: gw 33 | image: danielgrant/gw 34 | command: /app 35 | volumes: 36 | - type: volume 37 | source: /path/to/repo 38 | target: /app 39 | - type: volume 40 | source: ~/.ssh 41 | target: /root/.ssh 42 | read_only: true 43 | ``` 44 | 45 | If you are using ssh-keys, mount the `.ssh` directory as well, so it can pull. For more information, see [Authentication](/reference/authentication). 46 | 47 | ## Customization 48 | 49 | ### Copy binary from gw 50 | 51 | Most applications have many dependencies and complicated setups, and are already running on Docker. In these cases it is often preferable to build the `gw` image on top of the already existing application image. 52 | 53 | > **Note**: This doesn't mean that these should be running in the same container, but they can use the same base image in two separate containers. It is a common wisdom that one container should run one thing. 54 | 55 | For this we can start off of our application image as a base layer and add the `gw` binary in a `COPY` layer. You can simply wrap your existing command using subprocess mode (`-p`) and it will restart the script every time a pull happened. 56 | 57 | ```dockerfile 58 | FROM example.org/registry/node-image:ubuntu 59 | 60 | # Copy from the `gw` image 61 | COPY --from=danielgrant/gw:0.4.1 /usr/bin/gw /usr/bin/gw 62 | 63 | ENTRYPOINT ["/usr/bin/gw"] 64 | CMD ["/app", "-p", "npm start"] 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/content/usage/installation.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Installation" 3 | weight = 1 4 | +++ 5 | 6 | # Installation 7 | 8 | `gw` is a simple few MB binary, which you can install multiple ways. 9 | 10 | ## Install from script 11 | 12 | The simplest way is to run the installation script: 13 | 14 | ```sh 15 | curl https://gw.danielgrants.com/install.sh | sh 16 | ``` 17 | 18 | This will download the script to `~/.local/bin` or if run by root to `/usr/local/bin`. 19 | 20 | ## Download from GitHub releases 21 | 22 | Another way is to download the zipped binary from [Github Releases](https://github.com/daniel7grant/gw/releases) and install it to your path: 23 | 24 | ```sh 25 | curl -LO https://github.com/daniel7grant/gw/releases/download/v0.4.1/gw-bin_x86_64-unknown-linux-gnu.zip 26 | unzip gw-bin_x86_64-unknown-linux-gnu.zip 27 | mv gw ~/.local/bin/gw 28 | rm gw-bin_x86_64-unknown-linux-gnu.zip 29 | ``` 30 | 31 | ## Install with Cargo 32 | 33 | If you have Rust on your machine, you can also install the `gw` binary with Cargo. Use [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) for a faster install or `cargo install` will build it from source. 34 | 35 | ```sh 36 | cargo binstall gw-bin 37 | # or 38 | cargo install gw-bin 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/content/usage/start.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Get started" 3 | weight = 2 4 | +++ 5 | 6 | # Get started 7 | 8 | `gw` is a simple program, that you can use to pull changes from a remote repository and run scripts on the change. 9 | 10 | ### Prerequisites 11 | 12 | First, make sure, that `gw` is installed successfully and is in your PATH. If you don't have it, start with [Installation](/usage/installation): 13 | 14 | ```sh 15 | $ gw --version 16 | 0.4.1 17 | ``` 18 | 19 | The other necessary part is a git repository to which you have pull access. It is recommended to use a repository that you know, but if you don't have one at hand, you can use the [daniel7grant/time](https://github.com/daniel7grant/time) repository. This is an example repository that is updated in every minute, so it is useful to test the auto update of `gw`. First clone this repository (if you are using your own, clone again), and enter the cloned directory: 20 | 21 | ```sh 22 | git clone https://github.com/daniel7grant/time.git 23 | cd time 24 | ``` 25 | 26 | ## Pull files automatically 27 | 28 | To get started, point `gw` to this local repository. By default it pulls the changes every minute. We can add the `--verbose` or `-v` flag to see when the changes occur: 29 | 30 | ```sh 31 | gw /path/to/repo -v 32 | ``` 33 | 34 | If you are using your own repository, create a commit in a different place, and see how it gets automatically pulled (in the case of the `time` repo, there is a commit every minute). The verbose logs should print that a git pull happened: 35 | 36 | ```sh 37 | $ gw /path/to/repo -v 38 | # ... 39 | 2024-03-10T14:48:13.447Z [DEBUG] Checked out fc23d21 on branch main. 40 | 2024-03-10T14:48:13.447Z [INFO ] There are updates, pulling. 41 | ``` 42 | 43 | Also check the files or the `git log` to see that it the repository has been updated: 44 | 45 | ```sh 46 | cat DATETIME # it should contain the latest time 47 | git log -1 # it should be a commit in the last minute 48 | ``` 49 | 50 | ## Run scripts on pull 51 | 52 | Pulling files automatically is useful but the `--script` or `-s` flag unlocks `gw`'s potential: it can run any kind of custom script if there are any changes. For a simple example, we can print the content of a file to the log with `cat`: 53 | 54 | ```sh 55 | gw /path/to/repo -v --script 'cat DATETIME' 56 | ``` 57 | 58 | This will run every time there is a new commit, and after the pull it will print the file contents. You can see that the results are printed in the log: 59 | 60 | ```sh 61 | $ gw /path/to/repo -v --script 'cat DATETIME' 62 | # ... 63 | 2024-10-18T16:28:53.907Z [INFO ] There are updates, running actions. 64 | 2024-10-18T16:28:53.907Z [INFO ] Running script "cat" in /path/to/repo. 65 | 2024-10-18T16:28:53.913Z [DEBUG] [cat] 2024-10-18T16:28:00+0000 66 | 2024-10-18T16:28:53.913Z [INFO ] Script "cat" finished successfully. 67 | ``` 68 | 69 | You can add multiple scripts, which will run one after another. Use these scripts to build source files, restarts deployments and anything else that you can imagine. 70 | 71 | For more information, see [Scripts](/usage/actions#scripts). 72 | 73 | ### Run subprocess, restart on pull 74 | 75 | It is often enough to run scripts, but many times you also want to maintain a long-running process e.g. for web services. `gw` can help you with this, using the `--process` or `-p` flag. This will start a process in the background and restart it on pull. 76 | 77 | For example starting a python web server: 78 | 79 | ```sh 80 | $ gw /path/to/repo -v --process "python -m http.server" 81 | # ... 82 | 2024-10-06T21:58:21.306Z [DEBUG] Setting up ProcessAction "python -m http.server" on change. 83 | 2024-10-06T21:58:21.306Z [DEBUG] Starting process: "python" in directory /path/to/repo. 84 | 2024-10-06T21:58:56.211Z [DEBUG] [python] Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 85 | ``` 86 | 87 | This will run a python process in the background and stop and start it again if a git pull happened. Just wrap your deployment script with `gw` and see it gets updated every time you push to git. 88 | 89 | For more information, see [Processes](/usage/actions#processes). 90 | 91 | ## Run actions on tags 92 | 93 | Pulling on every commit might not be the fit for every product, especially ones that needs to maintains compatibility or strictly versioned. For these, you can instead trigger on tags. Use the `--on tag` flag to only pull changes if there is a tag on the current branch. 94 | 95 | ```sh 96 | $ gw /path/to/repo -v --on tag -S 'echo $GIT_TAG_NAME' 97 | # ... 98 | 2024-10-18T16:28:53.907Z [INFO ] There are updates, running actions. 99 | 2024-10-18T16:28:53.907Z [INFO ] Running script "echo" in /path/to/repo. 100 | 2024-10-18T16:28:53.913Z [DEBUG] [echo] v0.1.0 101 | 2024-10-18T16:28:53.913Z [INFO ] Script "echo" finished successfully. 102 | ``` 103 | 104 | This will always fetch the current branch, check for the latest tag on it and pull only the commits up to that tag. To match some kind of commit, you can use the `--on tag:v*` which will only pull if the tag is matching the passed glob (in this case starting with `v`). 105 | 106 | ```sh 107 | gw /path/to/repo -v --on 'tag:v*' -S 'echo "new version: $GIT_TAG_NAME"' 108 | ``` 109 | 110 | ## Next steps 111 | 112 | If you like `gw`, there are multiple ways to use it for real-life use-cases. 113 | 114 | If you want to put the `gw` script in the background, you can: 115 | 116 | - wrap into a [systemd unit](/usage/systemd), if you want to manage it with a single file; 117 | - start in a [docker container](/usage/docker), if you already use Docker in your workflow; 118 | - or run periodically with [cron](/usage/crontab), if you don't have shell access to the server. 119 | 120 | If you are interested in some ideas on how to use `gw`: 121 | 122 | - if you only need to pull files, see [PHP guide](/guides/php); 123 | - if you are using a dynamic language (e.g. JavaScript, Python, Ruby), see [Guide for dynamic languages](/guides/dynamic) for example on running a process; 124 | - if you are using a compiled language (e.g. TypeScript, Go, Rust), see [Guide for compiled languages](/guides/compiled) for example on compiling a program; 125 | - if you use a `docker-compose.yaml`, see [Guide for docker-compose](guides/docker-compose); 126 | - if you want to easily manage configuration files as GitOps, see [Configuration guide](/guides/configuration); 127 | - for a full-blown example, check out [Netlify](/guides/netlify); 128 | - and many other things, for the incomplete list [guides page](/guides). 129 | -------------------------------------------------------------------------------- /docs/content/usage/systemd.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Systemd unit" 3 | weight = 4 4 | +++ 5 | 6 | # Systemd unit 7 | 8 | If you just want to run `gw` process in the background without installing anything, you can use `systemctl`. You only have to create a systemd unit and `start` and `enable` it. 9 | 10 | > **Note**: by default systemd units run as root, so make sure to set up the necessary authentication (e.g. SSH keys) with the root user as well 11 | > (or use a `User` directive or [user systemd unit](#user-systemd-unit)). 12 | > You can test this by entering the directory with the root user and running `git pull` or `gw -vv .`. 13 | 14 | ## Usage 15 | 16 | To create a new unit you have to create a new unit file at the default systemd unit location, usually `/etc/systemd/system`. 17 | 18 | You can change this example systemd unit to your use-case and copy it under `/etc/systemd/system/gw.service`: 19 | 20 | ```ini 21 | # /etc/systemd/system/gw.service 22 | [Unit] 23 | Description=Watch git repository at /path/to/repo 24 | After=multi-user.target 25 | 26 | [Service] 27 | Type=simple 28 | ExecStart=/usr/bin/gw -v /path/to/repo -s 'echo ran from systemctl unit' 29 | Restart=always 30 | # run as a non-root user (recommended) 31 | User=myuser 32 | 33 | [Install] 34 | WantedBy=default.target 35 | ``` 36 | 37 | To reload the systemd unit database, you have to run `daemon-reload`: 38 | 39 | ```sh 40 | systemctl daemon-reload 41 | ``` 42 | 43 | With this you should see a new `gw.service` unit. You can start this with `systemctl start`: 44 | 45 | ```sh 46 | systemctl start gw 47 | ``` 48 | 49 | If you want to start this every time your server boots up, you can run `systemctl enable`: 50 | 51 | ```sh 52 | systemctl enable gw 53 | ``` 54 | 55 | To see if your unit is running you check the status, or read the logs with `journalctl`: 56 | 57 | ```sh 58 | systemctl status gw 59 | journalctl -fu gw 60 | ``` 61 | 62 | For a more complicated example, check out the [docker-compose systemd unit](/guides/docker-compose#systemd-unit). 63 | 64 | ### User systemd unit 65 | 66 | Most of the time the git configuration is only set up for some users, so it might make sense to run `gw` as a user systemd unit. You can do it, but you have to be careful with some things. 67 | 68 | The main issue with user services is that they are bound to the user session, so if you log out from the SSH, all started units will end. You can enable [lingering](https://wiki.archlinux.org/title/Systemd/User#Automatic_start-up_of_systemd_user_instances) with `loginctl` to keep the systemd units running after logout: 69 | 70 | ``` 71 | loginctl enable-linger 72 | ``` 73 | 74 | After you set up lingering, you can create a similar systemd unit except under `~/.config/systemd/user/`: 75 | 76 | ```ini 77 | # /home/myuser/.config/systemd/user/gw.service 78 | [Unit] 79 | Description=Watch git repository at /path/to/repo 80 | After=multi-user.target 81 | 82 | [Service] 83 | Type=simple 84 | ExecStart=/usr/bin/gw /path/to/repo -s 'echo ran from systemctl unit' 85 | Restart=always 86 | 87 | [Install] 88 | WantedBy=default.target 89 | ``` 90 | 91 | The same commands should work as above but with adding `--user` after `systemctl`. So to enable this unit above, you can start: 92 | 93 | ```sh 94 | systemctl --user daemon-reload 95 | systemctl --user start gw 96 | systemctl --user enable gw 97 | systemctl --user status gw 98 | ``` 99 | 100 | If you want to check the logs with `journalctl`, make sure to add your user to the `systemd-journal` group (requires root privileges): 101 | 102 | ```sh 103 | sudo usermod -aG $USER systemd-journal 104 | ``` 105 | 106 | After this, you can read the logs of your user services: 107 | 108 | ```sh 109 | journalctl -f --user-unit gw 110 | ``` 111 | 112 | User services can be a good way to use `gw` if you don't have or don't want to use root privileges, while still being able to use an automatic deployment workflow. 113 | -------------------------------------------------------------------------------- /docs/content/usage/webhook.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Webhook server" 3 | weight = 7 4 | +++ 5 | 6 | # Webhook server 7 | 8 | By default `gw` checks for updates every minute. Depending on your usecase it can be too slow or too often. If you only want to pull updates when a push happens, git servers (GitHub, GitLab or any other) usually have options to send a HTTP request to your `gw` service (webhook). `gw` can handle webhooks with a built-in web server. 9 | 10 | ## Usage 11 | 12 | To enable the webhook server, you can use the `--http` option. Most of the time you want to allow external connections, so to set to a high port (for example `10101`), you can use: 13 | 14 | ```sh 15 | gw /path/to/repo -v --http 0.0.0.0:10101 16 | ``` 17 | 18 | If you call this endpoint with any method on any URL, it will trigger a check for updates. To test this, you can use `curl`: 19 | 20 | ```sh 21 | curl http://localhost:10101 22 | ``` 23 | 24 | The `curl` output should print `OK` and the `gw` logs should include lines that show that it was updated: 25 | 26 | ```sh 27 | $ gw /path/to/repo -v --http 0.0.0.0:10101 28 | # ... 29 | 2024-03-10T16:52:51.531Z [DEBUG] Received request on GET / 30 | 2024-03-10T16:52:52.055Z [DEBUG] Checked out 5e25714 on branch main. 31 | 2024-03-10T16:52:52.055Z [INFO ] There are updates, pulling. 32 | ``` 33 | 34 | ### Using only webhooks 35 | 36 | If you want to disable the scheduled checks altogether and rely on the webhooks, you can set the schedule duration (`-d` flag) to zero seconds: 37 | 38 | ```sh 39 | gw /path/to/repo -v --http 0.0.0.0:10101 -d 0s 40 | ``` 41 | 42 | You can use this to create a push-based deployment, for example calling the update from your CI process after your testing has run. 43 | 44 | ## Setup webhooks 45 | 46 | Exposing a port is only one half of the problem, you also have to set the webhooks up with your git server. For this you will need a public IP or a domain name, which will be in the `$DOMAIN` variable in these examples. 47 | 48 | > **Warning:** if you can configure, you should setup your reverse proxy in front of the port to avoid exposing externally. 49 | 50 | ### GitHub 51 | 52 | For GitHub, you have to have administrator access to the repository. Navigate to **Settings > Webhooks**, and click to **Add webhook**. Fill the **Payload URL** with your `$DOMAIN` (make sure to add the `http://` protocol and the port) and select **application/json** for **Content Type**. Save this webhook to activate. 53 | 54 | > **Note**: Secrets are currently not supported. 55 | 56 |  57 | 58 | On save, the webhook should send a `ping` event to `gw`. If you click into new webhook, to **Recent deliveries** you can see this event. 59 | 60 |  61 | 62 | A `POST /` request will also appear in the `gw` logs, assuming debug logging was enabled: 63 | 64 | ```sh 65 | $ gw /path/to/repo -v --http 0.0.0.0:10101 66 | # ... 67 | 2024-03-10T17:18:24.424Z [DEBUG] Received request on POST / 68 | 2024-03-10T17:18:24.567Z [DEBUG] There are no updates. 69 | ``` 70 | 71 | ### GitLab 72 | 73 | For GitLab, you have to have Maintainer access to the repository. Navigate to **Settings > Webhooks**, and click to **Add new webhook**. Fill the **URL** with your `$DOMAIN` (make sure to add the `http://` protocol and the port) and check the **Trigger** to **Push events** , you can filter it for example to only trigger on the `main` branch. If you are using `http`, you should disable SSL verification. Save this webhook to activate. 74 | 75 |  76 | 77 | To test this webhook, you can click **Test > Push events** next to the name. GitLab should show a message that **Hook executed successfully: HTTP 200**, and you can find a `POST /` request in the `gw` logs, assuming debug logging was enabled: 78 | 79 | ```sh 80 | $ gw /path/to/repo -v --http 0.0.0.0:10101 81 | # ... 82 | 2024-03-10T17:58:28.919Z [DEBUG] Received request on POST / 83 | 2024-03-10T17:58:29.052Z [DEBUG] There are no updates. 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/static/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: relative; 3 | } 4 | 5 | main a[href^="#"] { 6 | opacity: 30%; 7 | } 8 | 9 | a:not([href^='#']) { 10 | border-bottom: 1px solid #c9c9c9; 11 | } 12 | 13 | aside a:not([href^='#']) { 14 | border-bottom: none; 15 | } 16 | 17 | aside a:not([href^='#']).active { 18 | border-bottom: 2px solid #c9c9c9; 19 | } 20 | 21 | a:not([href^='#']):hover { 22 | border-bottom: 2px solid #c9c9c9; 23 | } 24 | 25 | @media screen and (min-width: 1200px) { 26 | aside { 27 | position: fixed; 28 | top: calc(3rem + 13px); 29 | bottom: 13px; 30 | left: 50%; 31 | transform: translate(calc(-100% - 20em)); 32 | padding: 0 20px; 33 | max-width: 200px; 34 | overflow-y: auto; 35 | } 36 | 37 | aside h1 { 38 | margin-top: 0; 39 | } 40 | 41 | .tagline { 42 | line-height: 1.33; 43 | margin-bottom: 1rem; 44 | } 45 | 46 | .tagline a:hover { 47 | border-bottom: none; 48 | } 49 | 50 | .tagline img { 51 | margin-top: 1rem; 52 | } 53 | 54 | .mobile-only { 55 | display: none; 56 | } 57 | } 58 | 59 | table { 60 | text-align: left; 61 | } 62 | 63 | table td:nth-child(1), 64 | table td:nth-child(2) { 65 | max-width: 120px; 66 | } 67 | 68 | 69 | @media screen and (min-width: 800px) { 70 | table td:nth-child(1), 71 | table td:nth-child(2) { 72 | max-width: 200px; 73 | } 74 | } 75 | 76 | table code { 77 | word-wrap: break-word; 78 | } -------------------------------------------------------------------------------- /docs/static/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -o pipefail 2>/dev/null | true 3 | set -eu 4 | 5 | fail() { 6 | echo $1 7 | exit 1 8 | } 9 | 10 | # CONFIGURE VARIABLES 11 | REPO="${REPO:-https://github.com/daniel7grant/gw}" 12 | VERSION="${VERSION:-v0.4.1}" 13 | if [ "$(id -u)" -ne "0" ]; then 14 | BIN_DIR="$HOME/.local/bin" 15 | else 16 | BIN_DIR="/usr/local/bin" 17 | fi 18 | if ldd /bin/ls | grep -q "musl"; then 19 | LIBC="musl" 20 | else 21 | LIBC="gnu" 22 | fi 23 | 24 | # DETERMINE THE CORRECT FILENAME 25 | PLATFORM=$(uname -sm) 26 | case "$PLATFORM" in 27 | "Linux x86_64") 28 | FILE="gw-bin_x86_64-unknown-linux-$LIBC.zip" 29 | ;; 30 | "Linux aarch"* | "Linux arm"*) 31 | FILE="gw-bin_arm-unknown-linux-gnueabihf.zip" 32 | ;; 33 | "Darwin arm64") 34 | FILE="gw-bin_aarch64-apple-darwin.zip" 35 | ;; 36 | *) 37 | fail "Platform $PLATFORM is currently not supported." 38 | ;; 39 | esac 40 | 41 | # DOWNLOAD AND MOVE IT TO BIN_DIR 42 | echo "Downloading version $VERSION to $PLATFORM..." 43 | DOWNLOAD_URL="$REPO/releases/download/$VERSION/$FILE" 44 | curl -Lfq --progress-bar $DOWNLOAD_URL -o $FILE || fail "Failed to download $DOWNLOAD_URL." 45 | unzip -qo $FILE || fail "Failed to unzip $FILE." 46 | mkdir -p $BIN_DIR 47 | mv gw "$BIN_DIR/gw" 48 | rm $FILE 49 | 50 | echo "Successfully installed gw binary to $BIN_DIR/gw!" 51 | -------------------------------------------------------------------------------- /docs/static/sakura-dark.css: -------------------------------------------------------------------------------- 1 | /* $color-text: #dedce5; */ 2 | /* Sakura.css v1.3.1 3 | * ================ 4 | * Minimal css theme. 5 | * Project: https://github.com/oxalorg/sakura/ 6 | */ 7 | /* Body */ 8 | html { 9 | font-size: 62.5%; 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } 11 | 12 | body { 13 | font-size: 1.8rem; 14 | line-height: 1.618; 15 | max-width: 38em; 16 | margin: auto; 17 | color: #c9c9c9; 18 | background-color: #222222; 19 | padding: 13px; } 20 | 21 | h1, h2, h3, h4, h5, h6 { 22 | line-height: 1.1; 23 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 24 | font-weight: 700; 25 | margin-top: 3rem; 26 | margin-bottom: 1.5rem; 27 | overflow-wrap: break-word; 28 | word-wrap: break-word; 29 | -ms-word-break: break-all; 30 | word-break: break-word; } 31 | 32 | h1 { 33 | font-size: 2.35em; } 34 | 35 | h2 { 36 | font-size: 2.00em; } 37 | 38 | h3 { 39 | font-size: 1.75em; } 40 | 41 | h4 { 42 | font-size: 1.5em; } 43 | 44 | h5 { 45 | font-size: 1.25em; } 46 | 47 | h6 { 48 | font-size: 1em; } 49 | 50 | p { 51 | margin-top: 0px; 52 | margin-bottom: 2.5rem; } 53 | 54 | small, sub, sup { 55 | font-size: 75%; } 56 | 57 | hr { 58 | border-color: #ffffff; } 59 | 60 | a { 61 | text-decoration: none; 62 | color: #ffffff; } 63 | a:hover { 64 | color: #c9c9c9; 65 | border-bottom: 2px solid #c9c9c9; } 66 | a:visited { 67 | color: #e6e6e6; } 68 | 69 | ul { 70 | padding-left: 1.4em; 71 | margin-top: 0px; 72 | margin-bottom: 2.5rem; } 73 | 74 | li { 75 | margin-bottom: 0; } 76 | 77 | blockquote { 78 | margin-left: 0px; 79 | margin-right: 0px; 80 | padding-left: 1em; 81 | padding-top: 0.8em; 82 | padding-bottom: 0.8em; 83 | padding-right: 0.8em; 84 | border-left: 5px solid #ffffff; 85 | margin-bottom: 2.5rem; 86 | background-color: #4a4a4a; } 87 | 88 | blockquote p { 89 | margin-bottom: 0; } 90 | 91 | img { 92 | height: auto; 93 | max-width: 100%; 94 | margin-top: 0px; 95 | margin-bottom: 2.5rem; } 96 | 97 | /* Pre and Code */ 98 | pre { 99 | background-color: #4a4a4a; 100 | display: block; 101 | padding: 1em; 102 | overflow-x: auto; 103 | margin-top: 0px; 104 | margin-bottom: 2.5rem; } 105 | 106 | code { 107 | font-size: 0.9em; 108 | padding: 0 0.5em; 109 | background-color: #4a4a4a; 110 | white-space: pre-wrap; } 111 | 112 | pre > code { 113 | padding: 0; 114 | background-color: transparent; 115 | white-space: pre; } 116 | 117 | /* Tables */ 118 | table { 119 | text-align: justify; 120 | width: 100%; 121 | border-collapse: collapse; } 122 | 123 | td, th { 124 | padding: 0.5em; 125 | border-bottom: 1px solid #4a4a4a; } 126 | 127 | /* Buttons, forms and input */ 128 | input, textarea { 129 | border: 1px solid #c9c9c9; } 130 | input:focus, textarea:focus { 131 | border: 1px solid #ffffff; } 132 | 133 | textarea { 134 | width: 100%; } 135 | 136 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 137 | display: inline-block; 138 | padding: 5px 10px; 139 | text-align: center; 140 | text-decoration: none; 141 | white-space: nowrap; 142 | background-color: #ffffff; 143 | color: #222222; 144 | border-radius: 1px; 145 | border: 1px solid #ffffff; 146 | cursor: pointer; 147 | box-sizing: border-box; } 148 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 149 | cursor: default; 150 | opacity: .5; } 151 | .button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled { 152 | background-color: #c9c9c9; 153 | border-color: #c9c9c9; 154 | color: #222222; 155 | outline: 0; } 156 | 157 | textarea, select, input { 158 | color: #c9c9c9; 159 | padding: 6px 10px; 160 | /* The 6px vertically centers text on FF, ignored by Webkit */ 161 | margin-bottom: 10px; 162 | background-color: #4a4a4a; 163 | border: 1px solid #4a4a4a; 164 | border-radius: 4px; 165 | box-shadow: none; 166 | box-sizing: border-box; } 167 | textarea:focus, select:focus, input:focus { 168 | border: 1px solid #ffffff; 169 | outline: 0; } 170 | 171 | input[type="checkbox"]:focus { 172 | outline: 1px dotted #ffffff; } 173 | 174 | label, legend, fieldset { 175 | display: block; 176 | margin-bottom: .5rem; 177 | font-weight: 600; } 178 | -------------------------------------------------------------------------------- /docs/static/sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.3.1 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura/ 5 | */ 6 | /* Body */ 7 | html { 8 | font-size: 62.5%; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } 10 | 11 | body { 12 | font-size: 1.8rem; 13 | line-height: 1.618; 14 | max-width: 38em; 15 | margin: auto; 16 | color: #4a4a4a; 17 | background-color: #f9f9f9; 18 | padding: 13px; } 19 | 20 | @media (max-width: 684px) { 21 | body { 22 | font-size: 1.53rem; } } 23 | 24 | @media (max-width: 382px) { 25 | body { 26 | font-size: 1.35rem; } } 27 | 28 | h1, h2, h3, h4, h5, h6 { 29 | line-height: 1.1; 30 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 31 | font-weight: 700; 32 | margin-top: 3rem; 33 | margin-bottom: 1.5rem; 34 | overflow-wrap: break-word; 35 | word-wrap: break-word; 36 | -ms-word-break: break-all; 37 | word-break: break-word; } 38 | 39 | h1 { 40 | font-size: 2.35em; } 41 | 42 | h2 { 43 | font-size: 2.00em; } 44 | 45 | h3 { 46 | font-size: 1.75em; } 47 | 48 | h4 { 49 | font-size: 1.5em; } 50 | 51 | h5 { 52 | font-size: 1.25em; } 53 | 54 | h6 { 55 | font-size: 1em; } 56 | 57 | p { 58 | margin-top: 0px; 59 | margin-bottom: 2.5rem; } 60 | 61 | small, sub, sup { 62 | font-size: 75%; } 63 | 64 | hr { 65 | border-color: #1d7484; } 66 | 67 | a { 68 | text-decoration: none; 69 | color: #1d7484; } 70 | a:hover { 71 | color: #982c61; 72 | border-bottom: 2px solid #4a4a4a; } 73 | a:visited { 74 | color: #144f5a; } 75 | 76 | ul { 77 | padding-left: 1.4em; 78 | margin-top: 0px; 79 | margin-bottom: 2.5rem; } 80 | 81 | li { 82 | margin-bottom: 0.4em; } 83 | 84 | blockquote { 85 | margin-left: 0px; 86 | margin-right: 0px; 87 | padding-left: 1em; 88 | padding-top: 0.8em; 89 | padding-bottom: 0.8em; 90 | padding-right: 0.8em; 91 | border-left: 5px solid #1d7484; 92 | margin-bottom: 2.5rem; 93 | background-color: #f1f1f1; } 94 | 95 | blockquote p { 96 | margin-bottom: 0; } 97 | 98 | img { 99 | height: auto; 100 | max-width: 100%; 101 | margin-top: 0px; 102 | margin-bottom: 2.5rem; } 103 | 104 | /* Pre and Code */ 105 | pre { 106 | background-color: #f1f1f1; 107 | display: block; 108 | padding: 1em; 109 | overflow-x: auto; 110 | margin-top: 0px; 111 | margin-bottom: 2.5rem; } 112 | 113 | code { 114 | font-size: 0.9em; 115 | padding: 0 0.5em; 116 | background-color: #f1f1f1; 117 | white-space: pre-wrap; } 118 | 119 | pre > code { 120 | padding: 0; 121 | background-color: transparent; 122 | white-space: pre; } 123 | 124 | /* Tables */ 125 | table { 126 | text-align: justify; 127 | width: 100%; 128 | border-collapse: collapse; } 129 | 130 | td, th { 131 | padding: 0.5em; 132 | border-bottom: 1px solid #f1f1f1; } 133 | 134 | /* Buttons, forms and input */ 135 | input, textarea { 136 | border: 1px solid #4a4a4a; } 137 | input:focus, textarea:focus { 138 | border: 1px solid #1d7484; } 139 | 140 | textarea { 141 | width: 100%; } 142 | 143 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 144 | display: inline-block; 145 | padding: 5px 10px; 146 | text-align: center; 147 | text-decoration: none; 148 | white-space: nowrap; 149 | background-color: #1d7484; 150 | color: #f9f9f9; 151 | border-radius: 1px; 152 | border: 1px solid #1d7484; 153 | cursor: pointer; 154 | box-sizing: border-box; } 155 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 156 | cursor: default; 157 | opacity: .5; } 158 | .button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled { 159 | background-color: #982c61; 160 | border-color: #982c61; 161 | color: #f9f9f9; 162 | outline: 0; } 163 | 164 | textarea, select, input { 165 | color: #4a4a4a; 166 | padding: 6px 10px; 167 | /* The 6px vertically centers text on FF, ignored by Webkit */ 168 | margin-bottom: 10px; 169 | background-color: #f1f1f1; 170 | border: 1px solid #f1f1f1; 171 | border-radius: 4px; 172 | box-shadow: none; 173 | box-sizing: border-box; } 174 | textarea:focus, select:focus, input:focus { 175 | border: 1px solid #1d7484; 176 | outline: 0; } 177 | 178 | input[type="checkbox"]:focus { 179 | outline: 1px dotted #1d7484; } 180 | 181 | label, legend, fieldset { 182 | display: block; 183 | margin-bottom: .5rem; 184 | font-weight: 600; } 185 | -------------------------------------------------------------------------------- /docs/static/webhook-github-deliveries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniel7grant/gw/5546789d02a626cc273767f5c27fc40e7570d79d/docs/static/webhook-github-deliveries.png -------------------------------------------------------------------------------- /docs/static/webhook-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniel7grant/gw/5546789d02a626cc273767f5c27fc40e7570d79d/docs/static/webhook-github.png -------------------------------------------------------------------------------- /docs/static/webhook-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniel7grant/gw/5546789d02a626cc273767f5c27fc40e7570d79d/docs/static/webhook-gitlab.png -------------------------------------------------------------------------------- /docs/templates/anchor-link.html: -------------------------------------------------------------------------------- 1 | {% if level > 1 %} 2 | # 3 | {% endif %} -------------------------------------------------------------------------------- /docs/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |