├── .ci ├── build-demo.sh └── template.json ├── .dockerignore ├── .github └── workflows │ ├── ci.yml │ ├── demo-pages.yml │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin └── satis-gitlab ├── composer.json ├── composer.lock ├── docs ├── docker.md └── options.md ├── phpunit.xml.dist ├── src └── MBO │ └── SatisGitlab │ ├── Command │ └── GitlabToConfigCommand.php │ ├── GitFilter │ └── GitlabNamespaceFilter.php │ ├── Resources │ └── default-template.json │ └── Satis │ └── ConfigBuilder.php └── tests ├── Command ├── GitlabToConfigCommandTest.php └── expected-with-filter.json ├── Satis ├── ConfigBuilderTest.php └── expected-repositories.json └── TestCase.php /.ci/build-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | PROJECT_DIR=$(dirname "$SCRIPT_DIR") 5 | 6 | if [ -z "$SATIS_GITHUB_TOKEN" ]; 7 | then 8 | echo "SATIS_GITHUB_TOKEN required" 9 | exit 10 | fi 11 | 12 | cd "$PROJECT_DIR" 13 | 14 | # configure github authentication for composer 15 | if [ "$GITHUB_ACTIONS" = "true" ]; then 16 | composer config -g github-oauth.github.com $SATIS_GITHUB_TOKEN 17 | fi 18 | 19 | # generate the satis config file (satis.json) 20 | bin/satis-gitlab gitlab-to-config \ 21 | --template ".ci/template.json" \ 22 | https://github.com $SATIS_GITHUB_TOKEN \ 23 | --users=mborne \ 24 | --ignore="(^mborne\\/php-helloworld)" \ 25 | --output satis.json 26 | 27 | # build public directory 28 | bin/satis-gitlab build --no-interaction --skip-errors satis.json public 29 | -------------------------------------------------------------------------------- /.ci/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mborne/demo-satis-gitlab", 3 | "homepage": "https://mborne.github.io/satis-gitlab/", 4 | "repositories": [ 5 | { 6 | "type": "composer", 7 | "url": "https://packagist.org" 8 | } 9 | ], 10 | "require": [], 11 | "require-dependencies": false, 12 | "require-dev-dependencies": false 13 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /output/ 3 | /.git/ 4 | /satis.json 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | strategy: 16 | matrix: 17 | php-version: [8.2,8.3] 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: "Setup PHP ${{ matrix.php-version }}" 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php-version }} 28 | coverage: xdebug2 29 | #coverage: xdebug 30 | tools: php-cs-fixer, phpunit 31 | 32 | - name: Validate composer.json 33 | run: composer validate --strict 34 | 35 | - name: Cache Composer packages 36 | id: composer-cache 37 | uses: actions/cache@v3 38 | with: 39 | path: vendor 40 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-php- 43 | 44 | - name: Install dependencies 45 | run: composer update --prefer-dist --no-progress 46 | 47 | - name: Run tests 48 | run: make test 49 | env: 50 | SATIS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | SATIS_GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} 52 | 53 | - name: Upload coverage results to coveralls.io 54 | if: github.ref == 'refs/heads/master' && matrix.php-version == '8.1' 55 | run: | 56 | vendor/bin/php-coveralls --coverage_clover=output/clover.xml --json_path=output/coveralls.json -v 57 | env: 58 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.github/workflows/demo-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish demo on GitHub pages 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: "Setup PHP 8.3" 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: 8.3 18 | 19 | - name: Validate composer.json 20 | run: composer validate --strict 21 | 22 | - name: Cache Composer packages 23 | id: composer-cache 24 | uses: actions/cache@v3 25 | with: 26 | path: vendor 27 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-php- 30 | 31 | - name: Install dependencies 32 | run: composer install --prefer-dist --no-progress 33 | 34 | - name: Build demo 35 | run: bash .ci/build-demo.sh 36 | env: 37 | SATIS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Upload static files from public as artifact 40 | id: deployment 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: public/ 44 | 45 | # Deploy pages 46 | deploy: 47 | needs: build 48 | 49 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 50 | permissions: 51 | pages: write # to deploy to Pages 52 | id-token: write # to verify the deployment originates from an appropriate source 53 | 54 | # Deploy to the github-pages environment 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | 59 | # Specify runner + deployment step 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: [ 'v*.*.*' ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | # This is used to complete the identity challenge 22 | # with sigstore/fulcio when running outside of PRs. 23 | id-token: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | # Set up BuildKit Docker container builder to be able to build 30 | # multi-platform images and export cache 31 | # https://github.com/docker/setup-buildx-action 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | # Login against a Docker registry except on PR 36 | # https://github.com/docker/login-action 37 | - name: Log into registry ${{ env.REGISTRY }} 38 | if: github.event_name != 'pull_request' 39 | uses: docker/login-action@v3 40 | with: 41 | registry: ${{ env.REGISTRY }} 42 | username: ${{ github.actor }} 43 | password: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Extract metadata (tags, labels) for Docker 46 | # https://github.com/docker/metadata-action 47 | - name: Extract Docker metadata 48 | id: meta 49 | uses: docker/metadata-action@v5 50 | with: 51 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 52 | 53 | # Build and push Docker image with Buildx (don't push on PR) 54 | # https://github.com/docker/build-push-action 55 | - name: Build and push Docker image 56 | id: build-and-push 57 | uses: docker/build-push-action@v5 58 | with: 59 | context: . 60 | platforms: linux/arm64/v8,linux/amd64 61 | push: ${{ github.event_name != 'pull_request' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | cache-from: type=gha 65 | cache-to: type=gha,mode=max 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ 2 | /vendor/ 3 | /satis.json 4 | /web/ 5 | /output/ 6 | /public/ 7 | 8 | /composer.phar 9 | /.phpunit.result.cache 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer:latest 2 | 3 | ARG UID=1000 4 | ARG GID=1000 5 | RUN addgroup --gid "$GID" satis \ 6 | && adduser \ 7 | --disabled-password \ 8 | --gecos "" \ 9 | --home "/home/satis-gitlab" \ 10 | --ingroup "satis" \ 11 | --uid "$UID" \ 12 | satis 13 | 14 | RUN mkdir -p /opt/satis-gitlab 15 | WORKDIR /opt/satis-gitlab 16 | COPY composer.json . 17 | COPY composer.lock . 18 | RUN composer install 19 | 20 | WORKDIR /opt/satis-gitlab 21 | COPY src/ src 22 | COPY bin/ bin 23 | 24 | RUN mkdir -p /opt/satis-gitlab/config \ 25 | && chown -R satis:satis /opt/satis-gitlab/config 26 | VOLUME /opt/satis-gitlab/config 27 | 28 | RUN mkdir -p /opt/satis-gitlab/public \ 29 | && chown -R satis:satis /opt/satis-gitlab/public 30 | VOLUME /opt/satis-gitlab/public 31 | 32 | USER satis 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) mborne 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test install 2 | 3 | test: install 4 | XDEBUG_MODE=coverage vendor/bin/phpunit -c phpunit.xml.dist \ 5 | --log-junit output/junit-report.xml \ 6 | --coverage-clover output/clover.xml \ 7 | --coverage-html output/coverage 8 | 9 | install: composer.phar 10 | php composer.phar install 11 | 12 | composer.phar: 13 | curl -s https://getcomposer.org/installer | php 14 | chmod +x composer.phar 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mborne/satis-gitlab 2 | 3 | [![CI](https://github.com/mborne/satis-gitlab/actions/workflows/ci.yml/badge.svg)](https://github.com/mborne/satis-gitlab/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/mborne/satis-gitlab/badge.svg?branch=master)](https://coveralls.io/github/mborne/satis-gitlab?branch=master) 4 | 5 | [PHP composer/satis](https://github.com/composer/satis) extended with the ability to generate SATIS configuration according to CVS projects containing a `composer.json` file. 6 | 7 | It also provides a way to mirror PHP dependencies to allow offline builds. 8 | 9 | ## Requirements 10 | 11 | * [PHP >=8.2](https://www.php.net/supported-versions.php) 12 | * GitLab API v4 / GitHub API / Gogs API / Gitea API 13 | 14 | ## Usage 15 | 16 | ### 1) Create SATIS project 17 | 18 | ```bash 19 | git clone https://github.com/mborne/satis-gitlab 20 | cd satis-gitlab 21 | # PHP 8.1 22 | composer install 23 | # PHP 7.4 (downgrading versions refered in composer.lock is required) 24 | composer update 25 | ``` 26 | 27 | 28 | ### 2) Generate SATIS configuration 29 | 30 | ```bash 31 | # add --archive if you want to mirror tar archives 32 | bin/satis-gitlab gitlab-to-config \ 33 | --homepage https://satis.example.org \ 34 | --output satis.json \ 35 | https://gitlab.example.org [GitlabToken] 36 | ``` 37 | 38 | ### 3) Use SATIS as usual 39 | 40 | ```bash 41 | bin/satis-gitlab build satis.json web 42 | ``` 43 | 44 | ### 4) Configure a static file server for the web directory 45 | 46 | Use you're favorite tool to expose `web` directory as `https://satis.example.org`. 47 | 48 | **satis.json should not be exposed, it contains the GitlabToken by default (see `--no-token`)** 49 | 50 | ### 5) Configure clients 51 | 52 | #### Option 1 : Configure projects to use SATIS 53 | 54 | SATIS web page suggests to add the following configuration to composer.json in all your projects : 55 | 56 | ```json 57 | { 58 | "repositories": [{ 59 | "type": "composer", 60 | "url": "https://satis.example.org" 61 | }] 62 | } 63 | ``` 64 | 65 | #### Option 2 : Configure composer to use SATIS 66 | 67 | Alternatively, composer can be configured globally to use SATIS : 68 | 69 | ```bash 70 | composer config --global repo.satis.example.org composer https://satis.example.org 71 | ``` 72 | 73 | (it makes a weaker link between your projects and your SATIS instance(s)) 74 | 75 | 76 | ## Advanced usage 77 | 78 | ### Filter by organization/groups and users 79 | 80 | If you rely on gitlab.com, you will probably need to find projects according to groups and users : 81 | 82 | ```bash 83 | bin/satis-gitlab gitlab-to-config https://gitlab.com $SATIS_GITLAB_TOKEN -vv --users=mborne --orgs=drutopia 84 | ``` 85 | 86 | ## Build configuration according to github repositories 87 | 88 | github supports allows to perform : 89 | 90 | ```bash 91 | bin/satis-gitlab gitlab-to-config https://github.com $SATIS_GITHUB_TOKEN --orgs=symfony --users=mborne 92 | bin/satis-gitlab build --skip-errors satis.json web 93 | ``` 94 | 95 | (Note that SATIS_GITHUB_TOKEN is required to avoid rate request limitation) 96 | 97 | 98 | ### Mirror dependencies 99 | 100 | Note that `--archive` option allows to download `tar` archives for each tag and each branch in `web/dist` for : 101 | 102 | * The gitlab projects 103 | * The dependencies of the gitlab projects 104 | 105 | 106 | ### Expose only public repositories 107 | 108 | Note that `GitlabToken` is optional so that you can generate a SATIS instance only for you're public repositories. 109 | 110 | 111 | ### Disable GitlabToken saving 112 | 113 | Note that `gitlab-to-config` saves the `GitlabToken` to `satis.json` configuration file (so far you expose only the `web` directory, it is not a problem). 114 | 115 | You may disable this option using `--no-token` option and use the following composer command to configure `$COMPOSER_HOME/auth.json` file : 116 | 117 | `composer config -g gitlab-token.satis.example.org GitlabToken` 118 | 119 | 120 | ### Deep customization 121 | 122 | Some command line options provide a basic customization options. You may also use `--template my-satis-template.json` to replace the default template : 123 | 124 | [default-template.json](src/MBO/SatisGitlab/Resources/default-template.json) 125 | 126 | ## Usage with docker 127 | 128 | See [docs/docker.md](docs/docker.md). 129 | 130 | ## Testing 131 | 132 | ```bash 133 | export SATIS_GITLAB_TOKEN=AnyGitlabToken 134 | export SATIS_GITHUB_TOKEN=AnyGithubToken 135 | 136 | make test 137 | ``` 138 | 139 | Note that an HTML coverage report is generated to `output/coverage/index.html` 140 | 141 | 142 | ## Demo 143 | 144 | https://mborne.github.io/satis-gitlab/ is built using github actions. See : 145 | 146 | * [.github/workflows/demo-pages.yml](.github/workflows/demo-pages.yml) 147 | * [.ci/build-demo.sh](.ci/build-demo.sh) 148 | 149 | ## License 150 | 151 | [MIT](LICENSE). 152 | 153 | 154 | -------------------------------------------------------------------------------- /bin/satis-gitlab: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view 10 | * the LICENSE file that was distributed with this source code. 11 | */ 12 | 13 | function includeIfExists($file) 14 | { 15 | if (file_exists($file)) { 16 | return include $file; 17 | } 18 | } 19 | 20 | if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) { 21 | print('You must set up the project dependencies using Composer before you can use Satis.'); 22 | exit(1); 23 | } 24 | 25 | /* 26 | * create extended satis application 27 | */ 28 | $application = new Composer\Satis\Console\Application(); 29 | $application->add(new \MBO\SatisGitlab\Command\GitlabToConfigCommand()); 30 | $application->run(); 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mborne/satis-gitlab", 3 | "type": "project", 4 | "description": "composer/satis extended with the ability to generate SATIS configuration", 5 | "authors": [ 6 | { 7 | "name": "Mickaël BORNE", 8 | "email": "mborne@users.noreply.github.com" 9 | } 10 | ], 11 | "license": "MIT", 12 | "autoload": { 13 | "psr-4": { 14 | "MBO\\SatisGitlab\\": "src/MBO/SatisGitlab" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "MBO\\SatisGitlab\\Tests\\": "tests" 20 | } 21 | }, 22 | "bin": [ 23 | "bin/satis-gitlab" 24 | ], 25 | "require": { 26 | "mborne/remote-git": "^0.8", 27 | "symfony/console": "^5.4|^6.0|^7.0", 28 | "composer/satis": "dev-main" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^9", 32 | "php-coveralls/php-coveralls": "^2.5" 33 | }, 34 | "config": { 35 | "allow-plugins": { 36 | "composer/satis": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # satis-gitlab - Usage with docker 2 | 3 | ## Motivation 4 | 5 | Provide a docker image for satis-gitlab to : 6 | 7 | * Create a static site with pages using GitLab-CI or GitHub actions 8 | * Ease the built of a custom image with a custom update loop 9 | 10 | ## Build image 11 | 12 | > Alternative : use [ghcr.io/mborne/satis-gitlab:master](https://github.com/mborne/satis-gitlab/pkgs/container/satis-gitlab) instead of satis-gitlab bellow. 13 | 14 | ```bash 15 | docker build -t satis-gitlab . 16 | ``` 17 | 18 | ## Create static site content 19 | 20 | ```bash 21 | # create satis-gitlab container 22 | docker run \ 23 | -v satis-data:/opt/satis-gitlab/public \ 24 | -v satis-config:/opt/satis-gitlab/config \ 25 | --env-file=../satis-gitlab.env \ 26 | --rm -ti satis-gitlab /bin/bash 27 | 28 | # generate config/satis.json 29 | bin/satis-gitlab gitlab-to-config \ 30 | --homepage https://satis.dev.localhost \ 31 | --output config/satis.json https://github.com \ 32 | --users=mborne $SATIS_GITHUB_TOKEN 33 | 34 | # generate public from config/satis.json with satis 35 | git config --global github.accesstoken $SATIS_GITHUB_TOKEN 36 | bin/satis-gitlab build config/satis.json public -v 37 | ``` 38 | 39 | ## Serve static site content 40 | 41 | ```bash 42 | # see http://localhost:8888 43 | docker run --rm -ti -v satis-data:/usr/share/nginx/html -p 8888:8080 nginxinc/nginx-unprivileged:1.26 44 | ``` 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Notes about some options 2 | 3 | ## unsafe-ssl 4 | 5 | Using `--unsafe-ssl` produce the following output for repositories : 6 | 7 | ```json 8 | { 9 | "options": { 10 | "ssl": { 11 | "allow_self_signed": true, 12 | "verify_peer": false, 13 | "verify_peer_name": false 14 | } 15 | }, 16 | "type": "vcs", 17 | "url": "https://gitlab.com/mborne/sample-composer.git" 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/MBO/SatisGitlab/Command/GitlabToConfigCommand.php: -------------------------------------------------------------------------------- 1 | setName('gitlab-to-config') 48 | 49 | // the short description shown while running "php bin/console list" 50 | ->setDescription('generate satis configuration scanning gitlab repositories') 51 | ->setHelp('look for composer.json in default gitlab branche, extract project name and register them in SATIS configuration') 52 | 53 | /* 54 | * Git client options 55 | */ 56 | ->addArgument('gitlab-url', InputArgument::REQUIRED) 57 | ->addArgument('gitlab-token') 58 | ->addOption('unsafe-ssl', null, InputOption::VALUE_NONE, 'allows to ignore SSL problems') 59 | 60 | /* 61 | * Project listing options (hosted git api level) 62 | */ 63 | ->addOption('orgs', 'o', InputOption::VALUE_REQUIRED, 'Find projects according to given organization names') 64 | ->addOption('users', 'u', InputOption::VALUE_REQUIRED, 'Find projects according to given user names') 65 | ->addOption('projectFilter', 'p', InputOption::VALUE_OPTIONAL, 'filter for projects (deprecated : see organization and users)', null) 66 | 67 | /* 68 | * Project filters 69 | */ 70 | ->addOption('ignore', 'i', InputOption::VALUE_REQUIRED, 'ignore project according to a regexp, for ex : "(^phpstorm|^typo3\/library)"', null) 71 | ->addOption('include-if-has-file',null,InputOption::VALUE_REQUIRED, 'include in satis config if project contains a given file, for ex : ".satisinclude"', null) 72 | ->addOption('project-type',null,InputOption::VALUE_REQUIRED, 'include in satis config if project is of a specified type, for ex : "library"', null) 73 | ->addOption('gitlab-namespace',null,InputOption::VALUE_REQUIRED, 'include in satis config if gitlab project namespace is in the list, for ex : "2,Diaspora" (deprecated : see organization and users)', null) 74 | /* 75 | * satis config generation options 76 | */ 77 | // deep customization : template file extended with default configuration 78 | ->addOption('template', null, InputOption::VALUE_REQUIRED, 'template satis.json extended with gitlab repositories', $templatePath) 79 | 80 | // simple customization on default-template.json 81 | ->addOption('name', null, InputOption::VALUE_REQUIRED, 'satis repository name') 82 | ->addOption('homepage', null, InputOption::VALUE_REQUIRED, 'satis homepage') 83 | ->addOption('archive', null, InputOption::VALUE_NONE, 'enable archive mirroring') 84 | ->addOption('no-token', null, InputOption::VALUE_NONE, 'disable token writing in output configuration') 85 | 86 | /* 87 | * output options 88 | */ 89 | ->addOption('output', 'O', InputOption::VALUE_REQUIRED, 'output config file', 'satis.json') 90 | ; 91 | } 92 | 93 | /** 94 | * @{inheritDoc} 95 | */ 96 | protected function execute(InputInterface $input, OutputInterface $output): int 97 | { 98 | $logger = $this->createLogger($output); 99 | 100 | /* 101 | * Create git client according to parameters 102 | */ 103 | $clientOptions = new ClientOptions(); 104 | $clientOptions->setUrl($input->getArgument('gitlab-url')); 105 | $clientOptions->setToken($input->getArgument('gitlab-token')); 106 | 107 | if ( $input->getOption('unsafe-ssl') ){ 108 | $clientOptions->setUnsafeSsl(true); 109 | } 110 | 111 | $client = ClientFactory::createClient( 112 | $clientOptions, 113 | $logger 114 | ); 115 | 116 | $outputFile = $input->getOption('output'); 117 | 118 | /* 119 | * Create repository listing filter (git level) 120 | */ 121 | $findOptions = new FindOptions(); 122 | /* orgs option */ 123 | $orgs = $input->getOption('orgs'); 124 | if ( ! empty($orgs) ){ 125 | $findOptions->setOrganizations(explode(',',$orgs)); 126 | } 127 | /* users option */ 128 | $users = $input->getOption('users'); 129 | if ( ! empty($users) ){ 130 | $findOptions->setUsers(explode(',',$users)); 131 | } 132 | 133 | /* projectFilter option */ 134 | $projectFilter = $input->getOption('projectFilter'); 135 | if ( ! empty($projectFilter) ) { 136 | $logger->info(sprintf("Project filter : %s...", $projectFilter)); 137 | $findOptions->setSearch($projectFilter); 138 | } 139 | 140 | /* 141 | * Create project filters according to input arguments 142 | */ 143 | $filterCollection = new FilterCollection($logger); 144 | $findOptions->setFilter($filterCollection); 145 | 146 | /* 147 | * Filter according to "composer.json" file 148 | */ 149 | $composerFilter = new ComposerProjectFilter($client,$logger); 150 | /* project-type option */ 151 | if ( ! empty($input->getOption('project-type')) ){ 152 | $composerFilter->setProjectType($input->getOption('project-type')); 153 | } 154 | $filterCollection->addFilter($composerFilter); 155 | 156 | 157 | /* include-if-has-file option (TODO : project listing level) */ 158 | if ( ! empty($input->getOption('include-if-has-file')) ){ 159 | $filterCollection->addFilter(new RequiredFileFilter( 160 | $client, 161 | $input->getOption('include-if-has-file'), 162 | $logger 163 | )); 164 | } 165 | 166 | /* 167 | * Filter according to git project properties 168 | */ 169 | 170 | /* ignore option */ 171 | if ( ! empty($input->getOption('ignore')) ){ 172 | $filterCollection->addFilter(new IgnoreRegexpFilter( 173 | $input->getOption('ignore') 174 | )); 175 | } 176 | 177 | /* gitlab-namespace option */ 178 | if ( ! empty($input->getOption('gitlab-namespace')) ){ 179 | $filterCollection->addFilter(new GitlabNamespaceFilter( 180 | $input->getOption('gitlab-namespace') 181 | )); 182 | } 183 | 184 | /* 185 | * Create configuration builder 186 | */ 187 | $templatePath = $input->getOption('template'); 188 | $output->writeln(sprintf("Loading template %s...", $templatePath)); 189 | $configBuilder = new ConfigBuilder($templatePath); 190 | 191 | /* 192 | * customize according to command line options 193 | */ 194 | $name = $input->getOption('name'); 195 | if ( ! empty($name) ){ 196 | $configBuilder->setName($name); 197 | } 198 | 199 | $homepage = $input->getOption('homepage'); 200 | if ( ! empty($homepage) ){ 201 | $configBuilder->setHomepage($homepage); 202 | } 203 | 204 | // mirroring 205 | if ( $input->getOption('archive') ){ 206 | $configBuilder->enableArchive(); 207 | } 208 | 209 | /* 210 | * Register gitlab domain to enable composer gitlab-* authentications 211 | */ 212 | $gitlabDomain = parse_url($clientOptions->getUrl(), PHP_URL_HOST); 213 | $configBuilder->addGitlabDomain($gitlabDomain); 214 | 215 | if ( ! $input->getOption('no-token') && $clientOptions->hasToken() ){ 216 | $configBuilder->addGitlabToken( 217 | $gitlabDomain, 218 | $clientOptions->getToken(), 219 | $clientOptions->isUnsafeSsl() 220 | ); 221 | } 222 | 223 | /* 224 | * SCAN gitlab projects to find composer.json file in default branch 225 | */ 226 | $logger->info(sprintf( 227 | "Listing gitlab repositories from %s...", 228 | $clientOptions->getUrl() 229 | )); 230 | 231 | /* 232 | * Find projects 233 | */ 234 | $projects = $client->find($findOptions); 235 | 236 | /* Generate SATIS configuration */ 237 | $projectCount = 0; 238 | foreach ($projects as $project) { 239 | $projectUrl = $project->getHttpUrl(); 240 | 241 | try { 242 | /* look for composer.json in default branch */ 243 | $json = $client->getRawFile( 244 | $project, 245 | 'composer.json', 246 | $project->getDefaultBranch() 247 | ); 248 | 249 | /* retrieve project name from composer.json content */ 250 | $composer = json_decode($json, true); 251 | $projectName = isset($composer['name']) ? $composer['name'] : null; 252 | if (is_null($projectName)) { 253 | $logger->error($this->createProjectMessage( 254 | $project, 255 | "name not defined in composer.json" 256 | )); 257 | continue; 258 | } 259 | 260 | /* add project to satis config */ 261 | $projectCount++; 262 | $logger->info($this->createProjectMessage( 263 | $project, 264 | "$projectName:*" 265 | )); 266 | $configBuilder->addRepository( 267 | $projectName, 268 | $projectUrl, 269 | $clientOptions->isUnsafeSsl() 270 | ); 271 | } catch (\Exception $e) { 272 | $logger->debug($e->getMessage()); 273 | $logger->warning($this->createProjectMessage( 274 | $project, 275 | 'composer.json not found' 276 | )); 277 | } 278 | } 279 | 280 | /* notify number of project found */ 281 | if ( $projectCount == 0 ){ 282 | $logger->error("No project found!"); 283 | }else{ 284 | $logger->info(sprintf( 285 | "Number of project found : %s", 286 | $projectCount 287 | )); 288 | } 289 | 290 | /* 291 | * Write resulting config 292 | */ 293 | $satis = $configBuilder->getConfig(); 294 | $logger->info("Generate satis configuration file : $outputFile"); 295 | $result = json_encode($satis, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 296 | file_put_contents($outputFile, $result); 297 | 298 | return Command::SUCCESS; 299 | } 300 | 301 | 302 | /** 303 | * Create message for a given project 304 | */ 305 | protected function createProjectMessage( 306 | ProjectInterface $project, 307 | $message 308 | ){ 309 | return sprintf( 310 | '%s (branch %s) : %s', 311 | $project->getName(), 312 | $project->getDefaultBranch(), 313 | $message 314 | ); 315 | } 316 | 317 | /** 318 | * Create console logger 319 | * @param OutputInterface $output 320 | * @return ConsoleLogger 321 | */ 322 | protected function createLogger(OutputInterface $output){ 323 | $verbosityLevelMap = array( 324 | LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, 325 | LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, 326 | ); 327 | return new ConsoleLogger($output,$verbosityLevelMap); 328 | } 329 | 330 | } 331 | -------------------------------------------------------------------------------- /src/MBO/SatisGitlab/GitFilter/GitlabNamespaceFilter.php: -------------------------------------------------------------------------------- 1 | groups = explode(',',strtolower($groups)); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getDescription(): string 49 | { 50 | return "gitlab namespace should be one of [".implode(', ',$this->groups)."]"; 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function isAccepted(ProjectInterface $project): bool 57 | { 58 | $project_info = $project->getRawMetadata(); 59 | if (isset($project_info['namespace'])) { 60 | 61 | // Extra data from namespace to patch on. 62 | $valid_keys = [ 63 | 'name' => 'name', 64 | 'id' => 'id', 65 | ]; 66 | $namespace_info = array_intersect_key($project_info['namespace'], $valid_keys); 67 | $namespace_info = array_map('strtolower', $namespace_info); 68 | 69 | if (!empty($namespace_info) && !empty(array_intersect($namespace_info, $this->groups))) { 70 | // Accept any package with a permitted namespace name or id. 71 | return TRUE; 72 | } 73 | } 74 | return FALSE; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/MBO/SatisGitlab/Resources/default-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mborne/satis-gitlab-repository", 3 | "homepage": "https://satis.dev.localhost/", 4 | "repositories": [ 5 | { 6 | "type": "composer", 7 | "url": "https://packagist.org" 8 | } 9 | ], 10 | "require": [], 11 | "require-dependencies": true, 12 | "require-dev-dependencies": true 13 | } -------------------------------------------------------------------------------- /src/MBO/SatisGitlab/Satis/ConfigBuilder.php: -------------------------------------------------------------------------------- 1 | config = json_decode(file_get_contents($templatePath),true); 27 | } 28 | 29 | /** 30 | * Get resulting configuration 31 | */ 32 | public function getConfig(){ 33 | return $this->config; 34 | } 35 | 36 | /** 37 | * Set name 38 | */ 39 | public function setName($name){ 40 | $this->config['name'] = $name; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Set homepage 47 | * @return $self 48 | */ 49 | public function setHomepage($homepage){ 50 | $this->config['homepage'] = $homepage; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Turn on mirror mode 57 | * @return $self 58 | */ 59 | public function enableArchive(){ 60 | $this->config['archive'] = array( 61 | 'directory' => 'dist', 62 | 'format' => 'tar', 63 | 'skip-dev' => true 64 | ); 65 | } 66 | 67 | /** 68 | * Add gitlab domain to config 69 | * @return $self 70 | */ 71 | public function addGitlabDomain($gitlabDomain){ 72 | if ( ! isset($this->config['config']) ){ 73 | $this->config['config'] = array(); 74 | } 75 | if ( ! isset($this->config['config']['gitlab-domains']) ){ 76 | $this->config['config']['gitlab-domains'] = array(); 77 | } 78 | 79 | $this->config['config']['gitlab-domains'][] = $gitlabDomain ; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Add gitlab token 86 | * 87 | * TODO : Ensure addGitlabDomain is invoked? 88 | * 89 | * @return $self 90 | */ 91 | public function addGitlabToken($gitlabDomain, $gitlabAuthToken){ 92 | if ( ! isset($this->config['config']['gitlab-token']) ){ 93 | $this->config['config']['gitlab-token'] = array(); 94 | } 95 | $this->config['config']['gitlab-token'][$gitlabDomain] = $gitlabAuthToken; 96 | 97 | return $this; 98 | } 99 | 100 | 101 | /** 102 | * Add a repository to satis 103 | * 104 | * @param string $projectName "{vendorName}/{componentName}" 105 | * @param string $projectUrl 106 | * @param boolean $unsafeSsl allows to disable ssl checks 107 | * 108 | * @return $self 109 | */ 110 | public function addRepository( 111 | $projectName, 112 | $projectUrl, 113 | $unsafeSsl = false 114 | ){ 115 | if ( ! isset($this->config['repositories']) ){ 116 | $this->config['repositories'] = array(); 117 | } 118 | 119 | $repository = array( 120 | 'type' => 'vcs', 121 | 'url' => $projectUrl 122 | ); 123 | 124 | if ( $unsafeSsl ){ 125 | $repository['options'] = [ 126 | "ssl" => [ 127 | "verify_peer" => false, 128 | "verify_peer_name" => false, 129 | "allow_self_signed" => true 130 | ] 131 | ]; 132 | } 133 | 134 | $this->config['repositories'][] = $repository ; 135 | $this->config['require'][$projectName] = '*'; 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /tests/Command/GitlabToConfigCommandTest.php: -------------------------------------------------------------------------------- 1 | outputFile = tempnam(sys_get_temp_dir(),'satis-config'); 20 | } 21 | 22 | protected function tearDown(): void 23 | { 24 | if ( file_exists($this->outputFile) ){ 25 | unlink($this->outputFile); 26 | } 27 | } 28 | 29 | public function testWithFilter(){ 30 | $gitlabToken = getenv('SATIS_GITLAB_TOKEN'); 31 | if ( empty($gitlabToken) ){ 32 | $this->markTestSkipped("Missing SATIS_GITLAB_TOKEN for gitlab.com"); 33 | return; 34 | } 35 | $command = new GitlabToConfigCommand('gitlab-to-config'); 36 | $commandTester = new CommandTester($command); 37 | $commandTester->execute(array( 38 | 'gitlab-url' => 'http://gitlab.com', 39 | 'gitlab-token' => $gitlabToken, 40 | '--projectFilter' => 'sample-composer', 41 | '--include-if-has-file' => 'README.md', 42 | '--output' => $this->outputFile 43 | )); 44 | 45 | $output = $commandTester->getDisplay(); 46 | $this->assertStringContainsString( 47 | 'mborne/sample-composer', 48 | $output 49 | ); 50 | 51 | /* check and remove gitlab-token */ 52 | $result = file_get_contents($this->outputFile); 53 | $result = json_decode($result,true); 54 | $this->assertEquals($gitlabToken,$result['config']['gitlab-token']['gitlab.com']); 55 | $result['config']['gitlab-token']['gitlab.com'] = 'SECRET'; 56 | 57 | /* compare complete file */ 58 | $expectedPath = dirname(__FILE__).'/expected-with-filter.json'; 59 | //file_put_contents($expectedPath,json_encode($result,JSON_PRETTY_PRINT)); 60 | $this->assertJsonStringEqualsJsonFile( 61 | $expectedPath, 62 | json_encode($result,JSON_PRETTY_PRINT) 63 | ); 64 | } 65 | 66 | 67 | 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /tests/Command/expected-with-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mborne\/satis-gitlab-repository", 3 | "homepage": "https:\/\/satis.dev.localhost\/", 4 | "repositories": [ 5 | { 6 | "type": "composer", 7 | "url": "https:\/\/packagist.org" 8 | }, 9 | { 10 | "type": "vcs", 11 | "url": "https:\/\/gitlab.com\/mborne\/sample-composer.git" 12 | } 13 | ], 14 | "require": { 15 | "mborne\/sample-composer": "*" 16 | }, 17 | "require-dependencies": true, 18 | "require-dev-dependencies": true, 19 | "config": { 20 | "gitlab-domains": [ 21 | "gitlab.com" 22 | ], 23 | "gitlab-token": { 24 | "gitlab.com": "SECRET" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/Satis/ConfigBuilderTest.php: -------------------------------------------------------------------------------- 1 | getConfig(); 15 | 16 | // name 17 | $this->assertArrayHasKey('name',$result); 18 | $this->assertEquals('mborne/satis-gitlab-repository',$result['name']); 19 | 20 | // homepage 21 | $this->assertArrayHasKey('homepage',$result); 22 | $this->assertEquals('https://satis.dev.localhost/',$result['homepage']); 23 | } 24 | 25 | public function testSetName(){ 26 | $configBuilder = new ConfigBuilder(); 27 | $configBuilder->setName('acme/satis-repository'); 28 | $result = $configBuilder->getConfig(); 29 | // homepage 30 | $this->assertArrayHasKey('name',$result); 31 | $this->assertEquals('acme/satis-repository',$result['name']); 32 | } 33 | 34 | public function testSetHomepage(){ 35 | $configBuilder = new ConfigBuilder(); 36 | $configBuilder->setHomepage('http://satis.example.org'); 37 | $result = $configBuilder->getConfig(); 38 | // homepage 39 | $this->assertArrayHasKey('homepage',$result); 40 | $this->assertEquals('http://satis.example.org',$result['homepage']); 41 | } 42 | 43 | public function testEnableArchive(){ 44 | $configBuilder = new ConfigBuilder(); 45 | $configBuilder->enableArchive(); 46 | $result = $configBuilder->getConfig(); 47 | 48 | $this->assertArrayHasKey('archive',$result); 49 | 50 | $this->assertArrayHasKey('directory',$result['archive']); 51 | $this->assertEquals('dist',$result['archive']['directory']); 52 | 53 | $this->assertArrayHasKey('format',$result['archive']); 54 | $this->assertEquals('tar',$result['archive']['format']); 55 | 56 | $this->assertArrayHasKey('skip-dev',$result['archive']); 57 | $this->assertTrue($result['archive']['skip-dev']); 58 | } 59 | 60 | public function testAddGitlabDomain(){ 61 | $configBuilder = new ConfigBuilder(); 62 | $configBuilder->addGitlabDomain('gitlab.com'); 63 | $configBuilder->addGitlabDomain('my-gitlab.com'); 64 | 65 | $result = $configBuilder->getConfig(); 66 | 67 | $this->assertArrayHasKey('config',$result); 68 | 69 | $this->assertEquals( 70 | '{"gitlab-domains":["gitlab.com","my-gitlab.com"]}', 71 | json_encode($result['config']) 72 | ); 73 | } 74 | 75 | public function testAddGitlabToken(){ 76 | $configBuilder = new ConfigBuilder(); 77 | $configBuilder->addGitlabToken('gitlab.com','test'); 78 | 79 | $result = $configBuilder->getConfig(); 80 | 81 | $this->assertArrayHasKey('config',$result); 82 | 83 | $this->assertEquals( 84 | '{"gitlab-token":{"gitlab.com":"test"}}', 85 | json_encode($result['config']) 86 | ); 87 | } 88 | 89 | public function testAddRepository(){ 90 | $configBuilder = new ConfigBuilder(); 91 | $configBuilder->addRepository( 92 | 'mborne/fake-a', 93 | 'https://github.com/mborne/fake-a.git', 94 | false 95 | ); 96 | $configBuilder->addRepository( 97 | 'mborne/fake-b', 98 | 'https://github.com/mborne/fake-b.git', 99 | true 100 | ); 101 | 102 | $satis = $configBuilder->getConfig(); 103 | 104 | $this->assertArrayHasKey('repositories',$satis); 105 | 106 | $result = $satis['repositories']; 107 | /* compare complete file */ 108 | $expectedPath = dirname(__FILE__).'/expected-repositories.json'; 109 | //file_put_contents($expectedPath,json_encode($result,JSON_PRETTY_PRINT)); 110 | $this->assertJsonStringEqualsJsonFile( 111 | $expectedPath, 112 | json_encode($result,JSON_PRETTY_PRINT) 113 | ); 114 | } 115 | 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /tests/Satis/expected-repositories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "composer", 4 | "url": "https:\/\/packagist.org" 5 | }, 6 | { 7 | "type": "vcs", 8 | "url": "https:\/\/github.com\/mborne\/fake-a.git" 9 | }, 10 | { 11 | "type": "vcs", 12 | "url": "https:\/\/github.com\/mborne\/fake-b.git", 13 | "options": { 14 | "ssl": { 15 | "verify_peer": false, 16 | "verify_peer_name": false, 17 | "allow_self_signed": true 18 | } 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(ProjectInterface::class) 19 | ->getMock() 20 | ; 21 | $project->expects($this->any()) 22 | ->method('getName') 23 | ->willReturn($projectName) 24 | ; 25 | return $project; 26 | } 27 | 28 | } --------------------------------------------------------------------------------