├── .gitignore ├── LICENSE ├── README.md ├── Setup.hs ├── app ├── Main.hs ├── Register.hs └── Server.hs ├── docker ├── Dockerfile └── docker-entrypoint.sh ├── github-ipfs.cabal └── stack.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | cabal-dev 4 | *.o 5 | *.hi 6 | *.chi 7 | *.chs.h 8 | *.dyn_o 9 | *.dyn_hi 10 | .hpc 11 | .hsenv 12 | .cabal-sandbox/ 13 | cabal.sandbox.config 14 | *.prof 15 | *.aux 16 | *.hp 17 | *.eventlog 18 | .stack-work/ 19 | cabal.project.local 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Alexander Krupenkin (c) 2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Alexander Krupenkin nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub -> IPFS mirror 2 | --------------------- 3 | 4 | > GitHub webhook based 5 | 6 | ### How it works 7 | 8 | 1. `git push` 9 | 2. Webhook throw by GitHub 10 | 3. Webhook catch by github-ipfs 11 | 4. Clone repository & publish into IPFS 12 | 5. Write repository hash into *Registry* at **account**/**repository**/**branch** 13 | 6. Publish into IPNS updated *Registry* 14 | 15 | ### Install 16 | 17 | docker run airalab/github-ipfs -d -p 8080:8000/tcp -e 'REGISTRY_KEY=top_secret' 18 | 19 | Docker exports `8080` webhook port set webhook URI in repository `Settings`. 20 | 21 | Environment: 22 | 23 | * REGISTRY_KEY - GitHub Webhook secret 24 | * REGISTRY_PATH - Absolute registry path in container, default: /registry 25 | * PORT - Webhook port, default: 8000 26 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import qualified Data.Text as T 4 | import Data.Text.Encoding (encodeUtf8) 5 | import Register 6 | import Server 7 | import System.Environment (getEnv) 8 | 9 | main :: IO () 10 | main = do 11 | path <- T.pack <$> getEnv "REGISTRY_PATH" 12 | key <- T.pack <$> getEnv "REGISTRY_KEY" 13 | port <- read <$> getEnv "PORT" 14 | serve (repoRegister path) (encodeUtf8 key) port 15 | -------------------------------------------------------------------------------- /app/Register.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Register where 3 | 4 | import Data.Aeson 5 | import Data.Aeson.Types (parseMaybe) 6 | import Data.Text (Text) 7 | import qualified Data.Text as T 8 | import Shelly 9 | 10 | ipfsPublish :: Text -> Sh () 11 | ipfsPublish = command_ "ipfs" ["name", "publish"] 12 | . pure 13 | 14 | ipfsAddDir :: Text -> Sh Text 15 | ipfsAddDir = fmap (last . T.lines) 16 | . command "ipfs" ["add", "-rq"] 17 | . pure 18 | 19 | ipfsAddRepo :: Text -> Text -> Sh Text 20 | ipfsAddRepo repoUrl branch = 21 | withTmpDir $ \tmp -> do 22 | repoPath <- toTextWarn tmp 23 | run_ "git" ["clone", repoUrl, repoPath, "-b", branch] 24 | ipfsAddDir repoPath 25 | 26 | repoRegister :: Text -> Object -> IO () 27 | repoRegister path = maybe (return ()) go . parseMeta 28 | where parseMeta = parseMaybe $ \meta -> do 29 | ref <- meta .: "ref" 30 | let branch = T.takeWhileEnd (/= '/') ref 31 | repository <- meta .: "repository" 32 | name <- repository .: "full_name" 33 | url <- repository .: "html_url" 34 | return (name :: Text, branch, url) 35 | 36 | go (name, branch, url) = shelly $ do 37 | hash <- ipfsAddRepo url branch 38 | mkdir_p (path name) 39 | writefile (path name branch) hash 40 | regHash <- ipfsAddDir path 41 | ipfsPublish regHash 42 | -------------------------------------------------------------------------------- /app/Server.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE TypeFamilies #-} 3 | {-# LANGUAGE TypeOperators #-} 4 | -- | 5 | -- Module : Server 6 | -- Copyright : Alexander Krupenkin 2017 7 | -- License : BSD3 8 | -- 9 | -- Maintainer : mail@akru.me 10 | -- Stability : experimental 11 | -- Portability : portable 12 | -- 13 | -- Simple GitHub webhook server. 14 | -- 15 | module Server (serve) where 16 | 17 | import Control.Monad.IO.Class (liftIO) 18 | import Data.Aeson (Object) 19 | import Data.ByteString (ByteString) 20 | import Network.Wai.Handler.Warp (Port, run) 21 | import Servant hiding (serve) 22 | import Servant.GitHub.Webhook 23 | 24 | type API 25 | = GitHubEvent '[ 'WebhookPushEvent ] 26 | :> GitHubSignedReqBody '[JSON] Object 27 | :> Post '[JSON] () 28 | 29 | handler :: (Object -> IO ()) -> RepoWebhookEvent -> ((), Object) -> Handler () 30 | handler f _ = liftIO . f . snd 31 | 32 | serve :: (Object -> IO ()) 33 | -> ByteString 34 | -> Port 35 | -> IO () 36 | serve f key port = 37 | run port $ 38 | serveWithContext 39 | (Proxy :: Proxy API) 40 | (gitHubKey (pure key) :. EmptyContext) 41 | (handler f :: Server API) 42 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM airalab/ipfs-cloud 2 | MAINTAINER Alexander Krupenkin 3 | LABEL Description="GitHub -> IPFS mirror bot" Vendor="Airalab" Version="0.1" 4 | 5 | RUN apt-get update && apt-get install -y curl git 6 | RUN curl -SSl https://get.haskellstack.org/ | sh 7 | RUN apt-get autoclean 8 | 9 | RUN git clone --recursive https://github.com/airalab/github-ipfs 10 | RUN cd /github-ipfs && stack setup && stack install 11 | RUN mv /root/.local/bin/github-ipfs /usr/local/bin 12 | 13 | RUN rm -rf /github-ipfs /root/.stack /root/.local 14 | 15 | ADD docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh 16 | ENTRYPOINT ["docker-entrypoint.sh"] 17 | -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export IPFS_PATH=/ipfs 4 | 5 | # First init 6 | if [ ! -f /ipfs/config ]; then 7 | ipfs init 8 | ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001 9 | ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/80 10 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' 11 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' 12 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' 13 | fi 14 | 15 | export PORT=${PORT:-8000} 16 | export REGISTRY_PATH=${REGISTRY_PATH:-/registry} 17 | mkdir $REGISTRY_PATH 18 | 19 | github-ipfs & 20 | 21 | exec ipfs daemon --enable-gc 22 | -------------------------------------------------------------------------------- /github-ipfs.cabal: -------------------------------------------------------------------------------- 1 | name: github-ipfs 2 | version: 0.1.0.0 3 | -- synopsis: 4 | -- description: 5 | homepage: https://github.com/airalab/github-ipfs#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Alexander Krupenkin 9 | maintainer: mail@akru.me 10 | copyright: (c) Alexander Krupenkin 11 | category: Web 12 | build-type: Simple 13 | extra-source-files: README.md 14 | cabal-version: >=1.10 15 | 16 | executable github-ipfs 17 | hs-source-dirs: app 18 | main-is: Main.hs 19 | other-modules: Server, Register 20 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 21 | build-depends: base >= 4.9 && < 4.10 22 | , servant-github-webhook 23 | , servant-server 24 | , bytestring 25 | , shelly 26 | , aeson 27 | , text 28 | , warp 29 | default-language: Haskell2010 30 | 31 | source-repository head 32 | type: git 33 | location: https://github.com/airalab/github-ipfs 34 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 2 | # A snapshot resolver dictates the compiler version and the set of packages 3 | # to be used for project dependencies. For example: 4 | # 5 | # resolver: lts-3.5 6 | # resolver: nightly-2015-09-21 7 | # resolver: ghc-7.10.2 8 | # resolver: ghcjs-0.1.0_ghc-7.10.2 9 | # resolver: 10 | # name: custom-snapshot 11 | # location: "./custom-snapshot.yaml" 12 | resolver: lts-9.6 13 | 14 | # User packages to be built. 15 | # Various formats can be used as shown in the example below. 16 | # 17 | # packages: 18 | # - some-directory 19 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 20 | # - location: 21 | # git: https://github.com/commercialhaskell/stack.git 22 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 23 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 24 | # extra-dep: true 25 | # subdirs: 26 | # - auto-update 27 | # - wai 28 | # 29 | # A package marked 'extra-dep: true' will only be built if demanded by a 30 | # non-dependency (i.e. a user package), and its test suites and benchmarks 31 | # will not be run. This is useful for tweaking upstream packages. 32 | packages: 33 | - '.' 34 | # Dependency packages to be pulled from upstream that are not in the resolver 35 | # (e.g., acme-missiles-0.3) 36 | extra-deps: 37 | - servant-github-webhook-0.3.1.0 38 | - Crypto-4.2.5.1 39 | 40 | 41 | # Override default flag values for local packages and extra-deps 42 | flags: { 43 | servant-github-webhook: {old-base: false} 44 | } 45 | 46 | # Extra package databases containing global packages 47 | extra-package-dbs: [] 48 | 49 | # Control whether we use the GHC we find on the path 50 | # system-ghc: true 51 | # 52 | # Require a specific version of stack, using version ranges 53 | # require-stack-version: -any # Default 54 | # require-stack-version: ">=1.3" 55 | # 56 | # Override the architecture used by stack, especially useful on Windows 57 | # arch: i386 58 | # arch: x86_64 59 | # 60 | # Extra directories used by stack for building 61 | # extra-include-dirs: [/path/to/dir] 62 | # extra-lib-dirs: [/path/to/dir] 63 | # 64 | # Allow a newer minor version of GHC than the snapshot specifies 65 | # compiler-check: newer-minor 66 | --------------------------------------------------------------------------------